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:
@@ -20,28 +20,36 @@ os.environ.setdefault("JWT_SECRET_KEY", "dev-secret-key-for-testing")
|
||||
def test_normalize_emotion_english_values():
|
||||
from packages.application.cosyvoice_service import normalize_emotion
|
||||
|
||||
assert normalize_emotion("natural") == "natural"
|
||||
assert normalize_emotion("excited") == "excited"
|
||||
assert normalize_emotion("calm") == "calm"
|
||||
assert normalize_emotion("friendly") == "friendly"
|
||||
# 大小写 / 空白容错
|
||||
assert normalize_emotion(" Excited ") == "excited"
|
||||
# 英文情绪统一归一化为中文描述词(用于 instruction 自然语言指令)
|
||||
assert normalize_emotion("natural") == "自然"
|
||||
assert normalize_emotion("excited") == "兴奋开心"
|
||||
assert normalize_emotion("calm") == "沉稳平静"
|
||||
assert normalize_emotion("friendly") == "亲切友好"
|
||||
assert normalize_emotion("happy") == "开心愉快"
|
||||
assert normalize_emotion("sad") == "悲伤难过"
|
||||
assert normalize_emotion("angry") == "愤怒"
|
||||
assert normalize_emotion("surprised") == "惊讶"
|
||||
assert normalize_emotion("fearful") == "恐惧"
|
||||
assert normalize_emotion("disgusted") == "厌恶"
|
||||
|
||||
|
||||
def test_normalize_emotion_chinese_values():
|
||||
from packages.application.cosyvoice_service import normalize_emotion
|
||||
|
||||
assert normalize_emotion("自然") == "natural"
|
||||
assert normalize_emotion("兴奋") == "excited"
|
||||
assert normalize_emotion("沉稳") == "calm"
|
||||
assert normalize_emotion("亲切") == "friendly"
|
||||
assert normalize_emotion("自然") == "自然"
|
||||
assert normalize_emotion("兴奋") == "兴奋开心"
|
||||
assert normalize_emotion("沉稳") == "沉稳平静"
|
||||
assert normalize_emotion("亲切") == "亲切友好"
|
||||
assert normalize_emotion("开心") == "开心愉快"
|
||||
assert normalize_emotion("悲伤") == "悲伤难过"
|
||||
assert normalize_emotion("愤怒") == "愤怒"
|
||||
assert normalize_emotion("惊讶") == "惊讶"
|
||||
|
||||
|
||||
def test_normalize_emotion_invalid_returns_empty():
|
||||
from packages.application.cosyvoice_service import normalize_emotion
|
||||
|
||||
assert normalize_emotion("") == ""
|
||||
assert normalize_emotion("angry") == ""
|
||||
assert normalize_emotion("喜怒哀乐") == ""
|
||||
|
||||
|
||||
@@ -83,24 +91,43 @@ def _make_service_with_captured_client(captured: dict):
|
||||
return svc
|
||||
|
||||
|
||||
def test_submit_synthesize_payload_includes_emotion_and_rate():
|
||||
def test_submit_synthesize_payload_uses_instruction_and_language_hints():
|
||||
"""#1898: emotion 通过 instruction 中文自然语言指令传递;语言用 language_hints."""
|
||||
captured: dict = {}
|
||||
svc = _make_service_with_captured_client(captured)
|
||||
|
||||
svc.submit_synthesize_task(text="你好", voice_id="v-1", speed=1.5, emotion="兴奋")
|
||||
svc.submit_synthesize_task(text="你好", voice_id="longxiaochun_v3", speed=1.5, emotion="兴奋", language="zh-CN")
|
||||
|
||||
inp = captured["json"]["input"]
|
||||
assert inp["emotion"] == "excited"
|
||||
# 不再传 emotion 枚举字段
|
||||
assert "emotion" not in inp
|
||||
# 情绪通过 instruction 中文指令
|
||||
assert inp["instruction"] == "你说话的情感是兴奋开心。"
|
||||
# rate 字段保持
|
||||
assert inp["rate"] == 1.5
|
||||
# 系统音色 zh → language_hints=["zh"]
|
||||
assert inp["language_hints"] == ["zh"]
|
||||
|
||||
|
||||
def test_submit_synthesize_payload_omits_emotion_when_empty():
|
||||
def test_submit_synthesize_payload_omits_instruction_when_emotion_empty():
|
||||
captured: dict = {}
|
||||
svc = _make_service_with_captured_client(captured)
|
||||
|
||||
svc.submit_synthesize_task(text="你好", voice_id="v-1")
|
||||
svc.submit_synthesize_task(text="hello", voice_id="longxiaochun_v3", language="en-US")
|
||||
|
||||
assert "emotion" not in captured["json"]["input"]
|
||||
inp = captured["json"]["input"]
|
||||
assert "instruction" not in inp
|
||||
assert inp["language_hints"] == ["en"]
|
||||
|
||||
|
||||
def test_submit_synthesize_payload_english_emotion_maps_to_chinese():
|
||||
captured: dict = {}
|
||||
svc = _make_service_with_captured_client(captured)
|
||||
|
||||
svc.submit_synthesize_task(text="hi", voice_id="longxiaochun_v3", emotion="sad")
|
||||
|
||||
inp = captured["json"]["input"]
|
||||
assert inp["instruction"] == "你说话的情感是悲伤难过。"
|
||||
|
||||
|
||||
# ── 对口型 TTS 直生分支 ─────────────────────────────────────────────────
|
||||
@@ -146,7 +173,7 @@ def test_create_job_tts_direct_mode_synthesizes_audio():
|
||||
|
||||
# v4: TTS 模式下 create_job 返回 tts_processing 状态,dispatch Celery 任务
|
||||
assert job.status == "tts_processing"
|
||||
assert job.emotion == "excited"
|
||||
assert job.emotion == "兴奋" # 存储原始情绪值,normalize 在 CosyVoiceService 内部完成
|
||||
assert job.speed == 1.2
|
||||
# 不直接调用 CosyVoice(由 Celery 任务处理)
|
||||
cosy.submit_synthesize_task.assert_not_called()
|
||||
|
||||
@@ -254,7 +254,7 @@ class TestLipsyncServiceUnit:
|
||||
mock_task.apply_async.assert_called_once()
|
||||
# job 记录透传字段
|
||||
assert job.speed == 1.2
|
||||
assert job.emotion == "excited"
|
||||
assert job.emotion == "兴奋"
|
||||
# MediaKit 尚未提交(由 Celery 任务处理)
|
||||
mock_mediakit.submit_lipsync.assert_not_called()
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ class TestTTSPreviewEndpoint:
|
||||
voice_id="longxiaochun",
|
||||
speed=1.0,
|
||||
emotion="",
|
||||
language="zh-CN",
|
||||
)
|
||||
|
||||
def test_preview_with_speed(self):
|
||||
@@ -148,6 +149,7 @@ class TestTTSPreviewEndpoint:
|
||||
voice_id="v1",
|
||||
speed=1.5,
|
||||
emotion="",
|
||||
language="zh-CN",
|
||||
)
|
||||
|
||||
def test_preview_cosyvoice_error_returns_502(self):
|
||||
@@ -333,6 +335,7 @@ class TestTTSPreviewEndpoint:
|
||||
voice_id="cosyvoice_actual_voice_123",
|
||||
speed=1.0,
|
||||
emotion="",
|
||||
language="zh-CN",
|
||||
)
|
||||
# Verify repo was queried with the UUID
|
||||
mock_clone_repo.get.assert_called_once_with("abc123-uuid-of-profile")
|
||||
@@ -410,6 +413,7 @@ class TestTTSPreviewEndpoint:
|
||||
voice_id="longxiaoxia_v3",
|
||||
speed=1.0,
|
||||
emotion="",
|
||||
language="zh-CN",
|
||||
)
|
||||
|
||||
def test_preview_clone_voice_wrong_user_returns_403(self):
|
||||
|
||||
Executable → Regular
+1
@@ -176,6 +176,7 @@ class TestStreamShortText:
|
||||
sample_rate=22050,
|
||||
format="wav",
|
||||
speed=1.5,
|
||||
language="zh-CN",
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user