返回 ppt-master
backend_elevenlabs.py
根目录 / skills / ppt-master / scripts / tts_backends / backend_elevenlabs.py
1 """ElevenLabs backend for narration audio generation."""
2
3 from __future__ import annotations
4
5 import json
6 from pathlib import Path
7 from urllib import error, request
8
9 from tts_backends.backend_common import publish_audio_bytes, read_api_key
10
11
12 API_BASE = "https://api.elevenlabs.io/v1"
13
14
15 def read_elevenlabs_api_key(env_name: str) -> str:
16 return read_api_key(env_name, label="ElevenLabs")
17
18
19 def output_extension(output_format: str) -> str:
20 codec = output_format.split("_", 1)[0].lower()
21 if codec in {"mp3", "wav"}:
22 return f".{codec}"
23 raise RuntimeError(
24 f"Unsupported ElevenLabs output format for PPT narration: {output_format}. "
25 "Use an mp3_* or wav_* format."
26 )
27
28
29 def _read_http_error(exc: error.HTTPError) -> str:
30 try:
31 body = exc.read().decode("utf-8", errors="replace")
32 except Exception:
33 body = ""
34 return f"HTTP {exc.code}: {body or exc.reason}"
35
36
37 def generate(
38 text: str,
39 output_path: Path,
40 *,
41 api_key: str,
42 voice_id: str,
43 model: str,
44 output_format: str,
45 stability: float | None,
46 similarity_boost: float | None,
47 style: float | None,
48 speaker_boost: bool | None,
49 ) -> None:
50 payload: dict[str, object] = {
51 "text": text,
52 "model_id": model,
53 }
54
55 voice_settings: dict[str, object] = {}
56 if stability is not None:
57 voice_settings["stability"] = stability
58 if similarity_boost is not None:
59 voice_settings["similarity_boost"] = similarity_boost
60 if style is not None:
61 voice_settings["style"] = style
62 if speaker_boost is not None:
63 voice_settings["use_speaker_boost"] = speaker_boost
64 if voice_settings:
65 payload["voice_settings"] = voice_settings
66
67 body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
68 url = f"{API_BASE}/text-to-speech/{voice_id}?output_format={output_format}"
69 req = request.Request(
70 url,
71 data=body,
72 headers={
73 "Content-Type": "application/json",
74 "xi-api-key": api_key,
75 },
76 method="POST",
77 )
78 try:
79 with request.urlopen(req, timeout=120) as response:
80 publish_audio_bytes(response.read(), output_path)
81 except error.HTTPError as exc:
82 raise RuntimeError(_read_http_error(exc)) from exc
83 except error.URLError as exc:
84 raise RuntimeError(f"ElevenLabs request failed: {exc.reason}") from exc
85
86
87 def print_voices(api_key: str) -> None:
88 req = request.Request(
89 f"{API_BASE}/voices",
90 headers={"xi-api-key": api_key},
91 method="GET",
92 )
93 try:
94 with request.urlopen(req, timeout=60) as response:
95 data = json.loads(response.read().decode("utf-8"))
96 except error.HTTPError as exc:
97 raise RuntimeError(_read_http_error(exc)) from exc
98 except error.URLError as exc:
99 raise RuntimeError(f"ElevenLabs request failed: {exc.reason}") from exc
100
101 print("ElevenLabs voices:")
102 print("Voice ID Name Category")
103 print("----------------------- ----------------------------- ----------")
104 for item in data.get("voices", []):
105 voice_id = item.get("voice_id", "")
106 name = item.get("name", "")
107 category = item.get("category", "")
108 print(f"{voice_id:<23} {name:<29} {category}")
109
109 lines PYTHON