cpuai's picture
Install complete Linux VS Code + AHP MCP development workstation 2026-10-12
c12eb19 verified
Raw History Blame Contribute Delete
4.61 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""在线生成 6 种可试听 Edge TTS MP3,方便为新故事选择主讲和角色音色。
此脚本需要联网;不会自动修改工程现有配音配置。
用法:python scripts/preview_voices.py --output ./voice-previews
"""
import argparse
import asyncio
import html
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PRESETS = ROOT / "voice-presets.json" if (ROOT / "voice-presets.json").is_file() else ROOT / "assets/voice-presets.json"
DEFAULT_SAMPLE = "从前,有一个人发现了一口大钟。哇,这钟真漂亮!他举起锤子,铛的一声,钟声传遍了整个村子!你猜后来发生了什么?"
async def run(output: Path, sample: str, rate: str, verify: bool) -> None:
try:
import edge_tts
except ImportError as exc:
raise RuntimeError("缺少 edge-tts,请安装 requirements.txt") from exc
presets = json.loads(PRESETS.read_text(encoding="utf-8-sig"))
output.mkdir(parents=True, exist_ok=True)
available = {i["ShortName"] for i in await edge_tts.list_voices()} if verify else None
sem = asyncio.Semaphore(2)
async def make_item(v: dict) -> dict:
voice = v["id"]
if available is not None and voice not in available:
return {"id": voice, "error": "当前 Edge 服务未提供此声音"}
dest = output / (voice + ".mp3")
async with sem:
for attempt in range(3):
try:
if not dest.is_file() or dest.stat().st_size < 500:
await edge_tts.Communicate(sample, voice=voice, rate=rate,
pitch="+0Hz").save(str(dest))
if dest.stat().st_size < 500:
raise RuntimeError("空白或不完整音频")
return {"id": voice, "label": " / ".join(v.get("traits", [])),
"rate": rate, "file": dest.name, "bytes": dest.stat().st_size}
except Exception as exc:
if attempt == 2:
return {"id": voice, "error": f"{type(exc).__name__}: {exc}"}
await asyncio.sleep(2 + attempt * 2)
rows = await asyncio.gather(*(make_item(v) for v in presets["voices"]))
cards = []
for row in rows:
label = html.escape(row.get("label", "暂时不可用"))
name = html.escape(row["id"])
if row.get("file"):
player = f'<audio controls preload="none" src="{html.escape(row["file"])}"></audio>'
else:
player = f'<p class="error">{html.escape(row.get("error", "生成失败"))}</p>'
cards.append(f'<article><strong>{name}</strong><p>{label}</p>{player}</article>')
page = '''<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"><title>Edge TTS 成语配音试听</title>
<style>*{box-sizing:border-box}body{font-family:system-ui,'Microsoft YaHei',sans-serif;max-width:1090px;margin:30px auto;padding:20px;color:#253346;background:#f5f7f8}h1{font-size:27px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:18px}article{background:#fff;border:1px solid #e2e5ed;border-radius:14px;padding:22px}strong{font-size:16px}audio{width:100%}p{line-height:1.6}.error{color:#9e3b34}</style>
</head><body><h1>成语故事 Skill · 角色音色试听</h1>'''
page += f'<p>相同试听台词、相同语速 {html.escape(rate)},听完再决定适合角色的音色。</p>'
page += '<div class="grid">' + "".join(cards) + '</div></body></html>'
(output / "index.html").write_text(page, encoding="utf-8")
(output / "preview-results.json").write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps({"output": str(output / "index.html"), "voices": rows}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
ap = argparse.ArgumentParser(description="生成 Edge TTS 六种普通话音色的试听网页")
ap.add_argument("--output", default="voice-previews", help="要创建的试听目录")
ap.add_argument("--text", default=DEFAULT_SAMPLE, help="所有音色朗读的相同测试文本")
ap.add_argument("--rate", default="+5%", help="统一语速,例如 +5%")
ap.add_argument("--skip-availability-check", action="store_true", help="不请求最新音色列表")
args = ap.parse_args()
asyncio.run(run(Path(args.output).resolve(), args.text, args.rate, not args.skip_availability_check))