gen-lang-matrix-doc.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. #!/usr/bin/env node
  2. /**
  3. * gen-lang-matrix-doc.js — 从 alicloud-voices-matrix.json 生成 docs/alicloud-tts-lang-voice-matrix.md
  4. *
  5. * 用于未来"用户选语言"功能 — 这是核心数据库。
  6. */
  7. const fs = require('fs');
  8. const os = require('os');
  9. const path = require('path');
  10. const input = process.argv[2] || path.join(os.tmpdir(), 'alicloud-voices-matrix.json');
  11. const matrix = JSON.parse(fs.readFileSync(input, 'utf8'));
  12. // 方言别名:aliyun 不同写法 → "中文(方言)"
  13. const DIALECT = {
  14. '广东话': '粤语', '闽南话': '闽南', '东北话': '东北',
  15. '四川话': '四川', '山东话': '山东', '河南话': '河南',
  16. '湖南话': '湖南', '陕西话': '陕西', '安徽话': '安徽',
  17. };
  18. // 英语口音前缀
  19. const ENG_VARIANT = {
  20. '美式英语': '美式', '英式英语': '英式',
  21. '美式英文': '美式', '英式英文': '英式',
  22. };
  23. // 语种标准化: 把 "中文(普通话、英文)" → ["中文(普通话)", "英语(美式)"]
  24. function normalizeLang(s) {
  25. if (!s) return [];
  26. // 先替换中文括号为半角,再 split
  27. const norm = s.replace(/(/g, '(').replace(/)/g, ')').replace(/\s+/g, ' ');
  28. const out = new Set();
  29. for (const part of norm.split(/[、,,]/)) {
  30. const p = part.trim();
  31. if (!p) continue;
  32. // 已经是"中文(X)"或"英语(X)"完整
  33. let m = p.match(/^中文\(([^)]+)\)$/);
  34. if (m) { out.add(`中文(${m[1].trim()})`); continue; }
  35. m = p.match(/^英语\(([^)]+)\)$/);
  36. if (m) { out.add(`英语(${m[1].trim()})`); continue; }
  37. m = p.match(/^中[文语]?\(?(东北|广东|闽南|四川|山东|河南|湖南|陕西|安徽)/);
  38. if (m) { out.add(`中文(${m[1]})`); continue; }
  39. if (DIALECT[p]) { out.add(`中文(${DIALECT[p]})`); continue; }
  40. if (ENG_VARIANT[p]) { out.add(`英语(${ENG_VARIANT[p]})`); continue; }
  41. // 其他纯净外语音
  42. out.add(p);
  43. }
  44. return [...out];
  45. }
  46. // model × lang_key → Set<voice_id>
  47. const modelLangVoices = {};
  48. for (const [model, voices] of Object.entries(matrix)) {
  49. modelLangVoices[model] = {};
  50. for (const v of voices) {
  51. for (const l of normalizeLang(v.lang || '')) {
  52. if (!modelLangVoices[model][l]) modelLangVoices[model][l] = new Set();
  53. if (v.voice_id) modelLangVoices[model][l].add(v.voice_id);
  54. }
  55. }
  56. }
  57. const allLangs = new Set();
  58. for (const m of Object.keys(modelLangVoices)) {
  59. for (const k of Object.keys(modelLangVoices[m])) allLangs.add(k);
  60. }
  61. // 分类
  62. function categorize(lang) {
  63. if (/^中文/.test(lang)) return '中文';
  64. if (/^英语/.test(lang)) return '英语';
  65. if (/日语/.test(lang)) return '日语';
  66. if (/韩语/.test(lang)) return '韩语';
  67. if (/印尼|越南|泰|马来|菲律宾/.test(lang)) return '东南亚';
  68. if (/法[语]?|德[语]?|意[大利]?|西[班牙]?|葡[萄牙]?|俄[罗斯]?/.test(lang)) return '欧洲';
  69. return '其他';
  70. }
  71. const buckets = {};
  72. for (const l of allLangs) {
  73. const b = categorize(l);
  74. if (!buckets[b]) buckets[b] = [];
  75. buckets[b].push(l);
  76. }
  77. for (const b in buckets) buckets[b].sort();
  78. const allBuckets = ['中文', '英语', '日语', '韩语', '欧洲', '东南亚', '其他'];
  79. const out = [];
  80. out.push('# 阿里云 TTS 模型 × 语言 × 音色 矩阵');
  81. out.push('');
  82. out.push('> 数据来源:阿里云官方 [系统预置音色参数与特性列表](https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list)(本地下载的 `tts音色/` 目录)');
  83. out.push(`> 抓取方式:`);
  84. out.push('> - CosyVoice 4 个模型:v3-flash / v3-plus / v2 / v1,共 216 个音色');
  85. out.push('> - Qwen3-TTS v3-flash:48 个音色');
  86. out.push('> - 全文 JSON 数据:`/tmp/alicloud-voices-matrix.json`');
  87. out.push('');
  88. out.push('**用途**:未来"用户选语言"功能的**核心数据库** — 用户选 X 语言,后端路由先查表确认有可用音色再调 TTS,**避免"用户选了但生成失败"的尴尬**。');
  89. out.push('');
  90. out.push('---');
  91. out.push('');
  92. out.push('## 一、模型 × 音色 × 主推语种');
  93. out.push('');
  94. out.push('| 模型 | 音色数 | 主要语种 | Instruct 支持 | 现状 |');
  95. out.push('|------|-------|---------|--------------|------|');
  96. const modelInfo = {
  97. 'cosyvoice-v3-flash': { langs: '中文 9 种方言 + 英/日/韩/印尼', instruct: '✅(固定格式)', rec: '✅ 项目主力' },
  98. 'cosyvoice-v3-plus': { langs: '中/英(仅 2 音色)', instruct: '❌', rec: '⚠️ 兼容旧集成' },
  99. 'cosyvoice-v2': { langs: '普通话+粤/东北/闽南/陕西+英/日/韩', instruct: '部分', rec: '老牌音色最丰富' },
  100. 'cosyvoice-v1': { langs: '普通话 + 东北口音', instruct: '❌', rec: '❌ 历史,逐步淘汰' },
  101. };
  102. for (const [model, voices] of Object.entries(matrix)) {
  103. const info = modelInfo[model] || { langs: '?', instruct: '?', rec: '?' };
  104. out.push(`| \`${model}\` | ${voices.length} | ${info.langs} | ${info.instruct} | ${info.rec} |`);
  105. }
  106. out.push('');
  107. out.push('> ⚠️ **特别说明**:CosyVoice v3.5 (`-plus` / `-flash`) **没有系统音色**,只支持"声音复刻"和"声音设计"。复刻/设计音色理论上支持 11 种语言(中/英/法/德/日/韩/俄/葡/泰/印尼/越南),但需先录制训练,不在本表中。');
  108. out.push('');
  109. out.push('---');
  110. out.push('');
  111. out.push('## 二、语言选择可用性矩阵(用户视角)');
  112. out.push('');
  113. out.push('> 用户在 UI 选语言,看哪些模型/音色真的能生成。✅ = 有可用音色');
  114. out.push('');
  115. out.push('| 语言 | cosyvoice-v3-flash | cosyvoice-v3-plus | cosyvoice-v2 | cosyvoice-v1 |');
  116. out.push('|------|-------------------|------------------|-------------|-------------|');
  117. for (const bucket of allBuckets) {
  118. const langs = buckets[bucket] || [];
  119. if (langs.length === 0) continue;
  120. for (const lang of langs) {
  121. const cells = [];
  122. for (const model of ['cosyvoice-v3-flash', 'cosyvoice-v3-plus', 'cosyvoice-v2', 'cosyvoice-v1']) {
  123. const count = (modelLangVoices[model] || {})[lang]?.size || 0;
  124. cells.push(count > 0 ? `✅ ${count}` : '❌');
  125. }
  126. out.push(`| ${lang} | ${cells.join(' | ')} |`);
  127. }
  128. }
  129. out.push('');
  130. out.push('---');
  131. out.push('');
  132. out.push('## 三、推荐配置(给前端语言选择器)');
  133. out.push('');
  134. out.push('用户在前端选语言后,后端按这套规则路由:');
  135. out.push('');
  136. out.push('| 用户选 | 默认 vendor | 模型 | 默认音色 | 备选音色 |');
  137. out.push('|--------|------------|------|---------|---------|');
  138. out.push('| 中文普通话 | edge / cosyvoice | cosyvoice-v3-flash | `longanhuan_v3`(9 种方言) | 苏瑶 / 凯 |');
  139. out.push('| 中文(粤语) | cosyvoice | cosyvoice-v3-flash | `龙安粤` 或 `longanhuan_v3` | - |');
  140. out.push('| 中文(闽南) / 东北 / 河南 / 湖南 / 陕西 / 山东 / 安徽 / 四川 | cosyvoice | cosyvoice-v3-flash | **`longanhuan_v3`** 唯一支持多方言 | - |');
  141. out.push('| 中文(闽南) | cosyvoice | cosyvoice-v3-flash | `龙安闽` | - |');
  142. out.push('| 中文(四川) | cosyvoice | cosyvoice-v3-flash | `龙老铁` 或 `天津-李彼得` | - |');
  143. out.push('| 中文(陕西) | cosyvoice | cosyvoice-v3-flash | `龙陕哥` 或 `longanhuan_v3` | - |');
  144. out.push('| 中文(北京/上海/南京) | (cosyvoice 没专门) / edge | edge | `zh-CN-XiaoxiaoNeural` | - |');
  145. out.push('| 英语(美式) | qwen / cosyvoice | qwen3-tts-instruct-flash | 詹妮弗 / 艾登 | longandy / longava |');
  146. out.push('| 英语(英式) | qwen / cosyvoice | qwen3-tts-instruct-flash | Riko / loongluna | - |');
  147. out.push('| 日语 | qwen / cosyvoice | cosyvoice-v3-flash | loongtomoka / loongtomoya / 小野杏 | - |');
  148. out.push('| 韩语 | qwen / cosyvoice | cosyvoice-v3-flash | 素熙 | - |');
  149. out.push('| 法语 / 德语 / 俄语 | qwen | qwen3-tts-instruct-flash | 埃米尔安 / 莱恩 / 阿列克 | - |');
  150. out.push('| 西班牙语 / 拉美西班牙 | qwen | qwen3-tts-instruct-flash | 博德加 / 索尼莎 | - |');
  151. out.push('| 葡萄牙语 | qwen | qwen3-tts-instruct-flash | 拉迪奥·戈尔 | - |');
  152. out.push('| 意大利语 | qwen | qwen3-tts-instruct-flash | 多尔切 | - |');
  153. out.push('| 印尼语 | cosyvoice | cosyvoice-v3-flash | `loongindah` | - |');
  154. out.push('');
  155. out.push('---');
  156. out.push('');
  157. out.push('## 四、实现"用户选语言"功能的代码路径');
  158. out.push('');
  159. out.push('### 1. 前端增加语言选择组件');
  160. out.push('');
  161. out.push('```vue');
  162. out.push('<!-- /pages/create/index.vue -->');
  163. out.push('<template>');
  164. out.push(' <picker :range="supportedLanguages" v-model="form.language">');
  165. out.push(' <view slot="value">{{ form.language.label }}</view>');
  166. out.push(' </picker>');
  167. out.push('</template>');
  168. out.push('');
  169. out.push('<script setup>');
  170. out.push('// 加载后端 /api/tts/voices?lang=zh-Cantonese 拿到可用音色 + 试听链接');
  171. out.push('const supportedLanguages = ref([]);');
  172. out.push('onMounted(async () => {');
  173. out.push(' const r = await api.get(\'/api/tts/languages\'); // 返回矩阵 → 用户友好列表');
  174. out.push(' supportedLanguages.value = r.data;');
  175. out.push('});');
  176. out.push('</script>');
  177. out.push('```');
  178. out.push('');
  179. out.push('### 2. 后端路由逻辑(基于矩阵)');
  180. out.push('');
  181. out.push('```ts');
  182. out.push('// server/src/services/tts-router.ts');
  183. out.push('import matrix from \'../../data/alicloud-tts-lang-voice-matrix.json\';');
  184. out.push('');
  185. out.push('export function pickTtsVendor(lang: string, preferCheap = true) {');
  186. out.push(' // 1. 查表(矩阵)');
  187. out.push(' const candidates = matrix.candidates[lang] || [];');
  188. out.push(' if (candidates.length === 0) throw new BadRequest(`暂不支持 ${lang} 语言`);');
  189. out.push(' // 2. 优先免费(Edge)');
  190. out.push(' if (preferCheap) {');
  191. out.push(' const edge = candidates.find(c => c.vendor === \'edge\');');
  192. out.push(' if (edge) return edge;');
  193. out.push(' }');
  194. out.push(' // 3. 默认 cosyvoice-v3-flash');
  195. out.push(' const v3 = candidates.find(c => c.model === \'cosyvoice-v3-flash\');');
  196. out.push(' if (v3) return v3;');
  197. out.push(' // 4. 退化到任意可用');
  198. out.push(' return candidates[0];');
  199. out.push('}');
  200. out.push('```');
  201. out.push('');
  202. out.push('### 3. 防呆:用户选语言后实时预览');
  203. out.push('');
  204. out.push('```ts');
  205. out.push('GET /api/tts/voices?lang=zh-Cantonese');
  206. out.push('// → { voices: [{ name: "龙安粤", voice_id: "longan_yue_3", preview_url: "..." }] }');
  207. out.push('```');
  208. out.push('');
  209. out.push('### 4. 矩阵 JSON 入仓');
  210. out.push('');
  211. out.push('生成好的 216 个音色 JSON 在 `C:/Users/caoyg/AppData/Local/Temp/alicloud-voices-matrix.json`(4 个模型)。');
  212. out.push('可以入仓到 `server/data/alicloud-tts-lang-voice-matrix.json`,启动时加载,提供 `pickTtsVendor(lang)` API。');
  213. out.push('');
  214. out.push('---');
  215. out.push('');
  216. out.push('## 五、迁移指引');
  217. out.push('');
  218. out.push('现在 `server/src/modules/tts/tts.service.ts:243` 的 `ALIYUN_VOICE_MAP` 只有 10 个"龙"系列 + Cherry,远不足以覆盖未来"语言选择"功能。建议:');
  219. out.push('');
  220. out.push('1. 把矩阵简化版(只存音色 ID 列表)写到 `models.json` 供前端调用');
  221. out.push('2. 后端 `pickTtsVendor(lang)` 路由函数读这个矩阵');
  222. out.push('3. 前端只显示"矩阵确认可生成"的语言选项(不然选错体验崩)');
  223. out.push('');
  224. out.push('详细矩阵在 `alicloud-voices-matrix.json`,由 `analyze-model-lang-matrix.py` + `gen-lang-matrix-doc.js` 维护。');
  225. process.stdout.write(out.join('\n'));