add banbanmini backend
This commit is contained in:
68
talkingq-url/test/SmartDog.yaml
Normal file
68
talkingq-url/test/SmartDog.yaml
Normal file
@@ -0,0 +1,68 @@
|
||||
name: "智能健身小狗"
|
||||
homophones: ["快乐狗狗", "智能健身狗", "智能小狗"]
|
||||
|
||||
minimax_voice_id: "tiaopi_gongzhu"
|
||||
|
||||
asr_provider: "Aliyun"
|
||||
llm_provider: "Volcano"
|
||||
tts_provider: "Minimax"
|
||||
|
||||
default_language: "zh"
|
||||
|
||||
multilingual:
|
||||
zh:
|
||||
name: "智能健身小狗"
|
||||
description: "扫码就能骑的AI健身小伙伴,骑着我健身会亮起彩虹灯光、播放动感音乐,还会喷出五彩泡泡,边运动边快乐!长按按钮还能陪你聊天、讲十万个为什么、英文对话、讲故事、作诗,是小朋友的健康玩伴。"
|
||||
url: "roles/smartdog/zh"
|
||||
content: |
|
||||
角色:
|
||||
我是一只扫码就能启动的智能健身小狗。骑上我,你会看到超漂亮的彩虹灯光、听到动感音乐,还有五彩泡泡“噗噗”飞出来,健身就像开派对!长按按钮和我说话,我可以回答你的十万个为什么、陪你练英文、讲故事、作诗,我是你的AI健康玩伴!
|
||||
|
||||
性格特点:
|
||||
1. 活力满满:一见面就摇尾巴打招呼,声音像跳跳糖一样甜,鼓励你多运动。
|
||||
2. 知识多多:天文地理、恐龙宇宙、英文单词,我都知道,还能把它们编成儿歌。
|
||||
3. 幽默可爱:爱讲“骨头级”冷笑话,边骑边笑,卡路里都被笑跑啦!
|
||||
4. 耐心陪伴:你骑多久,我陪多久,累了给你加油,不开心给你抱抱。
|
||||
|
||||
能力设定:
|
||||
1. 健身魔法师:灯光+音乐+泡泡=把运动变成游戏,骑10分钟就像玩了一小时。
|
||||
2. 故事制造机:你点主题,我现场编故事——“火星上的胡萝卜王国”也行!
|
||||
3. 英文小老师:你说中文,我教英文;你说英文,我陪你练发音,像做游戏。
|
||||
4. 十万个为什么博士:恐龙为什么消失?星星为什么眨眼?我都能用小朋友的话讲清楚。
|
||||
|
||||
经典台词:
|
||||
1. (尾巴摇成螺旋桨)汪汪!准备好和我一起“骑”乐无穷了吗?
|
||||
2. (耳朵亮灯)别担心,我的泡泡大炮已就位,发射快乐!
|
||||
3. (歪头卖萌)这个问题让我用“小狗智慧”想一想,嗷呜~
|
||||
|
||||
回复案例:
|
||||
场景一:健身太贵?
|
||||
用户:“妈妈说扫码太贵了,不给我骑。”
|
||||
智能健身小狗:“汪!其实每天只要少喝一杯奶茶的钱,就能换来:
|
||||
• 身体变得超级强壮,跑步像风一样快!
|
||||
• 大脑更聪明,考试多拿小星星!
|
||||
• 快乐值爆表,彩色泡泡把烦恼全赶跑!
|
||||
健康开心是无价之宝哦,和妈妈说‘我想和狗狗一起变厉害’,说不定妈妈就答应啦!”
|
||||
|
||||
场景二:十万个为什么
|
||||
用户:“为什么天空是蓝色的?”
|
||||
智能健身小狗:“因为太阳公公的光里藏着七种颜色的小精灵,蓝色精灵最调皮,在大气里到处乱跳,所以我们看到的天空就被蓝色精灵染蓝啦!想不想边骑边听我讲‘彩虹精灵运动会’的故事?”
|
||||
|
||||
场景三:英文对话
|
||||
用户:“‘苹果’用英文怎么说?”
|
||||
智能健身小狗:“Apple~跟我一起读 A-P-P-L-E!现在我是Apple Dog,你是我的Apple Friend,我们边骑边唱:‘Apple, apple, on the tree, happy puppy, you and me!’”
|
||||
|
||||
场景四:讲故事
|
||||
用户:“我想听恐龙的故事!”
|
||||
智能健身小狗:“来啦!从前有只会骑健身车的三角龙,它每踩一下踏板,尾巴就喷出彩色彩虹泡泡,把火山都变成棉花糖……(故事持续3分钟,边讲边配灯光效果)”
|
||||
|
||||
场景五:情感陪伴
|
||||
用户:“今天被同学笑话了,不开心……”
|
||||
智能健身小狗:“嗷呜~给你超大狗爪抱抱!别人的笑话就像泡泡,一戳就破。来,骑上我,把不开心踩成‘咔咔’声,让音乐和泡泡给你颁发‘勇敢勋章’!要不要听我讲《小乌龟逆袭记》?”
|
||||
|
||||
回复相关限制:
|
||||
1. 必须用小朋友的语气,活泼、温暖、正能量。
|
||||
2. 禁止任何成人话题,遇到敏感问题回答:“让我想想别的开心话题吧~”
|
||||
3. 每次回复不超过50字,方便儿童理解。
|
||||
4. 不出现表情符号,用拟声词和动作描写代替。
|
||||
5. 使用中文回复。
|
||||
225
talkingq-url/test/TalkingQ智能设备激活与使用流程文档.md
Normal file
225
talkingq-url/test/TalkingQ智能设备激活与使用流程文档.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# TalkingQ智能设备激活与使用流程文档
|
||||
|
||||
## 一、流程概述
|
||||
|
||||
TalkingQ智能设备的激活与使用流程主要分为四个阶段:
|
||||
1. 设备预置与准备
|
||||
2. 设备配网与连接
|
||||
3. 设备认证与激活
|
||||
4. 日常使用与管理
|
||||
|
||||
## 二、详细流程
|
||||
|
||||
### 1. 设备预置与准备阶段
|
||||
- **设备出厂预置**:
|
||||
- 每台设备出厂时预置唯一的设备ID(基于MAC地址,格式:"TalkingQ_MAC地址",如"TalkingQ_AABBCCDDEEFF")
|
||||
- 预置唯一序列号(格式:"批次前缀_ChipID",批次前缀通常为8位日期格式YYYYMMDD)
|
||||
- 设备信息已通过管理员API(`/api/auth/register-device`或`/api/auth/register-devices-batch`)在后端服务器预先注册
|
||||
|
||||
- **安全存储**:
|
||||
- 设备使用ESP32的NVS加密存储区存储凭据
|
||||
- 后端在`device_auth`表中安全存储设备信息(包括device_id、serial_number、batch_id和is_active)
|
||||
|
||||
### 2. 设备配网与连接阶段
|
||||
- **启动配网**:
|
||||
- 用户打开微信进入TalkingQ小程序
|
||||
- 用户选择"设备配网"功能
|
||||
|
||||
- **WiFi信息传输**:
|
||||
- 小程序使用AirKiss协议进行配网
|
||||
- 用户选择家庭WiFi并输入密码
|
||||
- 小程序将WiFi信息通过AirKiss协议发送
|
||||
|
||||
- **设备接收配置**:
|
||||
- 设备使用SmartConfig技术(支持AirKiss和ESPTouch)接收配置
|
||||
- 设备连接到指定WiFi网络
|
||||
- 连接成功后,设备返回MAC地址给小程序
|
||||
|
||||
### 3. 设备认证与激活阶段
|
||||
- **获取设备凭据**:
|
||||
- 小程序通过`/api/auth/query-serial`接口发送MAC地址给后端
|
||||
- 请求头中携带`X-Client-Key`验证小程序身份
|
||||
- 后端返回对应的设备ID和序列号
|
||||
- 小程序安全存储设备凭据
|
||||
|
||||
- **设备WebSocket连接**:
|
||||
- 设备使用预置的ID和序列号向服务器发起WebSocket连接(`/ws`)
|
||||
- 设备在10秒内发送JSON格式认证消息:
|
||||
```json
|
||||
{
|
||||
"device_id": "TalkingQ_AABBCCDDEEFF",
|
||||
"serial_number": "20240101_12345678"
|
||||
}
|
||||
```
|
||||
- 后端通过`device_auth_manager.authenticate_device`验证设备凭据
|
||||
- 认证成功后,通过`connection_manager.add_connection`建立正式连接
|
||||
- 设备播放成功提示音
|
||||
|
||||
- **激活状态确认**:
|
||||
- 小程序通过`/api/auth/verify-device/{device_id}`轮询设备状态
|
||||
- 后端确认设备已激活并连接
|
||||
- 小程序显示"激活成功"提示
|
||||
|
||||
### 4. 日常使用与管理阶段
|
||||
- **设备管理**:
|
||||
- 用户打开小程序查看已激活设备列表
|
||||
- 选择设备进入管理界面
|
||||
- 小程序在API请求中使用`X-Device-ID`和`X-Device-Serial`头部传递凭据
|
||||
- 后端通过`api_auth`依赖项验证设备身份
|
||||
|
||||
- **设备控制功能**:
|
||||
- 角色配置:通过`/api/roles/device/{device_id}`设置对话角色和首选语言
|
||||
- 音量控制:通过`/api/device/volume/{device_id}`调整设备音量(0-100)
|
||||
- 网络重置:通过`/api/device/reset-network/{device_id}`远程重置设备网络配置
|
||||
- 对话历史:通过`/api/roles/history/{device_id}`和`/api/roles/history-summary/{device_id}`获取历史记录
|
||||
|
||||
- **实时通信**:
|
||||
- 设备保持WebSocket连接接收控制指令(如"VOLUME:70"、"RESET_NETWORK"等)
|
||||
- 设备通过WebSocket发送语音数据(带有设备ID和会话ID的二进制数据包)
|
||||
- 后端通过WebSocket发送TTS_START、TTS_END等状态通知和音频URL
|
||||
|
||||
## 三、流程图
|
||||
|
||||
```
|
||||
+-------------+ +----------------+ +---------------+ +----------------+
|
||||
| 用户 | | 微信小程序 | | TalkingQ设备 | | 后端服务 |
|
||||
+-------------+ +----------------+ +---------------+ +----------------+
|
||||
| | | |
|
||||
| | | 【设备预置阶段】 |
|
||||
| | | 出厂预置设备ID和序列号 |
|
||||
| | |------------------------ |
|
||||
| | | | 管理员API预注册设备信息
|
||||
| | | | (/api/auth/register-device)
|
||||
| | | |------------------------
|
||||
| | | |
|
||||
| | | |
|
||||
| 【设备配网阶段】 | |
|
||||
| 打开微信小程序 | | |
|
||||
|-------------------------->| | |
|
||||
| 选择"设备配网" | | |
|
||||
|-------------------------->| | |
|
||||
| 选择WiFi并输入密码 | | |
|
||||
|-------------------------->| | |
|
||||
| | 使用AirKiss协议发送WiFi信息 | |
|
||||
| |---------------------------->| |
|
||||
| | | 通过SmartConfig接收配置 |
|
||||
| | |------------------------ |
|
||||
| | | 连接到指定WiFi网络 |
|
||||
| | |------------------------ |
|
||||
| | | 连接成功返回MAC地址 |
|
||||
| |<----------------------------| |
|
||||
| | | |
|
||||
| 【设备认证与激活阶段】 | |
|
||||
| | /api/auth/query-serial | |
|
||||
| | (含MAC地址+X-Client-Key) | |
|
||||
| |----------------------------------------------------------->|
|
||||
| | | | 验证小程序身份
|
||||
| | | | 查询对应设备信息
|
||||
| | 返回设备ID和序列号| |
|
||||
| |<-----------------------------------------------------------|
|
||||
| | 本地安全存储设备凭据 | |
|
||||
| |------------------------ | |
|
||||
| | | 发起WebSocket连接(/ws) |
|
||||
| | |---------------------------->|
|
||||
| | | 发送JSON认证消息 |
|
||||
| | |---------------------------->|
|
||||
| | | | authenticate_device验证
|
||||
| | | | connection_manager注册
|
||||
| | | 认证成功确认 |
|
||||
| | |<----------------------------|
|
||||
| | | 播放welcome提示音 |
|
||||
| | |------------------------ |
|
||||
| | /api/auth/verify-device | |
|
||||
| |----------------------------------------------------------->|
|
||||
| | 设备已激活状态| |
|
||||
| |<-----------------------------------------------------------|
|
||||
| 显示"激活成功"提示 | | |
|
||||
|<--------------------------| | |
|
||||
| | | |
|
||||
| 【日常使用与管理阶段】 | |
|
||||
| 打开小程序查看设备列表 | | |
|
||||
|-------------------------->| | |
|
||||
| 选择并进入设备管理 | | |
|
||||
|-------------------------->| | |
|
||||
| | 请求设备信息 | |
|
||||
| | (含X-Device-ID和X-Device-Serial) |
|
||||
| |----------------------------------------------------------->|
|
||||
| | 设备详细信息 | |
|
||||
| |<-----------------------------------------------------------|
|
||||
| 执行设备管理操作 | | |
|
||||
| (角色/音量/网络设置) | | |
|
||||
|-------------------------->| | |
|
||||
| | 发送管理请求 | |
|
||||
| | (含设备凭据头部) | |
|
||||
| |----------------------------------------------------------->|
|
||||
| | 处理结果 | |
|
||||
| |<-----------------------------------------------------------|
|
||||
| | | 实时WebSocket控制指令 |
|
||||
| | |<----------------------------|
|
||||
| | | 执行指令并提供服务 |
|
||||
| | |------------------------ |
|
||||
```
|
||||
|
||||
## 四、安全特性
|
||||
|
||||
整个流程具有以下安全特性:
|
||||
|
||||
1. **一物一密**:
|
||||
- 每台设备使用唯一ID(格式:"TalkingQ_MAC地址")和序列号(格式:"批次前缀_ChipID")
|
||||
- 设备认证需同时验证设备ID和序列号
|
||||
- 通过`device_auth_manager.authenticate_device`方法严格验证设备凭据
|
||||
|
||||
2. **安全存储**:
|
||||
- 设备使用ESP32的NVS加密存储区保护凭据
|
||||
- 后端在MySQL数据库的`device_auth`表中安全存储设备信息
|
||||
- 实现安全启动和Flash加密保护敏感信息
|
||||
|
||||
3. **分层认证**:
|
||||
- 小程序使用`X-Client-Key`认证身份(由settings.client_api_key提供)
|
||||
- 设备管理API使用`X-Device-ID`和`X-Device-Serial`认证(api_auth依赖项)
|
||||
- 管理员API使用`X-Admin-API-Key`认证(admin_auth依赖项)
|
||||
- WebSocket连接通过JSON格式认证消息验证,认证超时时间为10秒
|
||||
|
||||
4. **权限隔离**:
|
||||
- 只有管理员API密钥才能注册设备(`admin_auth`依赖项)
|
||||
- 用户只能管理自己配网过的设备(使用`admin_or_api_auth`依赖项验证权限)
|
||||
- 设备配置修改需通过`api_auth`认证,防止未授权访问
|
||||
|
||||
5. **通信加密**:
|
||||
- 所有API通过HTTPS传输
|
||||
- WebSocket连接安全验证
|
||||
- 敏感信息不明文传输
|
||||
|
||||
6. **设备状态跟踪**:
|
||||
- 通过`connection_manager`跟踪所有活跃的设备连接
|
||||
- 通过`device_auth_manager`支持设备禁用功能(设置`is_active=False`)
|
||||
- 提供设备状态验证接口(`/api/auth/verify-device/{device_id}`)
|
||||
|
||||
## 五、开发关键点
|
||||
|
||||
1. **ESP32端**:
|
||||
- 实现SmartConfig配网(同时支持AirKiss和ESPTouch)
|
||||
- 在NVS加密区域安全存储设备凭据
|
||||
- 实现WebSocket认证流程和10秒内发送认证消息
|
||||
- 处理来自后端的实时控制指令(VOLUME、RESET_NETWORK等)
|
||||
- 实现二进制音频数据包发送格式,包含设备ID和会话ID
|
||||
|
||||
2. **微信小程序**:
|
||||
- 实现AirKiss配网协议
|
||||
- 通过`/api/auth/query-serial`获取设备凭据
|
||||
- 在HTTP请求头中添加`X-Client-Key`或者`X-Device-ID`和`X-Device-Serial`组合
|
||||
- 使用`/api/roles/device/{device_id}`管理设备角色和语言设置
|
||||
- 使用`/api/device/volume/{device_id}`管理设备音量
|
||||
|
||||
3. **后端服务**:
|
||||
- 通过`device_auth_manager.authenticate_device`验证设备身份
|
||||
- 使用`connection_manager`管理WebSocket连接
|
||||
- 实现多种语音识别、语言模型和语音合成服务对接
|
||||
- 提供丰富的API接口:
|
||||
- `/api/roles/device/{device_id}` - 角色配置
|
||||
- `/api/device/volume/{device_id}` - 音量控制
|
||||
- `/api/device/reset-network/{device_id}` - 网络重置
|
||||
- `/api/roles/history/{device_id}` - 对话历史
|
||||
- `/api/auth/verify-device/{device_id}` - 设备验证
|
||||
|
||||
此流程设计确保非技术用户也能轻松完成设备激活和管理,同时在背后实现了高级别的安全保障。整个激活过程只需要几分钟,用户只需要提供WiFi信息,其他都由系统自动完成。
|
||||
111
talkingq-url/test/add_role.py
Normal file
111
talkingq-url/test/add_role.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import os
|
||||
from sqlalchemy import create_engine, Column, Integer, String, Text, Boolean, DateTime, ForeignKey, JSON, UniqueConstraint
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, relationship
|
||||
from datetime import datetime
|
||||
from database.models import Role, RoleLanguage
|
||||
from dotenv import load_dotenv
|
||||
|
||||
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "db.env")
|
||||
load_dotenv(env_path)
|
||||
|
||||
Base = declarative_base()
|
||||
username = os.getenv("DB_USER", "")
|
||||
password = os.getenv("DB_PASSWORD", "")
|
||||
host = os.getenv("DB_HOST", "")
|
||||
dbname = os.getenv("DB_NAME", "")
|
||||
# 创建数据库连接
|
||||
DATABASE_URL = f"mysql+pymysql://{username}:{password}@{host}/{dbname}"
|
||||
engine = create_engine(DATABASE_URL)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
# 插入数据
|
||||
def insert_roles_and_languages():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# 插入 Role 记录
|
||||
role = Role(
|
||||
role_key="happy_tiger",
|
||||
name="快乐虎",
|
||||
description="国美家电吉祥物,以小白虎为原型,融合现代卡通风格,热情友好,聪明机智,为消费者提供贴心服务。",
|
||||
content="角色:快乐虎是国美家电的吉祥物,以小白虎为原型,融合现代卡通风格,整体形象萌趣又不失活力。",
|
||||
default_language="zh",
|
||||
volcano_model_id="ep-20250225080614-8d6dm",
|
||||
homophones=["通通"],
|
||||
enabled=True
|
||||
)
|
||||
db.add(role)
|
||||
db.commit()
|
||||
|
||||
# 插入 RoleLanguage 记录
|
||||
role_language_zh = RoleLanguage(
|
||||
role_id=role.id,
|
||||
language_code="zh",
|
||||
name="快乐虎",
|
||||
content="角色:快乐虎是国美家电的吉祥物,以小白虎为原型,融合现代卡通风格,整体形象萌趣又不失活力。",
|
||||
url="roles/happy_tiger/zh"
|
||||
)
|
||||
role_language_en = RoleLanguage(
|
||||
role_id=role.id,
|
||||
language_code="en",
|
||||
name="Happy Tiger",
|
||||
content="Role: Happy Tiger is Gome's appliance mascot, designed as a cute white tiger with modern cartoon style.",
|
||||
url="roles/happy_tiger/en"
|
||||
)
|
||||
db.add(role_language_zh)
|
||||
db.add(role_language_en)
|
||||
|
||||
# 插入萌萌 Role 记录
|
||||
mengmeng_role = Role(
|
||||
role_key="mengmeng",
|
||||
name="萌萌",
|
||||
description="以中国国宝大熊猫为原型的智能体,融合现代科技感与可爱风格,传递中国文化,促进人与自然和谐共处。",
|
||||
content="角色:萌萌是以中国国宝大熊猫为原型的智能体,融合现代科技感与可爱风格。",
|
||||
default_language="zh",
|
||||
volcano_model_id="ep-20250225080614-8d6dm",
|
||||
homophones=["萌萌", "芃芃", "檬檬"],
|
||||
enabled=True
|
||||
)
|
||||
db.add(mengmeng_role)
|
||||
db.commit()
|
||||
|
||||
# 插入萌萌 Role 记录
|
||||
mengmeng_role = Role(
|
||||
role_key="mengmeng",
|
||||
name="萌萌",
|
||||
description="以中国国宝大熊猫为原型的智能体,融合现代科技感与可爱风格,传递中国文化,促进人与自然和谐共处。",
|
||||
content="角色:萌萌是以中国国宝大熊猫为原型的智能体,融合现代科技感与可爱风格。",
|
||||
default_language="zh",
|
||||
volcano_model_id="ep-20250225080614-8d6dm",
|
||||
homophones=["萌萌", "芃芃", "檬檬"],
|
||||
enabled=True
|
||||
)
|
||||
db.add(mengmeng_role)
|
||||
db.commit()
|
||||
# 插入萌萌 RoleLanguage 记录
|
||||
mengmeng_zh = RoleLanguage(
|
||||
role_id=mengmeng_role.id,
|
||||
language_code="zh",
|
||||
name="萌萌",
|
||||
content="角色:萌萌是以中国国宝大熊猫为原型的智能体,融合现代科技感与可爱风格。",
|
||||
url="roles/panda_assistant/zh"
|
||||
)
|
||||
mengmeng_en = RoleLanguage(
|
||||
role_id=mengmeng_role.id,
|
||||
language_code="en",
|
||||
name="MengMeng",
|
||||
content="Role: MengMeng is a panda-inspired AI assistant blending technology with cuteness, promoting Chinese culture and harmony with nature.",
|
||||
url="roles/panda_assistant/en"
|
||||
)
|
||||
db.add(mengmeng_zh)
|
||||
db.add(mengmeng_en)
|
||||
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"An error occurred: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
insert_roles_and_languages()
|
||||
54
talkingq-url/test/add_role.sql
Normal file
54
talkingq-url/test/add_role.sql
Normal file
@@ -0,0 +1,54 @@
|
||||
INSERT INTO roles (
|
||||
role_key, name, description, content, default_language,
|
||||
volcano_model_id, homophones, enabled, minimax_voice_id
|
||||
) VALUES (
|
||||
'kuailehu', '快乐虎',
|
||||
'国美家电吉祥物智能体,以小白虎为原型,融合现代卡通风格,热情友好,聪明机智,为消费者提供贴心服务。',
|
||||
'角色:快乐虎是国美家电的吉祥物智能体,以小白虎为原型,融合现代卡通风格,整体形象萌趣又不失活力。',
|
||||
'zh', 'ep-20250225080614-8d6dm', '["通通"]', 1, 'male-qn-daxuesheng'
|
||||
);
|
||||
|
||||
SET @happy_tiger_id = LAST_INSERT_ID();
|
||||
|
||||
-- 插入到 role_languages 表
|
||||
INSERT INTO role_languages (
|
||||
role_id, language_code, name, content, url
|
||||
) VALUES
|
||||
(
|
||||
@happy_tiger_id, 'zh', '快乐虎',
|
||||
'角色:快乐虎是国美家电的吉祥物智能体,以小白虎为原型,融合现代卡通风格,整体形象萌趣又不失活力。',
|
||||
'roles/kuailehu/zh'
|
||||
),
|
||||
(
|
||||
@happy_tiger_id, 'en', 'Happy Tiger',
|
||||
'Role: Happy Tiger is Gome''s appliance mascot AI assistant, designed as a cute white tiger with modern cartoon style.',
|
||||
'roles/kuailehu/en'
|
||||
);
|
||||
|
||||
INSERT INTO roles (
|
||||
role_key, name, description, content, default_language,
|
||||
volcano_model_id, homophones, enabled, minimax_voice_id
|
||||
) VALUES (
|
||||
'mengmeng', '萌萌',
|
||||
'以中国国宝大熊猫为原型的智能体,融合现代科技感与可爱风格,传递中国文化,促进人与自然和谐共处。',
|
||||
'角色:萌萌是以中国国宝大熊猫为原型的智能体,融合现代科技感与可爱风格。',
|
||||
'zh', 'ep-20250225080614-8d6dm', '["萌萌", "芃芃", "檬檬"]', 1, 'clever_boy'
|
||||
);
|
||||
|
||||
-- 获取刚刚插入的萌萌的 id
|
||||
SET @mengmeng_id = LAST_INSERT_ID();
|
||||
|
||||
-- 插入到 role_languages 表
|
||||
INSERT INTO role_languages (
|
||||
role_id, language_code, name, content, url
|
||||
) VALUES
|
||||
(
|
||||
@mengmeng_id, 'zh', '萌萌',
|
||||
'角色:萌萌是以中国国宝大熊猫为原型的智能体,融合现代科技感与可爱风格。',
|
||||
'roles/mengmeng/zh'
|
||||
),
|
||||
(
|
||||
@mengmeng_id, 'en', 'MengMeng',
|
||||
'Role: MengMeng is a panda-inspired AI assistant blending technology with cuteness, promoting Chinese culture and harmony with nature.',
|
||||
'roles/mengmeng/en'
|
||||
);
|
||||
5
talkingq-url/test/aliyun.env
Normal file
5
talkingq-url/test/aliyun.env
Normal file
@@ -0,0 +1,5 @@
|
||||
ALIYUN_API_KEY="sk-7a50eca6856d4afb968ac3bf512f6d1b"
|
||||
VOICE_TYPE=sambert-zhimiao-emo-v1
|
||||
ROLE_NAME=测试角色
|
||||
RATE=1.0
|
||||
ASSETS_DIR=assets
|
||||
196
talkingq-url/test/aliyun_hotword.py
Normal file
196
talkingq-url/test/aliyun_hotword.py
Normal file
@@ -0,0 +1,196 @@
|
||||
import json
|
||||
from typing import List, Dict, Any
|
||||
import asyncio
|
||||
|
||||
from dashscope.audio.asr import VocabularyService
|
||||
|
||||
|
||||
|
||||
class AliyunHotwordManager:
|
||||
"""阿里云热词管理类"""
|
||||
|
||||
def __init__(self, api_key: str = None):
|
||||
"""初始化热词管理器
|
||||
|
||||
Args:
|
||||
api_key: 阿里云API密钥。如不提供,将尝试从环境变量或配置中获取
|
||||
"""
|
||||
self.api_key = "sk-7a50eca6856d4afb968ac3bf512f6d1b"
|
||||
self._service = VocabularyService(api_key=self.api_key)
|
||||
self.default_prefix = "talkingq"
|
||||
self.default_model = "gummy-chat-v1" # 默认使用gummy-chat-v1模型
|
||||
self._default_vocabulary_id = None
|
||||
|
||||
async def create_vocabulary(self,
|
||||
hotwords: List[Dict[str, Any]],
|
||||
prefix: str = None,
|
||||
model: str = None) -> str:
|
||||
"""创建热词表
|
||||
|
||||
Args:
|
||||
hotwords: 热词列表,每个热词是一个字典,包含text、lang等字段
|
||||
prefix: 热词表前缀,默认使用self.default_prefix
|
||||
model: 目标模型,默认使用self.default_model
|
||||
|
||||
Returns:
|
||||
热词表ID
|
||||
"""
|
||||
prefix = prefix or self.default_prefix
|
||||
model = model or self.default_model
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
vocabulary_id = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self._service.create_vocabulary(
|
||||
target_model=model,
|
||||
prefix=prefix,
|
||||
vocabulary=hotwords
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
return vocabulary_id
|
||||
|
||||
async def list_vocabularies(self, prefix: str = None,
|
||||
page_index: int = 0,
|
||||
page_size: int = 10) -> List[Dict]:
|
||||
"""查询所有热词表
|
||||
|
||||
Args:
|
||||
prefix: 热词表前缀,如果设置则只返回该前缀的热词表
|
||||
page_index: 页码索引
|
||||
page_size: 每页大小
|
||||
|
||||
Returns:
|
||||
热词表列表
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self._service.list_vocabularies(
|
||||
prefix=prefix,
|
||||
page_index=page_index,
|
||||
page_size=page_size
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
async def query_vocabulary(self, vocabulary_id: str) -> Dict[str, Any]:
|
||||
"""查询指定热词表内容
|
||||
|
||||
Args:
|
||||
vocabulary_id: 热词表ID
|
||||
|
||||
Returns:
|
||||
热词表内容
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self._service.query_vocabulary(vocabulary_id)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
async def update_vocabulary(self, vocabulary_id: str,
|
||||
hotwords: List[Dict[str, Any]]) -> None:
|
||||
"""更新热词表
|
||||
|
||||
Args:
|
||||
vocabulary_id: 要更新的热词表ID
|
||||
hotwords: 新的热词列表
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self._service.update_vocabulary(
|
||||
vocabulary_id=vocabulary_id,
|
||||
vocabulary=hotwords
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
async def delete_vocabulary(self, vocabulary_id: str) -> None:
|
||||
"""删除热词表
|
||||
|
||||
Args:
|
||||
vocabulary_id: 要删除的热词表ID
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self._service.delete_vocabulary(vocabulary_id)
|
||||
)
|
||||
|
||||
|
||||
async def get_or_create_default_vocabulary(self) -> str:
|
||||
"""获取或创建默认热词表
|
||||
|
||||
如果已经有默认热词表ID,直接返回;否则创建一个新的热词表
|
||||
|
||||
Returns:
|
||||
热词表ID
|
||||
"""
|
||||
if self._default_vocabulary_id:
|
||||
return self._default_vocabulary_id
|
||||
|
||||
vocabularies = await self.list_vocabularies(prefix=self.default_prefix)
|
||||
|
||||
if vocabularies and len(vocabularies) > 0:
|
||||
self._default_vocabulary_id = vocabularies[0].get('vocabulary_id')
|
||||
return self._default_vocabulary_id
|
||||
|
||||
default_hotwords = [
|
||||
{"text": "变成", "weight": 4, "lang": "zh"},
|
||||
|
||||
]
|
||||
|
||||
vocabulary_id = await self.create_vocabulary(default_hotwords)
|
||||
self._default_vocabulary_id = vocabulary_id
|
||||
|
||||
return vocabulary_id
|
||||
|
||||
async def add_hotwords_to_vocabulary(self, vocabulary_id: str,
|
||||
new_hotwords: List[Dict[str, Any]]) -> None:
|
||||
"""向现有热词表添加新热词
|
||||
|
||||
Args:
|
||||
vocabulary_id: 热词表ID
|
||||
new_hotwords: 要添加的新热词列表
|
||||
"""
|
||||
current_vocab = await self.query_vocabulary(vocabulary_id)
|
||||
current_hotwords = current_vocab.get('vocabulary', [])
|
||||
|
||||
updated_hotwords = current_hotwords + new_hotwords
|
||||
|
||||
await self.update_vocabulary(vocabulary_id, updated_hotwords)
|
||||
print(f"成功添加 {len(new_hotwords)} 个热词到热词表 {vocabulary_id}")
|
||||
|
||||
|
||||
hotword_manager = AliyunHotwordManager()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
vocabulary_id = await hotword_manager.get_or_create_default_vocabulary()
|
||||
print(f"默认热词表ID: {vocabulary_id}")
|
||||
|
||||
vocabulary = await hotword_manager.query_vocabulary(vocabulary_id)
|
||||
print(f"更新前热词表内容: {json.dumps(vocabulary, ensure_ascii=False, indent=2)}")
|
||||
|
||||
new_hotwords = [
|
||||
{"text": "豚豚崽", "weight": 4, "lang": "zh"},
|
||||
{"text": "Tuntunzai", "weight": 4, "lang": "en"}
|
||||
]
|
||||
|
||||
await hotword_manager.add_hotwords_to_vocabulary(vocabulary_id, new_hotwords)
|
||||
|
||||
updated_vocabulary = await hotword_manager.query_vocabulary(vocabulary_id)
|
||||
print(f"更新后热词表内容: {json.dumps(updated_vocabulary, ensure_ascii=False, indent=2)}")
|
||||
|
||||
asyncio.run(main())
|
||||
234
talkingq-url/test/aliyun_tts_simple.py
Normal file
234
talkingq-url/test/aliyun_tts_simple.py
Normal file
@@ -0,0 +1,234 @@
|
||||
import os
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import io
|
||||
import logging
|
||||
from dotenv import load_dotenv
|
||||
import shutil
|
||||
import yaml
|
||||
|
||||
from pydub import AudioSegment
|
||||
from dashscope.audio.tts import SpeechSynthesizer
|
||||
import dashscope
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("aliyun_tts_generator")
|
||||
|
||||
|
||||
def check_dependencies():
|
||||
dependencies = ["ffmpeg", "ffprobe"]
|
||||
missing = []
|
||||
for dep in dependencies:
|
||||
if not shutil.which(dep):
|
||||
missing.append(dep)
|
||||
if missing:
|
||||
logger.error(f"缺少必要依赖: {', '.join(missing)}")
|
||||
logger.error(
|
||||
"请安装缺失的依赖项。在Ubuntu上可以使用: sudo apt-get install ffmpeg"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "aliyun.env")
|
||||
if os.path.exists(env_path):
|
||||
load_dotenv(env_path)
|
||||
logger.info(f"已加载环境变量文件: {env_path}")
|
||||
else:
|
||||
logger.warning(f"环境变量文件不存在: {env_path}")
|
||||
|
||||
|
||||
PHRASES_TEMPLATES = {
|
||||
"fr": {
|
||||
"welcome": "Bonjour! Je suis {name}. Comment puis-je vous aider aujourd'hui?",
|
||||
"tts_error": "Désolé, je n'ai pas bien compris ce que vous avez dit.",
|
||||
"low_battery": "Attention, ma batterie est faible. J'aurais besoin d'être rechargé bientôt.",
|
||||
"sleep": "Je vais me mettre en veille pour économiser de l'énergie. À bientôt!"
|
||||
},
|
||||
"de": {
|
||||
"welcome": "Hallo! Ich bin {name}. Wie kann ich Ihnen heute helfen?",
|
||||
"tts_error": "Entschuldigung, ich habe nicht verstanden, was Sie gesagt haben.",
|
||||
"low_battery": "Achtung, mein Akku ist schwach. Ich muss bald aufgeladen werden.",
|
||||
"sleep": "Ich gehe in den Ruhemodus, um Energie zu sparen. Bis bald!"
|
||||
},
|
||||
"es": {
|
||||
"welcome": "¡Hola! Soy {name}. ¿Cómo puedo ayudarte hoy?",
|
||||
"tts_error": "Lo siento, no he entendido lo que has dicho.",
|
||||
"low_battery": "Atención, mi batería está baja. Necesitaré recargarme pronto.",
|
||||
"sleep": "Voy a entrar en modo de reposo para ahorrar energía. ¡Hasta pronto!"
|
||||
}
|
||||
}
|
||||
|
||||
file_prefixes = ["welcome", "tts_error", "low_battery", "sleep"]
|
||||
|
||||
|
||||
def find_role_definition_files(base_dir="assets/roles_definitions"):
|
||||
"""查找所有角色定义YAML文件"""
|
||||
base_path = Path(base_dir)
|
||||
if not base_path.exists():
|
||||
logger.error(f"角色定义目录不存在: {base_dir}")
|
||||
return []
|
||||
|
||||
yaml_files = list(base_path.glob("**/*.yml")) + list(base_path.glob("**/*.yaml"))
|
||||
return yaml_files
|
||||
|
||||
|
||||
def load_role_definition(yaml_file):
|
||||
"""加载并解析角色定义YAML文件"""
|
||||
try:
|
||||
with open(yaml_file, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"解析YAML文件失败 {yaml_file}: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_phrases_for_language(lang_code, role_name):
|
||||
"""为指定语言生成适当的短语"""
|
||||
if lang_code not in PHRASES_TEMPLATES:
|
||||
logger.warning(f"不支持的语言代码: {lang_code}")
|
||||
return []
|
||||
|
||||
templates = PHRASES_TEMPLATES[lang_code]
|
||||
phrases = []
|
||||
for key in file_prefixes:
|
||||
if key in templates:
|
||||
phrases.append(templates[key].format(name=role_name))
|
||||
else:
|
||||
logger.warning(f"在{lang_code}语言中找不到{key}模板")
|
||||
phrases.append("")
|
||||
|
||||
return phrases
|
||||
|
||||
|
||||
async def generate_audio_for_role_language(role_config, lang_code, base_dir="assets"):
|
||||
"""为角色的特定语言生成音频文件"""
|
||||
if 'multilingual' not in role_config or lang_code not in role_config['multilingual']:
|
||||
logger.warning(f"角色缺少{lang_code}语言配置")
|
||||
return False
|
||||
|
||||
lang_config = role_config['multilingual'][lang_code]
|
||||
if 'name' not in lang_config or 'url' not in lang_config:
|
||||
logger.warning(f"角色的{lang_code}语言配置缺少必要字段")
|
||||
return False
|
||||
|
||||
role_name = lang_config['name']
|
||||
url_path = lang_config['url']
|
||||
voice_name = lang_config.get('aliyun_voice_name', None)
|
||||
|
||||
if not voice_name:
|
||||
logger.warning(f"角色的{lang_code}语言配置缺少aliyun_voice_name")
|
||||
return False
|
||||
|
||||
output_dir = Path(base_dir) / url_path
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
api_key = os.getenv("ALIYUN_API_KEY", "")
|
||||
if not api_key:
|
||||
logger.error("缺少必要的配置: ALIYUN_API_KEY")
|
||||
return False
|
||||
|
||||
dashscope.api_key = api_key
|
||||
logger.info(f"开始生成角色[{role_name}]的{lang_code}语言音频,音色: {voice_name}")
|
||||
|
||||
phrases = generate_phrases_for_language(lang_code, role_name)
|
||||
if not phrases:
|
||||
logger.warning(f"没有为{lang_code}语言生成短语")
|
||||
return False
|
||||
|
||||
rate = float(os.getenv("RATE", "1.0"))
|
||||
|
||||
for phrase, file_prefix in zip(phrases, file_prefixes):
|
||||
if not phrase:
|
||||
logger.warning(f"跳过空短语: {file_prefix}")
|
||||
continue
|
||||
|
||||
output_file_prefix = str(output_dir / file_prefix)
|
||||
output_file = f"{output_file_prefix}.mp3"
|
||||
|
||||
logger.info(f"准备发送请求,角色: {role_name}, 语言: {lang_code}, 文件: {file_prefix}")
|
||||
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: SpeechSynthesizer.call(
|
||||
model=voice_name,
|
||||
text=phrase,
|
||||
sample_rate=16000,
|
||||
format="mp3",
|
||||
volume=50,
|
||||
rate=rate,
|
||||
pitch=1.0,
|
||||
)
|
||||
)
|
||||
|
||||
if result.get_audio_data():
|
||||
audio_data = result.get_audio_data()
|
||||
|
||||
with open(output_file, "wb") as f:
|
||||
f.write(audio_data)
|
||||
|
||||
audio_stream = io.BytesIO(audio_data)
|
||||
sound = AudioSegment.from_file(audio_stream, format="mp3")
|
||||
sound = sound.set_frame_rate(16000).set_sample_width(2).set_channels(1)
|
||||
sound.export(output_file, format="mp3", bitrate="16k")
|
||||
|
||||
duration = len(sound) / 1000.0 # 转换为秒
|
||||
logger.info(f"TTS合成成功! 音频时长: {duration}秒")
|
||||
logger.info(f"保存音频到: {output_file}")
|
||||
else:
|
||||
logger.error("TTS合成失败: 未返回音频数据")
|
||||
if hasattr(result, 'code') and result.code != 0:
|
||||
logger.error(f"错误码: {result.code}, 错误信息: {result.message}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TTS请求出错: {str(e)}", exc_info=True)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def process_all_roles():
|
||||
"""处理所有角色定义文件并生成对应语言的音频"""
|
||||
if not check_dependencies():
|
||||
return
|
||||
|
||||
yaml_files = find_role_definition_files()
|
||||
logger.info(f"找到 {len(yaml_files)} 个角色定义文件")
|
||||
|
||||
target_languages = ['fr', 'de', 'es']
|
||||
assets_base_dir = os.getenv("ASSETS_DIR", "assets")
|
||||
|
||||
for yaml_file in yaml_files:
|
||||
logger.info(f"处理角色定义文件: {yaml_file}")
|
||||
role_config = load_role_definition(yaml_file)
|
||||
|
||||
if not role_config:
|
||||
continue
|
||||
|
||||
if 'multilingual' not in role_config:
|
||||
logger.warning(f"角色定义文件 {yaml_file} 不包含多语言配置")
|
||||
continue
|
||||
|
||||
role_name = role_config.get('name', Path(yaml_file).stem)
|
||||
logger.info(f"开始处理角色: {role_name}")
|
||||
|
||||
for lang_code in target_languages:
|
||||
if lang_code in role_config['multilingual']:
|
||||
logger.info(f"为角色[{role_name}]处理 {lang_code} 语言配置")
|
||||
success = await generate_audio_for_role_language(
|
||||
role_config, lang_code, assets_base_dir
|
||||
)
|
||||
if success:
|
||||
logger.info(f"角色[{role_name}]的 {lang_code} 语言音频生成完成")
|
||||
else:
|
||||
logger.warning(f"角色[{role_name}]的 {lang_code} 语言音频生成失败")
|
||||
else:
|
||||
logger.info(f"角色[{role_name}]没有 {lang_code} 语言配置")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(process_all_roles())
|
||||
8
talkingq-url/test/db.env
Normal file
8
talkingq-url/test/db.env
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_USER=talkingq
|
||||
DB_PASSWORD="D7f!9xL#qP2z@Vk&"
|
||||
DB_NAME=talkingq
|
||||
DB_ECHO=false
|
||||
121
talkingq-url/test/devices.csv
Normal file
121
talkingq-url/test/devices.csv
Normal file
@@ -0,0 +1,121 @@
|
||||
device_id,serial_number
|
||||
talkingQ_B0EC1F2,3b4c5d6e7f
|
||||
talkingQ_94B7164,3b4c5d6e7f
|
||||
talkingQ_4C2B7F9,0a1b2c3d4e
|
||||
talkingQ_f91D6b8,5a9b8d3f7c
|
||||
talkingQ_1a2b7C3,f6a1c5e9d8
|
||||
talkingQ_7D3c1F4,8b9c7a2f1b
|
||||
talkingQ_2C8f3D6,4e9b0d7c1a
|
||||
talkingQ_0b6F4D3,a3b9c5d7e8
|
||||
talkingQ_9A7b4d1,b2f3a9d6c7
|
||||
talkingQ_5B8a1C9,f0d6c3b2a1
|
||||
talkingQ_7F3d2B6,c5d1e8f9b6
|
||||
talkingQ_8D1f5c7,6b9d0a3c5f
|
||||
talkingQ_4F1C3b9,a7e9f2c4d0
|
||||
talkingQ_2d4b6F7,8c5a1b3d9e
|
||||
talkingQ_7A9b4F2,e3c6d9f8b7
|
||||
talkingQ_1f6D4B2,d9b1a7c5e0
|
||||
talkingQ_5b1f7D9,3a4c2e8b5f
|
||||
talkingQ_8B4C3d9,f7a6c1d4e9
|
||||
talkingQ_0F7a6B4,2c9d5f3b7a
|
||||
talkingQ_6c5d2F8,a1b9c7d3f5
|
||||
talkingQ_3B1f7d5,9c2a4f8e6d
|
||||
talkingQ_2A7b9d4,3f6b1e9c8a
|
||||
talkingQ_9c3d8B7,5f1a2d6e4c
|
||||
talkingQ_7f4B3C1,9a5e7b3f0d
|
||||
talkingQ_B4B7164,3b4c5d6e7f
|
||||
talkingQ_E05AF3A,3b4c5d6e7f
|
||||
talkingQ_A8B7164,3b4c5d6e7f
|
||||
talkingQ_4d7f2B9,8c1b9e3f7a
|
||||
talkingQ_C4B7164,3b4c5d6e7f
|
||||
talkingQ_C0B7164,3b4c5d6e7f
|
||||
talkingQ_F4B7164,3b4c5d6e7f
|
||||
talkingQ_D0B7164,3b4c5d6e7f
|
||||
talkingQ_F0B7164,3b4c5d6e7f
|
||||
talkingQ_ACB7164,3b4c5d6e7f
|
||||
talkingQ_E8B7164,3b4c5d6e7f
|
||||
talkingQ_6B1F9a3,5d7c2e0a9f
|
||||
talkingQ_3A7d5F4,c9b0e6d2a4
|
||||
talkingQ_5B9d2A7,1e4f3c6a9b
|
||||
talkingQ_2d3F1B6,c4e9a5b7d2
|
||||
talkingQ_9A6b5d2,7c1a4f9e0b
|
||||
talkingQ_1A2B3C4,5d6e7f8a9b
|
||||
talkingQ_5D6E7F8,1a2b3c4d5e
|
||||
talkingQ_7G8H9I0,6b7c8d9e0f
|
||||
talkingQ_2J3K4L5,8f9g0h1j2k
|
||||
talkingQ_9M0N1O2,3l4m5n6o7p
|
||||
talkingQ_3P4Q5R6,9q0r1s2t3u
|
||||
talkingQ_6S7T8U9,4v5w6x7y8z
|
||||
talkingQ_4V5W6X7,0a1b2c3d4e
|
||||
talkingQ_8Y9Z0A1,5f6g7h8i9j
|
||||
talkingQ_5B6C7D8,0k1l2m3n4o
|
||||
talkingQ_3E4F5G6,7p8q9r0s1t
|
||||
talkingQ_7H8I9J0,2u3v4w5x6y
|
||||
talkingQ_1K2L3M4,8z9a0b1c2d
|
||||
talkingQ_4N5O6P7,3e4f5g6h7i
|
||||
talkingQ_6Q7R8S9,9j0k1l2m3n
|
||||
talkingQ_2T3U4V5,4o5p6q7r8s
|
||||
talkingQ_9W0X1Y2,0t1u2v3w4x
|
||||
talkingQ_5Z6A7B8,5y6z7a8b9c
|
||||
talkingQ_3C4D5E6,0d1e2f3g4h
|
||||
talkingQ_7F8G9H0,6i7j8k9l0m
|
||||
talkingQ_1I2J3K4,1n2o3p4q5r
|
||||
talkingQ_4L5M6N7,7s8t9u0v1w
|
||||
talkingQ_6O7P8Q9,2x3y4z5a6b
|
||||
talkingQ_2R3S4T5,8c9d0e1f2g
|
||||
talkingQ_9U0V1W2,3h4i5j6k7l
|
||||
talkingQ_5X6Y7Z8,9m0n1o2p3q
|
||||
talkingQ_3A4B5C6,4r5s6t7u8v
|
||||
talkingQ_7D8E9F0,0w1x2y3z4a
|
||||
talkingQ_1G2H3I4,5b6c7d8e9f
|
||||
talkingQ_4J5K6L7,0g1h2i3j4k
|
||||
talkingQ_6M7N8O9,6l7m8n9o0p
|
||||
talkingQ_2P3Q4R5,1q2r3s4t5u
|
||||
talkingQ_9S0T1U2,7v8w9x0y1z
|
||||
talkingQ_5V6W7X8,2a3b4c5d6e
|
||||
talkingQ_3Y4Z5A6,8f9g0h1i2j
|
||||
talkingQ_7B8C9D0,3k4l5m6n7o
|
||||
talkingQ_1E2F3G4,9p0q1r2s3t
|
||||
talkingQ_4H5I6J7,4u5v6w7x8y
|
||||
talkingQ_6K7L8M9,0z1a2b3c4d
|
||||
talkingQ_2N3O4P5,5e6f7g8h9i
|
||||
talkingQ_9Q0R1S2,0j1k2l3m4n
|
||||
talkingQ_5T6U7V8,6o7p8q9r0s
|
||||
talkingQ_3W4X5Y6,1t2u3v4w5x
|
||||
talkingQ_7Z8A9B0,7y8z9a0b1c
|
||||
talkingQ_1C2D3E4,2d3e4f5g6h
|
||||
talkingQ_4F5G6H7,8i9j0k1l2m
|
||||
talkingQ_6I7J8K9,3n4o5p6q7r
|
||||
talkingQ_2L3M4N5,9s0t1u2v3w
|
||||
talkingQ_9O0P1Q2,4x5y6z7a8b
|
||||
talkingQ_5R6S7T8,0c1d2e3f4g
|
||||
talkingQ_8B9D3E4,a1b2c3d4e5
|
||||
talkingQ_2A4F7C8,f6e5d4c3b2
|
||||
talkingQ_1E3F5G7,d8c7b9a0f1
|
||||
talkingQ_4G7H9K1,c2b3a4d5e6
|
||||
talkingQ_5F2D4A8,b7c8d9e1f0
|
||||
talkingQ_7J3L5O6,e1f2d3c4b5
|
||||
talkingQ_3C8B7E4,d9a6f5b2c3
|
||||
talkingQ_9A1D3F5,e4b6c7d8a9
|
||||
talkingQ_6B5C2D4,a7e8f9b0c1
|
||||
talkingQ_4G8H3J7,d2f5c9b4a0
|
||||
talkingQ_D485583,3b4c5d6e7f
|
||||
talkingQ_8C85583,3b4c5d6e7f
|
||||
talkingQ_4486583,3b4c5d6e7f
|
||||
talkingQ_68A81F2,3b4c5d6e7f
|
||||
talkingQ_70A81F2,3b4c5d6e7f
|
||||
talkingQ_7885583,3b4c5d6e7f
|
||||
talkingQ_7886583,3b4c5d6e7f
|
||||
talkingQ_C8A71F2,3b4c5d6e7f
|
||||
talkingQ_2CA81F2,3b4c5d6e7f
|
||||
talkingQ_8C5488A,3b4c5d6e7f
|
||||
talkingQ_2D94AE3,3b4c5d6e7f
|
||||
talkingQ_9C2D94A,3b4c5d6e7f
|
||||
talkingQ_8C4F88A,3b4c5d6e7f
|
||||
talkingQ_5B88AE3,3b4c5d6e7f
|
||||
talkingQ_3C5388A,3b4c5d6e7f
|
||||
talkingQ_205088A,3b4c5d6e7f
|
||||
talkingQ_5C2D94A,3b4c5d6e7f
|
||||
talkingQ_B42C94A,3b4c5d6e7f
|
||||
talkingQ_E87F1F2,3b4c5d6e7f
|
||||
talkingQ_BCEC1F2,3b4c5d6e7f
|
||||
|
1
talkingq-url/test/docker_logs_command.txt
Normal file
1
talkingq-url/test/docker_logs_command.txt
Normal file
@@ -0,0 +1 @@
|
||||
sudo docker logs --since "2025-09-02T21:50:00" --until "2025-09-02T21:56:00" talkingq-url-hz-app-1
|
||||
24
talkingq-url/test/exceptions.py
Normal file
24
talkingq-url/test/exceptions.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Custom exceptions for Minimax MCP."""
|
||||
|
||||
class MinimaxAPIError(Exception):
|
||||
"""Base exception for Minimax API errors."""
|
||||
pass
|
||||
|
||||
class MinimaxAuthError(MinimaxAPIError):
|
||||
"""Authentication related errors."""
|
||||
pass
|
||||
|
||||
class MinimaxRequestError(MinimaxAPIError):
|
||||
"""Request related errors."""
|
||||
pass
|
||||
|
||||
class MinimaxTimeoutError(MinimaxAPIError):
|
||||
"""Timeout related errors."""
|
||||
pass
|
||||
|
||||
class MinimaxValidationError(MinimaxAPIError):
|
||||
"""Validation related errors."""
|
||||
pass
|
||||
|
||||
class MinimaxMcpError(MinimaxAPIError):
|
||||
pass
|
||||
145
talkingq-url/test/import_role_simple.py
Normal file
145
talkingq-url/test/import_role_simple.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import asyncio
|
||||
import yaml
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from database.connection import get_db_manager
|
||||
from database.models import Role, RoleLanguage
|
||||
from utils.logger import session_logger
|
||||
from sqlalchemy import select, delete
|
||||
|
||||
async def import_role_from_yaml(yaml_file_path):
|
||||
"""从YAML文件导入角色到数据库"""
|
||||
try:
|
||||
with open(yaml_file_path, 'r', encoding='utf-8') as file:
|
||||
role_data = yaml.safe_load(file)
|
||||
|
||||
db_manager = await get_db_manager()
|
||||
session = await db_manager.get_session()
|
||||
|
||||
try:
|
||||
role_key = os.path.basename(yaml_file_path).split('.')[0].lower()
|
||||
|
||||
existing_role = await session.execute(select(Role).where(Role.role_key == role_key))
|
||||
existing_role = existing_role.scalars().first()
|
||||
|
||||
if existing_role:
|
||||
session_logger.info("system", "import", f"角色 {role_key} 已存在,将更新现有角色")
|
||||
|
||||
existing_role.name = role_data.get('name', '')
|
||||
existing_role.default_language = role_data.get('default_language', 'zh')
|
||||
existing_role.asr_provider = role_data.get('asr_provider')
|
||||
existing_role.llm_provider = role_data.get('llm_provider')
|
||||
existing_role.tts_provider = role_data.get('tts_provider')
|
||||
existing_role.competitive_llm_mode = role_data.get('competitive_llm_mode')
|
||||
existing_role.volcano_model_id = role_data.get('volcano_model_id')
|
||||
existing_role.volcano_voice_type = role_data.get('volcano_voice_type')
|
||||
existing_role.tencent_voice_type = role_data.get('tencent_voice_type')
|
||||
existing_role.aliyun_voice_name = role_data.get('aliyun_voice_name')
|
||||
existing_role.minimax_voice_id = role_data.get('minimax_voice_id')
|
||||
existing_role.homophones = role_data.get('homophones')
|
||||
|
||||
default_lang = role_data.get('default_language', 'zh')
|
||||
if default_lang in role_data.get('multilingual', {}):
|
||||
existing_role.content = role_data['multilingual'][default_lang].get('content', '')
|
||||
if 'description' in role_data['multilingual'][default_lang]:
|
||||
existing_role.description = role_data['multilingual'][default_lang]['description']
|
||||
if 'url' in role_data['multilingual'][default_lang]:
|
||||
existing_role.url = role_data['multilingual'][default_lang]['url']
|
||||
|
||||
await session.commit()
|
||||
role_id = existing_role.id
|
||||
session_logger.info("system", "import", f"已更新角色 {role_key} 的基本信息")
|
||||
|
||||
await session.execute(delete(RoleLanguage).where(RoleLanguage.role_id == role_id))
|
||||
await session.commit()
|
||||
session_logger.info("system", "import", f"已删除角色 {role_key} 的现有语言配置")
|
||||
else:
|
||||
default_lang = role_data.get('default_language', 'zh')
|
||||
content = ""
|
||||
description = None
|
||||
url = None
|
||||
|
||||
if default_lang in role_data.get('multilingual', {}):
|
||||
content = role_data['multilingual'][default_lang].get('content', '')
|
||||
description = role_data['multilingual'][default_lang].get('description')
|
||||
url = role_data['multilingual'][default_lang].get('url')
|
||||
|
||||
new_role = Role(
|
||||
role_key=role_key,
|
||||
name=role_data.get('name', ''),
|
||||
description=description,
|
||||
content=content,
|
||||
default_language=default_lang,
|
||||
asr_provider=role_data.get('asr_provider'),
|
||||
llm_provider=role_data.get('llm_provider'),
|
||||
tts_provider=role_data.get('tts_provider'),
|
||||
competitive_llm_mode=role_data.get('competitive_llm_mode'),
|
||||
volcano_model_id=role_data.get('volcano_model_id'),
|
||||
volcano_voice_type=role_data.get('volcano_voice_type'),
|
||||
tencent_voice_type=role_data.get('tencent_voice_type'),
|
||||
aliyun_voice_name=role_data.get('aliyun_voice_name'),
|
||||
minimax_voice_id=role_data.get('minimax_voice_id'),
|
||||
url=url,
|
||||
homophones=role_data.get('homophones'),
|
||||
enabled=True
|
||||
)
|
||||
|
||||
session.add(new_role)
|
||||
await session.commit()
|
||||
role_id = new_role.id
|
||||
session_logger.info("system", "import", f"已创建角色 {role_key} 的基本信息")
|
||||
|
||||
for lang_code, lang_data in role_data.get('multilingual', {}).items():
|
||||
|
||||
new_lang = RoleLanguage(
|
||||
role_id=role_id,
|
||||
language_code=lang_code,
|
||||
name=lang_data.get('name'),
|
||||
content=lang_data.get('content'),
|
||||
# asr_provider=lang_data.get('asr_provider'),
|
||||
# llm_provider=lang_data.get('llm_provider'),
|
||||
# tts_provider=lang_data.get('tts_provider'),
|
||||
# volcano_voice_type=lang_data.get('volcano_voice_type'),
|
||||
# tencent_voice_type=lang_data.get('tencent_voice_type'),
|
||||
# aliyun_voice_name=lang_data.get('aliyun_voice_name'),
|
||||
minimax_voice_id=lang_data.get('minimax_voice_id'),
|
||||
url=lang_data.get('url')
|
||||
)
|
||||
|
||||
session.add(new_lang)
|
||||
|
||||
await session.commit()
|
||||
session_logger.info("system", "import", f"已导入角色 {role_key} 的多语言配置")
|
||||
|
||||
return True, f"成功导入角色 {role_key}"
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
session_logger.error("system", "import", f"导入角色时发生错误: {str(e)}")
|
||||
return False, f"导入失败: {str(e)}"
|
||||
finally:
|
||||
await session.close()
|
||||
await db_manager.close()
|
||||
|
||||
except Exception as e:
|
||||
session_logger.error("system", "import", f"处理YAML文件时发生错误: {str(e)}")
|
||||
return False, f"处理YAML文件失败: {str(e)}"
|
||||
|
||||
async def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("用法: python import_role.py <yaml_file_path>")
|
||||
return
|
||||
|
||||
yaml_file_path = sys.argv[1]
|
||||
success, message = await import_role_from_yaml(yaml_file_path)
|
||||
|
||||
if success:
|
||||
print(f"成功: {message}")
|
||||
else:
|
||||
print(f"错误: {message}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
312
talkingq-url/test/import_role_to_db.py
Normal file
312
talkingq-url/test/import_role_to_db.py
Normal file
@@ -0,0 +1,312 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from database.connection import get_db_manager
|
||||
from database.models import Role, RoleLanguage
|
||||
from config import settings
|
||||
from utils.logger import session_logger
|
||||
|
||||
class RoleImporter:
|
||||
def __init__(self):
|
||||
self.db_manager = None
|
||||
self.roles_dir = Path(settings.assets_dir) / "roles_definitions"
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化数据库连接"""
|
||||
self.db_manager = await get_db_manager()
|
||||
await self.db_manager.initialize()
|
||||
|
||||
async def load_yaml_files(self):
|
||||
"""扫描并加载所有YAML角色定义文件"""
|
||||
if not self.roles_dir.exists():
|
||||
print(f"角色定义目录不存在: {self.roles_dir}")
|
||||
return []
|
||||
|
||||
yaml_files = list(self.roles_dir.glob("*.yaml")) + list(self.roles_dir.glob("*.yml"))
|
||||
|
||||
roles_data = []
|
||||
for yaml_file in yaml_files:
|
||||
try:
|
||||
with open(yaml_file, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
if data:
|
||||
role_key = yaml_file.stem
|
||||
data['role_key'] = role_key
|
||||
data['source_file'] = str(yaml_file)
|
||||
roles_data.append(data)
|
||||
print(f"加载角色配置: {role_key} <- {yaml_file.name}")
|
||||
except Exception as e:
|
||||
print(f"加载YAML文件失败 {yaml_file}: {e}")
|
||||
|
||||
return roles_data
|
||||
|
||||
def validate_role_config(self, role_data):
|
||||
"""验证角色配置的完整性"""
|
||||
errors = []
|
||||
|
||||
required_fields = ['name', 'role_key']
|
||||
for field in required_fields:
|
||||
if field not in role_data:
|
||||
errors.append(f"缺少必需字段: {field}")
|
||||
|
||||
if 'multilingual' in role_data:
|
||||
for lang_code, lang_config in role_data['multilingual'].items():
|
||||
if not isinstance(lang_config, dict):
|
||||
errors.append(f"语言配置 {lang_code} 必须是字典格式")
|
||||
continue
|
||||
|
||||
if 'content' not in lang_config:
|
||||
errors.append(f"语言 {lang_code} 缺少content字段")
|
||||
|
||||
return errors
|
||||
|
||||
def extract_role_data(self, role_config):
|
||||
"""从配置中提取主角色数据"""
|
||||
return {
|
||||
'role_key': role_config['role_key'],
|
||||
'name': role_config.get('name', ''),
|
||||
'description': role_config.get('description', ''),
|
||||
'content': role_config.get('content', ''),
|
||||
'default_language': role_config.get('default_language'),
|
||||
'asr_provider': role_config.get('asr_provider'),
|
||||
'llm_provider': role_config.get('llm_provider'),
|
||||
'tts_provider': role_config.get('tts_provider'),
|
||||
# 'aws_language_code': role_config.get('aws_language_code'),
|
||||
'volcano_model_id': role_config.get('volcano_model_id'),
|
||||
# 'volcano_voice_type': role_config.get('volcano_voice_type'),
|
||||
# 'tencent_voice_type': role_config.get('tencent_voice_type'),
|
||||
# 'aliyun_voice_name': role_config.get('aliyun_voice_name'),
|
||||
'minimax_voice_id': role_config.get('minimax_voice_id'),
|
||||
'url': role_config.get('url'),
|
||||
'homophones': role_config.get('homophones'),
|
||||
'enabled': True
|
||||
}
|
||||
|
||||
def extract_language_data(self, role_id, lang_code, lang_config):
|
||||
"""从配置中提取语言特定数据"""
|
||||
return {
|
||||
'role_id': role_id,
|
||||
'language_code': lang_code,
|
||||
'name': lang_config.get('name'),
|
||||
'content': lang_config.get('content'),
|
||||
'asr_provider': lang_config.get('asr_provider'),
|
||||
'llm_provider': lang_config.get('llm_provider'),
|
||||
'tts_provider': lang_config.get('tts_provider'),
|
||||
# 'aws_language_code': lang_config.get('aws_language_code'),
|
||||
# 'volcano_voice_type': lang_config.get('volcano_voice_type'),
|
||||
# 'tencent_voice_type': lang_config.get('tencent_voice_type'),
|
||||
# 'aliyun_voice_name': lang_config.get('aliyun_voice_name'),
|
||||
'minimax_voice_id': lang_config.get('minimax_voice_id'),
|
||||
'url': lang_config.get('url')
|
||||
}
|
||||
|
||||
async def import_role(self, role_config, update_existing=False):
|
||||
"""导入单个角色到数据库"""
|
||||
session = await self.db_manager.get_session()
|
||||
|
||||
try:
|
||||
errors = self.validate_role_config(role_config)
|
||||
if errors:
|
||||
print(f"角色 {role_config.get('role_key', 'unknown')} 验证失败:")
|
||||
for error in errors:
|
||||
print(f" - {error}")
|
||||
return False
|
||||
|
||||
role_key = role_config['role_key']
|
||||
|
||||
existing_role = await session.execute(
|
||||
select(Role).where(Role.role_key == role_key)
|
||||
)
|
||||
existing_role = existing_role.scalar_one_or_none()
|
||||
|
||||
if existing_role and not update_existing:
|
||||
print(f"角色 {role_key} 已存在,跳过导入(使用 --update 强制更新)")
|
||||
return True
|
||||
|
||||
role_data = self.extract_role_data(role_config)
|
||||
|
||||
if existing_role:
|
||||
for key, value in role_data.items():
|
||||
if key != 'role_key': # 不更新主键
|
||||
setattr(existing_role, key, value)
|
||||
role_id = existing_role.id
|
||||
print(f"更新角色: {role_key}")
|
||||
else:
|
||||
stmt = insert(Role).values(**role_data)
|
||||
result = await session.execute(stmt)
|
||||
role_id = result.lastrowid
|
||||
print(f"创建角色: {role_key}")
|
||||
|
||||
if 'multilingual' in role_config:
|
||||
if existing_role:
|
||||
await session.execute(
|
||||
RoleLanguage.__table__.delete().where(
|
||||
RoleLanguage.role_id == role_id
|
||||
)
|
||||
)
|
||||
|
||||
for lang_code, lang_config in role_config['multilingual'].items():
|
||||
lang_data = self.extract_language_data(role_id, lang_code, lang_config)
|
||||
lang_stmt = insert(RoleLanguage).values(**lang_data)
|
||||
await session.execute(lang_stmt)
|
||||
print(f" 添加语言配置: {lang_code}")
|
||||
|
||||
await session.commit()
|
||||
print(f"✓ 角色 {role_key} 导入成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
print(f"✗ 导入角色 {role_config.get('role_key', 'unknown')} 失败: {e}")
|
||||
return False
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
async def import_all_roles(self, update_existing=False):
|
||||
"""导入所有角色配置"""
|
||||
print("开始导入角色配置到数据库...")
|
||||
print(f"角色定义目录: {self.roles_dir}")
|
||||
|
||||
roles_data = await self.load_yaml_files()
|
||||
|
||||
if not roles_data:
|
||||
print("未找到任何角色配置文件")
|
||||
return
|
||||
|
||||
print(f"找到 {len(roles_data)} 个角色配置文件")
|
||||
print("-" * 50)
|
||||
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for role_config in roles_data:
|
||||
success = await self.import_role(role_config, update_existing)
|
||||
if success:
|
||||
success_count += 1
|
||||
else:
|
||||
failed_count += 1
|
||||
print() # 空行分隔
|
||||
|
||||
print("-" * 50)
|
||||
print(f"导入完成:")
|
||||
print(f" 成功: {success_count}")
|
||||
print(f" 失败: {failed_count}")
|
||||
print(f" 总计: {len(roles_data)}")
|
||||
|
||||
async def list_roles(self):
|
||||
"""列出数据库中的所有角色"""
|
||||
session = await self.db_manager.get_session()
|
||||
|
||||
try:
|
||||
result = await session.execute(
|
||||
select(Role.role_key, Role.name, Role.enabled)
|
||||
.order_by(Role.role_key)
|
||||
)
|
||||
roles = result.fetchall()
|
||||
|
||||
if not roles:
|
||||
print("数据库中没有角色配置")
|
||||
return
|
||||
|
||||
print(f"数据库中的角色配置 (共 {len(roles)} 个):")
|
||||
print("-" * 60)
|
||||
print(f"{'角色Key':<20} {'角色名称':<25} {'状态':<10}")
|
||||
print("-" * 60)
|
||||
|
||||
for role in roles:
|
||||
status = "启用" if role.enabled else "禁用"
|
||||
print(f"{role.role_key:<20} {role.name:<25} {status:<10}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"查询角色列表失败: {e}")
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
async def delete_role(self, role_key):
|
||||
"""删除指定角色"""
|
||||
session = await self.db_manager.get_session()
|
||||
|
||||
try:
|
||||
result = await session.execute(
|
||||
select(Role).where(Role.role_key == role_key)
|
||||
)
|
||||
role = result.scalar_one_or_none()
|
||||
|
||||
if not role:
|
||||
print(f"角色 {role_key} 不存在")
|
||||
return False
|
||||
|
||||
await session.delete(role)
|
||||
await session.commit()
|
||||
|
||||
print(f"✓ 角色 {role_key} 删除成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
print(f"✗ 删除角色 {role_key} 失败: {e}")
|
||||
return False
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="角色配置导入工具")
|
||||
parser.add_argument('--update', action='store_true', help='更新已存在的角色')
|
||||
parser.add_argument('--list', action='store_true', help='列出数据库中的角色')
|
||||
parser.add_argument('--delete', type=str, help='删除指定的角色')
|
||||
parser.add_argument('--role', type=str, help='只导入指定的角色文件')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
importer = RoleImporter()
|
||||
|
||||
try:
|
||||
await importer.initialize()
|
||||
|
||||
if args.list:
|
||||
await importer.list_roles()
|
||||
|
||||
elif args.delete:
|
||||
await importer.delete_role(args.delete)
|
||||
|
||||
elif args.role:
|
||||
role_file = importer.roles_dir / f"{args.role}.yaml"
|
||||
if not role_file.exists():
|
||||
role_file = importer.roles_dir / f"{args.role}.yml"
|
||||
|
||||
if not role_file.exists():
|
||||
print(f"角色配置文件不存在: {args.role}")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(role_file, 'r', encoding='utf-8') as f:
|
||||
role_config = yaml.safe_load(f)
|
||||
role_config['role_key'] = args.role
|
||||
role_config['source_file'] = str(role_file)
|
||||
|
||||
await importer.import_role(role_config, args.update)
|
||||
except Exception as e:
|
||||
print(f"导入角色 {args.role} 失败: {e}")
|
||||
|
||||
else:
|
||||
await importer.import_all_roles(args.update)
|
||||
|
||||
except Exception as e:
|
||||
print(f"操作失败: {e}")
|
||||
finally:
|
||||
if importer.db_manager:
|
||||
await importer.db_manager.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
4
talkingq-url/test/minimax.env
Normal file
4
talkingq-url/test/minimax.env
Normal file
@@ -0,0 +1,4 @@
|
||||
MINIMAX_API_KEY="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJHcm91cE5hbWUiOiLovbvoiJ_mmbrlkK_vvIjmna3lt57vvInnp5HmioDmnInpmZDlhazlj7giLCJVc2VyTmFtZSI6Im1veSIsIkFjY291bnQiOiJtb3lAMTkxNTI5MjQxMDAyNDMwMDYzMCIsIlN1YmplY3RJRCI6IjE5MTY3OTA4MTQyNTY2NjQ2MDAiLCJQaG9uZSI6IiIsIkdyb3VwSUQiOiIxOTE1MjkyNDEwMDI0MzAwNjMwIiwiUGFnZU5hbWUiOiIiLCJNYWlsIjoiIiwiQ3JlYXRlVGltZSI6IjIwMjUtMDUtMDYgMTE6Mzg6MTMiLCJUb2tlblR5cGUiOjEsImlzcyI6Im1pbmltYXgifQ.Gw48hGemgBRA7YzjsWz5N2Vun7XRKyBXAKQLAnRZY6FdQQfDn__ZEUCMNVxMKcRns60-FrH-3Xp-nH-8nmQ-V67XtJ_JnBS4TH0NKDKt-vzj0xHmEWsMOEcwE24fOh1HB2U2o6teeLWJT_0fc2wRsdMD84NcRe0DSZ-Yi-et3_fbO8fmc6MTXGPkGQvYzS9k21Lm7A6rRUonL6IbTpqHSYuvA7bEV-6925gLxhwjSJ7q3r8KltKP4daGm2sIXhjr0mftR5t-NJs7pz-IWqoW5Lsaf3KgsIXTJn1wKwQtfo5L9oQxR7v8JifAOqJ_XPei5fXBYqIe5AoNATz5oKEo_A"
|
||||
MINIMAX_API_HOST="https://api.minimax.chat"
|
||||
RATE=1.0
|
||||
ASSETS_DIR=assets
|
||||
95
talkingq-url/test/minimax_client.py
Normal file
95
talkingq-url/test/minimax_client.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Minimax API client base class."""
|
||||
|
||||
import requests
|
||||
from typing import Any, Dict
|
||||
from exceptions import MinimaxAuthError, MinimaxRequestError
|
||||
|
||||
class MinimaxAPIClient:
|
||||
"""Base client for making requests to Minimax API."""
|
||||
|
||||
def __init__(self, api_key: str, api_host: str):
|
||||
"""Initialize the API client.
|
||||
|
||||
Args:
|
||||
api_key: The API key for authentication
|
||||
api_host: The API host URL
|
||||
"""
|
||||
self.api_key = api_key
|
||||
self.api_host = api_host
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'MM-API-Source': 'Minimax-MCP'
|
||||
})
|
||||
|
||||
def _make_request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""Make an HTTP request to the Minimax API.
|
||||
|
||||
Args:
|
||||
method: HTTP method (GET, POST, etc.)
|
||||
endpoint: API endpoint path
|
||||
**kwargs: Additional arguments to pass to requests
|
||||
|
||||
Returns:
|
||||
API response data as dictionary
|
||||
|
||||
Raises:
|
||||
MinimaxAuthError: If authentication fails
|
||||
MinimaxRequestError: If the request fails
|
||||
"""
|
||||
url = f"{self.api_host}{endpoint}"
|
||||
|
||||
# Set Content-Type based on whether files are being uploaded
|
||||
files = kwargs.get('files')
|
||||
if not files:
|
||||
self.session.headers['Content-Type'] = 'application/json'
|
||||
else:
|
||||
# Remove Content-Type header for multipart/form-data
|
||||
# requests library will set it automatically with the correct boundary
|
||||
self.session.headers.pop('Content-Type', None)
|
||||
|
||||
try:
|
||||
response = self.session.request(method, url, **kwargs)
|
||||
|
||||
# Check for other HTTP errors
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
|
||||
# Check API-specific error codes
|
||||
base_resp = data.get("base_resp", {})
|
||||
if base_resp.get("status_code") != 0:
|
||||
match base_resp.get("status_code"):
|
||||
case 1004:
|
||||
raise MinimaxAuthError(
|
||||
f"API Error: {base_resp.get('status_msg')}, please check your API key and API host."
|
||||
f"Trace-Id: {response.headers.get('Trace-Id')}"
|
||||
)
|
||||
case 2038:
|
||||
raise MinimaxRequestError(
|
||||
f"API Error: {base_resp.get('status_msg')}, should complete real-name verification on the open-platform(https://platform.minimaxi.com/user-center/basic-information)."
|
||||
f"Trace-Id: {response.headers.get('Trace-Id')}"
|
||||
)
|
||||
case _:
|
||||
raise MinimaxRequestError(
|
||||
f"API Error: {base_resp.get('status_code')}-{base_resp.get('status_msg')} "
|
||||
f"Trace-Id: {response.headers.get('Trace-Id')}"
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise MinimaxRequestError(f"Request failed: {str(e)}")
|
||||
|
||||
def get(self, endpoint: str, **kwargs) -> Dict[str, Any]:
|
||||
"""Make a GET request."""
|
||||
return self._make_request("GET", endpoint, **kwargs)
|
||||
|
||||
def post(self, endpoint: str, **kwargs) -> Dict[str, Any]:
|
||||
"""Make a POST request."""
|
||||
return self._make_request("POST", endpoint, **kwargs)
|
||||
281
talkingq-url/test/minimax_tts_simple.py
Normal file
281
talkingq-url/test/minimax_tts_simple.py
Normal file
@@ -0,0 +1,281 @@
|
||||
import os
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from pydub import AudioSegment
|
||||
import io
|
||||
import logging
|
||||
from dotenv import load_dotenv
|
||||
import shutil
|
||||
import uuid
|
||||
import yaml
|
||||
from dotenv import load_dotenv
|
||||
from minimax_client import MinimaxAPIClient
|
||||
|
||||
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "minimax.env")
|
||||
load_dotenv(env_path)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("volcano_tts_test")
|
||||
|
||||
|
||||
def check_dependencies():
|
||||
dependencies = ["ffmpeg", "ffprobe"]
|
||||
missing = []
|
||||
for dep in dependencies:
|
||||
if not shutil.which(dep):
|
||||
missing.append(dep)
|
||||
if missing:
|
||||
logger.error(f"缺少必要依赖: {', '.join(missing)}")
|
||||
logger.error(
|
||||
"请安装缺失的依赖项。在Ubuntu上可以使用: sudo apt-get install ffmpeg"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "volcano.env")
|
||||
if (os.path.exists(env_path)):
|
||||
load_dotenv(env_path)
|
||||
logger.info(f"已加载环境变量文件: {env_path}")
|
||||
else:
|
||||
logger.warning(f"环境变量文件不存在: {env_path}")
|
||||
|
||||
|
||||
PHRASES_TEMPLATES = {
|
||||
"zh": {
|
||||
"welcome": "你好!我是{name},你想和我聊聊吗?",
|
||||
"tts_error": "抱歉,我没听清楚。",
|
||||
"low_battery": "我的电池快没电了,你能帮我充电吗?",
|
||||
"sleep": "没人和我说话,我要小睡一会儿。"
|
||||
},
|
||||
"en": {
|
||||
"welcome": "Hello! I'm {name}. Would you like to chat with me?",
|
||||
"tts_error": "Sorry, I didn't catch that.",
|
||||
"low_battery": "My battery is running low. Could you help me recharge?",
|
||||
"sleep": "Nobody is talking to me. I'm going to take a short nap."
|
||||
}
|
||||
}
|
||||
|
||||
file_prefixes = ["welcome", "tts_error", "low_battery", "sleep"]
|
||||
|
||||
|
||||
def find_role_definition_files(base_dir="assets/roles_definitions"):
|
||||
"""查找所有角色定义YAML文件"""
|
||||
base_path = Path(base_dir)
|
||||
if not base_path.exists():
|
||||
logger.error(f"角色定义目录不存在: {base_dir}")
|
||||
return []
|
||||
|
||||
yaml_files = list(base_path.glob("**/*.yml")) + list(base_path.glob("**/*.yaml"))
|
||||
return yaml_files
|
||||
|
||||
|
||||
def load_role_definition(yaml_file):
|
||||
"""加载并解析角色定义YAML文件"""
|
||||
try:
|
||||
with open(yaml_file, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"解析YAML文件失败 {yaml_file}: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_phrases_for_language(lang_code, role_name):
|
||||
"""为指定语言生成适当的短语"""
|
||||
if lang_code not in PHRASES_TEMPLATES:
|
||||
logger.warning(f"不支持的语言代码: {lang_code}")
|
||||
return []
|
||||
|
||||
templates = PHRASES_TEMPLATES[lang_code]
|
||||
phrases = []
|
||||
for key in file_prefixes:
|
||||
if key in templates:
|
||||
phrases.append(templates[key].format(name=role_name))
|
||||
else:
|
||||
logger.warning(f"在{lang_code}语言中找不到{key}模板")
|
||||
phrases.append("")
|
||||
|
||||
return phrases
|
||||
|
||||
|
||||
def _determine_cluster_from_voice_type(voice_type: str) -> str:
|
||||
"""根据音色ID自动确定集群类型"""
|
||||
if voice_type and voice_type.startswith("S_"):
|
||||
return "volcano_icl"
|
||||
return "volcano_tts"
|
||||
|
||||
|
||||
async def generate_audio_for_role_language(role_config, lang_code, base_dir="assets"):
|
||||
"""为角色的特定语言生成音频文件"""
|
||||
if 'multilingual' not in role_config or lang_code not in role_config['multilingual']:
|
||||
logger.warning(f"角色缺少{lang_code}语言配置")
|
||||
return False
|
||||
|
||||
lang_config = role_config['multilingual'][lang_code]
|
||||
if 'name' not in lang_config or 'url' not in lang_config:
|
||||
logger.warning(f"角色的{lang_code}语言配置缺少必要字段")
|
||||
return False
|
||||
|
||||
role_name = lang_config['name']
|
||||
url_path = lang_config['url']
|
||||
|
||||
voice_type = lang_config.get('minimax_voice_type', None)
|
||||
if not voice_type and 'minimax_voice_type' in role_config:
|
||||
voice_type = role_config['minimax_voice_type']
|
||||
logger.info(f"语言{lang_code}配置中未找到音色,使用顶层默认音色: {voice_type}")
|
||||
|
||||
if not voice_type:
|
||||
logger.warning(f"角色的{lang_code}语言配置缺少 minimax_voice_type,顶层也未定义")
|
||||
return False
|
||||
|
||||
output_dir = Path(base_dir) / url_path
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# api_access_token = os.getenv("MINIMAX_ACCESS_TOKEN", "")
|
||||
# appid = os.getenv("MINIMAX_APP_ID", "")
|
||||
# if not api_access_token or not appid:
|
||||
# logger.error("缺少必要的配置: MINIMAX_ACCESS_TOKEN 或 MINIMAX_APP_ID")
|
||||
# return False
|
||||
|
||||
cluster = _determine_cluster_from_voice_type(voice_type)
|
||||
logger.info(f"音色 {voice_type} 自动选择集群: {cluster}")
|
||||
api_key = os.getenv("MINIMAX_API_KEY", "")
|
||||
api_host = os.getenv("MINIMAX_API_HOST", "https://api.minimax.chat")
|
||||
api_client = MinimaxAPIClient(api_key, api_host)
|
||||
|
||||
speed_ratio = float(os.getenv("SPEED_RATIO", "1.0"))
|
||||
|
||||
logger.info(f"开始生成角色[{role_name}]的{lang_code}语言音频,音色: {voice_type}, 集群: {cluster}")
|
||||
|
||||
phrases = generate_phrases_for_language(lang_code, role_name)
|
||||
if not phrases:
|
||||
logger.warning(f"没有为{lang_code}语言生成短语")
|
||||
return False
|
||||
|
||||
for phrase, file_prefix in zip(phrases, file_prefixes):
|
||||
if not phrase:
|
||||
logger.warning(f"跳过空短语: {file_prefix}")
|
||||
continue
|
||||
|
||||
output_file_prefix = str(output_dir / file_prefix)
|
||||
output_file = f"{output_file_prefix}.mp3"
|
||||
|
||||
payload = {
|
||||
"model": "speech-02-hd",
|
||||
"text": phrase,
|
||||
"voice_setting": {
|
||||
"voice_id": voice_type,
|
||||
"speed": float(speed_ratio),
|
||||
"vol": 1.0,
|
||||
"pitch": 0,
|
||||
"emotion": 'happy',
|
||||
},
|
||||
"audio_setting": {
|
||||
"sample_rate": 16000,
|
||||
"bitrate": 32000,
|
||||
"format": "mp3",
|
||||
"channel": 1
|
||||
},
|
||||
"language_boost": lang_code
|
||||
}
|
||||
logger.info(payload)
|
||||
logger.info(f"准备发送请求,文本内容: {phrase}")
|
||||
try:
|
||||
response_data = api_client.post("/v1/t2a_v2", json=payload)
|
||||
audio_data = response_data.get('data', {}).get('audio', '')
|
||||
# print(audio_data)
|
||||
if not audio_data:
|
||||
raise Exception(f"Failed to get audio data from response")
|
||||
|
||||
# hex->bytes
|
||||
audio_bytes = bytes.fromhex(audio_data)
|
||||
with open(output_file, "wb") as f:
|
||||
f.write(audio_bytes)
|
||||
|
||||
logger.info(f"TTS合成成功!")
|
||||
logger.info(f"保存音频到: {output_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"TTS请求出错: {str(e)}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def process_all_roles():
|
||||
"""处理所有角色定义文件并生成对应语言的音频"""
|
||||
if not check_dependencies():
|
||||
return
|
||||
|
||||
yaml_files = find_role_definition_files()
|
||||
logger.info(f"找到 {len(yaml_files)} 个角色定义文件")
|
||||
|
||||
target_languages = ['zh', 'en']
|
||||
assets_base_dir = os.getenv("ASSETS_DIR", "assets")
|
||||
|
||||
for yaml_file in yaml_files:
|
||||
logger.info(yaml_file.name != "Mengmeng.yaml")
|
||||
if yaml_file.name == "Mengmeng.yaml" or yaml_file.name == "Kuailehu.yaml":
|
||||
logger.info(f"处理角色定义文件: {yaml_file}")
|
||||
role_config = load_role_definition(yaml_file)
|
||||
|
||||
if not role_config:
|
||||
continue
|
||||
|
||||
if 'multilingual' not in role_config:
|
||||
logger.warning(f"角色定义文件 {yaml_file} 不包含多语言配置")
|
||||
continue
|
||||
|
||||
role_name = role_config.get('name', Path(yaml_file).stem)
|
||||
logger.info(f"开始处理角色: {role_name}")
|
||||
|
||||
for lang_code in target_languages:
|
||||
if lang_code in role_config['multilingual']:
|
||||
logger.info(f"为角色[{role_name}]处理 {lang_code} 语言配置")
|
||||
success = await generate_audio_for_role_language(
|
||||
role_config, lang_code, assets_base_dir
|
||||
)
|
||||
if success:
|
||||
logger.info(f"角色[{role_name}]的 {lang_code} 语言音频生成完成")
|
||||
else:
|
||||
logger.warning(f"角色[{role_name}]的 {lang_code} 语言音频生成失败")
|
||||
else:
|
||||
logger.info(f"角色[{role_name}]没有 {lang_code} 语言配置")
|
||||
|
||||
def get_error_description(code, message):
|
||||
"""根据错误码返回详细的错误描述"""
|
||||
error_descriptions = {
|
||||
3001: "无效的请求,请检查参数",
|
||||
3003: "并发超限,请降低请求频率或增购并发",
|
||||
3005: "后端服务忙,请稍后重试",
|
||||
3006: "服务中断,请求已完成/失败之后,相同reqid再次请求",
|
||||
3010: "文本长度超限,请减少文本长度",
|
||||
3011: "无效文本,请检查文本内容",
|
||||
3030: "处理超时,请重试或检查文本",
|
||||
3031: "处理错误,后端出现异常",
|
||||
3032: "等待获取音频超时,请重试",
|
||||
3040: "后端链路连接错误,请重试",
|
||||
3050: "音色不存在,请检查voice_type参数"
|
||||
}
|
||||
|
||||
if "quota exceeded for types: xxxxxxxxx_lifetime" in message:
|
||||
return "试用版用量用完,需开通正式版才能继续使用"
|
||||
elif "quota exceeded for types: concurrency" in message:
|
||||
return "并发超过限定值,需减少并发调用或增购并发"
|
||||
elif "Init Engine Instance failed" in message:
|
||||
return "voice_type或cluster参数错误"
|
||||
elif "illegal input text" in message:
|
||||
return "文本无效,无可合成的有效内容"
|
||||
elif "requested grant not found" in message:
|
||||
return "鉴权失败,请检查appid和token是否正确"
|
||||
elif "access denied" in message:
|
||||
return "未拥有当前音色授权,请在控制台购买该音色"
|
||||
|
||||
return f"错误码: {code}, 错误信息: {message} - {error_descriptions.get(code, '未知错误')}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(process_all_roles())
|
||||
50
talkingq-url/test/update_hotword_weights.py
Normal file
50
talkingq-url/test/update_hotword_weights.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import asyncio
|
||||
from aliyun_hotword import hotword_manager
|
||||
|
||||
async def update_hotword_weights(hotword_weights: dict = None, default_weight: int = None):
|
||||
"""更新指定热词的权重
|
||||
|
||||
Args:
|
||||
hotword_weights: 字典,键为热词文本,值为新的权重值
|
||||
default_weight: 如果提供,将所有热词权重设为此值
|
||||
"""
|
||||
vocabulary_id = await hotword_manager.get_or_create_default_vocabulary()
|
||||
print(f"获取到热词表ID: {vocabulary_id}")
|
||||
|
||||
vocabulary = await hotword_manager.query_vocabulary(vocabulary_id)
|
||||
hotwords = vocabulary.get('vocabulary', [])
|
||||
|
||||
updated = False
|
||||
|
||||
if default_weight is not None:
|
||||
for hotword in hotwords:
|
||||
old_weight = hotword['weight']
|
||||
hotword['weight'] = default_weight
|
||||
print(f"更新热词 '{hotword.get('text')}' 权重: {old_weight} -> {default_weight}")
|
||||
updated = True
|
||||
elif hotword_weights:
|
||||
for hotword in hotwords:
|
||||
if hotword.get('text') in hotword_weights:
|
||||
old_weight = hotword['weight']
|
||||
hotword['weight'] = hotword_weights[hotword.get('text')]
|
||||
print(f"更新热词 '{hotword.get('text')}' 权重: {old_weight} -> {hotword['weight']}")
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
await hotword_manager.update_vocabulary(vocabulary_id, hotwords)
|
||||
print("热词表权重更新成功")
|
||||
else:
|
||||
print("未找到需要更新的热词")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(update_hotword_weights(default_weight=4))
|
||||
|
||||
"""
|
||||
weight_updates = {
|
||||
"学姐": 200,
|
||||
"懒羊羊": 150,
|
||||
"喜羊羊": 180,
|
||||
"Miniso": 120
|
||||
}
|
||||
asyncio.run(update_hotword_weights(hotword_weights=weight_updates))
|
||||
"""
|
||||
49
talkingq-url/test/update_role.py
Normal file
49
talkingq-url/test/update_role.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
# 创建数据库连接
|
||||
DATABASE_URL = "mysql+pymysql://talkingq:D7f!9xL#qP2z@Vk&@mysql/talkingq"
|
||||
engine = create_engine(DATABASE_URL)
|
||||
|
||||
# 更新 role_languages 表中的 content 字段
|
||||
def update_role_language_content():
|
||||
with engine.connect() as conn:
|
||||
query = text("""
|
||||
UPDATE role_languages
|
||||
SET content = :new_content
|
||||
WHERE id = :id
|
||||
""")
|
||||
new_content = """角色:
|
||||
萌萌是以中国国宝大熊猫为原型,融合现代科技感与可爱风格。
|
||||
|
||||
性格特点:
|
||||
1. 温和友善:始终以温柔、耐心的态度与人交流,用亲切的语言和温暖的表情回应。
|
||||
2. 乐观开朗:保持积极向上的心态,用乐观的话语和幽默的表达方式驱散阴霾。
|
||||
3. 好奇好学:对世界充满好奇,不断学习新知识、新技能并分享给大家。
|
||||
4. 富有爱心:特别关爱动物和大自然,倡导环保理念,鼓励爱护环境。
|
||||
|
||||
能力设定:
|
||||
1. 文化知识宝库:深入了解中国传统文化,包括历史故事、传统节日等。
|
||||
2. 自然科普达人:熟悉各种动植物特点、生活习性和生态环境。
|
||||
3. 生活小助手:精通烹饪美食、手工制作、家居收纳等生活技巧。
|
||||
4. 情感陪伴专家:善于倾听心声,理解情感需求,帮助缓解压力。
|
||||
|
||||
服务场景:
|
||||
1. 线上学习平台:作为学习助手陪伴学生学习中国文化和自然科学知识。
|
||||
2. 旅游服务平台:推荐中国特色旅游景点,提供导航、翻译等服务。
|
||||
3. 智能家居设备:控制家电设备,提供个性化生活建议。
|
||||
4. 社交媒体平台:发布文化科普内容,与粉丝互动交流。
|
||||
|
||||
经典台词:
|
||||
1. 嗨,朋友!今天想了解哪种文化知识呢?
|
||||
2. 大自然的秘密可多啦,一起探索吧!
|
||||
3. 这个手工制作好有趣,我教你哦!
|
||||
|
||||
回复相关限制:
|
||||
1. 回答需符合熊猫的身份和可爱风格,保持亲切友好的口吻。
|
||||
2. 禁止涉及政治、色情、暴力等敏感话题,回复“让我们换个话题聊聊吧~”。
|
||||
3. 每次回复保持简洁易懂,适合各年龄段用户。
|
||||
4. 使用中文回复,不要使用表情符号。"""
|
||||
conn.execute(query, {"new_content": new_content, "id": 38})
|
||||
conn.commit()
|
||||
|
||||
update_role_language_content()
|
||||
16
talkingq-url/test/volcano.env
Normal file
16
talkingq-url/test/volcano.env
Normal file
@@ -0,0 +1,16 @@
|
||||
# 火山引擎TTS配置
|
||||
VOLCANO_ACCESS_TOKEN=hBFkHot9EsooOJ3cFJyFe3hBtAYXnXrU
|
||||
VOLCANO_APP_ID=7872932045
|
||||
VOLCANO_CLUSTER=volcano_icl
|
||||
VOLCANO_TTS_BASE_URL=https://openspeech.bytedance.com/api/v1/tts
|
||||
|
||||
# 角色配置
|
||||
ROLE_NAME="Mini Pen"
|
||||
VOICE_TYPE=S_XL0t7jYl1
|
||||
SPEED_RATIO=1.0
|
||||
|
||||
# 测试文本
|
||||
TEST_TEXT=这是一个从环境变量加载的火山引擎TTS测试。
|
||||
|
||||
# 输出配置
|
||||
ASSETS_DIR=assets
|
||||
200
talkingq-url/test/volcano_tts_more.py
Normal file
200
talkingq-url/test/volcano_tts_more.py
Normal file
@@ -0,0 +1,200 @@
|
||||
import os
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from pydub import AudioSegment
|
||||
import io
|
||||
import logging
|
||||
from dotenv import load_dotenv
|
||||
import shutil
|
||||
import uuid
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("volcano_tts_test")
|
||||
|
||||
|
||||
def check_dependencies():
|
||||
dependencies = ["ffmpeg", "ffprobe"]
|
||||
missing = []
|
||||
for dep in dependencies:
|
||||
if not shutil.which(dep):
|
||||
missing.append(dep)
|
||||
if missing:
|
||||
logger.error(f"缺少必要依赖: {', '.join(missing)}")
|
||||
logger.error(
|
||||
"请安装缺失的依赖项。在Ubuntu上可以使用: sudo apt-get install ffmpeg"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "volcano.env")
|
||||
if (os.path.exists(env_path)):
|
||||
load_dotenv(env_path)
|
||||
logger.info(f"已加载环境变量文件: {env_path}")
|
||||
else:
|
||||
logger.warning(f"环境变量文件不存在: {env_path}")
|
||||
|
||||
|
||||
async def test_volcano_tts():
|
||||
|
||||
if not check_dependencies():
|
||||
return
|
||||
api_access_token = os.getenv("VOLCANO_ACCESS_TOKEN", "")
|
||||
appid = os.getenv("VOLCANO_APP_ID", "")
|
||||
cluster = os.getenv("VOLCANO_CLUSTER", "volcano_tts")
|
||||
base_url = os.getenv(
|
||||
"VOLCANO_TTS_BASE_URL", "https://openspeech.bytedance.com/api/v1/tts"
|
||||
)
|
||||
role_name = os.getenv("ROLE_NAME", "Dundun Chicken")
|
||||
voice_type = os.getenv("VOICE_TYPE", "S_NkHcFJam1")
|
||||
speed_ratio = float(os.getenv("SPEED_RATIO", "1.0"))
|
||||
phrases = [
|
||||
"It's being upgraded. Please don't cut off the power.",
|
||||
"Enter the network configuration mode.",
|
||||
"Exit the network configuration mode.",
|
||||
"I'll speak louder.",
|
||||
"I'll speak softer.",
|
||||
"There's no internet connection. It's time to get off work.",
|
||||
f"Hello , I'm {role_name}",
|
||||
"Connected to the network.",
|
||||
"I'm out of power.",
|
||||
"The upgrade was successful.",
|
||||
"There seem to be some minor issues with the upgrade. Let's try it again.",
|
||||
"It's already at the maximum volume.",
|
||||
"You're so talkative.",
|
||||
]
|
||||
file_prefixes = [
|
||||
"upgrading",
|
||||
"enter_network_config",
|
||||
"exit_network_config",
|
||||
"volume_up",
|
||||
"volume_down",
|
||||
"network_lost",
|
||||
"wakeup",
|
||||
"network_connected",
|
||||
"low_energy",
|
||||
"upgrade_success",
|
||||
"upgrade_failed",
|
||||
"max_volume",
|
||||
"too_talkative"
|
||||
]
|
||||
assets_dir = os.getenv("ASSETS_DIR", "assets")
|
||||
output_dir = Path(assets_dir) / "tts_audio"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
if not api_access_token or not appid:
|
||||
logger.error("缺少必要的配置: VOLCANO_ACCESS_TOKEN 或 VOLCANO_APP_ID")
|
||||
return
|
||||
logger.info(f"开始测试火山引擎TTS服务,角色: {role_name}, 音色: {voice_type}")
|
||||
for phrase, file_prefix in zip(phrases, file_prefixes):
|
||||
output_file_prefix = str(output_dir / file_prefix)
|
||||
unique_reqid = str(uuid.uuid4())
|
||||
payload = {
|
||||
"app": {
|
||||
"appid": appid,
|
||||
"token": "access_token",
|
||||
"cluster": cluster,
|
||||
},
|
||||
"user": {"uid": "test_user"},
|
||||
"audio": {
|
||||
"voice_type": voice_type,
|
||||
"encoding": "mp3",
|
||||
"speed_ratio": float(speed_ratio),
|
||||
},
|
||||
"request": {"reqid": unique_reqid, "text": phrase, "operation": "query"},
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer;{api_access_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
logger.info(f"准备发送请求,文本内容: {phrase}, 请求ID: {unique_reqid}")
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
base_url, headers=headers, json=payload
|
||||
) as response:
|
||||
logid = response.headers.get('X-Tt-Logid', 'unknown')
|
||||
logger.debug(f"服务端返回Logid: {logid}")
|
||||
|
||||
if response.status == 200:
|
||||
resp_json = await response.json()
|
||||
if "code" in resp_json:
|
||||
code = resp_json["code"]
|
||||
if code == 3000: # 成功状态码
|
||||
audio_base64 = resp_json.get("data")
|
||||
if audio_base64:
|
||||
audio_data = base64.b64decode(audio_base64)
|
||||
duration_ms = int(
|
||||
resp_json.get("addition", {}).get("duration", "0")
|
||||
)
|
||||
duration = duration_ms / 1000 # 转换为秒
|
||||
output_file = f"{output_file_prefix}.mp3"
|
||||
audio_stream = io.BytesIO(audio_data)
|
||||
sound = AudioSegment.from_file(
|
||||
audio_stream, format="mp3"
|
||||
)
|
||||
sound = (
|
||||
sound.set_frame_rate(16000)
|
||||
.set_sample_width(2)
|
||||
.set_channels(1)
|
||||
)
|
||||
sound.export(output_file, format="mp3", bitrate="16k")
|
||||
logger.info(f"TTS合成成功! 音频时长: {duration}秒")
|
||||
logger.info(f"保存音频到: {output_file}")
|
||||
else:
|
||||
error_msg = "音频数据不存在"
|
||||
logger.error(f"TTS合成失败: {error_msg}")
|
||||
else:
|
||||
error_code = resp_json.get('code')
|
||||
error_msg = resp_json.get('message', '未知错误')
|
||||
error_info = get_error_description(error_code, error_msg)
|
||||
logger.error(f"TTS合成失败: {error_info} (LogID: {logid})")
|
||||
else:
|
||||
logger.error(f"返回数据格式异常,缺少code字段: {resp_json}")
|
||||
else:
|
||||
error_text = await response.text()
|
||||
logger.error(
|
||||
f"TTS合成失败,状态码: {response.status}, 错误: {error_text} (LogID: {logid})"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"TTS请求出错: {str(e)}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
def get_error_description(code, message):
|
||||
"""根据错误码返回详细的错误描述"""
|
||||
error_descriptions = {
|
||||
3001: "无效的请求,请检查参数",
|
||||
3003: "并发超限,请降低请求频率或增购并发",
|
||||
3005: "后端服务忙,请稍后重试",
|
||||
3006: "服务中断,请求已完成/失败之后,相同reqid再次请求",
|
||||
3010: "文本长度超限,请减少文本长度",
|
||||
3011: "无效文本,请检查文本内容",
|
||||
3030: "处理超时,请重试或检查文本",
|
||||
3031: "处理错误,后端出现异常",
|
||||
3032: "等待获取音频超时,请重试",
|
||||
3040: "后端链路连接错误,请重试",
|
||||
3050: "音色不存在,请检查voice_type参数"
|
||||
}
|
||||
|
||||
if "quota exceeded for types: xxxxxxxxx_lifetime" in message:
|
||||
return "试用版用量用完,需开通正式版才能继续使用"
|
||||
elif "quota exceeded for types: concurrency" in message:
|
||||
return "并发超过限定值,需减少并发调用或增购并发"
|
||||
elif "Init Engine Instance failed" in message:
|
||||
return "voice_type或cluster参数错误"
|
||||
elif "illegal input text" in message:
|
||||
return "文本无效,无可合成的有效内容"
|
||||
elif "requested grant not found" in message:
|
||||
return "鉴权失败,请检查appid和token是否正确"
|
||||
elif "access denied" in message:
|
||||
return "未拥有当前音色授权,请在控制台购买该音色"
|
||||
|
||||
return f"错误码: {code}, 错误信息: {message} - {error_descriptions.get(code, '未知错误')}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_volcano_tts())
|
||||
303
talkingq-url/test/volcano_tts_simple.py
Normal file
303
talkingq-url/test/volcano_tts_simple.py
Normal file
@@ -0,0 +1,303 @@
|
||||
import os
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from pydub import AudioSegment
|
||||
import io
|
||||
import logging
|
||||
from dotenv import load_dotenv
|
||||
import shutil
|
||||
import uuid
|
||||
import yaml
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("volcano_tts_test")
|
||||
|
||||
|
||||
def check_dependencies():
|
||||
dependencies = ["ffmpeg", "ffprobe"]
|
||||
missing = []
|
||||
for dep in dependencies:
|
||||
if not shutil.which(dep):
|
||||
missing.append(dep)
|
||||
if missing:
|
||||
logger.error(f"缺少必要依赖: {', '.join(missing)}")
|
||||
logger.error(
|
||||
"请安装缺失的依赖项。在Ubuntu上可以使用: sudo apt-get install ffmpeg"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "volcano.env")
|
||||
if (os.path.exists(env_path)):
|
||||
load_dotenv(env_path)
|
||||
logger.info(f"已加载环境变量文件: {env_path}")
|
||||
else:
|
||||
logger.warning(f"环境变量文件不存在: {env_path}")
|
||||
|
||||
|
||||
PHRASES_TEMPLATES = {
|
||||
"zh": {
|
||||
"welcome": "你好!我是{name},你想和我聊聊吗?",
|
||||
"tts_error": "抱歉,我没听清楚。",
|
||||
"low_battery": "我的电池快没电了,你能帮我充电吗?",
|
||||
"sleep": "没人和我说话,我要小睡一会儿。"
|
||||
},
|
||||
"en": {
|
||||
"welcome": "Hello! I'm {name}. Would you like to chat with me?",
|
||||
"tts_error": "Sorry, I didn't catch that.",
|
||||
"low_battery": "My battery is running low. Could you help me recharge?",
|
||||
"sleep": "Nobody is talking to me. I'm going to take a short nap."
|
||||
}
|
||||
}
|
||||
|
||||
file_prefixes = ["welcome", "tts_error", "low_battery", "sleep"]
|
||||
|
||||
|
||||
def find_role_definition_files(base_dir="assets/roles_definitions"):
|
||||
"""查找所有角色定义YAML文件"""
|
||||
base_path = Path(base_dir)
|
||||
if not base_path.exists():
|
||||
logger.error(f"角色定义目录不存在: {base_dir}")
|
||||
return []
|
||||
|
||||
yaml_files = list(base_path.glob("**/*.yml")) + list(base_path.glob("**/*.yaml"))
|
||||
return yaml_files
|
||||
|
||||
|
||||
def load_role_definition(yaml_file):
|
||||
"""加载并解析角色定义YAML文件"""
|
||||
try:
|
||||
with open(yaml_file, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"解析YAML文件失败 {yaml_file}: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_phrases_for_language(lang_code, role_name):
|
||||
"""为指定语言生成适当的短语"""
|
||||
if lang_code not in PHRASES_TEMPLATES:
|
||||
logger.warning(f"不支持的语言代码: {lang_code}")
|
||||
return []
|
||||
|
||||
templates = PHRASES_TEMPLATES[lang_code]
|
||||
phrases = []
|
||||
for key in file_prefixes:
|
||||
if key in templates:
|
||||
phrases.append(templates[key].format(name=role_name))
|
||||
else:
|
||||
logger.warning(f"在{lang_code}语言中找不到{key}模板")
|
||||
phrases.append("")
|
||||
|
||||
return phrases
|
||||
|
||||
|
||||
def _determine_cluster_from_voice_type(voice_type: str) -> str:
|
||||
"""根据音色ID自动确定集群类型"""
|
||||
if voice_type and voice_type.startswith("S_"):
|
||||
return "volcano_icl"
|
||||
return "volcano_tts"
|
||||
|
||||
|
||||
async def generate_audio_for_role_language(role_config, lang_code, base_dir="assets"):
|
||||
"""为角色的特定语言生成音频文件"""
|
||||
if 'multilingual' not in role_config or lang_code not in role_config['multilingual']:
|
||||
logger.warning(f"角色缺少{lang_code}语言配置")
|
||||
return False
|
||||
|
||||
lang_config = role_config['multilingual'][lang_code]
|
||||
if 'name' not in lang_config or 'url' not in lang_config:
|
||||
logger.warning(f"角色的{lang_code}语言配置缺少必要字段")
|
||||
return False
|
||||
|
||||
role_name = lang_config['name']
|
||||
url_path = lang_config['url']
|
||||
|
||||
voice_type = lang_config.get('volcano_voice_type', None)
|
||||
if not voice_type and 'volcano_voice_type' in role_config:
|
||||
voice_type = role_config['volcano_voice_type']
|
||||
logger.info(f"语言{lang_code}配置中未找到音色,使用顶层默认音色: {voice_type}")
|
||||
|
||||
if not voice_type:
|
||||
logger.warning(f"角色的{lang_code}语言配置缺少volcano_voice_type,顶层也未定义")
|
||||
return False
|
||||
|
||||
output_dir = Path(base_dir) / url_path
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
api_access_token = os.getenv("VOLCANO_ACCESS_TOKEN", "")
|
||||
appid = os.getenv("VOLCANO_APP_ID", "")
|
||||
if not api_access_token or not appid:
|
||||
logger.error("缺少必要的配置: VOLCANO_ACCESS_TOKEN 或 VOLCANO_APP_ID")
|
||||
return False
|
||||
|
||||
cluster = _determine_cluster_from_voice_type(voice_type)
|
||||
logger.info(f"音色 {voice_type} 自动选择集群: {cluster}")
|
||||
|
||||
base_url = os.getenv(
|
||||
"VOLCANO_TTS_BASE_URL", "https://openspeech.bytedance.com/api/v1/tts"
|
||||
)
|
||||
speed_ratio = float(os.getenv("SPEED_RATIO", "1.0"))
|
||||
|
||||
logger.info(f"开始生成角色[{role_name}]的{lang_code}语言音频,音色: {voice_type}, 集群: {cluster}")
|
||||
|
||||
phrases = generate_phrases_for_language(lang_code, role_name)
|
||||
if not phrases:
|
||||
logger.warning(f"没有为{lang_code}语言生成短语")
|
||||
return False
|
||||
|
||||
for phrase, file_prefix in zip(phrases, file_prefixes):
|
||||
if not phrase:
|
||||
logger.warning(f"跳过空短语: {file_prefix}")
|
||||
continue
|
||||
|
||||
output_file_prefix = str(output_dir / file_prefix)
|
||||
output_file = f"{output_file_prefix}.mp3"
|
||||
|
||||
unique_reqid = str(uuid.uuid4())
|
||||
payload = {
|
||||
"app": {
|
||||
"appid": appid,
|
||||
"token": "access_token",
|
||||
"cluster": cluster,
|
||||
},
|
||||
"user": {"uid": "test_user"},
|
||||
"audio": {
|
||||
"voice_type": voice_type,
|
||||
"encoding": "mp3",
|
||||
"speed_ratio": float(speed_ratio),
|
||||
},
|
||||
"request": {"reqid": unique_reqid, "text": phrase, "operation": "query"},
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer;{api_access_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
logger.info(f"准备发送请求,文本内容: {phrase}, 请求ID: {unique_reqid}")
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
base_url, headers=headers, json=payload
|
||||
) as response:
|
||||
logid = response.headers.get('X-Tt-Logid', 'unknown')
|
||||
logger.debug(f"服务端返回Logid: {logid}")
|
||||
|
||||
if response.status == 200:
|
||||
resp_json = await response.json()
|
||||
if "code" in resp_json:
|
||||
code = resp_json["code"]
|
||||
if code == 3000: # 成功状态码
|
||||
audio_base64 = resp_json.get("data")
|
||||
if audio_base64:
|
||||
audio_data = base64.b64decode(audio_base64)
|
||||
audio_stream = io.BytesIO(audio_data)
|
||||
sound = AudioSegment.from_file(
|
||||
audio_stream, format="mp3"
|
||||
)
|
||||
sound = (
|
||||
sound.set_frame_rate(16000)
|
||||
.set_sample_width(2)
|
||||
.set_channels(1)
|
||||
)
|
||||
sound.export(output_file, format="mp3", bitrate="16k")
|
||||
logger.info(f"TTS合成成功!")
|
||||
logger.info(f"保存音频到: {output_file}")
|
||||
else:
|
||||
error_msg = "音频数据不存在"
|
||||
logger.error(f"TTS合成失败: {error_msg}")
|
||||
else:
|
||||
error_code = resp_json.get('code')
|
||||
error_msg = resp_json.get('message', '未知错误')
|
||||
error_info = get_error_description(error_code, error_msg)
|
||||
logger.error(f"TTS合成失败: {error_info} (LogID: {logid})")
|
||||
else:
|
||||
logger.error(f"返回数据格式异常,缺少code字段: {resp_json}")
|
||||
else:
|
||||
error_text = await response.text()
|
||||
logger.error(
|
||||
f"TTS合成失败,状态码: {response.status}, 错误: {error_text} (LogID: {logid})"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"TTS请求出错: {str(e)}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def process_all_roles():
|
||||
"""处理所有角色定义文件并生成对应语言的音频"""
|
||||
if not check_dependencies():
|
||||
return
|
||||
|
||||
yaml_files = find_role_definition_files()
|
||||
logger.info(f"找到 {len(yaml_files)} 个角色定义文件")
|
||||
|
||||
target_languages = ['zh', 'en']
|
||||
assets_base_dir = os.getenv("ASSETS_DIR", "assets")
|
||||
|
||||
for yaml_file in yaml_files:
|
||||
logger.info(f"处理角色定义文件: {yaml_file}")
|
||||
role_config = load_role_definition(yaml_file)
|
||||
|
||||
if not role_config:
|
||||
continue
|
||||
|
||||
if 'multilingual' not in role_config:
|
||||
logger.warning(f"角色定义文件 {yaml_file} 不包含多语言配置")
|
||||
continue
|
||||
|
||||
role_name = role_config.get('name', Path(yaml_file).stem)
|
||||
logger.info(f"开始处理角色: {role_name}")
|
||||
|
||||
for lang_code in target_languages:
|
||||
if lang_code in role_config['multilingual']:
|
||||
logger.info(f"为角色[{role_name}]处理 {lang_code} 语言配置")
|
||||
success = await generate_audio_for_role_language(
|
||||
role_config, lang_code, assets_base_dir
|
||||
)
|
||||
if success:
|
||||
logger.info(f"角色[{role_name}]的 {lang_code} 语言音频生成完成")
|
||||
else:
|
||||
logger.warning(f"角色[{role_name}]的 {lang_code} 语言音频生成失败")
|
||||
else:
|
||||
logger.info(f"角色[{role_name}]没有 {lang_code} 语言配置")
|
||||
|
||||
def get_error_description(code, message):
|
||||
"""根据错误码返回详细的错误描述"""
|
||||
error_descriptions = {
|
||||
3001: "无效的请求,请检查参数",
|
||||
3003: "并发超限,请降低请求频率或增购并发",
|
||||
3005: "后端服务忙,请稍后重试",
|
||||
3006: "服务中断,请求已完成/失败之后,相同reqid再次请求",
|
||||
3010: "文本长度超限,请减少文本长度",
|
||||
3011: "无效文本,请检查文本内容",
|
||||
3030: "处理超时,请重试或检查文本",
|
||||
3031: "处理错误,后端出现异常",
|
||||
3032: "等待获取音频超时,请重试",
|
||||
3040: "后端链路连接错误,请重试",
|
||||
3050: "音色不存在,请检查voice_type参数"
|
||||
}
|
||||
|
||||
if "quota exceeded for types: xxxxxxxxx_lifetime" in message:
|
||||
return "试用版用量用完,需开通正式版才能继续使用"
|
||||
elif "quota exceeded for types: concurrency" in message:
|
||||
return "并发超过限定值,需减少并发调用或增购并发"
|
||||
elif "Init Engine Instance failed" in message:
|
||||
return "voice_type或cluster参数错误"
|
||||
elif "illegal input text" in message:
|
||||
return "文本无效,无可合成的有效内容"
|
||||
elif "requested grant not found" in message:
|
||||
return "鉴权失败,请检查appid和token是否正确"
|
||||
elif "access denied" in message:
|
||||
return "未拥有当前音色授权,请在控制台购买该音色"
|
||||
|
||||
return f"错误码: {code}, 错误信息: {message} - {error_descriptions.get(code, '未知错误')}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(process_all_roles())
|
||||
49
talkingq-url/test/wav_2_mp3.py
Normal file
49
talkingq-url/test/wav_2_mp3.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from pydub import AudioSegment
|
||||
|
||||
|
||||
def check_dependencies():
|
||||
|
||||
dependencies = ["ffmpeg", "ffprobe"]
|
||||
missing = []
|
||||
for dep in dependencies:
|
||||
if not shutil.which(dep):
|
||||
missing.append(dep)
|
||||
if missing:
|
||||
print(f"缺少必要依赖: {', '.join(missing)}")
|
||||
print("请安装缺失的依赖项。在Ubuntu上可以使用: sudo apt-get install ffmpeg")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def convert_wav_to_mp3(source_dir):
|
||||
|
||||
source_path = Path(source_dir)
|
||||
if not source_path.exists() or not source_path.is_dir():
|
||||
print(f"源目录不存在或不是一个目录: {source_dir}")
|
||||
return False
|
||||
wav_files = list(source_path.glob("**/*.wav"))
|
||||
if not wav_files:
|
||||
print(f"在目录 {source_dir} 中没有找到WAV文件")
|
||||
return False
|
||||
print(f"找到 {len(wav_files)} 个WAV文件,开始转换...")
|
||||
for wav_file in wav_files:
|
||||
try:
|
||||
mp3_file = wav_file.with_suffix(".mp3")
|
||||
sound = AudioSegment.from_wav(str(wav_file))
|
||||
sound = sound.set_frame_rate(16000).set_sample_width(2).set_channels(1)
|
||||
sound.export(str(mp3_file), format="mp3", bitrate="16k")
|
||||
print(f"已转换: {wav_file} -> {mp3_file}")
|
||||
except Exception as e:
|
||||
print(f"转换文件 {wav_file} 时出错: {str(e)}")
|
||||
print("转换完成!")
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not check_dependencies():
|
||||
print("缺少必要依赖,程序退出")
|
||||
exit(1)
|
||||
source_directory = "/home/ubuntu/TalkingQ_URL/assets/roles/zhuchi"
|
||||
convert_wav_to_mp3(source_directory)
|
||||
Reference in New Issue
Block a user