← Files AgentBrokoARCHIVED FILE
src/video_forge/tts.py
2.31 KB · Sep 30, 2026 · 23:15 UTC
from __future__ import annotations
import platform
import shutil
import subprocess
from pathlib import Path
from .errors import VideoForgeError
from .process import run
def available_engines() -> list[str]:
engines: list[str] = []
if shutil.which("piper"):
engines.append("piper")
if shutil.which("espeak-ng") or shutil.which("espeak"):
engines.append("espeak")
if platform.system() == "Windows" and shutil.which("powershell"):
engines.append("windows")
if platform.system() == "Darwin" and shutil.which("say"):
engines.append("say")
return engines
def synthesize(
text: str,
output: Path,
*,
engine: str = "auto",
voice: Path | None = None,
rate: int = 175,
) -> str:
output.parent.mkdir(parents=True, exist_ok=True)
choices = available_engines()
selected = choices[0] if engine == "auto" and choices else engine
if not selected or selected == "auto":
raise VideoForgeError("No offline TTS engine found. See docs/TTS.md.")
if selected == "piper":
if not voice:
raise VideoForgeError("Piper requires --voice path/to/model.onnx")
command = ["piper", "--model", str(voice), "--output_file", str(output)]
print("+ piper --model ...")
subprocess.run(command, input=text, text=True, check=True)
elif selected == "espeak":
executable = shutil.which("espeak-ng") or shutil.which("espeak")
assert executable
run([executable, "-s", str(rate), "-w", output, text])
elif selected == "windows":
escaped_text = text.replace("'", "''")
escaped_output = str(output.resolve()).replace("'", "''")
script = (
"Add-Type -AssemblyName System.Speech; "
"$s=New-Object System.Speech.Synthesis.SpeechSynthesizer; "
f"$s.Rate={max(-10, min(10, round((rate - 175) / 15)))}; "
f"$s.SetOutputToWaveFile('{escaped_output}'); "
f"$s.Speak('{escaped_text}'); $s.Dispose()"
)
run(["powershell", "-NoProfile", "-Command", script])
elif selected == "say":
run(["say", "-r", str(rate), "-o", output, "--data-format=LEF32@22050", text])
else:
raise VideoForgeError(f"Unsupported TTS engine: {selected}")
return selected
SHA-256: 6b043c3fdb7a4ab576531294bce3c3401624ebed7255a91c4ce100296793e74a