Initial commit: CardioAI - 心血管疾病智能辅助系统

This commit is contained in:
Zhengzong
2026-01-30 18:09:22 +08:00
commit aa51c1f4ed
10 changed files with 1227 additions and 0 deletions

35
.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# IDE files
.idea/
.claude/
# Python files
__pycache__/
*.pyc
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Jupyter notebooks
.ipynb_checkpoints/
# Data files
data/
# Model files
*.pkl
*.joblib
# Log files
*.log
# OS files
.DS_Store
Thumbs.db
# Virtual environment
venv/
env/

47
aicodes/llm_streaming.py Normal file
View File

@@ -0,0 +1,47 @@
import os
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
def get_llm():
"""
初始化并返回 DeepSeek LLM 实例
"""
# 注意:这里的 ChatOpenAI 实际上是兼容 OpenAI 接口的类,用于连接 DeepSeek API
llm = ChatOpenAI(
base_url=os.getenv("base_url1"),
api_key=os.getenv("DEEPSEEK_API_KEY1"),
model="deepseek-chat",
temperature=0, # 设置为0确保结果稳定
)
return llm
# 1. 初始化 LLM
llm = get_llm()
# 2. 用户级 prompt
user_prompt = "你是谁?请详细介绍一下你自己。"
print(f"--- 用户提问:{user_prompt} ---")
print("--- LLM 流式回复开始 ---")
# 3. **核心修改:使用 .stream() 方法进行流式调用**
# .stream() 返回一个迭代器,我们可以遍历它来获取分块的输出。
response_stream = llm.stream(user_prompt)
# 4. 遍历并打印流式输出
full_response = ""
for chunk in response_stream:
# chunk.content 包含当前流片段的内容
# end="" 确保打印时不换行,模拟流式输出效果
print(chunk.content, end="", flush=True)
full_response += chunk.content
print("\n--- LLM 流式回复结束 ---")
# 5. 可选:打印完整的回复内容
# print(f"\n完整的回复内容{full_response}")

View File

