add banbanmini backend

This commit is contained in:
HycJack
2026-03-24 15:04:36 +08:00
parent 0d0f995dc2
commit 7510ca6df1
197 changed files with 13008 additions and 0 deletions

View 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. 使用中文回复。

View 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信息其他都由系统自动完成。

View 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()

View 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'
);

View File

@@ -0,0 +1,5 @@
ALIYUN_API_KEY="sk-7a50eca6856d4afb968ac3bf512f6d1b"
VOICE_TYPE=sambert-zhimiao-emo-v1
ROLE_NAME=测试角色
RATE=1.0
ASSETS_DIR=assets

View 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())

View 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
View 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

View 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 device_id serial_number
2 talkingQ_B0EC1F2 3b4c5d6e7f
3 talkingQ_94B7164 3b4c5d6e7f
4 talkingQ_4C2B7F9 0a1b2c3d4e
5 talkingQ_f91D6b8 5a9b8d3f7c
6 talkingQ_1a2b7C3 f6a1c5e9d8
7 talkingQ_7D3c1F4 8b9c7a2f1b
8 talkingQ_2C8f3D6 4e9b0d7c1a
9 talkingQ_0b6F4D3 a3b9c5d7e8
10 talkingQ_9A7b4d1 b2f3a9d6c7
11 talkingQ_5B8a1C9 f0d6c3b2a1
12 talkingQ_7F3d2B6 c5d1e8f9b6
13 talkingQ_8D1f5c7 6b9d0a3c5f
14 talkingQ_4F1C3b9 a7e9f2c4d0
15 talkingQ_2d4b6F7 8c5a1b3d9e
16 talkingQ_7A9b4F2 e3c6d9f8b7
17 talkingQ_1f6D4B2 d9b1a7c5e0
18 talkingQ_5b1f7D9 3a4c2e8b5f
19 talkingQ_8B4C3d9 f7a6c1d4e9
20 talkingQ_0F7a6B4 2c9d5f3b7a
21 talkingQ_6c5d2F8 a1b9c7d3f5
22 talkingQ_3B1f7d5 9c2a4f8e6d
23 talkingQ_2A7b9d4 3f6b1e9c8a
24 talkingQ_9c3d8B7 5f1a2d6e4c
25 talkingQ_7f4B3C1 9a5e7b3f0d
26 talkingQ_B4B7164 3b4c5d6e7f
27 talkingQ_E05AF3A 3b4c5d6e7f
28 talkingQ_A8B7164 3b4c5d6e7f
29 talkingQ_4d7f2B9 8c1b9e3f7a
30 talkingQ_C4B7164 3b4c5d6e7f
31 talkingQ_C0B7164 3b4c5d6e7f
32 talkingQ_F4B7164 3b4c5d6e7f
33 talkingQ_D0B7164 3b4c5d6e7f
34 talkingQ_F0B7164 3b4c5d6e7f
35 talkingQ_ACB7164 3b4c5d6e7f
36 talkingQ_E8B7164 3b4c5d6e7f
37 talkingQ_6B1F9a3 5d7c2e0a9f
38 talkingQ_3A7d5F4 c9b0e6d2a4
39 talkingQ_5B9d2A7 1e4f3c6a9b
40 talkingQ_2d3F1B6 c4e9a5b7d2
41 talkingQ_9A6b5d2 7c1a4f9e0b
42 talkingQ_1A2B3C4 5d6e7f8a9b
43 talkingQ_5D6E7F8 1a2b3c4d5e
44 talkingQ_7G8H9I0 6b7c8d9e0f
45 talkingQ_2J3K4L5 8f9g0h1j2k
46 talkingQ_9M0N1O2 3l4m5n6o7p
47 talkingQ_3P4Q5R6 9q0r1s2t3u
48 talkingQ_6S7T8U9 4v5w6x7y8z
49 talkingQ_4V5W6X7 0a1b2c3d4e
50 talkingQ_8Y9Z0A1 5f6g7h8i9j
51 talkingQ_5B6C7D8 0k1l2m3n4o
52 talkingQ_3E4F5G6 7p8q9r0s1t
53 talkingQ_7H8I9J0 2u3v4w5x6y
54 talkingQ_1K2L3M4 8z9a0b1c2d
55 talkingQ_4N5O6P7 3e4f5g6h7i
56 talkingQ_6Q7R8S9 9j0k1l2m3n
57 talkingQ_2T3U4V5 4o5p6q7r8s
58 talkingQ_9W0X1Y2 0t1u2v3w4x
59 talkingQ_5Z6A7B8 5y6z7a8b9c
60 talkingQ_3C4D5E6 0d1e2f3g4h
61 talkingQ_7F8G9H0 6i7j8k9l0m
62 talkingQ_1I2J3K4 1n2o3p4q5r
63 talkingQ_4L5M6N7 7s8t9u0v1w
64 talkingQ_6O7P8Q9 2x3y4z5a6b
65 talkingQ_2R3S4T5 8c9d0e1f2g
66 talkingQ_9U0V1W2 3h4i5j6k7l
67 talkingQ_5X6Y7Z8 9m0n1o2p3q
68 talkingQ_3A4B5C6 4r5s6t7u8v
69 talkingQ_7D8E9F0 0w1x2y3z4a
70 talkingQ_1G2H3I4 5b6c7d8e9f
71 talkingQ_4J5K6L7 0g1h2i3j4k
72 talkingQ_6M7N8O9 6l7m8n9o0p
73 talkingQ_2P3Q4R5 1q2r3s4t5u
74 talkingQ_9S0T1U2 7v8w9x0y1z
75 talkingQ_5V6W7X8 2a3b4c5d6e
76 talkingQ_3Y4Z5A6 8f9g0h1i2j
77 talkingQ_7B8C9D0 3k4l5m6n7o
78 talkingQ_1E2F3G4 9p0q1r2s3t
79 talkingQ_4H5I6J7 4u5v6w7x8y
80 talkingQ_6K7L8M9 0z1a2b3c4d
81 talkingQ_2N3O4P5 5e6f7g8h9i
82 talkingQ_9Q0R1S2 0j1k2l3m4n
83 talkingQ_5T6U7V8 6o7p8q9r0s
84 talkingQ_3W4X5Y6 1t2u3v4w5x
85 talkingQ_7Z8A9B0 7y8z9a0b1c
86 talkingQ_1C2D3E4 2d3e4f5g6h
87 talkingQ_4F5G6H7 8i9j0k1l2m
88 talkingQ_6I7J8K9 3n4o5p6q7r
89 talkingQ_2L3M4N5 9s0t1u2v3w
90 talkingQ_9O0P1Q2 4x5y6z7a8b
91 talkingQ_5R6S7T8 0c1d2e3f4g
92 talkingQ_8B9D3E4 a1b2c3d4e5
93 talkingQ_2A4F7C8 f6e5d4c3b2
94 talkingQ_1E3F5G7 d8c7b9a0f1
95 talkingQ_4G7H9K1 c2b3a4d5e6
96 talkingQ_5F2D4A8 b7c8d9e1f0
97 talkingQ_7J3L5O6 e1f2d3c4b5
98 talkingQ_3C8B7E4 d9a6f5b2c3
99 talkingQ_9A1D3F5 e4b6c7d8a9
100 talkingQ_6B5C2D4 a7e8f9b0c1
101 talkingQ_4G8H3J7 d2f5c9b4a0
102 talkingQ_D485583 3b4c5d6e7f
103 talkingQ_8C85583 3b4c5d6e7f
104 talkingQ_4486583 3b4c5d6e7f
105 talkingQ_68A81F2 3b4c5d6e7f
106 talkingQ_70A81F2 3b4c5d6e7f
107 talkingQ_7885583 3b4c5d6e7f
108 talkingQ_7886583 3b4c5d6e7f
109 talkingQ_C8A71F2 3b4c5d6e7f
110 talkingQ_2CA81F2 3b4c5d6e7f
111 talkingQ_8C5488A 3b4c5d6e7f
112 talkingQ_2D94AE3 3b4c5d6e7f
113 talkingQ_9C2D94A 3b4c5d6e7f
114 talkingQ_8C4F88A 3b4c5d6e7f
115 talkingQ_5B88AE3 3b4c5d6e7f
116 talkingQ_3C5388A 3b4c5d6e7f
117 talkingQ_205088A 3b4c5d6e7f
118 talkingQ_5C2D94A 3b4c5d6e7f
119 talkingQ_B42C94A 3b4c5d6e7f
120 talkingQ_E87F1F2 3b4c5d6e7f
121 talkingQ_BCEC1F2 3b4c5d6e7f

View File

@@ -0,0 +1 @@
sudo docker logs --since "2025-09-02T21:50:00" --until "2025-09-02T21:56:00" talkingq-url-hz-app-1

View 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

View 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())

View 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())

View 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

View 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)

View 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())

View 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))
"""

View 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()

View 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

View 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())

View 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())

View 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)