返回 ppt-master
backend_common.py
根目录 / skills / ppt-master / scripts / tts_backends / backend_common.py
1 """Shared helpers for TTS backends."""
2
3 from __future__ import annotations
4
5 import json
6 import os
7 import tempfile
8 from pathlib import Path
9 from urllib import error, request
10
11
12 def read_api_key(*env_names: str, label: str) -> str:
13 for env_name in env_names:
14 api_key = os.environ.get(env_name, "").strip()
15 if api_key:
16 return api_key
17 joined = " or ".join(env_names)
18 raise RuntimeError(f"Missing {label} API key. Set {joined}=<key>.")
19
20
21 def read_http_error(exc: error.HTTPError) -> str:
22 try:
23 body = exc.read().decode("utf-8", errors="replace")
24 except Exception:
25 body = ""
26 return f"HTTP {exc.code}: {body or exc.reason}"
27
28
29 def post_json(url: str, *, headers: dict[str, str], payload: dict, timeout: int = 120) -> dict:
30 body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
31 req = request.Request(
32 url,
33 data=body,
34 headers={
35 "Content-Type": "application/json",
36 **headers,
37 },
38 method="POST",
39 )
40 try:
41 with request.urlopen(req, timeout=timeout) as response:
42 return json.loads(response.read().decode("utf-8"))
43 except error.HTTPError as exc:
44 raise RuntimeError(read_http_error(exc)) from exc
45 except error.URLError as exc:
46 raise RuntimeError(f"HTTP request failed: {exc.reason}") from exc
47
48
49 def get_bytes(url: str, *, timeout: int = 180) -> bytes:
50 req = request.Request(url, method="GET")
51 try:
52 with request.urlopen(req, timeout=timeout) as response:
53 return response.read()
54 except error.HTTPError as exc:
55 raise RuntimeError(read_http_error(exc)) from exc
56 except error.URLError as exc:
57 raise RuntimeError(f"Audio download failed: {exc.reason}") from exc
58
59
60 def download_audio(url: str, output_path: Path) -> None:
61 publish_audio_bytes(get_bytes(url), output_path)
62
63
64 def publish_audio_bytes(audio: bytes, output_path: Path) -> None:
65 """Publish non-empty provider audio without exposing a partial target."""
66 if not audio:
67 raise RuntimeError("TTS provider returned empty audio data")
68
69 output_path.parent.mkdir(parents=True, exist_ok=True)
70 descriptor, raw_path = tempfile.mkstemp(
71 prefix=f".{output_path.name}.",
72 suffix=".tmp",
73 dir=output_path.parent,
74 )
75 staged_path = Path(raw_path)
76 try:
77 with os.fdopen(descriptor, "wb") as stream:
78 descriptor = -1
79 stream.write(audio)
80 stream.flush()
81 os.fsync(stream.fileno())
82 if staged_path.stat().st_size <= 0:
83 raise RuntimeError("TTS provider returned empty audio data")
84 os.replace(staged_path, output_path)
85 finally:
86 if descriptor >= 0:
87 os.close(descriptor)
88 staged_path.unlink(missing_ok=True)
89
90
91 def extension_from_format(audio_format: str) -> str:
92 normalized = audio_format.strip().lower()
93 if normalized in {"mp3", "wav"}:
94 return f".{normalized}"
95 raise RuntimeError(
96 f"Unsupported audio format for PPT narration: {audio_format}. "
97 "Use mp3 or wav, or transcode provider output before embedding."
98 )
99
99 lines PYTHON