@@ -0,0 +1,274 @@
# 语音合成CosyVoice Python SDK
https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#915a935d871ak
```python
dashscope.api_key = "sk-6fecb52bb96d4492bb7e58213eeeb00f"
```
### **流式调用**
在同一个语音合成任务中分多次提交文本,并通过回调的方式实时获取合成结果。
**说明**
- 流式输入时可多次调用`streaming_call`按顺序提交文本片段。服务端接收文本片段后自动进行分句:
- 完整语句立即合成
- 不完整语句缓存至完整后合成
调用 `streaming_complete` 时,服务端会强制合成所有已接收但未处理的文本片段(包括未完成的句子)。
- 发送文本片段的间隔不得超过23秒否则触发“request timeout after 23 seconds”异常。
若无待发送文本,需及时调用 `streaming_complete`结束任务。
> 服务端强制设定23秒超时机制客户端无法修改该配置。
![image](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/0165103671/CAEQVRiBgMDb7PahrBkiIDVkNjEwOTMxYjEwOTRmOWFhMmI1OTRiY2Q3ZDgzZmE54709861_20241015153444.149.svg)
1. 实例化SpeechSynthesizer类
实例化[SpeechSynthesizer类](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#d6bc1f133f871)绑定[请求参数](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#2fe363ace1l4k)和[回调接口ResultCallback](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#85d698b9f9g8s)。
2. 流式传输
多次调用[SpeechSynthesizer类](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#d6bc1f133f871)的`streaming_call`方法分片提交待合成文本,将待合成文本分段发送至服务端。
在发送文本的过程中,服务端会通过[回调接口ResultCallback](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#85d698b9f9g8s)的`on_data`方法,将合成结果实时返回给客户端。
每次调用`streaming_call`方法发送的文本片段(即`text`长度不得超过2000字符累计发送的文本总长度不得超过20万字符。
3. 结束处理
调用[SpeechSynthesizer类](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#d6bc1f133f871)的`streaming_complete`方法结束语音合成。
该方法会阻塞当前线程,直到[回调接口ResultCallback](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#85d698b9f9g8s)的`on_complete`或者`on_error`回调触发后才会释放线程阻塞。
请务必确保调用该方法,否则可能会导致结尾部分的文本无法成功转换为语音。
~~~python
# coding=utf-8
#
# pyaudio安装说明
# 如果是macOS操作系统执行如下命令
# brew install portaudio
# pip install pyaudio
# 如果是Debian/Ubuntu操作系统执行如下命令
# sudo apt-get install python-pyaudio python3-pyaudio
# 或者
# pip install pyaudio
# 如果是CentOS操作系统执行如下命令
# sudo yum install -y portaudio portaudio-devel && pip install pyaudio
# 如果是Microsoft Windows执行如下命令
# python -m pip install pyaudio
import time
import pyaudio
import dashscope
from dashscope.api_entities.dashscope_response import SpeechSynthesisResponse
from dashscope.audio.tts_v2 import *
from datetime import datetime
def get_timestamp():
now = datetime.now()
formatted_timestamp = now.strftime("[%Y-%m-%d %H:%M:%S.%f]")
return formatted_timestamp
# 若没有将API Key配置到环境变量中需将your-api-key替换为自己的API Key
# dashscope.api_key = "your-api-key"
# 模型
model = "cosyvoice-v2"
# 音色
voice = "longxiaochun_v2"
# 定义回调接口
class Callback(ResultCallback):
_player = None
_stream = None
def on_open(self):
print("连接建立:" + get_timestamp())
self._player = pyaudio.PyAudio()
self._stream = self._player.open(
format=pyaudio.paInt16, channels=1, rate=22050, output=True
)
def on_complete(self):
print("语音合成完成,所有合成结果已被接收:" + get_timestamp())
def on_error(self, message: str):
print(f"语音合成出现异常:{message}")
def on_close(self):
print("连接关闭:" + get_timestamp())
# 停止播放器
self._stream.stop_stream()
self._stream.close()
self._player.terminate()
def on_event(self, message):
pass
def on_data(self, data: bytes) -> None:
print(get_timestamp() + " 二进制音频长度为:" + str(len(data)))
self._stream.write(data)
callback = Callback()
test_text = [
"流式文本语音合成SDK",
"可以将输入的文本",
"合成为语音二进制数据,",
"相比于非流式语音合成,",
"流式合成的优势在于实时性",
"更强。用户在输入文本的同时",
"可以听到接近同步的语音输出,",
"极大地提升了交互体验,",
"减少了用户等待时间。",
"适用于调用大规模",
"语言模型LLM以",
"流式输入文本的方式",
"进行语音合成的场景。",
]
# 实例化SpeechSynthesizer并在构造方法中传入模型model、音色voice等请求参数
synthesizer = SpeechSynthesizer(
model=model,
voice=voice,
format=AudioFormat.PCM_22050HZ_MONO_16BIT,
callback=callback,
)
# 流式发送待合成文本。在回调接口的on_data方法中实时获取二进制音频
for text in test_text:
synthesizer.streaming_call(text)
time.sleep(0.1)
# 结束流式语音合成
synthesizer.streaming_complete()
# 首次发送文本时需建立 WebSocket 连接,因此首包延迟会包含连接建立的耗时
print('[Metric] requestId为{},首包延迟为:{}毫秒'.format(
synthesizer.get_last_request_id(),
synthesizer.get_first_package_delay()))
~~~
## **请求参数**
请求参数通过[SpeechSynthesizer类](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#d6bc1f133f871)的构造方法进行设置。
| | | | | |
| ---- | ---- | ---- | ---- | ---- |
| | | | | |
| **参数** | **类型** | **默认值** | **是否必须** | **说明** |
| ---------------------- | -------------- | ---------- | ------------ | ------------------------------------------------------------ |
| model | str | - | 是 | 指定模型。不同版本的模型编码方式一致,但使用时须确保模型(`model`)与音色(`voice`)匹配:每个版本的模型只能使用本版本的默认音色或专属音色。 |
| voice | str | - | 是 | 指定语音合成所使用的音色。支持默认音色和专属音色:**默认音色**:参见**音色列表**章节。**专属音色**:通过[声音复刻](https://help.aliyun.com/zh/model-studio/voice-replica-1/)功能定制。使用复刻音色时,请确保声音复刻与语音合成使用同一账号。详细操作步骤请参见[CosyVoice声音复刻API](https://help.aliyun.com/zh/model-studio/cosyvoice-clone-api#da30eeebc4uwk)。**⚠️ 使用声音复刻系列模型合成语音时,仅能使用该模型复刻生成的专属音色,不能使用默认音色。****⚠️ 使用专属音色合成语音时,语音合成模型(**`**model**`**)必须与声音复刻模型(**`**target_model**`**)相同。** |
| format | enum | 因音色而异 | 否 | 指定音频编码格式及采样率。若未指定`format`则合成音频采样率为22.05kHz格式为mp3。**说明**默认采样率代表当前音色的最佳采样率缺省条件下默认按照该采样率输出同时支持降采样或升采样。可指定的音频编码格式及采样率如下所有模型均支持的音频编码格式及采样率AudioFormat.WAV_8000HZ_MONO_16BIT代表音频格式为wav采样率为8kHzAudioFormat.WAV_16000HZ_MONO_16BIT代表音频格式为wav采样率为16kHzAudioFormat.WAV_22050HZ_MONO_16BIT代表音频格式为wav采样率为22.05kHzAudioFormat.WAV_24000HZ_MONO_16BIT代表音频格式为wav采样率为24kHzAudioFormat.WAV_44100HZ_MONO_16BIT代表音频格式为wav采样率为44.1kHzAudioFormat.WAV_48000HZ_MONO_16BIT代表音频格式为wav采样率为48kHzAudioFormat.MP3_8000HZ_MONO_128KBPS代表音频格式为mp3采样率为8kHzAudioFormat.MP3_16000HZ_MONO_128KBPS代表音频格式为mp3采样率为16kHzAudioFormat.MP3_22050HZ_MONO_256KBPS代表音频格式为mp3采样率为22.05kHzAudioFormat.MP3_24000HZ_MONO_256KBPS代表音频格式为mp3采样率为24kHzAudioFormat.MP3_44100HZ_MONO_256KBPS代表音频格式为mp3采样率为44.1kHzAudioFormat.MP3_48000HZ_MONO_256KBPS代表音频格式为mp3采样率为48kHzAudioFormat.PCM_8000HZ_MONO_16BIT代表音频格式为pcm采样率为8kHzAudioFormat.PCM_16000HZ_MONO_16BIT代表音频格式为pcm采样率为16kHzAudioFormat.PCM_22050HZ_MONO_16BIT代表音频格式为pcm采样率为22.05kHzAudioFormat.PCM_24000HZ_MONO_16BIT代表音频格式为pcm采样率为24kHzAudioFormat.PCM_44100HZ_MONO_16BIT代表音频格式为pcm采样率为44.1kHzAudioFormat.PCM_48000HZ_MONO_16BIT代表音频格式为pcm采样率为48kHz除`cosyvoice-v1`其他模型支持的音频编码格式及采样率音频格式为opus时支持通过`bit_rate`参数调整码率。仅对1.24.0及之后版本的DashScope适用。AudioFormat.OGG_OPUS_8KHZ_MONO_32KBPS代表音频格式为opus采样率为8kHz码率为32kbpsAudioFormat.OGG_OPUS_16KHZ_MONO_16KBPS代表音频格式为opus采样率为16kHz码率为16kbpsAudioFormat.OGG_OPUS_16KHZ_MONO_32KBPS代表音频格式为opus采样率为16kHz码率为32kbpsAudioFormat.OGG_OPUS_16KHZ_MONO_64KBPS代表音频格式为opus采样率为16kHz码率为64kbpsAudioFormat.OGG_OPUS_24KHZ_MONO_16KBPS代表音频格式为opus采样率为24kHz码率为16kbpsAudioFormat.OGG_OPUS_24KHZ_MONO_32KBPS代表音频格式为opus采样率为24kHz码率为32kbpsAudioFormat.OGG_OPUS_24KHZ_MONO_64KBPS代表音频格式为opus采样率为24kHz码率为64kbpsAudioFormat.OGG_OPUS_48KHZ_MONO_16KBPS代表音频格式为opus采样率为48kHz码率为16kbpsAudioFormat.OGG_OPUS_48KHZ_MONO_32KBPS代表音频格式为opus采样率为48kHz码率为32kbpsAudioFormat.OGG_OPUS_48KHZ_MONO_64KBPS代表音频格式为opus采样率为48kHz码率为64kbps |
| volume | int | 50 | 否 | 合成音频的音量取值范围0~100。**重要**该字段在不同版本的DashScope SDK中有所不同1.20.10及以后版本的SDKvolume1.20.10以前版本的SDKvolumn |
| speech_rate | float | 1.0 | 否 | 合成音频的语速取值范围0.5~2。0.5表示默认语速的0.5倍速。1表示默认语速。默认语速是指模型默认输出的合成语速语速会因音色不同而略有不同。约每秒钟4个字。2表示默认语速的2倍速。 |
| pitch_rate | float | 1.0 | 否 | 合成音频的语调取值范围0.5~2。 |
| bit_rate | int | 32 | 否 | 指定音频的[码率](https://opus-codec.org/)取值范围6~510kbps。码率越大音质越好音频文件体积越大。仅在音频格式`format`为opus时可用。`cosyvoice-v1`模型不支持该参数。**说明**`bit_rate`需要通过`additional_params`参数进行设置: `synthesizer = SpeechSynthesizer(model="cosyvoice-v2", voice="longxiaochun_v2", format=AudioFormat.OGG_OPUS_16KHZ_MONO_16KBPS, additional_params={"bit_rate": 32})` |
| word_timestamp_enabled | bool | False | 否 | 是否开启字级别时间戳默认关闭。仅cosyvoice-v2支持该功能。时间戳结果仅能通过回调接口获取**说明**`word_timestamp_enabled`需要通过`additional_params`参数进行设置: `synthesizer = SpeechSynthesizer(model="cosyvoice-v2", voice="longxiaochun_v2", callback=callback, # 时间戳结果仅能通过回调接口获取 additional_params={'word_timestamp_enabled': True})`**点击查看完整示例代码** |
| seed | int | 0 | 否 | 生成时使用的随机数种子使合成的效果产生变化。默认值0。取值范围0~65535。cosyvoice-v1不支持该功能。 |
| language_hints | list[str] | - | 否 | 合成文本语言提示,可选值为 `zh`(中文)或 `en`英文列表中仅第一个语言生效。仅cosyvoice-v3、cosyvoice-v3-plus支持该功能。此设置会影响阿拉伯数字等内容的读法。例如当合成“123”时若设置为`zh`,则读作“一百二十三”;而`en`则会读作“one hundred and twenty-three”。如果不设置系统会根据文本内容自动判断并应用相应的合成规则。 |
| instruction | String | - | 否 | 设置提示词。仅cosyvoice-v3、cosyvoice-v3-plus支持该功能。目前仅支持设置情感。格式`你说话的情感是<情感值>。`”(注意,结尾一定不要遗漏句号,使用时将“`<情感值>`”替换为具体的情感值,例如替换为`neutral`)。示例:“`你说话的情感是neutral。`”支持的情感值:`neutral``fearful``angry``sad``surprised``happy``disgusted`。 |
| callback | ResultCallback | - | 否 | [回调接口ResultCallback](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#85d698b9f9g8s). |
## **关键接口**
### `SpeechSynthesizer`类
`SpeechSynthesizer`通过“`from dashscope.audio.tts_v2 import *`”方式引入,提供语音合成的关键接口。
| | | | |
| ---- | ---- | ---- | ---- |
| | | | |
| **方法** | **参数** | **返回值** | **描述** |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------------ |
| `def call(self, text: str, timeout_millis=None)` | `text`:待合成文本`timeout_millis`阻塞线程的超时时间单位为毫秒不设置或值为0时不生效 | 没有指定`ResultCallback`时返回二进制音频数据否则返回None | 将整段文本(无论是纯文本还是包含[SSML](https://help.aliyun.com/zh/model-studio/introduction-to-cosyvoice-ssml-markup-language)的文本)转换为语音。在创建`SpeechSynthesizer`实例时,存在以下两种情况:没有指定`ResultCallback``call`方法会阻塞当前线程直到语音合成完成并返回二进制音频数据。使用方法请参见[同步调用](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#8341058094tc3)。指定了`ResultCallback``call`方法会立刻返回None并通过[回调接口ResultCallback](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#85d698b9f9g8s)的`on_data`方法返回语音合成的结果。使用方法请参见[异步调用](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#cc2a504f344s2)。**重要**每次调用`call`方法前,需要重新初始化`SpeechSynthesizer`实例。 |
| `def streaming_call(self, text: str)` | `text`:待合成文本片段 | 无 | 流式发送待合成文本不支持包含SSML的文本。您可以多次调用该接口将待合成文本分多次发送给服务端。合成结果通过[回调接口ResultCallback](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#85d698b9f9g8s)的`on_data`方法获取。使用方法请参见[流式调用](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#ba023aacfbr84)。 |
| `def streaming_complete(self, complete_timeout_millis=600000)` | `complete_timeout_millis`:等待时间,单位为毫秒 | 无 | 结束流式语音合成。该方法阻塞当前线程N毫秒具体时长由`complete_timeout_millis`决定),直到任务结束。如果`completeTimeoutMillis`设置为0则无限期等待。默认情况下如果等待时间超过10分钟则停止等待。使用方法请参见[流式调用](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#ba023aacfbr84)。**重要**在[流式调用](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#ba023aacfbr84)时,请务必确保调用该方法,否则可能会出现合成语音缺失的问题。 |
| `def get_last_request_id(self)` | 无 | 上一个任务的request_id | 获取上一个任务的request_id。 |
| `def get_first_package_delay(self)` | 无 | 首包延迟 | 获取首包延迟一般在500ms左右。首包延迟是开始发送文本和接收第一个音频包之间的时间单位为毫秒。在任务完成后使用。首次发送文本时需建立 WebSocket 连接,因此首包延迟会包含连接建立的耗时。 |
| `def get_response(self)` | 无 | 最后一次报文 | 获取最后一次报文为JSON格式的数据可以用于获取task-failed报错。 |
### **回调接口(**`ResultCallback`
[异步调用](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#cc2a504f344s2)或[流式调用](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#ba023aacfbr84)时,服务端会通过回调的方式,将关键流程信息和数据返回给客户端。您需要实现回调方法,处理服务端返回的信息或者数据。
通过“`from dashscope.audio.tts_v2 import *`”方式引入。
点击查看示例
| | | | |
| ---- | ---- | ---- | ---- |
| | | | |
| **方法** | **参数** | **返回值** | **描述** |
| ------------------------------------------- | ---------------------------------- | ---------- | ------------------------------------------------------------ |
| `def on_open(self) -> None` | 无 | 无 | 当和服务端建立连接完成后,该方法立刻被回调。 |
| `def on_event( self, message: str) -> None` | `message`:服务端返回的信息 | 无 | 当服务有回复时会被回调。`message`为JSON字符串解析可获取Task ID`task_id`参数)、本次请求中计费的有效字符数(`characters`参数)等信息。 |
| `def on_complete(self) -> None` | 无 | 无 | 当所有合成数据全部返回(语音合成完成)后被回调。 |
| `def on_error(self, message) -> None` | `message`:异常信息 | 无 | 发生异常时该方法被回调。 |
| `def on_data(self, data: bytes) -> None` | `data`:服务器返回的二进制音频数据 | 无 | 当服务器有合成音频返回时被回调。您可以将二进制音频数据合成为一个完整的音频文件后使用播放器播放,也可以通过支持流式播放的播放器实时播放。**重要**流式语音合成中对于mp3/opus等压缩格式音频分段传输需使用流式播放器不可逐帧播放避免解码失败。支持流式播放的播放器ffmpeg、pyaudio (Python)、AudioFormat (Java)、MediaSource (Javascript)等。将音频数据合成完整的音频文件时应以追加模式写入同一文件。流式语音合成的wav/mp3 格式音频仅首帧包含头信息,后续帧为纯音频数据。 |
| `def on_close(self) -> None` | 无 | 无 | 当服务已经关闭连接后被回调。 |
## **响应结果**
服务器返回二进制音频数据:
- [同步调用](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#8341058094tc3):对[SpeechSynthesizer类](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#d6bc1f133f871)的`call`方法返回的二进制音频数据进行处理。
- [异步调用](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#cc2a504f344s2)或[流式调用](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#ba023aacfbr84):对[回调接口ResultCallback](https://help.aliyun.com/zh/model-studio/cosyvoice-python-sdk#85d698b9f9g8s)的`on_data`方法的参数bytes类型数据进行处理。
## **错误码**
如遇报错问题,请参见[错误信息](https://help.aliyun.com/zh/model-studio/error-code)进行排查。
若问题仍未解决,请加入[开发者群](https://github.com/aliyun/alibabacloud-bailian-speech-demo)反馈遇到的问题并提供Request ID以便进一步排查问题。
## **音色列表**
当前默认支持的音色如下表所示。若您需要更加个性化的音色,可通过声音复刻功能免费定制专属音色,详情请参见[使用复刻的音色进行语音合成](https://help.aliyun.com/zh/model-studio/cosyvoice-clone-api#b6d3449fb336v)。
进行语音合成时,`model`参数必须与所选音色对应,否则调用将失败。
待合成文本text应使用与所选音色语种一致的语言否则可能出现发音错误或不自然
cosyvoice-v3
cosyvoice-v2
cosyvoice-v1
| **音色** | **音频试听(右键保存音频)** | **voice参数** | **适用场景** | **语言** |
| -------- | ---------------------------- | ------------- | ------------------------------------------------------------ | --------------- |
| 龙婉 | | longwan | 语音助手、导航播报、聊天数字人 | 中文普通话 |
| 龙橙 | | longcheng | 语音助手、导航播报、聊天数字人 | 中文普通话 |
| 龙华 | | longhua | 语音助手、导航播报、聊天数字人 | 中文普通话 |
| 龙小淳 | | longxiaochun | 语音助手、导航播报、聊天数字人 | 中文普通话+英文 |
| 龙小夏 | | longxiaoxia | 语音助手、聊天数字人 | 中文普通话 |
| 龙小诚 | | longxiaocheng | 语音助手、导航播报、聊天数字人 | 中文普通话+英文 |
| 龙小白 | | longxiaobai | 聊天数字人、有声书、语音助手 | 中文普通话 |
| 龙老铁 | | longlaotie | 新闻播报、有声书、语音助手、直播带货、导航播报 | 中文东北口音 |
| 龙书 | | longshu | 有声书、语音助手、导航播报、新闻播报、智能客服 | 中文普通话 |
| 龙硕 | | longshuo | 语音助手、导航播报、新闻播报、客服催收 | 中文普通话 |
| 龙婧 | | longjing | 语音助手、导航播报、新闻播报、客服催收 | 中文普通话 |
| 龙妙 | | longmiao | 客服催收、导航播报、有声书、语音助手 | 中文普通话 |
| 龙悦 | | longyue | 语音助手、诗词朗诵、有声书朗读、导航播报、新闻播报、客服催收 | 中文普通话 |
| 龙媛 | | longyuan | 有声书、语音助手、聊天数字人 | 中文普通话 |
| 龙飞 | | longfei | 会议播报、新闻播报、有声书 | 中文普通话 |
| 龙杰力豆 | | longjielidou | 新闻播报、有声书、聊天助手 | 中文普通话+英文 |
| 龙彤 | | longtong | 有声书、导航播报、聊天数字人 | 中文普通话 |
| 龙祥 | | longxiang | 新闻播报、有声书、导航播报 | 中文普通话 |
| Stella | | loongstella | 语音助手、直播带货、导航播报、客服催收、有声书 | 中文普通话+英文 |
| Bella | | loongbella | 语音助手、客服催收、新闻播报、导航播报 | 中文普通话 |

View File

@@ -0,0 +1,148 @@
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
# 数据路径
DATA_PATH = "D:\\AI_Coding\\data\\心血管疾病.xlsx"
# 缓存数据加载和清洗函数
@st.cache_data
def load_and_preprocess_data():
# 加载数据
df = pd.read_excel(DATA_PATH)
# 特征工程
# 将age转换为年四舍五入
df['age_years'] = round(df['age'] / 365.25, 0)
# 计算BMI
df['bmi'] = df['weight'] / ((df['height'] / 100) ** 2)
# 异常值处理
# 删除舒张压≥收缩压的记录
df = df[df['ap_lo'] < df['ap_hi']]
# 删除血压极端异常值
df = df[(df['ap_hi'] >= 90) & (df['ap_hi'] <= 250)]
df = df[(df['ap_lo'] >= 60) & (df['ap_lo'] <= 150)]
# 类别转换
# 将cholesterol数值转换为描述性字符串
cholesterol_map = {1: '正常', 2: '偏高', 3: '很高'}
df['cholesterol_desc'] = df['cholesterol'].map(cholesterol_map)
# 将gluc数值转换为描述性字符串
gluc_map = {1: '正常', 2: '偏高', 3: '很高'}
df['gluc_desc'] = df['gluc'].map(gluc_map)
# 创建bmi_category
def get_bmi_category(bmi):
if bmi < 18.5:
return '偏瘦'
elif bmi < 24:
return '正常'
elif bmi < 28:
return '超重'
else:
return '肥胖'
df['bmi_category'] = df['bmi'].apply(get_bmi_category)
return df
# 加载数据
df = load_and_preprocess_data()
# Streamlit应用
st.title('CardioAI - 心血管疾病数据可视化')
# 侧边栏筛选器
st.sidebar.header('数据筛选')
# age_years范围滑块
age_min = int(df['age_years'].min())
age_max = int(df['age_years'].max())
age_range = st.sidebar.slider(
'年龄范围',
min_value=age_min,
max_value=age_max,
value=(age_min, age_max)
)
# gender多选框
gender_options = df['gender'].unique()
gender_filter = st.sidebar.multiselect(
'性别 (1=女, 2=男)',
options=gender_options,
default=gender_options
)
# cardio多选框
cardio_options = df['cardio'].unique()
cardio_filter = st.sidebar.multiselect(
'心血管疾病 (0=无, 1=有)',
options=cardio_options,
default=cardio_options
)
# 应用筛选器
filtered_df = df[
(df['age_years'] >= age_range[0]) &
(df['age_years'] <= age_range[1]) &
(df['gender'].isin(gender_filter)) &
(df['cardio'].isin(cardio_filter))
]
# 主页展示
st.header('数据概览')
# 展示筛选后的总记录数
st.metric('筛选后的总记录数', len(filtered_df))
# 计算并展示心血管疾病总风险率
if len(filtered_df) > 0:
cardio_rate = (filtered_df['cardio'].sum() / len(filtered_df)) * 100
st.metric('心血管疾病总风险率', f"{cardio_rate:.2f}%")
# 图表
st.header('数据可视化')
# age_years分布直方图按cardio区分
st.subheader('年龄分布与心血管疾病关系')
fig_age = px.histogram(
filtered_df,
x='age_years',
color='cardio',
barmode='overlay',
nbins=20,
labels={'age_years': '年龄', 'cardio': '心血管疾病', 'count': '人数'},
title='年龄分布 histogram'
)
fig_age.update_layout(
xaxis_title='年龄',
yaxis_title='人数',
legend_title='心血管疾病'
)
st.plotly_chart(fig_age)
# bmi_category对cardio影响的堆叠柱状图
st.subheader('BMI类别与心血管疾病关系')
fig_bmi = px.histogram(
filtered_df,
x='bmi_category',
color='cardio',
barmode='stack',
labels={'bmi_category': 'BMI类别', 'cardio': '心血管疾病', 'count': '人数'},
title='BMI类别对心血管疾病影响的堆叠柱状图'
)
fig_bmi.update_layout(
xaxis_title='BMI类别',
yaxis_title='人数',
legend_title='心血管疾病'
)
st.plotly_chart(fig_bmi)
# 展示数据样本
st.header('数据样本')
st.dataframe(filtered_df.head())

54
module2_predictor/app.py Normal file
View File

@@ -0,0 +1,54 @@
from flask import Flask, request, jsonify, render_template
import pandas as pd
import joblib
app = Flask(__name__)
# 加载模型
model_path = "cardio_predictor_model.pkl"
model = joblib.load(model_path)
@app.route('/')
def home():
return render_template('index.html')
@app.route('/predict_cardio', methods=['POST'])
def predict_cardio():
try:
# 接收JSON请求数据
data = request.json
# 验证输入特征数量
required_features = ['age', 'gender', 'height', 'weight', 'ap_hi', 'ap_lo',
'cholesterol', 'gluc', 'smoke', 'alco', 'active']
if not all(feature in data for feature in required_features):
return jsonify({'error': '缺少必要的特征值'}), 400
# 转换为DataFrame
input_data = pd.DataFrame([data])
# 执行与训练时相同的特征工程
input_data['age_years'] = round(input_data['age'] / 365.25, 0)
input_data['bmi'] = input_data['weight'] / ((input_data['height'] / 100) ** 2)
# 删除原始age字段
input_data = input_data.drop('age', axis=1)
# 预测
probability = model.predict_proba(input_data)[0][1]
prediction = int(model.predict(input_data)[0])
# 返回结果
response = {
'probability': float(probability),
'prediction': prediction
}
return jsonify(response)
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)

View File

@@ -0,0 +1,257 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CardioAI - 心血管疾病预测</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
input[type="number"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
button {
background-color: #4CAF50;
color: white;
padding: 15px 30px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
width: 100%;
margin-top: 20px;
}
button:hover {
background-color: #45a049;
}
.result {
margin-top: 30px;
padding: 20px;
background-color: #f9f9f9;
border-radius: 5px;
border-left: 4px solid #4CAF50;
}
.error {
margin-top: 30px;
padding: 20px;
background-color: #ffebee;
border-radius: 5px;
border-left: 4px solid #f44336;
color: #c62828;
}
.form-row {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.form-row .form-group {
flex: 1;
}
@media (max-width: 768px) {
.form-row {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<h1>CardioAI - 心血管疾病预测</h1>
<form id="predictionForm">
<div class="form-row">
<div class="form-group">
<label for="age">年龄 (天)</label>
<input type="number" id="age" name="age" min="10000" max="25000" required>
</div>
<div class="form-group">
<label for="gender">性别</label>
<select id="gender" name="gender" required>
<option value="1"></option>
<option value="2"></option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="height">身高 (cm)</label>
<input type="number" id="height" name="height" min="100" max="250" required>
</div>
<div class="form-group">
<label for="weight">体重 (kg)</label>
<input type="number" id="weight" name="weight" min="30" max="200" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="ap_hi">收缩压</label>
<input type="number" id="ap_hi" name="ap_hi" min="90" max="250" required>
</div>
<div class="form-group">
<label for="ap_lo">舒张压</label>
<input type="number" id="ap_lo" name="ap_lo" min="60" max="150" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="cholesterol">胆固醇</label>
<select id="cholesterol" name="cholesterol" required>
<option value="1">正常</option>
<option value="2">偏高</option>
<option value="3">很高</option>
</select>
</div>
<div class="form-group">
<label for="gluc">血糖</label>
<select id="gluc" name="gluc" required>
<option value="1">正常</option>
<option value="2">偏高</option>
<option value="3">很高</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="smoke">吸烟</label>
<select id="smoke" name="smoke" required>
<option value="0"></option>
<option value="1"></option>
</select>
</div>
<div class="form-group">
<label for="alco">饮酒</label>
<select id="alco" name="alco" required>
<option value="0"></option>
<option value="1"></option>
</select>
</div>
<div class="form-group">
<label for="active">运动</label>
<select id="active" name="active" required>
<option value="0"></option>
<option value="1"></option>
</select>
</div>
</div>
<button type="submit">预测心血管疾病风险</button>
</form>
<div id="result" class="result" style="display: none;">
<h2>预测结果</h2>
<p><strong>预测结果:</strong> <span id="predictionValue"></span></p>
<p><strong>患病概率:</strong> <span id="probabilityValue"></span>%</p>
<p><strong>风险评估:</strong> <span id="riskAssessment"></span></p>
</div>
<div id="error" class="error" style="display: none;">
<h2>错误</h2>
<p id="errorMessage"></p>
</div>
</div>
<script>
document.getElementById('predictionForm').addEventListener('submit', function(e) {
e.preventDefault();
// 隐藏之前的结果和错误
document.getElementById('result').style.display = 'none';
document.getElementById('error').style.display = 'none';
// 收集表单数据
const formData = {
age: parseInt(document.getElementById('age').value),
gender: parseInt(document.getElementById('gender').value),
height: parseInt(document.getElementById('height').value),
weight: parseInt(document.getElementById('weight').value),
ap_hi: parseInt(document.getElementById('ap_hi').value),
ap_lo: parseInt(document.getElementById('ap_lo').value),
cholesterol: parseInt(document.getElementById('cholesterol').value),
gluc: parseInt(document.getElementById('gluc').value),
smoke: parseInt(document.getElementById('smoke').value),
alco: parseInt(document.getElementById('alco').value),
active: parseInt(document.getElementById('active').value)
};
// 发送POST请求
fetch('/predict_cardio', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.error) {
// 显示错误
document.getElementById('errorMessage').textContent = data.error;
document.getElementById('error').style.display = 'block';
} else {
// 显示结果
document.getElementById('predictionValue').textContent = data.prediction === 1 ? '有心血管疾病风险' : '无心血管疾病风险';
document.getElementById('probabilityValue').textContent = (data.probability * 100).toFixed(2);
// 风险评估
let riskAssessment = '';
if (data.probability < 0.3) {
riskAssessment = '低风险';
} else if (data.probability < 0.6) {
riskAssessment = '中等风险';
} else {
riskAssessment = '高风险';
}
document.getElementById('riskAssessment').textContent = riskAssessment;
document.getElementById('result').style.display = 'block';
}
})
.catch(error => {
document.getElementById('errorMessage').textContent = '预测过程中发生错误,请稍后重试';
document.getElementById('error').style.display = 'block';
console.error('Error:', error);
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,71 @@
import pandas as pd
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.model_selection import train_test_split
from xgboost import XGBClassifier
import joblib
# 数据路径
DATA_PATH = "D:\\AI_Coding\\data\\心血管疾病.xlsx"
# 加载和预处理数据
def load_and_preprocess_data():
# 加载数据
df = pd.read_excel(DATA_PATH)
# 特征工程
# 将age转换为年四舍五入
df['age_years'] = round(df['age'] / 365.25, 0)
# 计算BMI
df['bmi'] = df['weight'] / ((df['height'] / 100) ** 2)
# 异常值处理
# 删除舒张压≥收缩压的记录
df = df[df['ap_lo'] < df['ap_hi']]
# 删除血压极端异常值
df = df[(df['ap_hi'] >= 90) & (df['ap_hi'] <= 250)]
df = df[(df['ap_lo'] >= 60) & (df['ap_lo'] <= 150)]
# 删除id和原始age字段
df = df.drop(['id', 'age'], axis=1)
return df
# 加载数据
df = load_and_preprocess_data()
# 定义特征和目标变量
X = df.drop('cardio', axis=1)
y = df['cardio']
# 定义特征类型
continuous_features = ['age_years', 'height', 'weight', 'ap_hi', 'ap_lo', 'bmi']
categorical_features = ['gender', 'cholesterol', 'gluc', 'smoke', 'alco', 'active']
# 创建预处理器
preprocessor = ColumnTransformer(
transformers=[
('num', StandardScaler(), continuous_features),
('cat', OneHotEncoder(), categorical_features)
]
)
# 创建完整的Pipeline
pipeline = Pipeline([
('preprocessor', preprocessor),
('classifier', XGBClassifier(random_state=42))
])
# 训练模型
pipeline.fit(X, y)
# 保存模型
model_path = "module2_predictor\\cardio_predictor_model.pkl"
joblib.dump(pipeline, model_path)
print(f"模型已保存到: {model_path}")
print("训练完成!")

View File

@@ -0,0 +1,237 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CardioAI - 心血管健康语音助手</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 40px;
}
.input-section {
margin-bottom: 30px;
}
label {
display: block;
margin-bottom: 10px;
font-weight: bold;
color: #555;
}
textarea {
width: 100%;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
resize: vertical;
min-height: 100px;
}
button {
background-color: #4CAF50;
color: white;
padding: 15px 30px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
width: 100%;
margin-top: 15px;
}
button:hover {
background-color: #45a049;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.result {
margin-top: 40px;
padding: 25px;
background-color: #f9f9f9;
border-radius: 5px;
border-left: 4px solid #4CAF50;
}
.result h2 {
color: #333;
margin-top: 0;
margin-bottom: 20px;
}
.text-answer {
background-color: white;
padding: 20px;
border-radius: 5px;
border: 1px solid #ddd;
margin-bottom: 20px;
line-height: 1.6;
}
.audio-section {
margin-top: 20px;
}
audio {
width: 100%;
margin-top: 10px;
}
.error {
margin-top: 30px;
padding: 20px;
background-color: #ffebee;
border-radius: 5px;
border-left: 4px solid #f44336;
color: #c62828;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.button-text {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
h1 {
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>CardioAI - 心血管健康语音助手</h1>
<div class="input-section">
<label for="question">请输入您的心血管健康问题</label>
<textarea id="question" placeholder="例如:如何预防心血管疾病?高血压患者的饮食建议有哪些?"></textarea>
<button id="submitBtn" onclick="askQuestion()">
<span class="button-text">
<span>提交问题</span>
<span id="loading" class="loading" style="display: none;"></span>
</span>
</button>
</div>
<div id="result" class="result" style="display: none;">
<h2>回答结果</h2>
<div class="text-answer" id="textAnswer"></div>
<div class="audio-section">
<label>语音回答</label>
<audio id="audioPlayer" controls autoplay>
您的浏览器不支持音频播放。
</audio>
</div>
</div>
<div id="error" class="error" style="display: none;">
<h2>错误</h2>
<p id="errorMessage"></p>
</div>
</div>
<script>
function askQuestion() {
const question = document.getElementById('question').value.trim();
const submitBtn = document.getElementById('submitBtn');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
const error = document.getElementById('error');
const textAnswer = document.getElementById('textAnswer');
const audioPlayer = document.getElementById('audioPlayer');
if (!question) {
showError('请输入您的问题');
return;
}
// 显示加载状态
submitBtn.disabled = true;
loading.style.display = 'inline-block';
result.style.display = 'none';
error.style.display = 'none';
// 发送POST请求
fetch('/ask_cardio', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ question: question })
})
.then(response => response.json())
.then(data => {
if (data.error) {
showError(data.error);
} else {
// 显示文字回答
textAnswer.textContent = data.text_answer;
// 构造音频URL并设置到音频播放器
const audioUrl = `data:audio/mp3;base64,${data.audio_base64}`;
audioPlayer.src = audioUrl;
// 显示结果区域
result.style.display = 'block';
// 自动播放音频
audioPlayer.play().catch(e => {
console.error('音频播放失败:', e);
});
}
})
.catch(error => {
showError('请求过程中发生错误,请稍后重试');
console.error('Error:', error);
})
.finally(() => {
// 隐藏加载状态
submitBtn.disabled = false;
loading.style.display = 'none';
});
}
function showError(message) {
const error = document.getElementById('error');
const errorMessage = document.getElementById('errorMessage');
const result = document.getElementById('result');
errorMessage.textContent = message;
error.style.display = 'block';
result.style.display = 'none';
}
// 回车键提交
document.getElementById('question').addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.ctrlKey) {
askQuestion();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,84 @@
from flask import Flask, request, jsonify, render_template
import os
import base64
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from dashscope.audio.tts_v2 import SpeechSynthesizer, AudioFormat
import dashscope
# 加载环境变量
ENV_PATH = "D:\\AI_Coding\\aicodes\\.env"
load_dotenv(ENV_PATH)
# 初始化DashScope API Key
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
app = Flask(__name__)
# 初始化DeepSeek LLM
def get_llm():
llm = ChatOpenAI(
base_url=os.getenv("base_url1"),
api_key=os.getenv("DEEPSEEK_API_KEY1"),
model="deepseek-chat",
temperature=0,
)
return llm
# 系统提示词
system_prompt = "你是一位专业的心血管健康顾问,专注于提供科学、准确的心血管疾病预防和健康管理建议。请基于医学知识回答用户的问题,保持专业、友好的态度。"
@app.route('/')
def home():
return render_template('voice_index.html')
@app.route('/ask_cardio', methods=['POST'])
def ask_cardio():
try:
# 接收用户问题
data = request.json
user_question = data.get('question', '')
if not user_question:
return jsonify({'error': '请输入问题'}), 400
# 初始化LLM
llm = get_llm()
# 构建完整的prompt
full_prompt = f"{system_prompt}\n\n用户问题:{user_question}"
# 获取LLM回答
response = llm.invoke(full_prompt)
text_answer = response.content
# 使用CosyVoice合成语音
model = "cosyvoice-v2"
voice = "longxiaochun_v2"
# 初始化SpeechSynthesizer
synthesizer = SpeechSynthesizer(
model=model,
voice=voice,
format=AudioFormat.MP3_22050HZ_MONO_256KBPS
)
# 同步调用合成语音
audio_data = synthesizer.call(text_answer)
# 将音频数据转换为Base64编码
audio_base64 = base64.b64encode(audio_data).decode('utf-8')
# 返回结果
response_data = {
'text_answer': text_answer,
'audio_base64': audio_base64
}
return jsonify(response_data)
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5001)

20
requirements.txt Normal file
View File

@@ -0,0 +1,20 @@
# Conda 环境创建与激活指令
# 1. 创建名为 cardioenv 的虚拟环境
# conda create -n cardioenv python=3.10
# 2. 激活虚拟环境
# conda activate cardioenv
# 项目依赖
pandas
openpyxl
numpy
scikit-learn
xgboost
joblib
streamlit
plotly
Flask
python-dotenv
langchain-openai
dashscope
requests