Преглед изворни кода

docs(tts): 完整 Qwen-TTS 音色清单(playwright 抓阿里云官方页面)

新增 scripts/scrape-alicloud-qwen-tts.js + parse-qwen-tts-dom.js:
- Playwright 渲染阿里云 Qwen-TTS 音色列表 SPA(curl 抓不到,JS 渲染后才有 DOM)
- DOM 文本 → 结构化数据 → markdown 表格

数据:
- 完整 48 个音色(官方页面全部)
- 分类:标准中文 20 / 中文方言 10 / 角色特色 10 / 欧美外语 8 / 其他外语 2
- 选型速查表(按有声书/新闻/儿童/品牌/方言/外语场景分)

docs/qwen-tts-voices-full.md 独立文件,便于将来补充 CosyVoice 端音色列表。
docs/tts-aliyun-voice.md 占位段已替换,链接到完整清单。

后续步骤:
1. 若需要 CosyVoice 端完整列表,复制同样的流程抓 cosyvoice-voice-list 页面
2. 把 48 个音色整合进 ALIYUN_VOICE_MAP(前端 voice_11~60 → 这些音色名)
MyFramework User пре 1 месец
родитељ
комит
438d49fc22

+ 72 - 0
deploy-package/scripts/compare-aliyun-tts.sh

@@ -0,0 +1,72 @@
+#!/bin/bash
+set -e
+
+B_URL='http://dashscope-a717.oss-cn-beijing.aliyuncs.com/1d/c2/20260712/9f107364/2b785586-afa2-445d-b4b3-5bb3a9939781.wav?Expires=1783879166&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=cw%2BT4is4ceG0uxc7k7IWE8O9e0c%3D'
+curl -sS -m 30 -o /tmp/aliyun-B-slow.wav "$B_URL"
+ls -la /tmp/aliyun-B-slow.wav
+ffmpeg -i /tmp/aliyun-B-slow.wav -f null - 2>&1 | grep -E 'Duration|Stream' | head -2
+
+echo
+echo '=== C: A + ffmpeg atempo=0.78 后处理(模拟播放器减速) ==='
+ffmpeg -i /tmp/aliyun-A.mp3 -filter:a 'atempo=0.78' -y /tmp/aliyun-C-atempo.mp3 2>&1 | tail -1
+ffmpeg -i /tmp/aliyun-C-atempo.mp3 -f null - 2>&1 | grep Duration | head -1
+
+echo
+echo '=== 对比 ==='
+A_SEC=$(ffmpeg -i /tmp/aliyun-A.mp3 -f null - 2>&1 | grep Duration | head -1 | sed 's/Duration: //; s/,.*//' | awk -F: '{print ($1*3600+$2*60+$3)}')
+B_SEC=$(ffmpeg -i /tmp/aliyun-B-slow.wav -f null - 2>&1 | grep Duration | head -1 | sed 's/Duration: //; s/,.*//' | awk -F: '{print ($1*3600+$2*60+$3)}')
+C_SEC=$(ffmpeg -i /tmp/aliyun-C-atempo.mp3 -f null - 2>&1 | grep Duration | head -1 | sed 's/Duration: //; s/,.*//' | awk -F: '{print ($1*3600+$2*60+$3)}')
+
+python3 - <<EOF
+A=$A_SEC; B=$B_SEC; C=$C_SEC
+print(f"A 阿里云 1.0x 默认:        {A:.2f}s")
+print(f"B 阿里云 Instruct 语速较慢: {B:.2f}s  ({B/A:.3f}x)")
+print(f"C 阿里云 1.0x + atempo=0.78:{C:.2f}s  ({C/A:.3f}x)")
+print()
+print(f"B vs C 差异: {abs(B-C)/A*100:.2f}%")
+if abs(B-C)/A < 0.05:
+    print("=> 结论: B ≈ C, 阿里云 Instruct 语速控制也是后处理变速(类似 Edge TTS)")
+else:
+    print("=> 结论: B 与 C 显著不同, 阿里云 Instruct 是真正的模型级慢速生成")
+EOF
+
+echo
+echo '=== 顺便看看波形是不是真不同(语谱图) ==='
+ffmpeg -i /tmp/aliyun-A.mp3 -lavfi showspectrumpic=s=1024x512 /tmp/aliyun-A-spec.png 2>&1 | tail -1
+ffmpeg -i /tmp/aliyun-B-slow.wav -lavfi showspectrumpic=s=1024x512 /tmp/aliyun-B-spec.png 2>&1 | tail -1
+ffmpeg -i /tmp/aliyun-C-atempo.mp3 -lavfi showspectrumpic=s=1024x512 /tmp/aliyun-C-spec.png 2>&1 | tail -1
+ls -la /tmp/aliyun-*-spec.png
+
+echo
+echo '=== 把三个 mp3 上传到 OSS 让你试听 ==='
+cp /data/ai/audio/server/.env /tmp/dashscope.env 2>/dev/null
+cd /data/ai/audio/server && node -e '
+const OSS = require("ali-oss").default || require("ali-oss");
+require("dotenv").config({ path: "/data/ai/audio/server/.env" });
+const c = {
+  region: "oss-cn-shanghai",
+  accessKeyId: process.env.OSS_ACCESS_KEY_ID,
+  accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
+  bucket: process.env.OSS_BUCKET_NAME,
+  internal: true,
+  endpoint: process.env.OSS_INTERNAL_ENDPOINT,
+};
+const client = new OSS(c);
+const fs = require("fs");
+const files = {
+  "aliyun-A-default": "/tmp/aliyun-A.mp3",
+  "aliyun-B-instruct-slow": "/tmp/aliyun-B-slow.wav",
+  "aliyun-C-atempo-postprocess": "/tmp/aliyun-C-atempo.mp3",
+};
+(async () => {
+  for (const [name, path] of Object.entries(files)) {
+    const key = `audio/voice-samples/aliyun-${name}.mp3`;
+    await client.put(key, fs.readFileSync(path));
+    console.log("uploaded:", key);
+  }
+  const PUB = process.env.OSS_ENDPOINT;
+  for (const name of Object.keys(files)) {
+    console.log(`https://${process.env.OSS_BUCKET_NAME}.${PUB}/audio/voice-samples/aliyun-${name}.mp3`);
+  }
+})().catch(e => { console.error(e); process.exit(1); });
+'

