#!/usr/bin/env node /** * gen-edge-tts-doc.js — 从 `edge-tts --list-voices` 输出生成完整 TTS 语言/音色文档 * * Usage: * edge-tts --list-voices > /tmp/voices.txt * node gen-edge-tts-doc.js /tmp/voices.txt > docs/tts-edge-languages.md * * 输出:Markdown 表格 + 中文方言分类 + 完整 75 语言列表 */ const fs = require('fs'); const path = require('path'); const inputFile = process.argv[2]; if (!inputFile) { console.error('Usage: node gen-edge-tts-doc.js '); process.exit(1); } const raw = fs.readFileSync(inputFile, 'utf8'); const lines = raw.split('\n').filter(Boolean); // 头两行是表头 + 分隔线,数据从第 3 行开始 const dataLines = lines.slice(2); // 解析每行:ShortName \t Gender \t ContentCategories \t VoicePersonalities const voices = []; for (const line of dataLines) { const cols = line.trim().split(/\s{2,}/); if (cols.length < 2) continue; const shortName = cols[0]; // ShortName 形如 zh-CN-XiaoxiaoNeural → locale=zh-CN, name=XiaoxiaoNeural, lang=zh const m = shortName.match(/^([a-z]{2,3})(?:[-]([A-Z]{2}))?[-]([A-Za-z]+)$/); if (!m) continue; const [, lang, region, name] = m; const locale = region ? `${lang}-${region}` : lang; voices.push({ shortName, lang, region: region || '', locale, name, gender: cols[1] || '', contentCategories: cols[2] || '', personalities: cols[3] || '', }); } // 按 locale 分组 const byLocale = new Map(); for (const v of voices) { if (!byLocale.has(v.locale)) byLocale.set(v.locale, []); byLocale.get(v.locale).push(v); } // 按语言聚合统计 const langNames = { af: '南非荷兰语 Afrikaans', ar: '阿拉伯语 Arabic', am: '阿姆哈拉语 Amharic', az: '阿塞拜疆语 Azerbaijani', bg: '保加利亚语 Bulgarian', bn: '孟加拉语 Bengali', bs: '波斯尼亚语 Bosnian', ca: '加泰罗尼亚语 Catalan', cs: '捷克语 Czech', cy: '威尔士语 Welsh', da: '丹麦语 Danish', de: '德语 German', el: '希腊语 Greek', en: '英语 English', es: '西班牙语 Spanish', et: '爱沙尼亚语 Estonian', fa: '波斯语 Persian', fi: '芬兰语 Finnish', fil: '菲律宾语 Filipino', fr: '法语 French', ga: '爱尔兰语 Irish', gl: '加利西亚语 Galician', gu: '古吉拉特语 Gujarati', he: '希伯来语 Hebrew', hi: '印地语 Hindi', hr: '克罗地亚语 Croatian', hu: '匈牙利语 Hungarian', id: '印度尼西亚语 Indonesian', is: '冰岛语 Icelandic', it: '意大利语 Italian', iu: '因纽特语 Inuktitut', ja: '日语 Japanese', jv: '爪哇语 Javanese', ka: '格鲁吉亚语 Georgian', kk: '哈萨克语 Kazakh', km: '高棉语 Khmer', kn: '卡纳达语 Kannada', ko: '韩语 Korean', lo: '老挝语 Lao', lt: '立陶宛语 Lithuanian', lv: '拉脱维亚语 Latvian', mk: '马其顿语 Macedonian', ml: '马拉雅拉姆语 Malayalam', mn: '蒙古语 Mongolian', mr: '马拉地语 Marathi', ms: '马来语 Malay', mt: '马耳他语 Maltese', my: '缅甸语 Burmese', nb: '挪威语(书面) Norwegian Bokmål', ne: '尼泊尔语 Nepali', nl: '荷兰语 Dutch', pl: '波兰语 Polish', ps: '普什图语 Pashto', pt: '葡萄牙语 Portuguese', ro: '罗马尼亚语 Romanian', ru: '俄语 Russian', si: '僧伽罗语 Sinhala', sk: '斯洛伐克语 Slovak', sl: '斯洛文尼亚语 Slovenian', so: '索马里语 Somali', sq: '阿尔巴尼亚语 Albanian', sr: '塞尔维亚语 Serbian', su: '巽他语 Sundanese', sv: '瑞典语 Swedish', sw: '斯瓦希里语 Swahili', ta: '泰米尔语 Tamil', te: '泰卢固语 Telugu', th: '泰语 Thai', tr: '土耳其语 Turkish', uk: '乌克兰语 Ukrainian', ur: '乌尔都语 Urdu', uz: '乌兹别克语 Uzbek', vi: '越南语 Vietnamese', zh: '中文 Chinese', zu: '祖鲁语 Zulu', }; const localesByLang = new Map(); for (const [locale, arr] of byLocale.entries()) { const lang = arr[0].lang; if (!localesByLang.has(lang)) localesByLang.set(lang, []); localesByLang.get(lang).push(locale); } for (const [, arr] of localesByLang) arr.sort(); // 中文方言单独分类 const zhDialects = { 'zh-CN': '普通话(中国大陆)', 'zh-HK': '粤语(香港)', 'zh-TW': '国语(台湾)', 'zh-CN-sichuan': '四川话', 'zh-CN-liaoning': '东北话', 'zh-CN-shaanxi': '陕西方言', }; const md = []; md.push('# Edge TTS 支持的语言 / 音色清单'); md.push(''); md.push(`> 自动生成自 edge-tts 7.2.8 (--list-voices)。共 **${voices.length} 个音色**,覆盖 **${localesByLang.size} 种语言**。`); md.push(''); md.push('**项目当前使用:** `zh-CN-XiaoxiaoNeural`(中文温柔女声,在 `server/src/config/models.json` 的 `tts.defaultVoice` 配置)。'); md.push(''); md.push('**特性:** Edge TTS 是 Microsoft Edge 浏览器的"大声朗读"引擎,基于 Azure 神经网络,**完全免费**、**免 API Key**、单次最长 ~3000 字符、中文安全 1000 字。'); md.push(''); // ========== 中文方言(单独高亮) ========== md.push('## 中文方言(项目最常用)'); md.push(''); md.push('| Locale | 方言 | 音色数 | 常用音色 |'); md.push('|--------|------|--------|---------|'); for (const [locale, name] of Object.entries(zhDialects)) { const arr = byLocale.get(locale) || []; const sampleNames = arr.map(v => v.name).slice(0, 4).join(', '); md.push(`| ${locale} | ${name} | ${arr.length} | ${sampleNames}${arr.length > 4 ? '...' : ''} |`); } md.push(''); // ========== 完整中文音色列表 ========== md.push('### 全部中文音色(14 个)'); md.push(''); md.push('| ShortName | 性别 | 风格 |'); md.push('|-----------|------|------|'); const zhVoices = voices.filter(v => v.lang === 'zh').sort((a, b) => a.shortName.localeCompare(b.shortName)); for (const v of zhVoices) { md.push(`| \`${v.shortName}\` | ${v.gender} | ${v.personalities || v.contentCategories} |`); } md.push(''); // ========== 75 种语言汇总 ========== md.push(`## 全部 ${localesByLang.size} 种语言汇总`); md.push(''); md.push('按字母排序,展示每个 Locale 的音色数:'); md.push(''); md.push('| Locale | 语言 | 音色数 | 主要音色 |'); md.push('|--------|------|--------|---------|'); const sortedLangs = [...localesByLang.keys()].sort(); for (const lang of sortedLangs) { const locales = localesByLang.get(lang); const langName = langNames[lang] || lang; for (const locale of locales) { const arr = byLocale.get(locale); const sampleNames = arr.map(v => v.name).slice(0, 3).join(', '); md.push(`| ${locale} | ${langName} | ${arr.length} | ${sampleNames}${arr.length > 3 ? '...' : ''} |`); } } md.push(''); // ========== 使用示例 ========== md.push('## 切换音色 / 语言'); md.push(''); md.push('### 命令行测试'); md.push(''); md.push('```bash'); md.push('# 中文女声(默认)'); md.push('edge-tts --voice zh-CN-XiaoxiaoNeural --text "你好世界" --write-media out.mp3'); md.push(''); md.push('# 粤语女声'); md.push('edge-tts --voice zh-HK-HiuMaanNeural --text "你好,這是粵語測試" --write-media cantonese.mp3'); md.push(''); md.push('# 四川话'); md.push('edge-tts --voice zh-CN-sichuan-YunxiNeural --text "今天天气好巴适" --write-media sc.mp3'); md.push(''); md.push('# 英语男声'); md.push('edge-tts --voice en-US-GuyNeural --text "Hello, world." --write-media en.mp3'); md.push(''); md.push('# 日语女声'); md.push('edge-tts --voice ja-JP-NanamiNeural --text "こんにちは" --write-media ja.mp3'); md.push(''); md.push('# 一次性列出全部语音'); md.push('edge-tts --list-voices > voices.txt'); md.push('```'); md.push(''); md.push('### 项目内修改默认音色'); md.push(''); md.push('编辑 `server/src/config/models.json`:'); 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(' }'); md.push('}'); md.push('```'); md.push(''); md.push('或者环境变量覆盖(在 `server/.env`):'); md.push(''); md.push('```bash'); md.push('EDGE_TTS_VOICE=zh-HK-HiuMaanNeural'); md.push('```'); md.push(''); md.push('### API 调用时按音色生成'); md.push(''); md.push('前端 → 后端 `/api/tts/generate` 接口传 `voiceId` 字段:'); md.push(''); md.push('```json'); md.push('{'); md.push(' "text": "你好世界",'); md.push(' "voiceId": "zh-CN-YunxiNeural"'); md.push('}'); md.push('```'); md.push(''); // ========== 前端统一音色 ID 映射 ========== md.push('## 前端统一音色 ID → 实际 Edge TTS 音色映射'); md.push(''); md.push('前端用 10 个统一 ID(`voice_01`~`voice_10`)展示,后端 `edge-tts.provider.ts:51-62` 的 `EDGE_VOICE_MAP` 把它们映射到实际 Edge 音色:'); md.push(''); md.push('| 前端 ID | 实际 Edge 音色 | 描述 |'); md.push('|---------|----------------|------|'); const map = [ ['voice_01', 'zh-CN-XiaoxiaoNeural', '温柔女声'], ['voice_02', 'zh-CN-YunxiNeural', '磁性男声'], ['voice_03', 'zh-CN-XiaoyiNeural', '活泼女声'], ['voice_04', 'zh-CN-YunyangNeural', '知性女声(新闻)'], ['voice_05', 'zh-CN-YunjianNeural', '阳光男声'], ['voice_06', 'zh-CN-YunyangNeural', '沧桑男声'], ['voice_07', 'zh-CN-XiaoxiaoNeural', '甜美女声'], ['voice_08', 'zh-CN-YunxiNeural', '清朗男声'], ['voice_09', 'zh-CN-XiaoyiNeural', '亲切女声'], ['voice_10', 'zh-CN-XiaoshuangNeural', '稚嫩童声'], ]; for (const [id, voice, desc] of map) { md.push(`| ${id} | \`${voice}\` | ${desc} |`); } md.push(''); md.push('> 当前 10 个统一 ID 实际只用到 6 种 Edge 音色,语音方案日后续可补充粤语、英语等跨语言音色(增加 voiceId 11~20 等)。'); md.push(''); // ========== 已知限制 ========== md.push('## 已知限制'); md.push(''); md.push('- **文本长度:** 单次 ~3000 字符(中文约 1000 字),超出会被服务端拒绝。代码已自动按 1000 字分段拼接(`tts.service.ts` 的 `splitText`)。'); md.push('- **语速控制:** `--rate` 参数是**后处理时间拉伸**,听感等同于播放器 `audio.playbackRate`(见 `docs/tts-speed-note.md` 或代码 commit dd1fea03)。'); md.push('- **稳定性:** 依赖微软服务器,偶尔会限频(502/timeout),代码已加 retry + 多个 vendor fallback(`provider.registry.ts`)。'); md.push('- **没商用授权:** 内部使用 OK,正式商用建议切阿里云 CosyVoice/Qwen-TTS。'); md.push(''); // ========== 维护 ========== md.push('## 如何更新本文档'); md.push(''); md.push('Edge TTS 升级可能新增/删除音色,重新生成:'); md.push(''); md.push('```bash'); md.push('# 1. 服务器上导出最新列表'); md.push('ssh root@8.159.134.106 "edge-tts --list-voices" > /tmp/voices.txt'); md.push(''); md.push('# 2. 本地跑脚本生成'); md.push('node deploy-package/scripts/gen-edge-tts-doc.js /tmp/voices.txt > docs/tts-edge-languages.md'); md.push(''); md.push('# 3. 提交'); md.push('git add docs/tts-edge-languages.md deploy-package/edge-voices-snapshot.txt'); md.push('git commit -m "docs(tts): 更新 Edge TTS 音色清单"'); md.push('```'); md.push(''); process.stdout.write(md.join('\n'));