manikandanj's picture
Prepare AI Time Machine hackathon Space
5862322 verified
Raw
History Blame Contribute Delete
4.12 kB
from __future__ import annotations
import hashlib
import os
import subprocess
import wave
from pathlib import Path
from time_machine.domain.models import AudioResult, Persona, VoiceProfile
class SapiTTSAdapter:
"""Fast local Windows SAPI TTS adapter for low-latency Walk testing."""
def __init__(
self,
output_dir: Path | str = "data/audio",
script_path: Path | str | None = None,
voice_id: str = "windows-sapi",
) -> None:
self.output_dir = Path(output_dir)
self.voice_id = voice_id
self.script_path = (
Path(script_path)
if script_path is not None
else Path(__file__).resolve().parents[4] / "scripts" / "sapi_tts.ps1"
)
def prepare_voice(self, persona: Persona) -> VoiceProfile:
return VoiceProfile(
voice_id=self.voice_id,
description=(
f"Windows SAPI voice for {persona.name}: {persona.speaking_style}. "
"Used for fast local Walk testing."
),
pace="fast",
emotion="curious",
accent_hint=persona.local_identifier,
)
def synthesize(
self,
text: str,
voice_profile: VoiceProfile,
prosody_hint: str | None = None,
) -> AudioResult:
if os.name != "nt":
raise RuntimeError("SapiTTSAdapter requires Windows.")
if not self.script_path.exists():
raise RuntimeError(f"Missing SAPI synthesis script: {self.script_path}")
self.output_dir.mkdir(parents=True, exist_ok=True)
cache_key = self._cache_key(text, voice_profile, prosody_hint)
audio_path = self.output_dir / f"sapi-{cache_key}.wav"
if audio_path.exists():
return AudioResult(
path=str(audio_path),
mime_type="audio/wav",
duration_seconds=_wav_duration(audio_path),
description=f"Cached Windows SAPI synthesis using {voice_profile.voice_id}.",
)
text_path = self.output_dir / f"sapi-{cache_key}.txt"
text_path.write_text(text, encoding="utf-8")
rate = {"slow": -2, "medium": 0, "fast": 2}[voice_profile.pace]
completed = subprocess.run(
[
"powershell.exe",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
str(self.script_path),
"-TextPath",
str(text_path),
"-OutputPath",
str(audio_path),
"-Rate",
str(rate),
],
check=False,
capture_output=True,
text=True,
timeout=int(os.getenv("TIME_MACHINE_SAPI_TIMEOUT_SECONDS", "30")),
)
if completed.returncode != 0:
raise RuntimeError(
"Windows SAPI synthesis failed: "
f"{completed.stderr.strip() or completed.stdout.strip()}"
)
return AudioResult(
path=str(audio_path),
mime_type="audio/wav",
duration_seconds=_wav_duration(audio_path),
description=f"Windows SAPI synthesis using {voice_profile.voice_id}.",
)
def warm_up(self, voice_profile: VoiceProfile | None = None) -> None:
return None
def _cache_key(
self,
text: str,
voice_profile: VoiceProfile,
prosody_hint: str | None,
) -> str:
payload = "\n".join(
[
self.voice_id,
voice_profile.pace,
prosody_hint or "",
text,
]
)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
def _wav_duration(path: Path) -> float | None:
try:
with wave.open(str(path), "rb") as handle:
frames = handle.getnframes()
rate = handle.getframerate()
if rate <= 0:
return None
return round(frames / rate, 3)
except wave.Error:
return None