"""TTS provider abstraction. ``SupertonicProvider`` gives each suspect a distinct on-device voice. ``NullProvider`` (silent) is the safe fallback, so the game stays fully playable as text if the voice model is unavailable. """ from __future__ import annotations from pathlib import Path from typing import Protocol from ..config import Settings, TTSEngine from ..schemas.suspect import VoiceAssignment class TTSProvider(Protocol): is_local: bool available: bool def synth_to_file(self, text: str, voice: VoiceAssignment | None, out_path: Path) -> Path | None: """Write a WAV for ``text`` and return its path, or None if unavailable.""" ... class NullProvider: is_local = True available = False def synth_to_file(self, text: str, voice: VoiceAssignment | None, out_path: Path) -> None: return None def make_tts_provider(settings: Settings) -> TTSProvider: if settings.tts_engine is TTSEngine.SUPERTONIC: try: from .supertonic_provider import SupertonicProvider provider = SupertonicProvider() return provider if provider.available else NullProvider() except Exception: return NullProvider() return NullProvider()