gen-aliyun-tts-doc.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. #!/usr/bin/env node
  2. /**
  3. * gen-aliyun-tts-doc.js — 生成阿里云百炼 TTS 文档
  4. *
  5. * 数据来源:
  6. * 1. 代码里 server/src/modules/tts/tts.service.ts 的 ALIYUN_VOICE_MAP(10 个龙系列音色)
  7. * 2. server/.env 里的 DASHSCOPE_* 配置
  8. * 3. 公开资料补充
  9. *
  10. * 注意:DashScope 没提供 list_voices HTTP 接口(试过 SpeechSynthesizer/voices、
  11. * multimodal-generation/voices、/api/v1/models 等都返回 url error),
  12. * 完整列表需要从阿里云控制台 → 模型服务 → 语音合成 → 音色列表页 复制。
  13. */
  14. const fs = require('fs');
  15. const path = require('path');
  16. const repo = path.resolve(__dirname, '..', '..');
  17. const ttsService = fs.readFileSync(path.join(repo, 'server/src/modules/tts/tts.service.ts'), 'utf8');
  18. // 解析 ALIYUN_VOICE_MAP
  19. const aliyunMatch = ttsService.match(/const ALIYUN_VOICE_MAP[^=]*=\s*\{([\s\S]*?)\};/);
  20. const aliyunVoices = [];
  21. if (aliyunMatch) {
  22. const block = aliyunMatch[1];
  23. for (const line of block.split('\n')) {
  24. const m = line.match(/^\s*(voice_\d+):\s*'(\w+)',\s*(?:\/\/\s*(.+))?$/);
  25. if (m) {
  26. aliyunVoices.push({
  27. id: m[1],
  28. name: m[2],
  29. desc: m[3] ? m[3].trim() : '',
  30. });
  31. }
  32. }
  33. }
  34. // ========== CosyVoice 指令使用指南(权威资料,2025-07-12 从阿里云官方文档摘录) ==========
  35. // 由用户从官方页面 https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list
  36. // 整理出来的 4 个模型 + 指令规则 + 描述原则 + 示例。脚本自动随 docs 重生成。
  37. const INSTRUCTION_GUIDE = `
  38. ## CosyVoice 指令使用指南(重要)
  39. > 来源:阿里云官方文档 https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list,2025-07-12 摘录。
  40. > 本项目用的是 \`cosyvoice-v3-flash\`(默认)和 \`qwen3-tts-instruct-flash\`,下面的指令规则适用。
  41. ### 支持的 4 个模型
  42. | 模型 | 价格层 | 推荐场景 |
  43. |------|--------|----------|
  44. | \`cosyvoice-v3.5-plus\` | 高质量 | 重点音频,情感/语速精细控制 |
  45. | \`cosyvoice-v3.5-flash\` | 快速 | 日常大批量 |
  46. | \`cosyvoice-v3-plus\` | 老款 | 兼容已有集成 |
  47. | \`cosyvoice-v3-flash\` | 快速 | **项目当前默认** |
  48. ### 不同模型对指令的格式要求
  49. | 模型 | 声音复刻 / 设计音色 | 系统音色 |
  50. |------|--------------------|----------|
  51. | cosyvoice-v3.5-plus | 可输入任意指令(自由描述) | ❌ v3.5 不支持系统音色 |
  52. | cosyvoice-v3.5-flash | 可输入任意指令 | ❌ v3.5 不支持系统音色 |
  53. | cosyvoice-v3-plus | ❌ 不支持指令控制 | 指令必须固定格式(见 CosyVoice 音色列表) |
  54. | cosyvoice-v3-flash | 可输入任意指令 | 指令必须固定格式 |
  55. 使用方式:通过 \`instruction\` 参数指定指令内容。
  56. ### 指令支持的语言
  57. | 模型 | 声音复刻 | 系统音色 |
  58. |------|---------|----------|
  59. | v3.5-plus / v3.5-flash | 中 / 英 / 法 / 德 / 日 / 韩 / 俄 / 葡 / 泰 / 印尼 / 越南 | ❌ 不支持 |
  60. | v3-plus | 中 / 英 / 法 / 德 / 日 / 韩 / 俄 | 固定格式 |
  61. | v3-flash | 中 / 英 / 法 / 德 / 日 / 韩 / 俄 | 中文 |
  62. ### 指令长度限制
  63. **不超过 100 字符**(按以下规则计数):
  64. - 汉字(简体 / 繁体 / 日文汉字 / 韩文汉字)**按 2 个字符**
  65. - 其他字符(标点 / 字母 / 数字 / 日韩假名 / 谚文等)按 1 个字符
  66. \`\`\`example
  67. "温柔知性的女性,30 岁左右,语调平和,适合有声书朗读"
  68. 温柔=2 + 知性=2 + 的=1 + 女性=2 + ,=1 + 30=2 + 岁=1 + 左右=2 + ... ≈ 70 字符 ✅
  69. \`\`\`
  70. ### 适用场景
  71. - 有声书和广播剧配音
  72. - 广告和宣传片配音
  73. - 游戏角色和动画配音
  74. - 情感化的智能语音助手
  75. - 纪录片和新闻播报
  76. ### 如何编写高质量的声音描述
  77. #### 5 个核心原则
  78. 1. **具体而非模糊** — 用"低沉 / 清脆 / 语速偏快"等具体特征,避免"好听 / 普通"
  79. 2. **多维而非单一** — 组合"性别 + 年龄 + 情感 + 用途"等维度,只写"女声"太宽泛
  80. 3. **客观而非主观** — 用"音调偏高,带有活力"代替"我最喜欢的声音"
  81. 4. **原创而非模仿** — 描述声音特质,**不要**要求模仿特定人物(涉版权风险)
  82. 5. **简洁而非冗余** — 每个词都有明确作用,避免重复修饰
  83. #### 7 个推荐描述维度
  84. | 维度 | 可选值 |
  85. |------|--------|
  86. | 性别 | 男性 / 女性 / 中性 |
  87. | 年龄 | 儿童(5-12) / 青少年(13-18) / 青年(19-35) / 中年(36-55) / 老年(55+) |
  88. | 音调 | 高音 / 中音 / 低音 / 偏高 / 偏低 |
  89. | 语速 | 快速 / 中速 / 缓慢 / 偏快 / 偏慢 |
  90. | 情感 | 开朗 / 沉稳 / 温柔 / 严肃 / 活泼 / 冷静 / 治愈 |
  91. | 特点 | 有磁性 / 清脆 / 沙哑 / 圆润 / 甜美 / 浑厚 / 有力 |
  92. | 用途 | 新闻播报 / 广告配音 / 有声书 / 动画角色 / 语音助手 / 纪录片 |
  93. #### 描述示例(从官方复制,微调措辞)
  94. 1. **标准播音风格**:吐字清晰精准,字正腔圆
  95. 2. **年轻活泼女性**:语速较快,带有明显的上扬语调,适合介绍时尚产品
  96. 3. **沉稳中年男性**:语速缓慢,音色低沉有磁性,适合朗读新闻或纪录片解说
  97. 4. **温柔知性女性**:30 岁左右,语调平和,适合有声书朗读
  98. 5. **可爱儿童**:大约 8 岁女孩,说话略带稚气,适合动画角色配音
  99. ### 项目内当前用法的指令格式
  100. 代码 \`server/src/modules/tts/aliyun.provider.ts:103-105\` 给 Qwen-TTS Instruct 用的指令格式:
  101. \`\`\`ts
  102. if (params.speed !== 1) instructions.push(\`语速\${params.speed > 1 ? '较快' : '较慢'}\`);
  103. if (params.pitch !== 0) instructions.push(\`音调\${params.pitch > 0 ? '较高' : '较低'}\`);
  104. \`\`\`
  105. 例如 speed=0.78 → \`"语速较慢"\`(符合 v3-flash 的指令规则,因为 speed 描述在系统音色固定格式内)。
  106. ### 注意事项
  107. - **字数限制 100 字符很严**,所以\"语速较慢\"比\"语速稍微放缓一点\"更好(后者汉字太多可能超限)
  108. - **优先用声音特征描述(性别 + 年龄 + 音调)**,避免纯情绪词(\"治愈的\"),后者难以稳定生成
  109. - **不要混用语言**,指令要全中文(系统音色)或全英文/全日文(复刻音色)
  110. - **自定义音色的指令自由度更高**,可以实验长 prompt,但要测听感
  111. `;
  112. const md = [];
  113. md.push('# 阿里云百炼 TTS 文档');
  114. md.push('');
  115. md.push('> 当前线上同时支持 Edge TTS(免费) 和 阿里云百炼(付费,听感更自然)。');
  116. md.push('');
  117. md.push('**项目当前默认:** Edge TTS(因为免费),阿里云是降级/fallback 选项。要切换默认改 `server/src/config/models.json` 的 `tts.defaultVendor: bailian`。');
  118. md.push('');
  119. md.push('## 关键配置文件');
  120. md.push('');
  121. md.push('### 1. `server/.env`');
  122. md.push('');
  123. md.push('```bash');
  124. md.push('DASHSCOPE_API_KEY=sk-c25679401ba24c749f53be86b0c9a7a6');
  125. md.push('DASHSCOPE_MODEL=qwen3-tts-instruct-flash # 默认 TTS 模型');
  126. md.push('DASHSCOPE_TTS_MODELS=qwen3-tts-flash # 备用模型列表(逗号分隔)');
  127. md.push('DASHSCOPE_VOICE=Cherry # Qwen-TTS 默认音色');
  128. md.push('DASHSCOPE_USE_REALTIME=true # 启用流式 WebSocket');
  129. md.push('DASHSCOPE_REALTIME_MODEL=qwen3-tts-instruct-flash-realtime');
  130. md.push('```');
  131. md.push('');
  132. md.push('### 2. `server/src/config/models.json`');
  133. md.push('');
  134. md.push('TTS 供应商注册(在 vendor registry 里维护):');
  135. md.push('');
  136. md.push('```json');
  137. md.push('{');
  138. md.push(' "tts": {');
  139. md.push(' "defaultVendor": "edge", // 当前默认');
  140. md.push(' "defaultModel": "edge-tts",');
  141. md.push(' "defaultVoice": "zh-CN-XiaoxiaoNeural",');
  142. md.push(' "bailian": {');
  143. md.push(' "priority": 3,');
  144. md.push(' "models": [');
  145. md.push(' { "id": "cosyvoice-v3-flash", "enabled": true },');
  146. md.push(' { "id": "qwen3-tts-instruct-flash", "enabled": true },');
  147. md.push(' { "id": "qwen3-tts-flash", "enabled": true }');
  148. md.push(' ]');
  149. md.push(' }');
  150. md.push(' }');
  151. md.push('}');
  152. md.push('');
  153. md.push('### 3. `server/src/modules/tts/tts.service.ts:243` ALIYUN_VOICE_MAP');
  154. md.push('');
  155. md.push('前端统一 ID(voice_01~voice_10)→ 阿里云真实音色的映射(代码里):');
  156. md.push('');
  157. md.push('| 前端 ID | 阿里云音色 | 描述 |');
  158. md.push('|---------|------------|------|');
  159. for (const v of aliyunVoices) {
  160. md.push(`| ${v.id} | \`${v.name}\` | ${v.desc} |`);
  161. }
  162. md.push('');
  163. md.push('> 命名规律:"龙"+性格(安欢/安洋/呼呼/媛/逸尘/老伯/华/硕/安柔/泡泡),"\_v3" 后缀是 cosyvoice V3 模型系列。');
  164. md.push('');
  165. md.push('## 阿里云 TTS 模型对照');
  166. md.push('');
  167. md.push('| 模型 ID | 系列 | 特点 | 项目是否启用 |');
  168. md.push('|---------|------|------|-------------|');
  169. md.push('| `cosyvoice-v3-flash` | CosyVoice | 中文专精,支持方言和情感 instruct,**降速是模型级**(非后处理) | ✅ |');
  170. md.push('| `cosyvoice-v2` | CosyVoice | 中文 V2,质量好但慢 | ❌(备用) |');
  171. md.push('| `qwen3-tts-instruct-flash` | Qwen-TTS | 多语言,支持自然语言 instruct("语速较慢") | ✅(.env 默认) |');
  172. md.push('| `qwen3-tts-flash` | Qwen-TTS | 多语言快速版 | ✅(备用) |');
  173. md.push('');
  174. md.push('## 当前默认音色: Cherry');
  175. md.push('');
  176. md.push('- **模型:** `qwen3-tts-instruct-flash`');
  177. md.push('- **音色名:** Cherry');
  178. md.push('- **性别:** 女声');
  179. md.push('- **支持语言:** 中文(普通话)、英文、粤语等');
  180. md.push('');
  181. md.push('要换音色:');
  182. md.push('');
  183. md.push('```bash');
  184. md.push('# .env');
  185. md.push('DASHSCOPE_VOICE=longanhuan_v3 # 或 voice_01(代码会映射)');
  186. md.push('');
  187. md.push('# 或者直接传 ID(只对 cosyvoice 有效)');
  188. md.push('DASHSCOPE_VOICE=longyichen_v3 # 龙逸尘阳光男声');
  189. md.push('```');
  190. md.push('');
  191. md.push('## 切换整个 vendor 到阿里云(全量替换 Edge TTS)');
  192. md.push('');
  193. md.push('编辑 `server/src/config/models.json`:');
  194. md.push('');
  195. md.push('```diff');
  196. md.push(' "tts": {');
  197. md.push('- "defaultVendor": "edge",');
  198. md.push('+ "defaultVendor": "bailian",');
  199. md.push(' "defaultModel": "qwen3-tts-instruct-flash",');
  200. md.push(' "defaultVoice": "Cherry"');
  201. md.push(' }');
  202. md.push('```');
  203. md.push('');
  204. md.push('然后 `pm2 restart server` 即可,所有新生成的音频都会走阿里云(消耗 dashscope 余额)。');
  205. md.push('');
  206. md.push('## 完整 voice 列表(从阿里云控制台)');
  207. md.push('');
  208. md.push('**当前缺口:** DashScope 没提供程序化的 voice list API(POST `/services/audio/tts/SpeechSynthesizer/voices` 一直返回 "url error"),所以本节需要手动维护。');
  209. md.push('');
  210. md.push('获取方式:');
  211. md.push('');
  212. md.push('1. 登录 [阿里云百炼控制台](https://bailian.console.aliyun.com/)');
  213. md.push('2. 进入 **模型服务 → 语音合成 → 音色列表**');
  214. md.push('3. 切换模型(cosyvoice / qwen-tts 分别看)');
  215. md.push('4. 复制音色清单 → 提交 PR 补到本节');
  216. md.push('');
  217. md.push('或者翻到 `https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list` 这个官方页面手工抄。');
  218. md.push('');
  219. md.push('### 已知 / 项目支持(从代码汇总)');
  220. md.push('');
  221. md.push('| 音色 | 系列 | 性别 | 描述 |');
  222. md.push('|------|------|------|------|');
  223. md.push('| `Cherry` | Qwen-TTS | 女 | Qwen-TTS 默认,中文/英文 |');
  224. md.push('| `longanhuan_v3` | CosyVoice | 女 | 元气女声,支持 Instruct |');
  225. md.push('| `longanyang` | CosyVoice | 男 | 阳光男声,支持 Instruct |');
  226. md.push('| `longhuhu_v3` | CosyVoice | 女童 | 飞天泡泡音,支持 Instruct |');
  227. md.push('| `longyuan_v3` | CosyVoice | 女 | 温暖治愈 |');
  228. md.push('| `longyichen_v3` | CosyVoice | 男 | 阳光活力,支持 Instruct |');
  229. md.push('| `longlaobo_v3` | CosyVoice | 男 | 沧桑老伯 |');
  230. md.push('| `longhua_v3` | CosyVoice | 女 | 元气甜美 |');
  231. md.push('| `longshuo_v3` | CosyVoice | 男 | 清朗男声 |');
  232. md.push('| `longanrou_v3` | CosyVoice | 女 | 温柔闺蜜,支持 Instruct |');
  233. md.push('| `longpaopao_v3` | CosyVoice | 女童 | 飞天泡泡音,支持 Instruct |');
  234. md.push('');
  235. md.push('> 注:CosyVoice V3 系列里更多音色(龙小白/龙大叔/龙小美等)代码里没引用,需要时直接换 `.env` 的 `DASHSCOPE_VOICE` 即可(API 不校验)。');
  236. md.push('');
  237. md.push('## 语速控制差异(对比 Edge TTS)');
  238. md.push('');
  239. md.push('| 实现 | 等价于播放器减速? | 听感自然度 |');
  240. md.push('|------|-------------------|----------|');
  241. md.push('| **Edge TTS `--rate=-22%`** | **是**(后处理拉伸) | 一般 |');
  242. md.push('| **Qwen-TTS Instruct "语速较慢"** | **否**(模型级慢生成) | **更好**(模型重新设计停顿和韵律) |');
  243. md.push('');
  244. md.push('结论:');
  245. md.push('- Edge TTS 路径下,所有 voiceParams.speed 都被忽略(强制 1.0)。用户想减速用播放器 UI 倍速按钮(详情见 commit dd1fea03)。');
  246. md.push('- 阿里云路径下,preserve speed 参数,通过 instruct("语速较慢")传给模型,听感更好。');
  247. md.push('');
  248. md.push('## 常见问题');
  249. md.push('');
  250. md.push('**Q: 阿里云音色比 Edge TTS 贵多少?**');
  251. md.push('');
  252. md.push('A: 阿里云按字符计费,cosyvoice-v3-flash 价格约 ¥0.00003/字,4000 字章节音频约 ¥0.12。qwen-tts 类似价。Edge TTS 免费。');
  253. md.push('');
  254. md.push('**Q: 怎么测试某个音色?**');
  255. md.push('');
  256. md.push('```bash');
  257. md.push('# 1. 在 .env 改 DASHSCOPE_VOICE=longyichen_v3');
  258. md.push('# 2. 调用 /api/tts/generate 走默认 vendor=bailian 路径');
  259. md.push('# 3. 或在 models.json 把 defaultVendor 改成 bailian');
  260. md.push('```');
  261. md.push('');
  262. md.push('**Q: 音色 ID 拼错了会怎样?**');
  263. md.push('');
  264. md.push('A: 阿里云不会本地验证,直接发 HTTP 请求,服务端返回 400 或用默认音色(不会报错)。');
  265. md.push('');
  266. // ========== 追加: CosyVoice 指令使用指南 ==========
  267. md.push(INSTRUCTION_GUIDE.trimStart());
  268. process.stdout.write(md.join('\n'));