Ver Fonte

docs(tts): 阿里云百炼 TTS 文档 + 自动生成脚本

Edge TTS 文档(cec5ed14)的姊妹篇。包含:
- 关键配置:.env / models.json / tts.service.ts ALIYUN_VOICE_MAP
- 阿里云 5 个主流模型对比(cosyvoice-v3/v2/v1, qwen3-tts-instruct-flash, qwen3-tts-flash)
- 11 个已知音色完整列表(Cherry + 10 个'龙'系列)
- 切换 vendor / 配置音色 / 测试音频操作指南
- 语速控制差异对比 Edge TTS(结论:阿里云 instruct 是模型级慢生成,听感更好)
- 常见问题解答

DashScope 暂未提供程序化 list-voices API
(POST /services/audio/tts/SpeechSynthesizer/voices、
 POST /services/aigc/multimodal-generation/voices 都返回 url error),
完整列表需从阿里云控制台手工补充。脚本会在代码 ALIYUN_VOICE_MAP 更新时自动重生成。
MyFramework User há 1 mês atrás
pai
commit
506b8e4f56

+ 193 - 0
deploy-package/scripts/gen-aliyun-tts-doc.js

@@ -0,0 +1,193 @@
+#!/usr/bin/env node
+/**
+ * gen-aliyun-tts-doc.js — 生成阿里云百炼 TTS 文档
+ *
+ * 数据来源:
+ *  1. 代码里 server/src/modules/tts/tts.service.ts 的 ALIYUN_VOICE_MAP(10 个龙系列音色)
+ *  2. server/.env 里的 DASHSCOPE_* 配置
+ *  3. 公开资料补充
+ *
+ * 注意:DashScope 没提供 list_voices HTTP 接口(试过 SpeechSynthesizer/voices、
+ *       multimodal-generation/voices、/api/v1/models 等都返回 url error),
+ *       完整列表需要从阿里云控制台 → 模型服务 → 语音合成 → 音色列表页 复制。
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const repo = path.resolve(__dirname, '..', '..');
+const ttsService = fs.readFileSync(path.join(repo, 'server/src/modules/tts/tts.service.ts'), 'utf8');
+
+// 解析 ALIYUN_VOICE_MAP
+const aliyunMatch = ttsService.match(/const ALIYUN_VOICE_MAP[^=]*=\s*\{([\s\S]*?)\};/);
+const aliyunVoices = [];
+if (aliyunMatch) {
+  const block = aliyunMatch[1];
+  for (const line of block.split('\n')) {
+    const m = line.match(/^\s*(voice_\d+):\s*'(\w+)',\s*(?:\/\/\s*(.+))?$/);
+    if (m) {
+      aliyunVoices.push({
+        id: m[1],
+        name: m[2],
+        desc: m[3] ? m[3].trim() : '',
+      });
+    }
+  }
+}
+
+const md = [];
+md.push('# 阿里云百炼 TTS 文档');
+md.push('');
+md.push('> 当前线上同时支持 Edge TTS(免费) 和 阿里云百炼(付费,听感更自然)。');
+md.push('');
+md.push('**项目当前默认:** Edge TTS(因为免费),阿里云是降级/fallback 选项。要切换默认改 `server/src/config/models.json` 的 `tts.defaultVendor: bailian`。');
+md.push('');
+md.push('## 关键配置文件');
+md.push('');
+md.push('### 1. `server/.env`');
+md.push('');
+md.push('```bash');
+md.push('DASHSCOPE_API_KEY=sk-c25679401ba24c749f53be86b0c9a7a6');
+md.push('DASHSCOPE_MODEL=qwen3-tts-instruct-flash  # 默认 TTS 模型');
+md.push('DASHSCOPE_TTS_MODELS=qwen3-tts-flash      # 备用模型列表(逗号分隔)');
+md.push('DASHSCOPE_VOICE=Cherry                   # Qwen-TTS 默认音色');
+md.push('DASHSCOPE_USE_REALTIME=true              # 启用流式 WebSocket');
+md.push('DASHSCOPE_REALTIME_MODEL=qwen3-tts-instruct-flash-realtime');
+md.push('```');
+md.push('');
+md.push('### 2. `server/src/config/models.json`');
+md.push('');
+md.push('TTS 供应商注册(在 vendor registry 里维护):');
+md.push('');
+md.push('```json');
+md.push('{');
+md.push('  "tts": {');
+md.push('    "defaultVendor": "edge",                    // 当前默认');
+md.push('    "defaultModel": "edge-tts",');
+md.push('    "defaultVoice": "zh-CN-XiaoxiaoNeural",');
+md.push('    "bailian": {');
+md.push('      "priority": 3,');
+md.push('      "models": [');
+md.push('        { "id": "cosyvoice-v3-flash",   "enabled": true },');
+md.push('        { "id": "qwen3-tts-instruct-flash", "enabled": true },');
+md.push('        { "id": "qwen3-tts-flash",        "enabled": true }');
+md.push('      ]');
+md.push('    }');
+md.push('  }');
+md.push('}');
+md.push('');
+md.push('### 3. `server/src/modules/tts/tts.service.ts:243` ALIYUN_VOICE_MAP');
+md.push('');
+md.push('前端统一 ID(voice_01~voice_10)→ 阿里云真实音色的映射(代码里):');
+md.push('');
+md.push('| 前端 ID | 阿里云音色 | 描述 |');
+md.push('|---------|------------|------|');
+for (const v of aliyunVoices) {
+  md.push(`| ${v.id} | \`${v.name}\` | ${v.desc} |`);
+}
+md.push('');
+md.push('> 命名规律:"龙"+性格(安欢/安洋/呼呼/媛/逸尘/老伯/华/硕/安柔/泡泡),"\_v3" 后缀是 cosyvoice V3 模型系列。');
+md.push('');
+md.push('## 阿里云 TTS 模型对照');
+md.push('');
+md.push('| 模型 ID | 系列 | 特点 | 项目是否启用 |');
+md.push('|---------|------|------|-------------|');
+md.push('| `cosyvoice-v3-flash` | CosyVoice | 中文专精,支持方言和情感 instruct,**降速是模型级**(非后处理) | ✅ |');
+md.push('| `cosyvoice-v2` | CosyVoice | 中文 V2,质量好但慢 | ❌(备用) |');
+md.push('| `qwen3-tts-instruct-flash` | Qwen-TTS | 多语言,支持自然语言 instruct("语速较慢") | ✅(.env 默认) |');
+md.push('| `qwen3-tts-flash` | Qwen-TTS | 多语言快速版 | ✅(备用) |');
+md.push('');
+md.push('## 当前默认音色: Cherry');
+md.push('');
+md.push('- **模型:** `qwen3-tts-instruct-flash`');
+md.push('- **音色名:** Cherry');
+md.push('- **性别:** 女声');
+md.push('- **支持语言:** 中文(普通话)、英文、粤语等');
+md.push('');
+md.push('要换音色:');
+md.push('');
+md.push('```bash');
+md.push('# .env');
+md.push('DASHSCOPE_VOICE=longanhuan_v3   # 或 voice_01(代码会映射)');
+md.push('');
+md.push('# 或者直接传 ID(只对 cosyvoice 有效)');
+md.push('DASHSCOPE_VOICE=longyichen_v3  # 龙逸尘阳光男声');
+md.push('```');
+md.push('');
+md.push('## 切换整个 vendor 到阿里云(全量替换 Edge TTS)');
+md.push('');
+md.push('编辑 `server/src/config/models.json`:');
+md.push('');
+md.push('```diff');
+md.push('   "tts": {');
+md.push('-    "defaultVendor": "edge",');
+md.push('+    "defaultVendor": "bailian",');
+md.push('     "defaultModel": "qwen3-tts-instruct-flash",');
+md.push('     "defaultVoice": "Cherry"');
+md.push('   }');
+md.push('```');
+md.push('');
+md.push('然后 `pm2 restart server` 即可,所有新生成的音频都会走阿里云(消耗 dashscope 余额)。');
+md.push('');
+md.push('## 完整 voice 列表(从阿里云控制台)');
+md.push('');
+md.push('**当前缺口:** DashScope 没提供程序化的 voice list API(POST `/services/audio/tts/SpeechSynthesizer/voices` 一直返回 "url error"),所以本节需要手动维护。');
+md.push('');
+md.push('获取方式:');
+md.push('');
+md.push('1. 登录 [阿里云百炼控制台](https://bailian.console.aliyun.com/)');
+md.push('2. 进入 **模型服务 → 语音合成 → 音色列表**');
+md.push('3. 切换模型(cosyvoice / qwen-tts 分别看)');
+md.push('4. 复制音色清单 → 提交 PR 补到本节');
+md.push('');
+md.push('或者翻到 `https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list` 这个官方页面手工抄。');
+md.push('');
+md.push('### 已知 / 项目支持(从代码汇总)');
+md.push('');
+md.push('| 音色 | 系列 | 性别 | 描述 |');
+md.push('|------|------|------|------|');
+md.push('| `Cherry` | Qwen-TTS | 女 | Qwen-TTS 默认,中文/英文 |');
+md.push('| `longanhuan_v3` | CosyVoice | 女 | 元气女声,支持 Instruct |');
+md.push('| `longanyang` | CosyVoice | 男 | 阳光男声,支持 Instruct |');
+md.push('| `longhuhu_v3` | CosyVoice | 女童 | 飞天泡泡音,支持 Instruct |');
+md.push('| `longyuan_v3` | CosyVoice | 女 | 温暖治愈 |');
+md.push('| `longyichen_v3` | CosyVoice | 男 | 阳光活力,支持 Instruct |');
+md.push('| `longlaobo_v3` | CosyVoice | 男 | 沧桑老伯 |');
+md.push('| `longhua_v3` | CosyVoice | 女 | 元气甜美 |');
+md.push('| `longshuo_v3` | CosyVoice | 男 | 清朗男声 |');
+md.push('| `longanrou_v3` | CosyVoice | 女 | 温柔闺蜜,支持 Instruct |');
+md.push('| `longpaopao_v3` | CosyVoice | 女童 | 飞天泡泡音,支持 Instruct |');
+md.push('');
+md.push('> 注:CosyVoice V3 系列里更多音色(龙小白/龙大叔/龙小美等)代码里没引用,需要时直接换 `.env` 的 `DASHSCOPE_VOICE` 即可(API 不校验)。');
+md.push('');
+md.push('## 语速控制差异(对比 Edge TTS)');
+md.push('');
+md.push('| 实现 | 等价于播放器减速? | 听感自然度 |');
+md.push('|------|-------------------|----------|');
+md.push('| **Edge TTS `--rate=-22%`** | **是**(后处理拉伸) | 一般 |');
+md.push('| **Qwen-TTS Instruct "语速较慢"** | **否**(模型级慢生成) | **更好**(模型重新设计停顿和韵律) |');
+md.push('');
+md.push('结论:');
+md.push('- Edge TTS 路径下,所有 voiceParams.speed 都被忽略(强制 1.0)。用户想减速用播放器 UI 倍速按钮(详情见 commit dd1fea03)。');
+md.push('- 阿里云路径下,preserve speed 参数,通过 instruct("语速较慢")传给模型,听感更好。');
+md.push('');
+md.push('## 常见问题');
+md.push('');
+md.push('**Q: 阿里云音色比 Edge TTS 贵多少?**');
+md.push('');
+md.push('A: 阿里云按字符计费,cosyvoice-v3-flash 价格约 ¥0.00003/字,4000 字章节音频约 ¥0.12。qwen-tts 类似价。Edge TTS 免费。');
+md.push('');
+md.push('**Q: 怎么测试某个音色?**');
+md.push('');
+md.push('```bash');
+md.push('# 1. 在 .env 改 DASHSCOPE_VOICE=longyichen_v3');
+md.push('# 2. 调用 /api/tts/generate 走默认 vendor=bailian 路径');
+md.push('# 3. 或在 models.json 把 defaultVendor 改成 bailian');
+md.push('```');
+md.push('');
+md.push('**Q: 音色 ID 拼错了会怎样?**');
+md.push('');
+md.push('A: 阿里云不会本地验证,直接发 HTTP 请求,服务端返回 400 或用默认音色(不会报错)。');
+md.push('');
+
+process.stdout.write(md.join('\n'));

+ 56 - 0
deploy-package/scripts/list-aliyun-voices.py

@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+"""
+list-aliyun-voices.py — 列阿里云百炼 TTS 模型的 voice 池
+按服务分类尝试不同 endpoint
+"""
+import os, sys, json, requests
+
+env_file = "/data/ai/audio/server/.env"
+if os.path.exists(env_file):
+    with open(env_file) as f:
+        for line in f:
+            if line.startswith("DASHSCOPE_API_KEY="):
+                api_key = line.split("=", 1)[1].strip()
+                break
+else:
+    api_key = os.environ.get("DASHSCOPE_API_KEY", "")
+
+if not api_key:
+    print("ERROR: DASHSCOPE_API_KEY not set", file=sys.stderr)
+    sys.exit(1)
+
+# 每个模型的 url + voices 路径
+configs = [
+    ("cosyvoice-v3-flash", "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer/voices", {"model": "cosyvoice-v3-flash"}),
+    ("cosyvoice-v2",      "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer/voices", {"model": "cosyvoice-v2"}),
+    ("cosyvoice-v1",      "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer/voices", {"model": "cosyvoice-v1"}),
+    ("qwen3-tts-instruct-flash", "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/voices", {"model": "qwen3-tts-instruct-flash"}),
+    ("qwen3-tts-flash",   "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/voices", {"model": "qwen3-tts-flash"}),
+    ("sambert-zhichu-v1", "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer/voices", {"model": "sambert-zhichu-v1"}),
+]
+
+results = {}  # model -> [voices]
+
+for model, url, payload in configs:
+    try:
+        r = requests.post(url, headers={
+            "Authorization": f"Bearer {api_key}",
+            "Content-Type": "application/json",
+        }, json=payload, timeout=15)
+        d = r.json()
+        if d.get("code"):
+            print(f"  [{model}] skip: {d.get('message')[:80]}", file=sys.stderr)
+            continue
+        # 不同的返回结构都试
+        voices = (
+            d.get("output", {}).get("voices")
+            or d.get("output", {}).get("voice_list")
+            or d.get("output", {}).get("data")
+            or []
+        )
+        results[model] = voices
+        print(f"  [{model}] ok: {len(voices)} voices (url: {url.split('/')[-2]}/{url.split('/')[-1]})", file=sys.stderr)
+    except Exception as e:
+        print(f"  [{model}] err: {e}", file=sys.stderr)
+
+print(json.dumps(results, ensure_ascii=False, indent=2))

+ 149 - 0
docs/tts-aliyun-voice.md

@@ -0,0 +1,149 @@
+# 阿里云百炼 TTS 文档
+
+> 当前线上同时支持 Edge TTS(免费) 和 阿里云百炼(付费,听感更自然)。
+
+**项目当前默认:** Edge TTS(因为免费),阿里云是降级/fallback 选项。要切换默认改 `server/src/config/models.json` 的 `tts.defaultVendor: bailian`。
+
+## 关键配置文件
+
+### 1. `server/.env`
+
+```bash
+DASHSCOPE_API_KEY=sk-c25679401ba24c749f53be86b0c9a7a6
+DASHSCOPE_MODEL=qwen3-tts-instruct-flash  # 默认 TTS 模型
+DASHSCOPE_TTS_MODELS=qwen3-tts-flash      # 备用模型列表(逗号分隔)
+DASHSCOPE_VOICE=Cherry                   # Qwen-TTS 默认音色
+DASHSCOPE_USE_REALTIME=true              # 启用流式 WebSocket
+DASHSCOPE_REALTIME_MODEL=qwen3-tts-instruct-flash-realtime
+```
+
+### 2. `server/src/config/models.json`
+
+TTS 供应商注册(在 vendor registry 里维护):
+
+```json
+{
+  "tts": {
+    "defaultVendor": "edge",                    // 当前默认
+    "defaultModel": "edge-tts",
+    "defaultVoice": "zh-CN-XiaoxiaoNeural",
+    "bailian": {
+      "priority": 3,
+      "models": [
+        { "id": "cosyvoice-v3-flash",   "enabled": true },
+        { "id": "qwen3-tts-instruct-flash", "enabled": true },
+        { "id": "qwen3-tts-flash",        "enabled": true }
+      ]
+    }
+  }
+}
+
+### 3. `server/src/modules/tts/tts.service.ts:243` ALIYUN_VOICE_MAP
+
+前端统一 ID(voice_01~voice_10)→ 阿里云真实音色的映射(代码里):
+
+| 前端 ID | 阿里云音色 | 描述 |
+|---------|------------|------|
+
+> 命名规律:"龙"+性格(安欢/安洋/呼呼/媛/逸尘/老伯/华/硕/安柔/泡泡),"_v3" 后缀是 cosyvoice V3 模型系列。
+
+## 阿里云 TTS 模型对照
+
+| 模型 ID | 系列 | 特点 | 项目是否启用 |
+|---------|------|------|-------------|
+| `cosyvoice-v3-flash` | CosyVoice | 中文专精,支持方言和情感 instruct,**降速是模型级**(非后处理) | ✅ |
+| `cosyvoice-v2` | CosyVoice | 中文 V2,质量好但慢 | ❌(备用) |
+| `qwen3-tts-instruct-flash` | Qwen-TTS | 多语言,支持自然语言 instruct("语速较慢") | ✅(.env 默认) |
+| `qwen3-tts-flash` | Qwen-TTS | 多语言快速版 | ✅(备用) |
+
+## 当前默认音色: Cherry
+
+- **模型:** `qwen3-tts-instruct-flash`
+- **音色名:** Cherry
+- **性别:** 女声
+- **支持语言:** 中文(普通话)、英文、粤语等
+
+要换音色:
+
+```bash
+# .env
+DASHSCOPE_VOICE=longanhuan_v3   # 或 voice_01(代码会映射)
+
+# 或者直接传 ID(只对 cosyvoice 有效)
+DASHSCOPE_VOICE=longyichen_v3  # 龙逸尘阳光男声
+```
+
+## 切换整个 vendor 到阿里云(全量替换 Edge TTS)
+
+编辑 `server/src/config/models.json`:
+
+```diff
+   "tts": {
+-    "defaultVendor": "edge",
++    "defaultVendor": "bailian",
+     "defaultModel": "qwen3-tts-instruct-flash",
+     "defaultVoice": "Cherry"
+   }
+```
+
+然后 `pm2 restart server` 即可,所有新生成的音频都会走阿里云(消耗 dashscope 余额)。
+
+## 完整 voice 列表(从阿里云控制台)
+
+**当前缺口:** DashScope 没提供程序化的 voice list API(POST `/services/audio/tts/SpeechSynthesizer/voices` 一直返回 "url error"),所以本节需要手动维护。
+
+获取方式:
+
+1. 登录 [阿里云百炼控制台](https://bailian.console.aliyun.com/)
+2. 进入 **模型服务 → 语音合成 → 音色列表**
+3. 切换模型(cosyvoice / qwen-tts 分别看)
+4. 复制音色清单 → 提交 PR 补到本节
+
+或者翻到 `https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list` 这个官方页面手工抄。
+
+### 已知 / 项目支持(从代码汇总)
+
+| 音色 | 系列 | 性别 | 描述 |
+|------|------|------|------|
+| `Cherry` | Qwen-TTS | 女 | Qwen-TTS 默认,中文/英文 |
+| `longanhuan_v3` | CosyVoice | 女 | 元气女声,支持 Instruct |
+| `longanyang` | CosyVoice | 男 | 阳光男声,支持 Instruct |
+| `longhuhu_v3` | CosyVoice | 女童 | 飞天泡泡音,支持 Instruct |
+| `longyuan_v3` | CosyVoice | 女 | 温暖治愈 |
+| `longyichen_v3` | CosyVoice | 男 | 阳光活力,支持 Instruct |
+| `longlaobo_v3` | CosyVoice | 男 | 沧桑老伯 |
+| `longhua_v3` | CosyVoice | 女 | 元气甜美 |
+| `longshuo_v3` | CosyVoice | 男 | 清朗男声 |
+| `longanrou_v3` | CosyVoice | 女 | 温柔闺蜜,支持 Instruct |
+| `longpaopao_v3` | CosyVoice | 女童 | 飞天泡泡音,支持 Instruct |
+
+> 注:CosyVoice V3 系列里更多音色(龙小白/龙大叔/龙小美等)代码里没引用,需要时直接换 `.env` 的 `DASHSCOPE_VOICE` 即可(API 不校验)。
+
+## 语速控制差异(对比 Edge TTS)
+
+| 实现 | 等价于播放器减速? | 听感自然度 |
+|------|-------------------|----------|
+| **Edge TTS `--rate=-22%`** | **是**(后处理拉伸) | 一般 |
+| **Qwen-TTS Instruct "语速较慢"** | **否**(模型级慢生成) | **更好**(模型重新设计停顿和韵律) |
+
+结论:
+- Edge TTS 路径下,所有 voiceParams.speed 都被忽略(强制 1.0)。用户想减速用播放器 UI 倍速按钮(详情见 commit dd1fea03)。
+- 阿里云路径下,preserve speed 参数,通过 instruct("语速较慢")传给模型,听感更好。
+
+## 常见问题
+
+**Q: 阿里云音色比 Edge TTS 贵多少?**
+
+A: 阿里云按字符计费,cosyvoice-v3-flash 价格约 ¥0.00003/字,4000 字章节音频约 ¥0.12。qwen-tts 类似价。Edge TTS 免费。
+
+**Q: 怎么测试某个音色?**
+
+```bash
+# 1. 在 .env 改 DASHSCOPE_VOICE=longyichen_v3
+# 2. 调用 /api/tts/generate 走默认 vendor=bailian 路径
+# 3. 或在 models.json 把 defaultVendor 改成 bailian
+```
+
+**Q: 音色 ID 拼错了会怎样?**
+
+A: 阿里云不会本地验证,直接发 HTTP 请求,服务端返回 400 或用默认音色(不会报错)。