{label}
{player}#!/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'{label}
{player}相同试听台词、相同语速 {html.escape(rate)},听完再决定适合角色的音色。
' page += '