first commit
This commit is contained in:
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dir \"E:\\\\project_ai\\\\claude_project1\\\\aicodes\" /b)",
|
||||
"Bash(D:\\\\software\\\\anaconda\\\\envs\\\\cardioenv\\\\python.exe:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
12
.idea/claude_project1.iml
generated
Normal file
12
.idea/claude_project1.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="D:\software\anaconda\envs\cardioenv" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="D:\software\anaconda\envs\cardioenv" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="D:\software\anaconda\envs\cardioenv" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/claude_project1.iml" filepath="$PROJECT_DIR$/.idea/claude_project1.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
28
aicodes/.env
Normal file
28
aicodes/.env
Normal file
@@ -0,0 +1,28 @@
|
||||
# ============================================
|
||||
# CardioAI - 心血管疾病智能辅助系统
|
||||
# 环境配置文件
|
||||
# ============================================
|
||||
|
||||
# 数据文件路径
|
||||
DATA_PATH=E:\project_ai\claude_project1\data\心血管疾病.xlsx
|
||||
|
||||
# Module 2: XGBoost 预测服务配置
|
||||
FLASK_PORT=5000
|
||||
FLASK_HOST=127.0.0.1
|
||||
|
||||
# Module 3: AI 语音助手配置
|
||||
# DeepSeek API 配置
|
||||
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
||||
DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||
|
||||
# 阿里云 DashScope 配置 (CosyVoice 语音合成)
|
||||
DASHSCOPE_API_KEY=your_dashscope_api_key_here
|
||||
|
||||
# 语言模型配置
|
||||
MODEL_NAME=deepseek-chat
|
||||
TEMPERATURE=0.7
|
||||
MAX_TOKENS=2000
|
||||
|
||||
# 语音合成配置
|
||||
VOICE_MODEL=cosyvoice-v2
|
||||
VOICE=longxiaochun_v2
|
||||
47
aicodes/llm_streaming.py
Normal file
47
aicodes/llm_streaming.py
Normal 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}")
|
||||
494
aicodes/module1_dashboard/cardio_dashboard.py
Normal file
494
aicodes/module1_dashboard/cardio_dashboard.py
Normal file
@@ -0,0 +1,494 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CardioAI - Module 1: 数据可视化仪表板
|
||||
心血管疾病数据清洗、特征工程与交互式可视化
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import plotly.express as px
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# ============================================
|
||||
# 配置与常量
|
||||
# ============================================
|
||||
CODE_ROOT = Path(r"E:\project_ai\claude_project1\aicodes")
|
||||
DATA_PATH = Path(r"E:\project_ai\claude_project1\data\心血管疾病.xlsx")
|
||||
|
||||
# 类别映射字典
|
||||
CHOLESTEROL_MAP = {
|
||||
1: "正常",
|
||||
2: "高于正常",
|
||||
3: "远高于正常"
|
||||
}
|
||||
|
||||
GLUC_MAP = {
|
||||
1: "正常",
|
||||
2: "高于正常",
|
||||
3: "远高于正常"
|
||||
}
|
||||
|
||||
GENDER_MAP = {
|
||||
1: "女性",
|
||||
2: "男性"
|
||||
}
|
||||
|
||||
CARDIO_MAP = {
|
||||
0: "无疾病",
|
||||
1: "有疾病"
|
||||
}
|
||||
|
||||
|
||||
# ============================================
|
||||
# 数据加载与清洗函数
|
||||
# ============================================
|
||||
@st.cache_data
|
||||
def load_data():
|
||||
"""
|
||||
加载心血管疾病数据
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: 原始数据
|
||||
"""
|
||||
try:
|
||||
df = pd.read_excel(DATA_PATH, engine='openpyxl')
|
||||
return df
|
||||
except Exception as e:
|
||||
st.error(f"数据加载失败: {str(e)}")
|
||||
return pd.DataFrame()
|
||||
|
||||
|
||||
@st.cache_data
|
||||
def clean_and_process_data(df):
|
||||
"""
|
||||
数据清洗与特征工程
|
||||
|
||||
处理步骤:
|
||||
1. 将age(天)转换为年
|
||||
2. 计算BMI
|
||||
3. 删除血压异常值
|
||||
4. 类别转换
|
||||
5. 创建BMI分类
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): 原始数据
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: 处理后的数据
|
||||
"""
|
||||
if df.empty:
|
||||
return df
|
||||
|
||||
df_clean = df.copy()
|
||||
|
||||
# 1. 年龄转换: 天 -> 年 (四舍五入)
|
||||
df_clean['age_years'] = (df_clean['age'] / 365.25).round().astype(int)
|
||||
|
||||
# 2. 计算BMI: weight / (height/100)^2
|
||||
df_clean['bmi'] = df_clean['weight'] / ((df_clean['height'] / 100) ** 2)
|
||||
|
||||
# 3. 异常值处理
|
||||
|
||||
# 删除舒张压 >= 收缩压的记录 (生理上不可能)
|
||||
df_clean = df_clean[df_clean['ap_lo'] < df_clean['ap_hi']]
|
||||
|
||||
# 删除血压极端异常值 (收缩压范围: [90, 250], 舒张压范围: [60, 150])
|
||||
df_clean = df_clean[
|
||||
(df_clean['ap_hi'] >= 90) & (df_clean['ap_hi'] <= 250) &
|
||||
(df_clean['ap_lo'] >= 60) & (df_clean['ap_lo'] <= 150)
|
||||
]
|
||||
|
||||
# 4. 类别转换
|
||||
df_clean['cholesterol_desc'] = df_clean['cholesterol'].map(CHOLESTEROL_MAP)
|
||||
df_clean['gluc_desc'] = df_clean['gluc'].map(GLUC_MAP)
|
||||
df_clean['gender_desc'] = df_clean['gender'].map(GENDER_MAP)
|
||||
df_clean['cardio_desc'] = df_clean['cardio'].map(CARDIO_MAP)
|
||||
|
||||
# 5. 创建BMI分类
|
||||
def categorize_bmi(bmi):
|
||||
if bmi < 18.5:
|
||||
return "体重过轻"
|
||||
elif bmi < 24:
|
||||
return "正常体重"
|
||||
elif bmi < 28:
|
||||
return "超重"
|
||||
else:
|
||||
return "肥胖"
|
||||
|
||||
df_clean['bmi_category'] = df_clean['bmi'].apply(categorize_bmi)
|
||||
|
||||
return df_clean
|
||||
|
||||
|
||||
# ============================================
|
||||
# 筛选函数
|
||||
# ============================================
|
||||
def apply_filters(df, age_range, gender_filter, cardio_filter):
|
||||
"""
|
||||
根据用户选择的筛选条件过滤数据
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): 处理后的数据
|
||||
age_range (tuple): 年龄范围 (min, max)
|
||||
gender_filter (list): 性别筛选列表
|
||||
cardio_filter (list): 心血管疾病状态筛选列表
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: 筛选后的数据
|
||||
"""
|
||||
if df.empty:
|
||||
return df
|
||||
|
||||
df_filtered = df.copy()
|
||||
|
||||
# 年龄范围筛选
|
||||
df_filtered = df_filtered[
|
||||
df_filtered['age_years'].between(age_range[0], age_range[1])
|
||||
]
|
||||
|
||||
# 性别筛选
|
||||
if gender_filter:
|
||||
df_filtered = df_filtered[df_filtered['gender_desc'].isin(gender_filter)]
|
||||
|
||||
# 心血管疾病状态筛选
|
||||
if cardio_filter:
|
||||
df_filtered = df_filtered[df_filtered['cardio_desc'].isin(cardio_filter)]
|
||||
|
||||
return df_filtered
|
||||
|
||||
|
||||
# ============================================
|
||||
# 统计指标函数
|
||||
# ============================================
|
||||
def calculate_statistics(df):
|
||||
"""
|
||||
计算统计指标
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): 筛选后的数据
|
||||
|
||||
Returns:
|
||||
dict: 包含总记录数和风险率的字典
|
||||
"""
|
||||
if df.empty:
|
||||
return {"total_records": 0, "risk_rate": 0.0}
|
||||
|
||||
total_records = len(df)
|
||||
disease_count = df['cardio'].sum()
|
||||
risk_rate = (disease_count / total_records * 100) if total_records > 0 else 0
|
||||
|
||||
return {
|
||||
"total_records": total_records,
|
||||
"risk_rate": risk_rate
|
||||
}
|
||||
|
||||
|
||||
# ============================================
|
||||
# 可视化图表函数
|
||||
# ============================================
|
||||
def plot_age_distribution(df):
|
||||
"""
|
||||
绘制年龄分布直方图 (按cardio区分)
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): 数据
|
||||
|
||||
Returns:
|
||||
plotly.graph_objects.Figure: 年龄分布图
|
||||
"""
|
||||
if df.empty:
|
||||
return None
|
||||
|
||||
fig = px.histogram(
|
||||
df,
|
||||
x='age_years',
|
||||
color='cardio_desc',
|
||||
nbins=30,
|
||||
title='年龄分布 (按心血管疾病状态)',
|
||||
labels={'age_years': '年龄 (岁)', 'count': '人数'},
|
||||
color_discrete_map={'无疾病': '#2ecc71', '有疾病': '#e74c3c'},
|
||||
barmode='overlay'
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
xaxis_title="年龄 (岁)",
|
||||
yaxis_title="人数",
|
||||
legend_title="疾病状态",
|
||||
hovermode='x unified'
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def plot_bmi_vs_cardio(df):
|
||||
"""
|
||||
绘制BMI分类对心血管疾病影响的堆叠柱状图
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): 数据
|
||||
|
||||
Returns:
|
||||
plotly.graph_objects.Figure: BMI与疾病关系图
|
||||
"""
|
||||
if df.empty:
|
||||
return None
|
||||
|
||||
# 计算每个BMI分类的疾病比例
|
||||
bmi_cardio = df.groupby(['bmi_category', 'cardio_desc']).size().reset_index(name='count')
|
||||
|
||||
# 确保BMI分类顺序正确
|
||||
bmi_order = ["体重过轻", "正常体重", "超重", "肥胖"]
|
||||
bmi_cardio['bmi_category'] = pd.Categorical(
|
||||
bmi_cardio['bmi_category'],
|
||||
categories=bmi_order,
|
||||
ordered=True
|
||||
)
|
||||
bmi_cardio = bmi_cardio.sort_values('bmi_category')
|
||||
|
||||
fig = px.bar(
|
||||
bmi_cardio,
|
||||
x='bmi_category',
|
||||
y='count',
|
||||
color='cardio_desc',
|
||||
title='BMI分类与心血管疾病关系',
|
||||
labels={'bmi_category': 'BMI分类', 'count': '人数'},
|
||||
color_discrete_map={'无疾病': '#2ecc71', '有疾病': '#e74c3c'},
|
||||
category_orders={'bmi_category': bmi_order}
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
xaxis_title="BMI分类",
|
||||
yaxis_title="人数",
|
||||
legend_title="疾病状态",
|
||||
barmode='stack'
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
# ============================================
|
||||
# 主应用程序
|
||||
# ============================================
|
||||
def main():
|
||||
"""Streamlit 主应用程序"""
|
||||
|
||||
# 页面配置
|
||||
st.set_page_config(
|
||||
page_title="CardioAI - 心血管疾病数据分析仪表板",
|
||||
page_icon="❤️",
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded"
|
||||
)
|
||||
|
||||
# 标题与描述
|
||||
st.title("❤️ CardioAI - 心血管疾病智能辅助系统")
|
||||
st.markdown("### Module 1: 数据可视化仪表板")
|
||||
st.markdown("---")
|
||||
|
||||
# 数据加载
|
||||
with st.spinner("正在加载数据..."):
|
||||
raw_data = load_data()
|
||||
|
||||
if raw_data.empty:
|
||||
st.error("无法加载数据,请检查数据路径是否正确。")
|
||||
st.stop()
|
||||
|
||||
# 数据清洗与特征工程
|
||||
processed_data = clean_and_process_data(raw_data)
|
||||
|
||||
if processed_data.empty:
|
||||
st.warning("数据清洗后无有效记录,请检查数据质量。")
|
||||
st.stop()
|
||||
|
||||
# ============================================
|
||||
# 侧边栏 - 筛选器
|
||||
# ============================================
|
||||
st.sidebar.header("🔍 数据筛选")
|
||||
|
||||
# 年龄范围滑块
|
||||
age_min = int(processed_data['age_years'].min())
|
||||
age_max = int(processed_data['age_years'].max())
|
||||
age_range = st.sidebar.slider(
|
||||
"年龄范围 (岁)",
|
||||
min_value=age_min,
|
||||
max_value=age_max,
|
||||
value=(age_min, age_max),
|
||||
step=1
|
||||
)
|
||||
|
||||
# 性别多选框
|
||||
gender_options = processed_data['gender_desc'].unique().tolist()
|
||||
gender_filter = st.sidebar.multiselect(
|
||||
"性别",
|
||||
options=gender_options,
|
||||
default=gender_options
|
||||
)
|
||||
|
||||
# 心血管疾病状态多选框
|
||||
cardio_options = processed_data['cardio_desc'].unique().tolist()
|
||||
cardio_filter = st.sidebar.multiselect(
|
||||
"心血管疾病状态",
|
||||
options=cardio_options,
|
||||
default=cardio_options
|
||||
)
|
||||
|
||||
st.sidebar.markdown("---")
|
||||
st.sidebar.markdown("### 📊 数据概览")
|
||||
st.sidebar.markdown(f"- 原始记录数: **{len(raw_data):,}**")
|
||||
st.sidebar.markdown(f"- 清洗后记录数: **{len(processed_data):,}**")
|
||||
st.sidebar.markdown(f"- 数据清洗率: **{(1 - len(processed_data)/len(raw_data))*100:.2f}%**")
|
||||
|
||||
# ============================================
|
||||
# 应用筛选条件
|
||||
# ============================================
|
||||
filtered_data = apply_filters(
|
||||
processed_data,
|
||||
age_range,
|
||||
gender_filter,
|
||||
cardio_filter
|
||||
)
|
||||
|
||||
# ============================================
|
||||
# 主页 - 统计指标
|
||||
# ============================================
|
||||
st.subheader("📈 筛选结果统计")
|
||||
|
||||
stats = calculate_statistics(filtered_data)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.metric(
|
||||
label="筛选后总记录数",
|
||||
value=f"{stats['total_records']:,}",
|
||||
delta=None
|
||||
)
|
||||
|
||||
with col2:
|
||||
# 根据风险率设置颜色
|
||||
risk_rate = stats['risk_rate']
|
||||
risk_color = "normal" if risk_rate < 50 else "inverse"
|
||||
|
||||
st.metric(
|
||||
label="心血管疾病风险率",
|
||||
value=f"{risk_rate:.2f}%",
|
||||
delta=None,
|
||||
delta_color=risk_color
|
||||
)
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# ============================================
|
||||
# 图表展示
|
||||
# ============================================
|
||||
st.subheader("📊 数据可视化")
|
||||
|
||||
# 第一行: 年龄分布图
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
age_fig = plot_age_distribution(filtered_data)
|
||||
if age_fig:
|
||||
st.plotly_chart(age_fig, use_container_width=True)
|
||||
|
||||
with col2:
|
||||
# 添加性别分布饼图
|
||||
gender_dist = filtered_data['gender_desc'].value_counts().reset_index()
|
||||
gender_dist.columns = ['性别', '人数']
|
||||
gender_pie = px.pie(
|
||||
gender_dist,
|
||||
values='人数',
|
||||
names='性别',
|
||||
title='性别分布',
|
||||
color_discrete_sequence=['#3498db', '#e91e63']
|
||||
)
|
||||
st.plotly_chart(gender_pie, use_container_width=True)
|
||||
|
||||
# 第二行: BMI与疾病关系图
|
||||
st.markdown("### BMI分类与心血管疾病风险")
|
||||
bmi_fig = plot_bmi_vs_cardio(filtered_data)
|
||||
if bmi_fig:
|
||||
st.plotly_chart(bmi_fig, use_container_width=True)
|
||||
|
||||
# 第三行: 其他因素分析
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
# 胆固醇分布
|
||||
chol_cardio = filtered_data.groupby(['cholesterol_desc', 'cardio_desc']).size().reset_index(name='count')
|
||||
chol_fig = px.bar(
|
||||
chol_cardio,
|
||||
x='cholesterol_desc',
|
||||
y='count',
|
||||
color='cardio_desc',
|
||||
title='胆固醇水平与心血管疾病',
|
||||
labels={'cholesterol_desc': '胆固醇水平', 'count': '人数'},
|
||||
color_discrete_map={'无疾病': '#2ecc71', '有疾病': '#e74c3c'}
|
||||
)
|
||||
st.plotly_chart(chol_fig, use_container_width=True)
|
||||
|
||||
with col2:
|
||||
# 血糖分布
|
||||
gluc_cardio = filtered_data.groupby(['gluc_desc', 'cardio_desc']).size().reset_index(name='count')
|
||||
gluc_order = ["正常", "高于正常", "远高于正常"]
|
||||
gluc_cardio['gluc_desc'] = pd.Categorical(
|
||||
gluc_cardio['gluc_desc'],
|
||||
categories=gluc_order,
|
||||
ordered=True
|
||||
)
|
||||
gluc_cardio = gluc_cardio.sort_values('gluc_desc')
|
||||
|
||||
gluc_fig = px.bar(
|
||||
gluc_cardio,
|
||||
x='gluc_desc',
|
||||
y='count',
|
||||
color='cardio_desc',
|
||||
title='血糖水平与心血管疾病',
|
||||
labels={'gluc_desc': '血糖水平', 'count': '人数'},
|
||||
category_orders={'gluc_desc': gluc_order},
|
||||
color_discrete_map={'无疾病': '#2ecc71', '有疾病': '#e74c3c'}
|
||||
)
|
||||
st.plotly_chart(gluc_fig, use_container_width=True)
|
||||
|
||||
# ============================================
|
||||
# 数据表格预览
|
||||
# ============================================
|
||||
st.markdown("---")
|
||||
st.subheader("📋 数据预览")
|
||||
|
||||
display_columns = [
|
||||
'age_years', 'gender_desc', 'height', 'weight', 'bmi', 'bmi_category',
|
||||
'ap_hi', 'ap_lo', 'cholesterol_desc', 'gluc_desc',
|
||||
'smoke', 'alco', 'active', 'cardio_desc'
|
||||
]
|
||||
|
||||
column_rename = {
|
||||
'age_years': '年龄(岁)',
|
||||
'gender_desc': '性别',
|
||||
'height': '身高(cm)',
|
||||
'weight': '体重(kg)',
|
||||
'bmi': 'BMI',
|
||||
'bmi_category': 'BMI分类',
|
||||
'ap_hi': '收缩压',
|
||||
'ap_lo': '舒张压',
|
||||
'cholesterol_desc': '胆固醇',
|
||||
'gluc_desc': '血糖',
|
||||
'smoke': '吸烟',
|
||||
'alco': '饮酒',
|
||||
'active': '运动',
|
||||
'cardio_desc': '心血管疾病'
|
||||
}
|
||||
|
||||
if not filtered_data.empty:
|
||||
display_df = filtered_data[display_columns].copy()
|
||||
display_df = display_df.rename(columns=column_rename)
|
||||
st.dataframe(display_df.head(100), use_container_width=True, height=400)
|
||||
|
||||
|
||||
# ============================================
|
||||
# 程序入口
|
||||
# ============================================
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
186
aicodes/module2_predictor/app.py
Normal file
186
aicodes/module2_predictor/app.py
Normal file
@@ -0,0 +1,186 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CardioAI - Module 2: Flask API 部署
|
||||
心血管疾病预测服务后端
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import joblib
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# ============================================
|
||||
# 配置与初始化
|
||||
# ============================================
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
CODE_ROOT = Path(r"E:\project_ai\claude_project1\aicodes")
|
||||
MODEL_PATH = CODE_ROOT / "module2_predictor" / "cardio_predictor_model.pkl"
|
||||
TEMPLATE_DIR = CODE_ROOT / "module2_predictor" / "templates"
|
||||
|
||||
# 创建 Flask 应用
|
||||
app = Flask(__name__,
|
||||
template_folder=str(TEMPLATE_DIR),
|
||||
static_folder=str(TEMPLATE_DIR.parent / 'static'))
|
||||
|
||||
# 加载模型
|
||||
print("正在加载模型...")
|
||||
try:
|
||||
model_pipeline = joblib.load(MODEL_PATH)
|
||||
print(f"模型加载成功: {MODEL_PATH}")
|
||||
except Exception as e:
|
||||
print(f"模型加载失败: {e}")
|
||||
model_pipeline = None
|
||||
|
||||
|
||||
# ============================================
|
||||
# 辅助函数
|
||||
# ============================================
|
||||
def preprocess_input(data):
|
||||
"""
|
||||
预处理输入数据
|
||||
|
||||
Args:
|
||||
data (dict): 包含11个特征的字典
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: 格式化后的输入数据
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
# 构建输入DataFrame
|
||||
input_data = {
|
||||
'age_years': int(data['age_years']),
|
||||
'gender': int(data['gender']),
|
||||
'height': float(data['height']),
|
||||
'weight': float(data['weight']),
|
||||
'ap_hi': int(data['ap_hi']),
|
||||
'ap_lo': int(data['ap_lo']),
|
||||
'cholesterol': int(data['cholesterol']),
|
||||
'gluc': int(data['gluc']),
|
||||
'smoke': int(data['smoke']),
|
||||
'alco': int(data['alco']),
|
||||
'active': int(data['active'])
|
||||
}
|
||||
|
||||
# 计算BMI
|
||||
input_data['bmi'] = input_data['weight'] / ((input_data['height'] / 100) ** 2)
|
||||
|
||||
df = pd.DataFrame([input_data])
|
||||
|
||||
# 按照模型训练时的特征顺序排列
|
||||
feature_order = ['age_years', 'height', 'weight', 'bmi',
|
||||
'ap_hi', 'ap_lo', 'gender', 'cholesterol',
|
||||
'gluc', 'smoke', 'alco', 'active']
|
||||
|
||||
return df[feature_order]
|
||||
|
||||
|
||||
# ============================================
|
||||
# 路由定义
|
||||
# ============================================
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""主页 - 渲染预测表单"""
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
@app.route('/predict_cardio', methods=['POST'])
|
||||
def predict_cardio():
|
||||
"""
|
||||
心血管疾病预测API接口
|
||||
|
||||
接收11个原始特征值的JSON POST请求
|
||||
返回预测结果和概率
|
||||
"""
|
||||
if model_pipeline is None:
|
||||
return jsonify({
|
||||
'error': '模型未加载,请先运行训练脚本'
|
||||
}), 500
|
||||
|
||||
try:
|
||||
# 获取JSON数据
|
||||
data = request.get_json()
|
||||
|
||||
# 验证必填字段
|
||||
required_fields = [
|
||||
'age_years', 'gender', 'height', 'weight',
|
||||
'ap_hi', 'ap_lo', 'cholesterol', 'gluc',
|
||||
'smoke', 'alco', 'active'
|
||||
]
|
||||
|
||||
missing_fields = [field for field in required_fields if field not in data]
|
||||
if missing_fields:
|
||||
return jsonify({
|
||||
'error': f'缺少必填字段: {", ".join(missing_fields)}'
|
||||
}), 400
|
||||
|
||||
# 预处理输入数据
|
||||
input_df = preprocess_input(data)
|
||||
|
||||
# 模型预测
|
||||
prediction = model_pipeline.predict(input_df)[0]
|
||||
probability = model_pipeline.predict_proba(input_df)[0, 1]
|
||||
|
||||
# 返回结果
|
||||
return jsonify({
|
||||
'prediction': int(prediction),
|
||||
'probability': float(probability),
|
||||
'risk_level': get_risk_level(probability),
|
||||
'message': '预测成功'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'error': f'预测失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
def get_risk_level(probability):
|
||||
"""
|
||||
根据概率返回风险等级描述
|
||||
|
||||
Args:
|
||||
probability (float): 疾病概率
|
||||
|
||||
Returns:
|
||||
str: 风险等级描述
|
||||
"""
|
||||
if probability < 0.3:
|
||||
return "低风险"
|
||||
elif probability < 0.5:
|
||||
return "中等风险"
|
||||
elif probability < 0.7:
|
||||
return "高风险"
|
||||
else:
|
||||
return "极高风险"
|
||||
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""健康检查接口"""
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'model_loaded': model_pipeline is not None
|
||||
})
|
||||
|
||||
|
||||
# ============================================
|
||||
# 主程序入口
|
||||
# ============================================
|
||||
if __name__ == '__main__':
|
||||
# 从环境变量读取配置,使用默认值
|
||||
host = os.getenv('FLASK_HOST', '127.0.0.1')
|
||||
port = int(os.getenv('FLASK_PORT', 5000))
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("CardioAI - Module 2: 心血管疾病预测服务")
|
||||
print("=" * 50)
|
||||
print(f"服务地址: http://{host}:{port}")
|
||||
print("=" * 50 + "\n")
|
||||
|
||||
app.run(host=host, port=port, debug=True)
|
||||
BIN
aicodes/module2_predictor/cardio_predictor_model.pkl
Normal file
BIN
aicodes/module2_predictor/cardio_predictor_model.pkl
Normal file
Binary file not shown.
532
aicodes/module2_predictor/templates/index.html
Normal file
532
aicodes/module2_predictor/templates/index.html
Normal file
@@ -0,0 +1,532 @@
|
||||
<!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>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-group .hint {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.btn-predict {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 50px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-predict:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||
}
|
||||
|
||||
.btn-predict:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
border: none;
|
||||
padding: 15px 40px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.btn-reset:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.result-container {
|
||||
margin-top: 30px;
|
||||
padding: 25px;
|
||||
border-radius: 15px;
|
||||
display: none;
|
||||
animation: slideIn 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.result-container.low-risk {
|
||||
background: linear-gradient(135deg, #d4fc79 0%, #96e6a1 100%);
|
||||
}
|
||||
|
||||
.result-container.medium-risk {
|
||||
background: linear-gradient(135deg, #ffeaa7 0%, #fdcb6e 100%);
|
||||
}
|
||||
|
||||
.result-container.high-risk {
|
||||
background: linear-gradient(135deg, #fab1a0 0%, #e17055 100%);
|
||||
}
|
||||
|
||||
.result-container.very-high-risk {
|
||||
background: linear-gradient(135deg, #ff7675 0%, #d63031 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.result-header h2 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.result-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
background: rgba(255,255,255,0.3);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.stat-box .label {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-box .value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.probability-bar {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.probability-bar .bar-bg {
|
||||
background: rgba(255,255,255,0.3);
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.probability-bar .bar-fill {
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-radius: 10px;
|
||||
transition: width 1s ease;
|
||||
}
|
||||
|
||||
.probability-bar .label {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #ff7675;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin-top: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.result-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>❤️ CardioAI</h1>
|
||||
<p>心血管疾病智能预测系统</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form id="predictionForm">
|
||||
<div class="form-grid">
|
||||
<!-- 年龄 -->
|
||||
<div class="form-group">
|
||||
<label for="age_years">年龄 (岁)</label>
|
||||
<input type="number" id="age_years" name="age_years"
|
||||
min="1" max="120" value="45" required>
|
||||
<span class="hint">请输入您的年龄</span>
|
||||
</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 class="form-group">
|
||||
<label for="height">身高 (cm)</label>
|
||||
<input type="number" id="height" name="height"
|
||||
min="100" max="250" value="165" required>
|
||||
<span class="hint">范围: 100-250 cm</span>
|
||||
</div>
|
||||
|
||||
<!-- 体重 -->
|
||||
<div class="form-group">
|
||||
<label for="weight">体重 (kg)</label>
|
||||
<input type="number" id="weight" name="weight"
|
||||
min="30" max="200" value="65" required>
|
||||
<span class="hint">范围: 30-200 kg</span>
|
||||
</div>
|
||||
|
||||
<!-- 收缩压 -->
|
||||
<div class="form-group">
|
||||
<label for="ap_hi">收缩压 (mmHg)</label>
|
||||
<input type="number" id="ap_hi" name="ap_hi"
|
||||
min="90" max="250" value="120" required>
|
||||
<span class="hint">范围: 90-250 mmHg</span>
|
||||
</div>
|
||||
|
||||
<!-- 舒张压 -->
|
||||
<div class="form-group">
|
||||
<label for="ap_lo">舒张压 (mmHg)</label>
|
||||
<input type="number" id="ap_lo" name="ap_lo"
|
||||
min="60" max="150" value="80" required>
|
||||
<span class="hint">范围: 60-150 mmHg</span>
|
||||
</div>
|
||||
|
||||
<!-- 胆固醇 -->
|
||||
<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 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>
|
||||
|
||||
<div class="btn-container">
|
||||
<button type="submit" class="btn-predict" id="predictBtn">
|
||||
🔍 开始预测
|
||||
</button>
|
||||
<button type="button" class="btn-reset" onclick="resetForm()">
|
||||
🔄 重置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p style="margin-top: 15px; color: #666;">正在分析您的健康数据...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误消息 -->
|
||||
<div class="error-message" id="errorMessage"></div>
|
||||
|
||||
<!-- 结果展示 -->
|
||||
<div class="result-container" id="resultContainer">
|
||||
<div class="result-header">
|
||||
<h2 id="resultTitle">预测结果</h2>
|
||||
<p id="riskLevel" style="font-size: 1.2rem; font-weight: 600;"></p>
|
||||
</div>
|
||||
|
||||
<div class="result-stats">
|
||||
<div class="stat-box">
|
||||
<div class="label">预测结果</div>
|
||||
<div class="value" id="predictionResult">-</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="label">疾病概率</div>
|
||||
<div class="value" id="probabilityValue">-</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="label">风险等级</div>
|
||||
<div class="value" id="riskLevelValue">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="probability-bar">
|
||||
<div class="bar-bg">
|
||||
<div class="bar-fill" id="probabilityBar"></div>
|
||||
</div>
|
||||
<div class="label" id="probabilityLabel">疾病风险概率</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 20px; color: white; opacity: 0.8;">
|
||||
<p>⚠️ 本预测结果仅供参考,不能替代专业医疗诊断</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('predictionForm');
|
||||
const loading = document.getElementById('loading');
|
||||
const resultContainer = document.getElementById('resultContainer');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const predictBtn = document.getElementById('predictBtn');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 获取表单数据
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
// 验证血压数据
|
||||
if (parseInt(data.ap_lo) >= parseInt(data.ap_hi)) {
|
||||
showError('舒张压不能大于或等于收缩压,请检查输入!');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载动画
|
||||
loading.style.display = 'block';
|
||||
resultContainer.style.display = 'none';
|
||||
errorMessage.style.display = 'none';
|
||||
predictBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/predict_cardio', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.error) {
|
||||
showError(result.error);
|
||||
} else {
|
||||
displayResult(result);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('网络错误,请确保服务已启动');
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
loading.style.display = 'none';
|
||||
predictBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
function displayResult(result) {
|
||||
const probability = result.probability * 100;
|
||||
const prediction = result.prediction === 1 ? '有疾病风险' : '无疾病风险';
|
||||
|
||||
// 设置预测结果
|
||||
document.getElementById('predictionResult').textContent = prediction;
|
||||
document.getElementById('probabilityValue').textContent = probability.toFixed(1) + '%';
|
||||
document.getElementById('riskLevelValue').textContent = result.risk_level;
|
||||
document.getElementById('riskLevel').textContent = result.risk_level;
|
||||
|
||||
// 设置概率条
|
||||
const bar = document.getElementById('probabilityBar');
|
||||
setTimeout(() => {
|
||||
bar.style.width = probability + '%';
|
||||
}, 100);
|
||||
|
||||
// 根据风险等级设置样式
|
||||
resultContainer.className = 'result-container';
|
||||
if (result.risk_level === '低风险') {
|
||||
resultContainer.classList.add('low-risk');
|
||||
} else if (result.risk_level === '中等风险') {
|
||||
resultContainer.classList.add('medium-risk');
|
||||
} else if (result.risk_level === '高风险') {
|
||||
resultContainer.classList.add('high-risk');
|
||||
} else {
|
||||
resultContainer.classList.add('very-high-risk');
|
||||
}
|
||||
|
||||
// 显示结果
|
||||
resultContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
errorMessage.textContent = message;
|
||||
errorMessage.style.display = 'block';
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.reset();
|
||||
resultContainer.style.display = 'none';
|
||||
errorMessage.style.display = 'none';
|
||||
document.getElementById('probabilityBar').style.width = '0%';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
211
aicodes/module2_predictor/train_and_save.py
Normal file
211
aicodes/module2_predictor/train_and_save.py
Normal file
@@ -0,0 +1,211 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CardioAI - Module 2: 模型训练与保存
|
||||
一次性脚本 - 训练XGBoost模型并保存Pipeline
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import joblib
|
||||
from pathlib import Path
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.compose import ColumnTransformer
|
||||
from sklearn.preprocessing import StandardScaler, OneHotEncoder
|
||||
from sklearn.pipeline import Pipeline
|
||||
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
|
||||
import xgboost as xgb
|
||||
|
||||
# ============================================
|
||||
# 配置与常量
|
||||
# ============================================
|
||||
CODE_ROOT = Path(r"E:\project_ai\claude_project1\aicodes")
|
||||
DATA_PATH = Path(r"E:\project_ai\claude_project1\data\心血管疾病.xlsx")
|
||||
MODEL_SAVE_PATH = CODE_ROOT / "module2_predictor" / "cardio_predictor_model.pkl"
|
||||
|
||||
# 类别映射字典
|
||||
CHOLESTEROL_MAP = {1: "正常", 2: "高于正常", 3: "远高于正常"}
|
||||
GLUC_MAP = {1: "正常", 2: "高于正常", 3: "远高于正常"}
|
||||
GENDER_MAP = {1: "女性", 2: "男性"}
|
||||
|
||||
|
||||
# ============================================
|
||||
# 数据加载与清洗函数
|
||||
# ============================================
|
||||
def load_data():
|
||||
"""加载心血管疾病数据"""
|
||||
print("正在加载数据...")
|
||||
df = pd.read_excel(DATA_PATH, engine='openpyxl')
|
||||
print(f"数据加载完成: {df.shape[0]} 条记录, {df.shape[1]} 个特征")
|
||||
return df
|
||||
|
||||
|
||||
def clean_and_process_data(df):
|
||||
"""
|
||||
数据清洗与特征工程
|
||||
|
||||
处理步骤:
|
||||
1. 将age(天)转换为年
|
||||
2. 计算BMI
|
||||
3. 删除血压异常值
|
||||
4. 类别转换
|
||||
5. 删除id和原始age字段
|
||||
"""
|
||||
print("\n开始数据清洗与特征工程...")
|
||||
|
||||
df_clean = df.copy()
|
||||
|
||||
# 1. 年龄转换: 天 -> 年 (四舍五入)
|
||||
df_clean['age_years'] = (df_clean['age'] / 365.25).round().astype(int)
|
||||
|
||||
# 2. 计算BMI: weight / (height/100)^2
|
||||
df_clean['bmi'] = df_clean['weight'] / ((df_clean['height'] / 100) ** 2)
|
||||
|
||||
# 3. 异常值处理
|
||||
original_count = len(df_clean)
|
||||
|
||||
# 删除舒张压 >= 收缩压的记录
|
||||
df_clean = df_clean[df_clean['ap_lo'] < df_clean['ap_hi']]
|
||||
|
||||
# 删除血压极端异常值 (收缩压范围: [90, 250], 舒张压范围: [60, 150])
|
||||
df_clean = df_clean[
|
||||
(df_clean['ap_hi'] >= 90) & (df_clean['ap_hi'] <= 250) &
|
||||
(df_clean['ap_lo'] >= 60) & (df_clean['ap_lo'] <= 150)
|
||||
]
|
||||
|
||||
removed_count = original_count - len(df_clean)
|
||||
print(f" - 删除异常值: {removed_count} 条记录")
|
||||
|
||||
# 4. 删除id和原始age字段
|
||||
df_clean = df_clean.drop(columns=['id', 'age'], errors='ignore')
|
||||
|
||||
print(f"数据清洗完成: {len(df_clean)} 条有效记录")
|
||||
|
||||
return df_clean
|
||||
|
||||
|
||||
# ============================================
|
||||
# 模型训练函数
|
||||
# ============================================
|
||||
def train_model(df):
|
||||
"""
|
||||
训练XGBoost分类模型
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): 清洗后的数据
|
||||
|
||||
Returns:
|
||||
Pipeline: 包含预处理器和模型的完整Pipeline
|
||||
"""
|
||||
print("\n开始模型训练...")
|
||||
|
||||
# 定义特征列
|
||||
# 连续特征
|
||||
numeric_features = ['age_years', 'height', 'weight', 'bmi', 'ap_hi', 'ap_lo']
|
||||
|
||||
# 分类特征
|
||||
categorical_features = ['gender', 'cholesterol', 'gluc', 'smoke', 'alco', 'active']
|
||||
|
||||
# 特征与目标变量
|
||||
X = df[numeric_features + categorical_features]
|
||||
y = df['cardio']
|
||||
|
||||
# 划分训练集和测试集
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
X, y, test_size=0.2, random_state=42, stratify=y
|
||||
)
|
||||
|
||||
print(f" - 训练集: {X_train.shape[0]} 条记录")
|
||||
print(f" - 测试集: {X_test.shape[0]} 条记录")
|
||||
|
||||
# 构建预处理器
|
||||
preprocessor = ColumnTransformer(
|
||||
transformers=[
|
||||
('num', StandardScaler(), numeric_features),
|
||||
('cat', OneHotEncoder(drop='first', handle_unknown='ignore'), categorical_features)
|
||||
],
|
||||
remainder='passthrough'
|
||||
)
|
||||
|
||||
# 构建完整Pipeline
|
||||
pipeline = Pipeline(steps=[
|
||||
('preprocessor', preprocessor),
|
||||
('classifier', xgb.XGBClassifier(
|
||||
n_estimators=100,
|
||||
max_depth=6,
|
||||
learning_rate=0.1,
|
||||
subsample=0.8,
|
||||
colsample_bytree=0.8,
|
||||
random_state=42,
|
||||
eval_metric='logloss',
|
||||
use_label_encoder=False
|
||||
))
|
||||
])
|
||||
|
||||
# 训练模型
|
||||
print(" - 正在训练 XGBoost 模型...")
|
||||
pipeline.fit(X_train, y_train)
|
||||
|
||||
# 评估模型
|
||||
print("\n模型评估:")
|
||||
y_pred = pipeline.predict(X_test)
|
||||
y_pred_proba = pipeline.predict_proba(X_test)[:, 1]
|
||||
|
||||
print("\n分类报告:")
|
||||
print(classification_report(y_test, y_pred, target_names=['无疾病', '有疾病']))
|
||||
|
||||
print("\n混淆矩阵:")
|
||||
cm = confusion_matrix(y_test, y_pred)
|
||||
print(f" 真阴性(TN): {cm[0,0]} 假阳性(FP): {cm[0,1]}")
|
||||
print(f" 假阴性(FN): {cm[1,0]} 真阳性(TP): {cm[1,1]}")
|
||||
|
||||
auc_score = roc_auc_score(y_test, y_pred_proba)
|
||||
print(f"\nAUC-ROC: {auc_score:.4f}")
|
||||
|
||||
return pipeline
|
||||
|
||||
|
||||
# ============================================
|
||||
# 模型保存函数
|
||||
# ============================================
|
||||
def save_model(pipeline, save_path):
|
||||
"""
|
||||
保存训练好的模型
|
||||
|
||||
Args:
|
||||
pipeline (Pipeline): 训练好的Pipeline
|
||||
save_path (Path): 模型保存路径
|
||||
"""
|
||||
print(f"\n正在保存模型到: {save_path}")
|
||||
joblib.dump(pipeline, save_path)
|
||||
print("模型保存完成!")
|
||||
|
||||
|
||||
# ============================================
|
||||
# 主程序
|
||||
# ============================================
|
||||
def main():
|
||||
"""主程序入口"""
|
||||
print("=" * 60)
|
||||
print("CardioAI - Module 2: XGBoost模型训练与保存")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 加载数据
|
||||
df = load_data()
|
||||
|
||||
# 2. 数据清洗与特征工程
|
||||
df_clean = clean_and_process_data(df)
|
||||
|
||||
# 3. 训练模型
|
||||
pipeline = train_model(df_clean)
|
||||
|
||||
# 4. 保存模型
|
||||
save_model(pipeline, MODEL_SAVE_PATH)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("训练完成! 模型已保存至:")
|
||||
print(f" {MODEL_SAVE_PATH}")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
468
aicodes/module3_voice_assistant/templates/voice_index.html
Normal file
468
aicodes/module3_voice_assistant/templates/voice_index.html
Normal file
@@ -0,0 +1,468 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CardioAI - AI 语音助手</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
min-height: 400px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
max-width: 75%;
|
||||
padding: 15px 20px;
|
||||
border-radius: 18px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message.user .message-bubble {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.message.assistant .message-bubble {
|
||||
background: white;
|
||||
color: #333;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
margin: 0 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message.user .message-avatar {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.message.assistant .message-avatar {
|
||||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#userInput {
|
||||
width: 100%;
|
||||
padding: 15px 20px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 25px;
|
||||
font-size: 1rem;
|
||||
resize: none;
|
||||
min-height: 50px;
|
||||
max-height: 150px;
|
||||
font-family: inherit;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#userInput:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.btn-send {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-send:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||
}
|
||||
|
||||
.btn-send:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.loading.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #ff7675;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error-message.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.welcome-message .icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.welcome-message h3 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.suggested-questions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.suggested-question {
|
||||
background: #f0f0f0;
|
||||
color: #555;
|
||||
padding: 8px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.suggested-question:hover {
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.input-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
.message-bubble {
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🤖 CardioAI 语音助手</h1>
|
||||
<p>专业心血管健康顾问 - DeepSeek + CosyVoice</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<!-- 错误消息 -->
|
||||
<div class="error-message" id="errorMessage"></div>
|
||||
|
||||
<!-- 聊天区域 -->
|
||||
<div class="chat-container" id="chatContainer">
|
||||
<div class="welcome-message" id="welcomeMessage">
|
||||
<div class="icon">❤️</div>
|
||||
<h3>您好!我是 CardioAI 健康助手</h3>
|
||||
<p>我可以回答您关于心血管健康的任何问题</p>
|
||||
<div class="suggested-questions">
|
||||
<span class="suggested-question" onclick="askQuestion('什么是高血压?')">什么是高血压?</span>
|
||||
<span class="suggested-question" onclick="askQuestion('如何预防心血管疾病?')">如何预防心血管疾病?</span>
|
||||
<span class="suggested-question" onclick="askQuestion('正常的心率是多少?')">正常的心率是多少?</span>
|
||||
<span class="suggested-question" onclick="askQuestion('胆固醇高怎么办?')">胆固醇高怎么办?</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p class="loading-text">正在思考并生成语音回答...</p>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="input-container">
|
||||
<div class="input-wrapper">
|
||||
<textarea id="userInput" placeholder="输入您关于心血管健康的问题..." rows="1"></textarea>
|
||||
</div>
|
||||
<button class="btn-send" id="sendBtn" onclick="sendMessage()">
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; color: white; opacity: 0.8; font-size: 0.9rem;">
|
||||
<p>⚠️ 本助手提供的健康建议仅供参考,不能替代专业医疗诊断</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const chatContainer = document.getElementById('chatContainer');
|
||||
const userInput = document.getElementById('userInput');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const loading = document.getElementById('loading');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const welcomeMessage = document.getElementById('welcomeMessage');
|
||||
|
||||
// 自动调整文本框高度
|
||||
userInput.addEventListener('input', function() {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
|
||||
});
|
||||
|
||||
// 回车发送消息
|
||||
userInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
function askQuestion(question) {
|
||||
userInput.value = question;
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const question = userInput.value.trim();
|
||||
if (!question) return;
|
||||
|
||||
// 隐藏欢迎消息
|
||||
if (welcomeMessage) {
|
||||
welcomeMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
// 添加用户消息
|
||||
addMessage('user', question);
|
||||
userInput.value = '';
|
||||
userInput.style.height = '50px';
|
||||
|
||||
// 显示加载动画
|
||||
loading.classList.add('active');
|
||||
sendBtn.disabled = true;
|
||||
errorMessage.classList.remove('active');
|
||||
|
||||
// 调用 API
|
||||
fetch('/voice_assistant', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ question: question })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
loading.classList.remove('active');
|
||||
sendBtn.disabled = false;
|
||||
|
||||
if (data.error) {
|
||||
showError(data.error);
|
||||
} else {
|
||||
addAssistantMessage(data.text_answer, data.audio_base64);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
loading.classList.remove('active');
|
||||
sendBtn.disabled = false;
|
||||
showError('网络错误,请确保服务已启动');
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function addMessage(type, text) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${type}`;
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'message-avatar';
|
||||
avatar.textContent = type === 'user' ? '👤' : '🤖';
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'message-bubble';
|
||||
bubble.textContent = text;
|
||||
|
||||
if (type === 'user') {
|
||||
messageDiv.appendChild(bubble);
|
||||
messageDiv.appendChild(avatar);
|
||||
} else {
|
||||
messageDiv.appendChild(avatar);
|
||||
messageDiv.appendChild(bubble);
|
||||
}
|
||||
|
||||
chatContainer.appendChild(messageDiv);
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function addAssistantMessage(text, audioBase64) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message assistant';
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'message-avatar';
|
||||
avatar.textContent = '🤖';
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'message-bubble';
|
||||
|
||||
const textContent = document.createElement('div');
|
||||
textContent.textContent = text;
|
||||
bubble.appendChild(textContent);
|
||||
|
||||
// 创建音频播放器
|
||||
if (audioBase64) {
|
||||
const audio = document.createElement('audio');
|
||||
audio.className = 'audio-player';
|
||||
audio.controls = true;
|
||||
audio.src = `data:audio/mp3;base64,${audioBase64}`;
|
||||
audio.autoplay = true;
|
||||
bubble.appendChild(audio);
|
||||
}
|
||||
|
||||
messageDiv.appendChild(avatar);
|
||||
messageDiv.appendChild(bubble);
|
||||
|
||||
chatContainer.appendChild(messageDiv);
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
errorMessage.textContent = message;
|
||||
errorMessage.classList.add('active');
|
||||
setTimeout(() => {
|
||||
errorMessage.classList.remove('active');
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
216
aicodes/module3_voice_assistant/voice_assistant_app.py
Normal file
216
aicodes/module3_voice_assistant/voice_assistant_app.py
Normal file
@@ -0,0 +1,216 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CardioAI - Module 3: AI 语音助手
|
||||
Deepseek 问答 + CosyVoice 语音合成服务
|
||||
"""
|
||||
|
||||
import os
|
||||
import base64
|
||||
import dashscope
|
||||
from pathlib import Path
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
from dotenv import load_dotenv
|
||||
from langchain_openai import ChatOpenAI
|
||||
from dashscope.audio.tts_v2 import SpeechSynthesizer, AudioFormat
|
||||
|
||||
# ============================================
|
||||
# 配置与初始化
|
||||
# ============================================
|
||||
# 加载环境变量
|
||||
ENV_PATH = Path(r"E:\project_ai\claude_project1\aicodes\.env")
|
||||
load_dotenv(dotenv_path=ENV_PATH)
|
||||
|
||||
# 配置 DashScope API Key
|
||||
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
|
||||
|
||||
# 创建 Flask 应用
|
||||
CODE_ROOT = Path(r"E:\project_ai\claude_project1\aicodes")
|
||||
TEMPLATE_DIR = CODE_ROOT / "module3_voice_assistant" / "templates"
|
||||
|
||||
app = Flask(__name__,
|
||||
template_folder=str(TEMPLATE_DIR),
|
||||
static_folder=str(TEMPLATE_DIR.parent / 'static'))
|
||||
|
||||
|
||||
# ============================================
|
||||
# DeepSeek LLM 配置
|
||||
# ============================================
|
||||
def get_llm():
|
||||
"""
|
||||
初始化 DeepSeek LLM 实例
|
||||
|
||||
Returns:
|
||||
ChatOpenAI: DeepSeek LLM 实例
|
||||
"""
|
||||
llm = ChatOpenAI(
|
||||
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
|
||||
api_key=os.getenv("DEEPSEEK_API_KEY"),
|
||||
model=os.getenv("MODEL_NAME", "deepseek-chat"),
|
||||
temperature=float(os.getenv("TEMPERATURE", 0.7)),
|
||||
max_tokens=int(os.getenv("MAX_TOKENS", 2000))
|
||||
)
|
||||
return llm
|
||||
|
||||
|
||||
# System Prompt - 专业心血管健康顾问
|
||||
SYSTEM_PROMPT = """你是一位专业的心血管健康顾问,名为 CardioAI 助手。
|
||||
|
||||
你的职责:
|
||||
1. 回答用户关于心血管疾病预防、诊断、治疗和管理的问题
|
||||
2. 提供基于医学证据的健康建议
|
||||
3. 解释心血管相关的医学概念和检查指标
|
||||
4. 给出生活方式改善建议(饮食、运动、戒烟限酒等)
|
||||
|
||||
注意事项:
|
||||
- 回答要专业、准确、通俗易懂
|
||||
- 对于严重的健康问题,建议用户及时就医
|
||||
- 不要替代医生的诊断,你的建议仅供参考
|
||||
- 回答要简洁明了,避免过于冗长
|
||||
|
||||
请用温暖、专业的语气回答用户的问题。"""
|
||||
|
||||
|
||||
# ============================================
|
||||
# CosyVoice 语音合成函数
|
||||
# ============================================
|
||||
def text_to_speech(text: str) -> bytes:
|
||||
"""
|
||||
使用 CosyVoice 将文本转换为语音
|
||||
|
||||
Args:
|
||||
text (str): 待合成的文本
|
||||
|
||||
Returns:
|
||||
bytes: MP3 格式的音频数据
|
||||
"""
|
||||
try:
|
||||
# 实例化 SpeechSynthesizer
|
||||
synthesizer = SpeechSynthesizer(
|
||||
model=os.getenv("VOICE_MODEL", "cosyvoice-v2"),
|
||||
voice=os.getenv("VOICE", "longxiaochun_v2"),
|
||||
format=AudioFormat.MP3_22050HZ_MONO_256KBPS
|
||||
)
|
||||
|
||||
# 同步调用 - 将文本转换为语音
|
||||
audio_data = synthesizer.call(text)
|
||||
|
||||
return audio_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"语音合成失败: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
# ============================================
|
||||
# 路由定义
|
||||
# ============================================
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""主页 - 渲染语音助手界面"""
|
||||
return render_template('voice_index.html')
|
||||
|
||||
|
||||
@app.route('/voice_assistant', methods=['POST'])
|
||||
def voice_assistant():
|
||||
"""
|
||||
AI 语音助手 API 接口
|
||||
|
||||
接收用户问题,返回 LLM 文字回答和 Base64 编码的语音
|
||||
"""
|
||||
try:
|
||||
# 获取请求数据
|
||||
data = request.get_json()
|
||||
user_question = data.get('question', '').strip()
|
||||
|
||||
if not user_question:
|
||||
return jsonify({
|
||||
'error': '请输入您的问题'
|
||||
}), 400
|
||||
|
||||
# 检查 API Key 配置
|
||||
deepseek_key = os.getenv("DEEPSEEK_API_KEY")
|
||||
dashscope_key = os.getenv("DASHSCOPE_API_KEY")
|
||||
|
||||
if not deepseek_key or deepseek_key == "your_deepseek_api_key_here":
|
||||
return jsonify({
|
||||
'error': 'DeepSeek API Key 未配置,请在 .env 文件中设置 DEEPSEEK_API_KEY'
|
||||
}), 500
|
||||
|
||||
if not dashscope_key or dashscope_key == "your_dashscope_api_key_here":
|
||||
return jsonify({
|
||||
'error': 'DashScope API Key 未配置,请在 .env 文件中设置 DASHSCOPE_API_KEY'
|
||||
}), 500
|
||||
|
||||
print(f"\n用户问题: {user_question}")
|
||||
|
||||
# 1. 调用 DeepSeek LLM 获取文字回答
|
||||
print("正在调用 DeepSeek LLM...")
|
||||
llm = get_llm()
|
||||
|
||||
# 构建完整 prompt
|
||||
messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_question}
|
||||
]
|
||||
|
||||
# 调用 LLM
|
||||
llm_response = llm.invoke(messages)
|
||||
text_answer = llm_response.content
|
||||
|
||||
print(f"LLM 回答: {text_answer}")
|
||||
|
||||
# 2. 将文字回答转换为语音
|
||||
print("正在调用 CosyVoice 合成语音...")
|
||||
audio_data = text_to_speech(text_answer)
|
||||
|
||||
if audio_data is None:
|
||||
return jsonify({
|
||||
'error': '语音合成失败,请检查 API Key 配置和网络连接'
|
||||
}), 500
|
||||
|
||||
# 3. 将音频编码为 Base64
|
||||
audio_base64 = base64.b64encode(audio_data).decode('utf-8')
|
||||
|
||||
print(f"语音合成完成,音频大小: {len(audio_data)} 字节")
|
||||
|
||||
# 4. 返回结果
|
||||
return jsonify({
|
||||
'text_answer': text_answer,
|
||||
'audio_base64': audio_base64,
|
||||
'message': '问答成功'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"处理失败: {str(e)}")
|
||||
return jsonify({
|
||||
'error': f'处理失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""健康检查接口"""
|
||||
deepseek_configured = os.getenv("DEEPSEEK_API_KEY") not in [None, "your_deepseek_api_key_here"]
|
||||
dashscope_configured = os.getenv("DASHSCOPE_API_KEY") not in [None, "your_dashscope_api_key_here"]
|
||||
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'deepseek_configured': deepseek_configured,
|
||||
'dashscope_configured': dashscope_configured
|
||||
})
|
||||
|
||||
|
||||
# ============================================
|
||||
# 主程序入口
|
||||
# ============================================
|
||||
if __name__ == '__main__':
|
||||
host = os.getenv('FLASK_HOST', '127.0.0.1')
|
||||
port = int(os.getenv('FLASK_PORT', 5001)) # 使用 5001 避免与 Module 2 冲突
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("CardioAI - Module 3: AI 语音助手服务")
|
||||
print("=" * 50)
|
||||
print(f"服务地址: http://{host}:{port}")
|
||||
print("=" * 50 + "\n")
|
||||
|
||||
app.run(host=host, port=port, debug=True)
|
||||
40
aicodes/requirements.txt
Normal file
40
aicodes/requirements.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
# ============================================
|
||||
# CardioAI - 心血管疾病智能辅助系统
|
||||
# 项目依赖配置文件
|
||||
# ============================================
|
||||
|
||||
# Conda 环境设置说明:
|
||||
# 1. 创建名为 cardioenv 的 conda 虚拟环境 (Python 3.10):
|
||||
# conda create -n cardioenv python=3.10
|
||||
#
|
||||
# 2. 激活虚拟环境:
|
||||
# conda activate cardioenv
|
||||
#
|
||||
# 3. 安装项目依赖:
|
||||
# pip install -r requirements.txt
|
||||
# ============================================
|
||||
|
||||
# 数据处理与科学计算
|
||||
pandas>=1.5.0
|
||||
openpyxl>=3.0.0
|
||||
numpy>=1.23.0
|
||||
|
||||
# 机器学习与模型
|
||||
scikit-learn>=1.2.0
|
||||
xgboost>=2.0.0
|
||||
joblib>=1.3.0
|
||||
|
||||
# 数据可视化
|
||||
streamlit>=1.28.0
|
||||
plotly>=5.18.0
|
||||
|
||||
# Web 服务框架
|
||||
Flask>=3.0.0
|
||||
|
||||
# 环境配置
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# AI 与 LLM 集成
|
||||
langchain-openai>=0.0.5
|
||||
dashscope>=1.14.0
|
||||
requests>=2.31.0
|
||||
274
aicodes/语音合成CosyVoice.md
Normal file
274
aicodes/语音合成CosyVoice.md
Normal 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秒超时机制,客户端无法修改该配置。
|
||||
|
||||

|
||||
|
||||
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及以后版本的SDK:volume1.20.10以前版本的SDK:volumn |
|
||||
| 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 | 语音助手、客服催收、新闻播报、导航播报 | 中文普通话 |
|
||||
BIN
data/心血管疾病.xlsx
Normal file
BIN
data/心血管疾病.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user