Ver Fonte

feat(tts): 方言指令支持 — longanhuan_v3 通过 instruction 控方言

根据官方文档实测,cosyvoice-v3-flash + longanhuan_v3 支持 instruction 字段,
格式为单数字符串 "请用河南话表达。"(非数组 instructions)。

改动:
- aliyun.provider.ts: 对 cosyvoice-v3-flash 开放 instruction 字段,
  接收 VoiceParams.dialectInstruction 参数
- book-generator.store.ts: processTtsTask() 从 pickTtsVendor 读取
  dialectInstruction,注入 generateAudio 的 VoiceParams
- build-lang-matrix-json.js: RECOMMENDED 每条加 dialectInstruction
  (普通话/外语=undefined,方言="请用XX话表达。")
- tts-lang-router.service.ts: LangRoute.dialectInstruction 字段
- 矩阵 JSON 同步更新

方言语音生成链路:
  用户选 zh-HEN → pickTtsVendor → voice=longanhuan_v3
  → dialectInstruction="请用河南话表达。"
  → VoiceParams.dialectInstruction → CosyVoice API instruction
  → 河南话输出
MyFramework User há 1 mês atrás
pai
commit
ed0f9532b0

+ 36 - 15
deploy-package/scripts/build-lang-matrix-json.js

@@ -150,22 +150,43 @@ function langLabel(key, base, dialect) {
 }
 
 // ── 3. 推荐音色(从 docs/alicloud-tts-lang-voice-matrix.md §三 手动权威) ───────
+const DIALECT_INSTRUCTIONS = {
+  // 普通话:不需要指令
+  'zh-CMN':  undefined,
+  // 方言:通过 instruction 控制 longanhuan_v3 的方言输出
+  'zh-YUE':  '请用粤语表达。',
+  'zh-NEN':  '请用东北话表达。',
+  'zh-SCN':  '请用四川话表达。',
+  'zh-SHD':  '请用山东话表达。',
+  'zh-HEN':  '请用河南话表达。',
+  'zh-HNN':  '请用湖南话表达。',
+  'zh-SHX':  '请用陕西话表达。',
+  'zh-AHM':  '请用安徽话表达。',
+  'zh-MNN':  '请用闽南话表达。',
+  // 外语:不需要指令(音色本身就说那种语言)
+  'en-US':   undefined,
+  'en-GB':   undefined,
+  'ja-JP':   undefined,
+  'ko-KR':   undefined,
+  'id-ID':   undefined,
+};
+
 const RECOMMENDED = {
-  'zh-CMN':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '欢脱元气女,支持 9 种方言' },
-  'zh-YUE':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanyue',     desc: '粤语标准女声' },
-  'zh-NEN':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '东北话 → longanhuan_v3 兜底' },
-  'zh-SCN':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '四川话兜底' },
-  'zh-SHD':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '山东话兜底' },
-  'zh-HEN':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '河南话兜底' },
-  'zh-HNN':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '湖南话兜底' },
-  'zh-SHX':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longshange',    desc: '陕西话专用户(陕哥)' },
-  'zh-AHM':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '安徽话 → longanhuan_v3 兜底' },
-  'zh-MNN':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanmin',     desc: '闽南话专用户' },
-  'en-US':   { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longava',       desc: '美式英语女声' },
-  'en-GB':   { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'loongluna',     desc: '英语英伦少女' },
-  'ja-JP':   { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'loongtomoka',   desc: '日语女声' },
-  'ko-KR':   { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'sohee',         desc: '韩语女声' },
-  'id-ID':   { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'loongindah',    desc: '印尼女声' },
+  'zh-CMN':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '欢脱元气女,支持 9 种方言', dialectInstruction: DIALECT_INSTRUCTIONS['zh-CMN'] },
+  'zh-YUE':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanyue',     desc: '粤语标准女声', dialectInstruction: DIALECT_INSTRUCTIONS['zh-YUE'] },
+  'zh-NEN':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '东北话,指令控制', dialectInstruction: DIALECT_INSTRUCTIONS['zh-NEN'] },
+  'zh-SCN':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '四川话,指令控制', dialectInstruction: DIALECT_INSTRUCTIONS['zh-SCN'] },
+  'zh-SHD':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '山东话,指令控制', dialectInstruction: DIALECT_INSTRUCTIONS['zh-SHD'] },
+  'zh-HEN':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '河南话,指令控制', dialectInstruction: DIALECT_INSTRUCTIONS['zh-HEN'] },
+  'zh-HNN':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '湖南话,指令控制', dialectInstruction: DIALECT_INSTRUCTIONS['zh-HNN'] },
+  'zh-SHX':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longshange',    desc: '陕西话专用(无需指令)', dialectInstruction: DIALECT_INSTRUCTIONS['zh-SHX'] },
+  'zh-AHM':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanhuan_v3', desc: '安徽话,指令控制', dialectInstruction: DIALECT_INSTRUCTIONS['zh-AHM'] },
+  'zh-MNN':  { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longanmin',     desc: '闽南话专用(无需指令)', dialectInstruction: DIALECT_INSTRUCTIONS['zh-MNN'] },
+  'en-US':   { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'longava',       desc: '美式英语女声', dialectInstruction: DIALECT_INSTRUCTIONS['en-US'] },
+  'en-GB':   { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'loongluna',     desc: '英语英伦少女', dialectInstruction: DIALECT_INSTRUCTIONS['en-GB'] },
+  'ja-JP':   { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'loongtomoka',   desc: '日语女声', dialectInstruction: DIALECT_INSTRUCTIONS['ja-JP'] },
+  'ko-KR':   { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'sohee',         desc: '韩语女声', dialectInstruction: DIALECT_INSTRUCTIONS['ko-KR'] },
+  'id-ID':   { vendor: 'bailian', model: 'cosyvoice-v3-flash', voice: 'loongindah',    desc: '印尼女声', dialectInstruction: DIALECT_INSTRUCTIONS['id-ID'] },
 };
 
 // ── 4. 主流程: 读 raw → 生成 routes ─────────────────────────────

