Spaces:
Running
Running
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55 | """Supertonic TTS - lightning-fast, on-device, ~99M-param ONNX voices. | |
| Each suspect gets a distinct preset voice (M1-M5 / F1-F5). Lazily loaded and excluded | |
| from unit coverage (needs the supertonic package + ONNX assets). Falls back to | |
| unavailable so the game degrades to text-only. | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| from ..schemas.suspect import VoiceAssignment | |
| # Males 0-4, females 5-9, matching tts.assignment.assign_voice's gendered speaker_id. | |
| _VOICES = ("M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5") | |
| class SupertonicProvider: # pragma: no cover - requires the supertonic package + models | |
| is_local = True | |
| def __init__(self) -> None: | |
| self.available = False | |
| self._tts = None | |
| self._styles: dict[str, object] = {} | |
| try: | |
| from supertonic import TTS | |
| self._tts = TTS(auto_download=True) | |
| self.available = True | |
| except Exception: | |
| self.available = False | |
| def _style(self, name: str): | |
| if name not in self._styles: | |
| self._styles[name] = self._tts.get_voice_style(voice_name=name) # type: ignore[union-attr] | |
| return self._styles[name] | |
| def synth_to_file(self, text: str, voice: VoiceAssignment | None, out_path: Path) -> Path | None: | |
| if not self.available or self._tts is None or not text.strip(): | |
| return None | |
| name = _VOICES[(voice.speaker_id if voice else 0) % len(_VOICES)] | |
| # length_scale (0.95-1.15) -> a gentle per-suspect speed variation around 1.0. | |
| speed = round(2.05 - (voice.length_scale if voice else 1.05), 2) | |
| speed = min(1.2, max(0.85, speed)) | |
| try: | |
| wav, _ = self._tts.synthesize( # type: ignore[union-attr] | |
| text=text, lang="en", voice_style=self._style(name), total_steps=6, speed=speed | |
| ) | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| self._tts.save_audio(wav, str(out_path)) # type: ignore[union-attr] | |
| return out_path | |
| except Exception: | |
| return None | |