| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- // 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');
|