#!/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'' else: player = f'

{html.escape(row.get("error", "生成失败"))}

' cards.append(f'
{name}

{label}

{player}
') page = ''' Edge TTS 成语配音试听

成语故事 Skill · 角色音色试听

''' page += f'

相同试听台词、相同语速 {html.escape(rate)},听完再决定适合角色的音色。

' page += '
' + "".join(cards) + '
' (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))