+ 8 - 0
deploy-package/scripts/list-edge-voices.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+edge-tts --list-voices > /tmp/voices.txt 2>/dev/null
+echo "=== Edge TTS 总音色数:$(tail -n +3 /tmp/voices.txt | wc -l) ==="
+echo "=== 支持的语言(共 $(tail -n +3 /tmp/voices.txt | sed 's/\([^-]*\)-.*/\1/' | sort -u | wc -l) 种) ==="
+tail -n +3 /tmp/voices.txt | sed 's/\([^-]*\)-.*/\1/' | sort -u | while read lang; do
+  count=$(grep -c "^${lang}-" /tmp/voices.txt)
+  printf '  %-6s %3d 个音色\n' "$lang" "$count"
+done

+ 121 - 0
deploy-package/scripts/parse-qwen-tts-dom.js

@@ -0,0 +1,121 @@
+// scripts/parse-qwen-tts-dom.js — 从 playwright 渲染的 DOM 文本提取 Qwen-TTS 音色列表
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const input = process.argv[2] || path.join(os.tmpdir(), 'qwen-tts-dom.txt');
+const raw = fs.readFileSync(input, 'utf8');
+
+// 用一个 normalizer 把所有空白都换成换行,合并空行
+const normalized = raw
+  .replace(/[ \t ]+/g, ' ')   // 折叠空白(空格/tab/全角空格)
+  .replace(/\r\n/g, '\n')
+  .replace(/\r/g, '\n')
+  .replace(/\n{2,}/g, '\n');
+
+const lines = normalized.split('\n').map(l => l.trim()).filter(Boolean);
+
+// 找所有"音色名:X"行
+const voices = [];
+for (let i = 0; i < lines.length; i++) {
+  if (!/^音色名[::]\s*(.+)$/.test(lines[i])) continue;
+  // 已经处理过的(第二个表格里又出现 Cherry),跳过
+  const m = lines[i].match(/^音色名[::]\s*(.+)$/);
+  if (!m) continue;
+  const name = m[1].trim();
+  // 下一行非空就是描述
+  let desc = '';
+  let gender = '';
+  for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
+    const dm = lines[j].match(/^描述[::]\s*(.+)$/);
+    if (!dm) continue;
+    const full = dm[1].trim();
+    const g = full.match(/[((]([男女])性[))]\s*$/);
+    if (g) gender = g[1] === '女' ? '女' : '男';
+    desc = full.replace(/[((][男女]性[))]\s*$/, '').trim();
+    break;
+  }
+  if (desc) voices.push({ name, desc, gender });
+}
+
+// 去重(同样的音色名只保留第一个出现,忽略表格末尾重复的"芊悦 Cherry"那段)
+const seen = new Set();
+const unique = voices.filter(v => {
+  if (seen.has(v.name)) return false;
+  seen.add(v.name);
+  return true;
+});
+
+console.error(`解析出 ${unique.length} 个音色(去重前 ${voices.length})`);
+
+// 输出 markdown 表格
+const out = [`## Qwen-TTS 音色清单(完整,官方页面抓取)\n`];
+out.push(`> 数据来源:[阿里云 Qwen-TTS 音色列表](https://help.aliyun.com/zh/model-studio/qwen-tts-voice-list),2026-07 用 playwright 渲染抓取。共 ${unique.length} 个音色。`);
+out.push('');
+out.push('| 音色名 | 描述 | 性别 |');
+out.push('|--------|------|------|');
+for (const v of unique) {
+  const d = v.desc.replace(/\|/g, '\\|');
+  out.push(`| \`${v.name}\` | ${d} | ${v.gender} |`);
+}
+
+// 外语音色分类
+const foreign = unique.filter(v => /(英语|美语|法|德|意|俄|韩|西班牙|葡萄牙|拉美|日语|阿拉伯)/.test(v.desc));
+if (foreign.length) {
+  out.push('');
+  out.push('### 外语音色(按语言)');
+  out.push('');
+  out.push('| 音色名 | 描述 | 性别 |');
+  out.push('|--------|------|------|');
+  for (const v of foreign) {
+    out.push(`| \`${v.name}\` | ${v.desc.replace(/\|/g, '\\|')} | ${v.gender} |`);
+  }
+}
+
+// 方言/地区音色分类
+const dialect = unique.filter(v => /(北京|上海|南京|陕西|闽南|天津|四川|粤语|胡同|沪上)/.test(v.name + v.desc));
+if (dialect.length) {
+  out.push('');
+  out.push('### 中文方言/地区音色');
+  out.push('');
+  out.push('| 音色名 | 描述 | 性别 |');
+  out.push('|--------|------|------|');
+  for (const v of dialect) {
+    out.push(`| \`${v.name}\` | ${v.desc.replace(/\|/g, '\\|')} | ${v.gender} |`);
+  }
+}
+
+// 角色/特殊
+const character = unique.filter(v => /(萝莉|小大人|少年|大叔|小哥|小新|老李|瑜伽|阿姐|哥仔|老者|设计师|少年)/.test(v.desc));
+if (character.length) {
+  out.push('');
+  out.push('### 角色/特色音色');
+  out.push('');
+  out.push('| 音色名 | 描述 | 性别 |');
+  out.push('|--------|------|------|');
+  for (const v of character) {
+    out.push(`| \`${v.name}\` | ${v.desc.replace(/\|/g, '\\|')} | ${v.gender} |`);
+  }
+}
+
+out.push('');
+out.push('### 选型速查');
+out.push('');
+out.push('| 场景 | 推荐音色 |');
+out.push('|------|---------|');
+out.push('| **有声书/广播剧(温柔女)** | 芊悦(Cherry,默认)、苏瑶、四月 |');
+out.push('| **有声书(磁性男)** | 凯、晨煦(带北方口音)、安德雷 |');
+out.push('| **新闻播报** | 阿闻(最专业主持人)、晨煦 |');
+out.push('| **儿童/童趣** | 萌宝(小萝莉)、十三(拽拽小暴躁)、顽屁小孩(调皮捣蛋)、沙小弥(小大人) |');
+out.push('| **品牌级朗读** | 詹妮弗(电影质感美语女声)、御姐系卡捷琳娜 |');
+out.push('| **戏感配音** | 甜茶(节奏张力) |');
+out.push('| **老者/沉稳** | 沧明子(沧桑睿智)、徐大爷(沙哑烟嗓) |');
+out.push('| **方言版本** | 上海-阿珍、北京-晓东、南京-老李、陕西-秦川、闽南-阿杰、天津-李彼得、四川-晴儿/程川、粤语-阿强/阿清 |');
+out.push('| **欧美外语** | 詹妮弗/艾登(美)、莱恩(德)、埃米尔安(法)、博德加(西班牙,热情大叔)、拉迪奥·戈尔(葡,足球解说) |');
+out.push('| **东亚/俄** | 素熙(韩)、小野杏(日)、阿列克(俄,战斗民族冷) |');
+
+console.log(out.join('\n'));
+fs.writeFileSync(path.join(os.tmpdir(), 'qwen-tts-voices.json'),
+  JSON.stringify(unique, null, 2), 'utf8');
+fs.writeFileSync(path.join(os.tmpdir(), 'qwen-tts-section.md'),
+  out.join('\n'), 'utf8');

