|
|
@@ -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'));
|