blizzarman's picture
feat: M0 walking skeleton (app, service protocols, CI/CD, Space deploy)
00d5ae5
Raw
History Blame Contribute Delete
1.37 kB
"""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}")