parse-qwen-tts-dom.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. // scripts/parse-qwen-tts-dom.js — 从 playwright 渲染的 DOM 文本提取 Qwen-TTS 音色列表
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const input = process.argv[2] || path.join(os.tmpdir(), 'qwen-tts-dom.txt');
  6. const raw = fs.readFileSync(input, 'utf8');
  7. // 用一个 normalizer 把所有空白都换成换行,合并空行
  8. const normalized = raw
  9. .replace(/[ \t ]+/g, ' ') // 折叠空白(空格/tab/全角空格)
  10. .replace(/\r\n/g, '\n')
  11. .replace(/\r/g, '\n')
  12. .replace(/\n{2,}/g, '\n');
  13. const lines = normalized.split('\n').map(l => l.trim()).filter(Boolean);
  14. // 找所有"音色名:X"行
  15. const voices = [];
  16. for (let i = 0; i < lines.length; i++) {
  17. if (!/^音色名[::]\s*(.+)$/.test(lines[i])) continue;
  18. // 已经处理过的(第二个表格里又出现 Cherry),跳过
  19. const m = lines[i].match(/^音色名[::]\s*(.+)$/);
  20. if (!m) continue;
  21. const name = m[1].trim();
  22. // 下一行非空就是描述
  23. let desc = '';
  24. let gender = '';
  25. for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
  26. const dm = lines[j].match(/^描述[::]\s*(.+)$/);
  27. if (!dm) continue;
  28. const full = dm[1].trim();
  29. const g = full.match(/[((]([男女])性[))]\s*$/);
  30. if (g) gender = g[1] === '女' ? '女' : '男';
  31. desc = full.replace(/[((][男女]性[))]\s*$/, '').trim();
  32. break;
  33. }
  34. if (desc) voices.push({ name, desc, gender });
  35. }
  36. // 去重(同样的音色名只保留第一个出现,忽略表格末尾重复的"芊悦 Cherry"那段)
  37. const seen = new Set();
  38. const unique = voices.filter(v => {
  39. if (seen.has(v.name)) return false;
  40. seen.add(v.name);
  41. return true;
  42. });
  43. console.error(`解析出 ${unique.length} 个音色(去重前 ${voices.length})`);
  44. // 输出 markdown 表格
  45. const out = [`## Qwen-TTS 音色清单(完整,官方页面抓取)\n`];
  46. out.push(`> 数据来源:[阿里云 Qwen-TTS 音色列表](https://help.aliyun.com/zh/model-studio/qwen-tts-voice-list),2026-07 用 playwright 渲染抓取。共 ${unique.length} 个音色。`);
  47. out.push('');
  48. out.push('| 音色名 | 描述 | 性别 |');
  49. out.push('|--------|------|------|');
  50. for (const v of unique) {
  51. const d = v.desc.replace(/\|/g, '\\|');
  52. out.push(`| \`${v.name}\` | ${d} | ${v.gender} |`);
  53. }
  54. // 外语音色分类
  55. const foreign = unique.filter(v => /(英语|美语|法|德|意|俄|韩|西班牙|葡萄牙|拉美|日语|阿拉伯)/.test(v.desc));
  56. if (foreign.length) {
  57. out.push('');
  58. out.push('### 外语音色(按语言)');
  59. out.push('');
  60. out.push('| 音色名 | 描述 | 性别 |');
  61. out.push('|--------|------|------|');
  62. for (const v of foreign) {
  63. out.push(`| \`${v.name}\` | ${v.desc.replace(/\|/g, '\\|')} | ${v.gender} |`);
  64. }
  65. }
  66. // 方言/地区音色分类
  67. const dialect = unique.filter(v => /(北京|上海|南京|陕西|闽南|天津|四川|粤语|胡同|沪上)/.test(v.name + v.desc));
  68. if (dialect.length) {
  69. out.push('');
  70. out.push('### 中文方言/地区音色');
  71. out.push('');
  72. out.push('| 音色名 | 描述 | 性别 |');
  73. out.push('|--------|------|------|');
  74. for (const v of dialect) {
  75. out.push(`| \`${v.name}\` | ${v.desc.replace(/\|/g, '\\|')} | ${v.gender} |`);
  76. }
  77. }
  78. // 角色/特殊
  79. const character = unique.filter(v => /(萝莉|小大人|少年|大叔|小哥|小新|老李|瑜伽|阿姐|哥仔|老者|设计师|少年)/.test(v.desc));
  80. if (character.length) {
  81. out.push('');
  82. out.push('### 角色/特色音色');
  83. out.push('');
  84. out.push('| 音色名 | 描述 | 性别 |');
  85. out.push('|--------|------|------|');
  86. for (const v of character) {
  87. out.push(`| \`${v.name}\` | ${v.desc.replace(/\|/g, '\\|')} | ${v.gender} |`);
  88. }
  89. }
  90. out.push('');
  91. out.push('### 选型速查');
  92. out.push('');
  93. out.push('| 场景 | 推荐音色 |');
  94. out.push('|------|---------|');
  95. out.push('| **有声书/广播剧(温柔女)** | 芊悦(Cherry,默认)、苏瑶、四月 |');
  96. out.push('| **有声书(磁性男)** | 凯、晨煦(带北方口音)、安德雷 |');
  97. out.push('| **新闻播报** | 阿闻(最专业主持人)、晨煦 |');
  98. out.push('| **儿童/童趣** | 萌宝(小萝莉)、十三(拽拽小暴躁)、顽屁小孩(调皮捣蛋)、沙小弥(小大人) |');
  99. out.push('| **品牌级朗读** | 詹妮弗(电影质感美语女声)、御姐系卡捷琳娜 |');
  100. out.push('| **戏感配音** | 甜茶(节奏张力) |');
  101. out.push('| **老者/沉稳** | 沧明子(沧桑睿智)、徐大爷(沙哑烟嗓) |');
  102. out.push('| **方言版本** | 上海-阿珍、北京-晓东、南京-老李、陕西-秦川、闽南-阿杰、天津-李彼得、四川-晴儿/程川、粤语-阿强/阿清 |');
  103. out.push('| **欧美外语** | 詹妮弗/艾登(美)、莱恩(德)、埃米尔安(法)、博德加(西班牙,热情大叔)、拉迪奥·戈尔(葡,足球解说) |');
  104. out.push('| **东亚/俄** | 素熙(韩)、小野杏(日)、阿列克(俄,战斗民族冷) |');
  105. console.log(out.join('\n'));
  106. fs.writeFileSync(path.join(os.tmpdir(), 'qwen-tts-voices.json'),
  107. JSON.stringify(unique, null, 2), 'utf8');
  108. fs.writeFileSync(path.join(os.tmpdir(), 'qwen-tts-section.md'),
  109. out.join('\n'), 'utf8');