"""TTS client contract + offline fake. Real implementations land in M2: edge-tts as primary (unofficial API, hence fragile) plus a local fallback behind this same Protocol (ADR 0001). The fake returns a *valid* WAV (short silence) so audio components and tests exercise the real byte path end to end. """ import io import wave from typing import Protocol, runtime_checkable from pydantic import BaseModel class TTSResult(BaseModel): audio: bytes format: str = "wav" voice: str | None = None @runtime_checkable class TTSClient(Protocol): async def synthesize( self, text: str, *, language: str = "en", voice: str | None = None ) -> TTSResult: ... def _silent_wav(duration_s: float = 0.2, sample_rate: int = 16_000) -> bytes: buffer = io.BytesIO() with wave.open(buffer, "wb") as wav: wav.setnchannels(1) wav.setsampwidth(2) # 16-bit PCM wav.setframerate(sample_rate) wav.writeframes(b"\x00\x00" * int(duration_s * sample_rate)) return buffer.getvalue() class FakeTTSClient: def __init__(self) -> None: self.calls: list[str] = [] async def synthesize( self, text: str, *, language: str = "en", voice: str | None = None ) -> TTSResult: self.calls.append(text) return TTSResult(audio=_silent_wav(), format="wav", voice=voice or f"fake-{language}")