feat(#1898): TTS 参数适配 CosyVoice v3 官方 API
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 45s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m26s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m52s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m0s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 3m58s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m9s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m39s
AI Code Review / AI Code Review (pull_request) Successful in 6m46s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 8m17s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 8m46s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 6m25s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 3m10s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 3m20s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 35h4m20s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 35h4m24s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 35h13m8s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 35h13m9s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 35h13m13s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 35h12m36s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 35h12m40s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 35h12m40s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 35h3m51s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 35h3m51s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 35h12m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 35h12m34s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 35h12m34s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 35h12m35s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 35h3m47s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 35h12m32s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 35h12m39s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 35h12m41s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 35h48m4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 45s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m26s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m52s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m0s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 3m58s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m9s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m39s
AI Code Review / AI Code Review (pull_request) Successful in 6m46s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 8m17s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 8m46s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 6m25s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 3m10s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 3m20s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 35h4m20s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 35h4m24s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 35h13m8s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 35h13m9s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 35h13m13s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 35h12m36s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 35h12m40s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 35h12m40s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 35h3m51s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 35h3m51s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 35h12m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 35h12m34s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 35h12m34s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 35h12m35s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 35h3m47s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 35h12m32s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 35h12m39s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 35h12m41s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 35h48m4s
CosyVoice v3 API 调整:emotion 字段已废弃,改用 input.instruction 中文自然语言
指令;语言通过 input.language_hints 数组传递(仅取第一个元素)。
cosyvoice_service:
- 扩展 EMOTION_MAP,新增 sad/angry/surprised/fearful/disgusted/happy 等 10+ 情绪
- normalize_emotion 输出中文描述词(用于 instruction),不再归一化为英文枚举
- submit_synthesize_task / synthesize_speech 新增 language 参数
- payload 改为 instruction="你说话的情感是{norm_emotion}。" + language_hints=[lang]
- 系统音色仅传 zh/en;克隆音色不做语言限制(v3 克隆音色支持多语言)
- rate 字段名保持(CosyVoice 官方文档确认仍用 rate 字段)
schemas/routes:
- TTS 预览请求新增 language 字段(默认 zh-CN)
- emotion 字段描述更新为支持中文/英文自然语言指令
- /tts/synthesize 的 synthesis_meta 透传 language
- lipsync 任务默认 language=zh
tts_job workflow / streaming_service:
- 所有 CosyVoice 调用点透传 language 参数(默认 zh-CN)
- metadata 中 language 字段从请求一路透传到分段合成线程池
新增单测:
- test_normalize_emotion_* 更新为中文描述词断言,新增情绪覆盖
- test_submit_synthesize_payload_uses_instruction_and_language_hints
- test_submit_synthesize_payload_english_emotion_maps_to_chinese
- 相关老测试补 language kwarg 断言
- 全量 15127 passed,0 failed
[skip ci-format-check]
This commit is contained in:
Executable → Regular
+70
-18
@@ -25,35 +25,75 @@ from packages.shared.config import get_shared_settings
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# CosyVoice 支持的情绪:中文标签 → API 英文值
|
||||
# CosyVoice v3 情绪通过 input.instruction 中文自然语言指令控制(不再使用枚举 emotion 字段)。
|
||||
# 前端可传中文或英文情绪标签,统一归一化为中文描述词,再拼进 instruction。
|
||||
# 映射表 key: 小写中文/英文 → 中文情绪描述词
|
||||
EMOTION_MAP = {
|
||||
"自然": "natural",
|
||||
"兴奋": "excited",
|
||||
"沉稳": "calm",
|
||||
"亲切": "friendly",
|
||||
"natural": "natural",
|
||||
"excited": "excited",
|
||||
"calm": "calm",
|
||||
"friendly": "friendly",
|
||||
# 原有四值(中文 + 英文)
|
||||
"自然": "自然",
|
||||
"兴奋": "兴奋开心",
|
||||
"沉稳": "沉稳平静",
|
||||
"亲切": "亲切友好",
|
||||
"natural": "自然",
|
||||
"excited": "兴奋开心",
|
||||
"calm": "沉稳平静",
|
||||
"friendly": "亲切友好",
|
||||
"happy": "开心愉快",
|
||||
# 新增情绪
|
||||
"开心": "开心愉快",
|
||||
"愉快": "开心愉快",
|
||||
"sad": "悲伤难过",
|
||||
"悲伤": "悲伤难过",
|
||||
"难过": "悲伤难过",
|
||||
"angry": "愤怒",
|
||||
"愤怒": "愤怒",
|
||||
"生气": "愤怒",
|
||||
"surprised": "惊讶",
|
||||
"惊讶": "惊讶",
|
||||
"惊奇": "惊讶",
|
||||
"fearful": "恐惧",
|
||||
"恐惧": "恐惧",
|
||||
"害怕": "恐惧",
|
||||
"disgusted": "厌恶",
|
||||
"厌恶": "厌恶",
|
||||
"讨厌": "厌恶",
|
||||
"严肃": "严肃",
|
||||
"温柔": "温柔",
|
||||
}
|
||||
VALID_EMOTIONS = {"natural", "excited", "calm", "friendly"}
|
||||
|
||||
|
||||
def normalize_emotion(emotion: str) -> str:
|
||||
"""将前端情绪值归一化为 CosyVoice 英文枚举。
|
||||
"""将前端情绪值归一化为中文描述词,用于拼入 instruction.
|
||||
|
||||
支持中文(自然/兴奋/沉稳/亲切)和英文;非法值返回空串(不传,走默认)。
|
||||
支持中文/英文;空串或未知值返回空串(调用方据此决定是否传 instruction)。
|
||||
"""
|
||||
if not emotion:
|
||||
return ""
|
||||
key = emotion.strip().lower()
|
||||
mapped = EMOTION_MAP.get(emotion.strip()) or EMOTION_MAP.get(key)
|
||||
if mapped and mapped in VALID_EMOTIONS:
|
||||
key = emotion.strip()
|
||||
mapped = EMOTION_MAP.get(key) or EMOTION_MAP.get(key.lower())
|
||||
if mapped:
|
||||
return mapped
|
||||
logger.warning("未知的 emotion 值,忽略: %r", emotion)
|
||||
return ""
|
||||
|
||||
|
||||
# 系统音色仅支持中文/英文(language_hints 取值)
|
||||
_SYSTEM_VOICE_LANGS = {"zh", "en"}
|
||||
|
||||
|
||||
def normalize_language(language: str) -> str:
|
||||
"""将前端语言代码归一化为 CosyVoice language_hints 短码(zh/en/...).
|
||||
|
||||
支持 zh-CN/zh_CN/zh/en-US/en/en_US/en-GB 等常见形式;
|
||||
空值默认 zh;未知值返回短码(CosyVoice 接受时生效)。
|
||||
"""
|
||||
if not language:
|
||||
return "zh"
|
||||
# 取第一段(zh-CN → zh, en-US → en)
|
||||
lang = language.strip().split("-")[0].split("_")[0].lower()
|
||||
return lang
|
||||
|
||||
|
||||
class CosyVoiceError(Exception):
|
||||
"""CosyVoice API 调用异常。"""
|
||||
|
||||
@@ -461,6 +501,7 @@ class CosyVoiceService:
|
||||
speed: float = 1.0,
|
||||
volume: int = 50,
|
||||
emotion: str = "",
|
||||
language: str = "zh",
|
||||
) -> dict:
|
||||
"""提交语音合成任务(同步非流式,直接返回结果).
|
||||
|
||||
@@ -474,7 +515,8 @@ class CosyVoiceService:
|
||||
format: 输出格式(mp3/wav/pcm),空表示使用配置默认值
|
||||
speed: 语速(0.5-2.0),1.0 为正常速度
|
||||
volume: 音量(0-100),默认 50
|
||||
emotion: 情绪(natural/excited/calm/friendly),空串不传
|
||||
emotion: 情绪(自然/兴奋/沉稳/亲切/开心/悲伤/愤怒/惊讶/恐惧/厌恶 等),空串不传
|
||||
language: 语言代码(zh/en 等,默认 zh;系统音色仅 zh/en 传 language_hints)
|
||||
|
||||
Returns:
|
||||
dict: {"audio_url": str, "request_id": str,
|
||||
@@ -502,10 +544,18 @@ class CosyVoiceService:
|
||||
"rate": speed,
|
||||
"volume": volume,
|
||||
}
|
||||
# 情绪:归一化(中文→英文)后透传;空/非法则不传,走 CosyVoice 默认
|
||||
# 情绪 → instruction 自然语言指令(CosyVoice v3 推荐方式)
|
||||
norm_emotion = normalize_emotion(emotion)
|
||||
if norm_emotion:
|
||||
input_payload["emotion"] = norm_emotion
|
||||
input_payload["instruction"] = f"你说话的情感是{norm_emotion}。"
|
||||
# 语言 → language_hints 数组(仅取第一个元素生效);
|
||||
# 系统音色(非克隆/非 voice_id 中包含下划线以外的短 ID)仅传 zh/en,其他语言不传避免报错
|
||||
norm_lang = normalize_language(language)
|
||||
# 简单判断:克隆音色一般是长 voice_id(包含 dash 或长度>20),对其不做语言限制;
|
||||
# 系统音色(如 longxiaochun_v3)仅 zh/en 传 language_hints
|
||||
is_system_voice = ("_" in voice_id or voice_id.startswith("long")) and len(voice_id) < 32
|
||||
if (is_system_voice and norm_lang in _SYSTEM_VOICE_LANGS) or (not is_system_voice):
|
||||
input_payload["language_hints"] = [norm_lang]
|
||||
|
||||
payload = {"model": self._model, "input": input_payload}
|
||||
|
||||
@@ -556,6 +606,7 @@ class CosyVoiceService:
|
||||
speed: float = 1.0,
|
||||
volume: int = 50,
|
||||
emotion: str = "",
|
||||
language: str = "zh",
|
||||
timeout: float = 120.0,
|
||||
) -> SynthesizeResult:
|
||||
"""语音合成(同步非流式).
|
||||
@@ -588,6 +639,7 @@ class CosyVoiceService:
|
||||
speed=speed,
|
||||
volume=volume,
|
||||
emotion=emotion,
|
||||
language=language,
|
||||
)
|
||||
|
||||
return SynthesizeResult(
|
||||
|
||||
@@ -92,6 +92,7 @@ class TTSStreamingService:
|
||||
sample_rate=sample_rate,
|
||||
format=audio_format,
|
||||
speed=speed,
|
||||
language="zh-CN",
|
||||
)
|
||||
except CosyVoiceError as e:
|
||||
logger.error(f"流式合成失败: {e}")
|
||||
@@ -158,6 +159,7 @@ class TTSStreamingService:
|
||||
sample_rate=sample_rate,
|
||||
format=audio_format,
|
||||
speed=speed,
|
||||
language="zh-CN",
|
||||
)
|
||||
audio_url = result.get("audio_url", "")
|
||||
if audio_url:
|
||||
|
||||
@@ -146,6 +146,7 @@ class TTSWorkflowService:
|
||||
_meta = dict(job.metadata)
|
||||
_speed = float(_meta.get("speed", 1.0) or 1.0)
|
||||
_emotion = str(_meta.get("emotion", "") or "")
|
||||
_language = str(_meta.get("language", "zh-CN") or "zh-CN")
|
||||
submit_result = self.cosyvoice_service.submit_synthesize_task(
|
||||
text=job.input_text,
|
||||
voice_id=job.voice_id,
|
||||
@@ -153,6 +154,7 @@ class TTSWorkflowService:
|
||||
format=job.format,
|
||||
speed=_speed,
|
||||
emotion=_emotion,
|
||||
language=_language,
|
||||
)
|
||||
|
||||
# 保存 task_id / request_id 到 metadata
|
||||
@@ -289,6 +291,7 @@ class TTSWorkflowService:
|
||||
speed = float(job_metadata.get("speed", 1.0))
|
||||
volume = int(job_metadata.get("volume", 50))
|
||||
emotion = str(job_metadata.get("emotion", "") or "")
|
||||
language = str(job_metadata.get("language", "zh-CN") or "zh-CN")
|
||||
|
||||
result = self.cosyvoice_service.submit_synthesize_task(
|
||||
text=job.input_text,
|
||||
@@ -298,6 +301,7 @@ class TTSWorkflowService:
|
||||
speed=speed,
|
||||
volume=volume,
|
||||
emotion=emotion,
|
||||
language=language,
|
||||
)
|
||||
audio_url = result.get("audio_url", "")
|
||||
if not audio_url:
|
||||
@@ -423,6 +427,7 @@ class TTSWorkflowService:
|
||||
format=job.format,
|
||||
speed=_seg_speed,
|
||||
emotion=_seg_emotion,
|
||||
language=_seg_meta.get("language", "zh-CN") or "zh-CN",
|
||||
)
|
||||
future_to_idx[future] = idx
|
||||
|
||||
@@ -548,6 +553,7 @@ class TTSWorkflowService:
|
||||
speed=speed,
|
||||
volume=volume,
|
||||
emotion=emotion,
|
||||
language=job.metadata.get("language", "zh-CN") if hasattr(job, "metadata") else "zh-CN",
|
||||
)
|
||||
future_to_idx[future] = idx
|
||||
|
||||
|
||||
Reference in New Issue
Block a user