+ 36 - 0
deploy-package/scripts/scrape-alicloud-qwen-tts.js

@@ -0,0 +1,36 @@
+// scripts/scrape-alicloud-qwen-tts.js — playwright 拿到阿里云页面渲染后 DOM 文本
+const { chromium } = require('playwright');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+(async () => {
+  const url = process.argv[2] || 'https://help.aliyun.com/zh/model-studio/qwen-tts-voice-list';
+  const out = process.argv[3] || path.join(os.tmpdir(), 'qwen-tts-dom.txt');
+
+  const browser = await chromium.launch({ headless: true });
+  const page = await browser.newPage({ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' });
+
+  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
+
+  // 等动态内容渲染(阿里云页面通常会做几秒的分批加载)
+  for (let i = 0; i < 10; i++) {
+    await page.waitForTimeout(2000);
+    const tables = await page.locator('table').count();
+    console.log(`  attempt ${i + 1}: tables=${tables}`);
+    if (tables > 0) break;
+  }
+
+  // 渲染后 DOM 文本(去 script/style)
+  const text = await page.evaluate(() => {
+    document.querySelectorAll('script,style,noscript').forEach(e => e.remove());
+    return document.body.innerText;
+  });
+
+  fs.writeFileSync(out, text, 'utf8');
+  console.log(`dom text: ${text.length} bytes → ${out}`);
+  console.log(`first 500 chars:`);
+  console.log(text.slice(0, 500));
+
+  await browser.close();
+})().catch(e => { console.error(e); process.exit(1); });

+ 26 - 0
deploy-package/scripts/test-aliyun-tts.sh

@@ -0,0 +1,26 @@
+#!/bin/bash
+# Test Aliyun TTS 3 ways: 默认 / Instruct 正常 / Instruct 慢速
+set -e
+
+TEXT='窗台上落着几样旧物。一只白瓷杯,杯沿有一道细细的釉裂,里面还留着半盏凉透的茶。'
+API_KEY=$(grep '^DASHSCOPE_API_KEY=' /data/ai/audio/server/.env | cut -d= -f2)
+
+echo "=== A: Qwen-TTS 默认 1.0x (基准) ==="
+A_RESP=$(curl -sS -m 30 -X POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \
+  -H "Authorization: Bearer ${API_KEY}" -H 'Content-Type: application/json' \
+  -d "{\"model\":\"qwen3-tts-flash\",\"input\":{\"text\":\"${TEXT}\",\"voice\":\"Cherry\"},\"parameters\":{\"format\":\"mp3\",\"sample_rate\":24000}}")
+A_URL=$(echo "$A_RESP" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("output",{}).get("audio",{}).get("url","") or "")')
+echo "URL: $A_URL"
+[ -n "$A_URL" ] && curl -sS -m 30 -o /tmp/aliyun-A.mp3 "$A_URL" && ls -la /tmp/aliyun-A.mp3
+ffmpeg -i /tmp/aliyun-A.mp3 -f null - 2>&1 | grep Duration | head -1
+
+echo
+echo "=== B: Qwen-TTS Instruct 语速较慢 ==="
+B_RESP=$(curl -sS -m 30 -X POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \
+  -H "Authorization: Bearer ${API_KEY}" -H 'Content-Type: application/json' \
+  -d "{\"model\":\"qwen3-tts-instruct-flash\",\"input\":{\"text\":\"${TEXT}\",\"voice\":\"Cherry\",\"instructions\":\"语速较慢\"},\"parameters\":{\"format\":\"mp3\",\"sample_rate\":24000}}")
+B_URL=$(echo "$B_RESP" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("output",{}).get("audio",{}).get("url","") or "")')
+echo "URL: $B_URL"
+echo "Raw response (truncated):"
+echo "$B_RESP" | head -c 500
+echo

+ 114 - 0
docs/qwen-tts-voices-full.md

@@ -0,0 +1,114 @@
+## Qwen-TTS 音色清单(完整,官方页面抓取)
+
+> 数据来源:[阿里云 Qwen-TTS 音色列表](https://help.aliyun.com/zh/model-studio/qwen-tts-voice-list),2026-07 用 playwright 渲染抓取。共 48 个音色。
+
+| 音色名 | 描述 | 性别 |
+|--------|------|------|
+| `芊悦` | 阳光积极、亲切自然小姐姐 | 女 |
+| `苏瑶` | 温柔小姐姐 | 女 |
+| `晨煦` | 标准普通话,带部分北方口音。阳光、温暖、活力、朝气 | 男 |
+| `千雪` | 二次元虚拟女友 | 女 |
+| `茉兔` | 撒娇搞怪,逗你开心 | 女 |
+| `十三` | 拽拽的、可爱的小暴躁 | 女 |
+| `月白` | 率性帅气的月白 | 男 |
+| `四月` | 知性与温柔的碰撞 | 女 |
+| `凯` | 耳朵的一场SPA | 男 |
+| `不吃鱼` | 不会翘舌音的设计师 | 男 |
+| `萌宝` | 喝酒不打醉拳的小萝莉 | 女 |
+| `詹妮弗` | 品牌级、电影质感般美语女声 | 女 |
+| `甜茶` | 节奏拉满,戏感炸裂,真实与张力共舞 | 男 |
+| `卡捷琳娜` | 御姐音色,韵律回味十足 | 女 |
+| `艾登` | 精通厨艺的美语大男孩 | 男 |
+| `沧明子` | 沉稳睿智的老者,沧桑如松却心明如镜 | 男 |
+| `乖小妹` | 温顺如春水,乖巧如初雪 | 女 |
+| `沙小弥` | 聪明伶俐的小大人,童真未泯却早慧如禅 | 男 |
+| `燕铮莺` | 声音洪亮,吐字清晰,人物鲜活,听得人热血沸腾;金戈铁马入梦来,字正腔圆间尽显千面人声的江湖 | 女 |
+| `田叔` | 一口独特的沙哑烟嗓,一开口便道尽了千军万马与江湖豪情 | 男 |
+| `萌小姬` | “萌属性”爆棚的小萝莉 | 女 |
+| `阿闻` | 平直的基线语调,字正腔圆的咬字发音,这就是最专业的新闻主持人 | 男 |
+| `墨讲师` | 既保持学科严谨性,又通过叙事技巧将复杂知识转化为可消化的认知模块 | 女 |
+| `徐大爷` | 被岁月和旱烟浸泡过的质朴嗓音,不疾不徐地摇开了满村的奇闻异事 | 男 |
+| `邻家妹妹` | 糯米糍一样又软又黏的嗓音,那一声声拉长了的“哥哥”,甜得能把人的骨头都叫酥了 | 女 |
+| `小婉` | 温和舒缓的声线,助你更快地进入睡眠,晚安,好梦 | 女 |
+| `顽屁小孩` | 调皮捣蛋却充满童真的他来了,这是你记忆中的小新吗 | 男 |
+| `少女阿月` | 平时是甜到发腻的迷糊少女音,但在喊出“代表月亮消灭你”时,瞬间充满不容置疑的爱与正义 | 女 |
+| `博德加` | 热情的西班牙大叔 | 男 |
+| `索尼莎` | 热情开朗的拉美大姐 | 女 |
+| `阿列克` | 一开口,是战斗民族的冷,也是毛呢大衣下的暖 | 男 |
+| `多尔切` | 慵懒的意大利大叔 | 男 |
+| `素熙` | 温柔开朗,情绪丰富的韩国欧尼 | 女 |
+| `小野杏` | 鬼灵精怪的青梅竹马 | 女 |
+| `莱恩` | 理性是底色,叛逆藏在细节里——穿西装也听后朋克的德国青年 | 男 |
+| `埃米尔安` | 浪漫的法国大哥哥 | 男 |
+| `安德雷` | 声音磁性,自然舒服、沉稳男生 | 男 |
+| `拉迪奥·戈尔` | 足球诗人Rádio Gol!今天我要用名字为你们解说足球 | 男 |
+| `上海-阿珍` | 风风火火的沪上阿姐 | 女 |
+| `北京-晓东` | 北京胡同里长大的少年 | 男 |
+| `南京-老李` | 耐心的瑜伽老师 | 男 |
+| `陕西-秦川` | 面宽话短,心实声沉——老陕的味道 | 男 |
+| `闽南-阿杰` | 诙谐直爽、市井活泼的台湾哥仔形象 | 男 |
+| `天津-李彼得` | 天津相声,专业捧哏 | 男 |
+| `四川-晴儿` | 甜到你心里的川妹子 | 女 |
+| `四川-程川` | 一个跳脱市井的四川成都男子 | 男 |
+| `粤语-阿强` | 幽默风趣的阿强,在线陪聊 | 男 |
+| `粤语-阿清` | 甜美的港妹闺蜜 | 女 |
+
+### 外语音色(按语言)
+
+| 音色名 | 描述 | 性别 |
+|--------|------|------|
+| `詹妮弗` | 品牌级、电影质感般美语女声 | 女 |
+| `艾登` | 精通厨艺的美语大男孩 | 男 |
+| `博德加` | 热情的西班牙大叔 | 男 |
+| `索尼莎` | 热情开朗的拉美大姐 | 女 |
+| `多尔切` | 慵懒的意大利大叔 | 男 |
+| `素熙` | 温柔开朗,情绪丰富的韩国欧尼 | 女 |
+| `莱恩` | 理性是底色,叛逆藏在细节里——穿西装也听后朋克的德国青年 | 男 |
+| `埃米尔安` | 浪漫的法国大哥哥 | 男 |
+
+### 中文方言/地区音色
+
+| 音色名 | 描述 | 性别 |
+|--------|------|------|
+| `上海-阿珍` | 风风火火的沪上阿姐 | 女 |
+| `北京-晓东` | 北京胡同里长大的少年 | 男 |
+| `南京-老李` | 耐心的瑜伽老师 | 男 |
+| `陕西-秦川` | 面宽话短,心实声沉——老陕的味道 | 男 |
+| `闽南-阿杰` | 诙谐直爽、市井活泼的台湾哥仔形象 | 男 |
+| `天津-李彼得` | 天津相声,专业捧哏 | 男 |
+| `四川-晴儿` | 甜到你心里的川妹子 | 女 |
+| `四川-程川` | 一个跳脱市井的四川成都男子 | 男 |
+| `粤语-阿强` | 幽默风趣的阿强,在线陪聊 | 男 |
+| `粤语-阿清` | 甜美的港妹闺蜜 | 女 |
+
+### 角色/特色音色
+
+| 音色名 | 描述 | 性别 |
+|--------|------|------|
+| `不吃鱼` | 不会翘舌音的设计师 | 男 |
+| `萌宝` | 喝酒不打醉拳的小萝莉 | 女 |
+| `沧明子` | 沉稳睿智的老者,沧桑如松却心明如镜 | 男 |
+| `沙小弥` | 聪明伶俐的小大人,童真未泯却早慧如禅 | 男 |
+| `萌小姬` | “萌属性”爆棚的小萝莉 | 女 |
+| `顽屁小孩` | 调皮捣蛋却充满童真的他来了,这是你记忆中的小新吗 | 男 |
+| `博德加` | 热情的西班牙大叔 | 男 |
+| `多尔切` | 慵懒的意大利大叔 | 男 |
+| `上海-阿珍` | 风风火火的沪上阿姐 | 女 |
+| `北京-晓东` | 北京胡同里长大的少年 | 男 |
+| `南京-老李` | 耐心的瑜伽老师 | 男 |
+| `闽南-阿杰` | 诙谐直爽、市井活泼的台湾哥仔形象 | 男 |
+
+### 选型速查
+
+| 场景 | 推荐音色 |
+|------|---------|
+| **有声书/广播剧(温柔女)** | 芊悦(Cherry,默认)、苏瑶、四月 |
+| **有声书(磁性男)** | 凯、晨煦(带北方口音)、安德雷 |
+| **新闻播报** | 阿闻(最专业主持人)、晨煦 |
+| **儿童/童趣** | 萌宝(小萝莉)、十三(拽拽小暴躁)、顽屁小孩(调皮捣蛋)、沙小弥(小大人) |
+| **品牌级朗读** | 詹妮弗(电影质感美语女声)、御姐系卡捷琳娜 |
+| **戏感配音** | 甜茶(节奏张力) |
+| **老者/沉稳** | 沧明子(沧桑睿智)、徐大爷(沙哑烟嗓) |
+| **方言版本** | 上海-阿珍、北京-晓东、南京-老李、陕西-秦川、闽南-阿杰、天津-李彼得、四川-晴儿/程川、粤语-阿强/阿清 |
+| **欧美外语** | 詹妮弗/艾登(美)、莱恩(德)、埃米尔安(法)、博德加(西班牙,热情大叔)、拉迪奥·戈尔(葡,足球解说) |
+| **东亚/俄** | 素熙(韩)、小野杏(日)、阿列克(俄,战斗民族冷) |

+ 10 - 9
docs/tts-aliyun-voice.md

@@ -88,18 +88,19 @@ DASHSCOPE_VOICE=longyichen_v3  # 龙逸尘阳光男声
 
 然后 `pm2 restart server` 即可,所有新生成的音频都会走阿里云(消耗 dashscope 余额)。
 
-## 完整 voice 列表(从阿里云控制台)
+## 完整 voice 列表
 
-**当前缺口:** DashScope 没提供程序化的 voice list API(POST `/services/audio/tts/SpeechSynthesizer/voices` 一直返回 "url error"),所以本节需要手动维护
+完整 48 个 Qwen-TTS 音色已抓取并存放在独立文档:**[`docs/qwen-tts-voices-full.md`](qwen-tts-voices-full.md)**
 
-获取方式:
+| 类别 | 数量 | 代表 |
+|------|------|------|
+| 标准中文 | ~20 | 芊悦(Cherry,默认)、苏瑶、晨煦、四月、凯、安德雷、阿闻 |
+| 中文方言 | 10 | 上海-阿珍、北京-晓东、南京-老李、陕西-秦川、闽南-阿杰、天津-李彼得、四川-晴儿/程川、粤语-阿强/阿清 |
+| 角色/特色 | ~10 | 萌宝(小萝莉)、十三(小暴躁)、沧明子(老者)、徐大爷(烟嗓)、沙小弥(小大人)、燕铮莺(江湖)、田叔(说书) |
+| 欧美外语 | 8 | 詹妮弗/艾登(美语)、莱恩(德语)、埃米尔安(法语)、博德加(西班牙)、索尼莎(拉美)、多尔切(意)、素熙(韩)、阿列克(俄) |
+| 其他外语 | 2 | 小野杏(日)、拉迪奥·戈尔(葡,足球解说) |
 
-1. 登录 [阿里云百炼控制台](https://bailian.console.aliyun.com/)
-2. 进入 **模型服务 → 语音合成 → 音色列表**
-3. 切换模型(cosyvoice / qwen-tts 分别看)
-4. 复制音色清单 → 提交 PR 补到本节
-
-或者翻到 `https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list` 这个官方页面手工抄。
+**CosyVoice 完整音色列表待后续从 https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list 抓取补全**(同样的 playwright 流程,见 `scripts/scrape-alicloud-qwen-tts.js` 和 `scripts/parse-qwen-tts-dom.js`)。
 
 ### 已知 / 项目支持(从代码汇总)