+ 18 - 9
server/src/config/alicloud-tts-lang-voice-matrix.json

@@ -412,7 +412,8 @@
         "vendor": "bailian",
         "model": "cosyvoice-v3-flash",
         "voice": "longanhuan_v3",
-        "desc": "安徽话 → longanhuan_v3 兜底"
+        "desc": "安徽话,指令控制",
+        "dialectInstruction": "请用安徽话表达。"
       },
       "fallbackChain": [
         "longanhuan_v3",
@@ -610,7 +611,8 @@
         "vendor": "bailian",
         "model": "cosyvoice-v3-flash",
         "voice": "longanhuan_v3",
-        "desc": "河南话兜底"
+        "desc": "河南话,指令控制",
+        "dialectInstruction": "请用河南话表达。"
       },
       "fallbackChain": [
         "longanhuan_v3",
@@ -636,7 +638,8 @@
         "vendor": "bailian",
         "model": "cosyvoice-v3-flash",
         "voice": "longanhuan_v3",
-        "desc": "湖南话兜底"
+        "desc": "湖南话,指令控制",
+        "dialectInstruction": "请用湖南话表达。"
       },
       "fallbackChain": [
         "longanhuan_v3",
@@ -662,7 +665,8 @@
         "vendor": "bailian",
         "model": "cosyvoice-v3-flash",
         "voice": "longanmin",
-        "desc": "闽南话专用户"
+        "desc": "闽南话专用(无需指令)",
+        "dialectInstruction": "请用闽南话表达。"
       },
       "fallbackChain": [
         "longanmin",
@@ -688,7 +692,8 @@
         "vendor": "bailian",
         "model": "cosyvoice-v3-flash",
         "voice": "longanhuan_v3",
-        "desc": "东北话 → longanhuan_v3 兜底"
+        "desc": "东北话,指令控制",
+        "dialectInstruction": "请用东北话表达。"
       },
       "fallbackChain": [
         "longanhuan_v3",
@@ -719,7 +724,8 @@
         "vendor": "bailian",
         "model": "cosyvoice-v3-flash",
         "voice": "longanhuan_v3",
-        "desc": "四川话兜底"
+        "desc": "四川话,指令控制",
+        "dialectInstruction": "请用四川话表达。"
       },
       "fallbackChain": [
         "longanhuan_v3",
@@ -745,7 +751,8 @@
         "vendor": "bailian",
         "model": "cosyvoice-v3-flash",
         "voice": "longanhuan_v3",
-        "desc": "山东话兜底"
+        "desc": "山东话,指令控制",
+        "dialectInstruction": "请用山东话表达。"
       },
       "fallbackChain": [
         "longanhuan_v3",
@@ -771,7 +778,8 @@
         "vendor": "bailian",
         "model": "cosyvoice-v3-flash",
         "voice": "longshange",
-        "desc": "陕西话专用户(陕哥)"
+        "desc": "陕西话专用(无需指令)",
+        "dialectInstruction": "请用陕西话表达。"
       },
       "fallbackChain": [
         "longshange",
@@ -802,7 +810,8 @@
         "vendor": "bailian",
         "model": "cosyvoice-v3-flash",
         "voice": "longanyue",
-        "desc": "粤语标准女声"
+        "desc": "粤语标准女声",
+        "dialectInstruction": "请用粤语表达。"
       },
       "fallbackChain": [
         "longanyue",

+ 5 - 2
server/src/modules/book-generator/book-generator.store.ts

@@ -1540,14 +1540,17 @@ export class BookStore {
       const bookId = chapter.bookId;
       // 语速在创建书籍时已确定(用户传入 或 儿童内容自动检测),此处直接使用
       const voiceSpeed = task.voiceSpeed ?? chapter.book?.voiceSpeed ?? DEFAULT_VOICE_SPEED;
+      // 🆕 方言指令:选的语言是方言 → pickTtsVendor 返回 dialectInstruction
+      const langRoute = pickTtsVendor(task.targetLanguage || undefined);
+      const dialectInstruction = langRoute?.dialectInstruction;
 
       // 设置上下文,后续 TTS 调用自动携带 userId/bookId/chapterId
       await runWithContext({ userId, bookId, chapterId }, async () => {
       const result = await generateAudio(
         String(userId),
         content,
-        task.voiceId || 'longanhuan_v3',
-        { speed: voiceSpeed, pitch: 0, volume: 50 },
+        task.voiceId || langRoute?.voice || 'longanhuan_v3',
+        { speed: voiceSpeed, pitch: 0, volume: 50, dialectInstruction: dialectInstruction || undefined } as any,
         async (audioUrl: string, duration: number) => {
           console.log(`[TtsTask] #${taskId} 音频就绪: ${audioUrl?.substring(0, 60)}...`);
           try {

+ 8 - 7
server/src/modules/tts/aliyun.provider.ts

@@ -89,13 +89,14 @@ export class AliyunTtsProvider implements ITtsProvider {
         if (isCosyVoice) {
           requestBody.input.format = 'mp3';
           requestBody.input.sample_rate = 48000;
-          // 修复 #tts-cosyvoice-428: cosyvoice-v3-flash 的 SpeechSynthesizer 接口
-          // 不接受 instruction 字段,任何带 instruction 的请求都返回
-          // 400 InvalidParameter (Engine return error code: 428),
-          // 连官方宣称支持 Instruct 的音色(longanhuan_v3 等)也一样。
-          // 实测:纯净请求 200,带 instruction 100% 失败。
-          // 因此 cosyvoice 不注入 instruction —— 情感控制改由 qwen3-tts-instruct 承担。
-          // (如需在 cosyvoice 上做情感,应改用 WebSocket streaming 接口,而非此同步接口)
+          // 旧版 cosyvoice 不接受 instruction,新版(2026-07 实测)已修复。
+          // 形式为单数 "instruction"(字符串),方言控制走此路径。
+          if (activeModel === 'cosyvoice-v3-flash') {
+            const dialectInstr = (params as any).dialectInstruction as string | undefined;
+            if (dialectInstr) {
+              requestBody.input.instruction = dialectInstr;
+            }
+          }
         }
         if (!isCosyVoice && activeModel.includes('instruct')) {
           const instructions: string[] = [];

+ 1 - 0
server/src/services/tts-lang-router.service.ts

@@ -22,6 +22,7 @@ export interface LangRoute {
   voice: string;
   preferredName: string;  // provider registry key, 例 'bailian-tts' / 'edge-tts'
   reason: string;         // 选这条的 log 用
+  dialectInstruction?: string;  // 方言指令(如 "请用河南话表达。"), null=普通话/外语无需
 }
 
 interface LangDef {