"""M0 PoC #2 — TTS bake-off: Azure Neural vs ElevenLabs Flash (§14 Phase 0). Synthesizes typical agent sentences with every candidate voice, saves WAVs for the founder to listen to, and prints latency + rough per-character cost. Env: GOGO_AZURE_SPEECH_KEY / GOGO_AZURE_SPEECH_REGION and/or GOGO_ELEVENLABS_API_KEY. Run: python scripts/poc/tts_bakeoff.py [out_dir] """ from __future__ import annotations import asyncio import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from gogo.config import get_settings # noqa: E402 from gogo.voice.audio import pcm_to_wav # noqa: E402 from gogo.voice.tts import AzureTTS, ElevenLabsTTS # noqa: E402 SENTENCES = [ "Dobar dan, dobili ste salon Merima. Ja sam Gogo, virtuelni asistent — razgovor se snima. Kako vam mogu pomoći?", "U srijedu poslijepodne slobodno je u dva i trideset ili u pet. Šta vam više odgovara?", "Farbanje cijele dužine je od šezdeset do devedeset maraka, zavisno od dužine kose.", "Važi. Na koje ime da zavedem zahtjev?", "Prosljeđujem salonu zahtjev: šišanje i feniranje, srijeda u pet. Kontaktiraće vas u najkraćem roku radi potvrde. Hvala na pozivu i prijatno!", "Izvinite, nisam vas dobro razumio. Možete li ponoviti?", ] AZURE_VOICES = ["sr-RS-SophieNeural", "sr-RS-NicholasNeural", "hr-HR-GabrijelaNeural", "hr-HR-SreckoNeural"] ELEVEN_VOICES = ["JBFqnCBsd6RMkjVDRZzb"] # replace with shortlisted hr/sr voices # rough public prices (2026): Azure Neural ~$15/1M chars, ElevenLabs Flash ~$50/1M chars COST_PER_CHAR_USD = {"azure": 15 / 1_000_000, "elevenlabs": 50 / 1_000_000} async def bake(provider_name: str, provider, voices: list[str], out: Path) -> None: for voice in voices: total_chars = total_ms = 0 for i, sentence in enumerate(SENTENCES, 1): t0 = time.monotonic() try: pcm = await provider.synthesize(sentence, voice) except Exception as e: # noqa: BLE001 print(f" {voice} #{i}: FAILED — {e}") continue ms = int((time.monotonic() - t0) * 1000) total_chars += len(sentence) total_ms += ms path = out / f"{provider_name}-{voice.replace(':', '_')}-{i}.wav" path.write_bytes(pcm_to_wav(pcm)) print(f" {voice} #{i}: {ms} ms, {len(pcm) // 16} ms audio → {path.name}") if total_chars: cost = total_chars * COST_PER_CHAR_USD[provider_name] print( f" {voice}: avg latency {total_ms // len(SENTENCES)} ms, " f"~${cost:.4f} for all {total_chars} chars " f"(~${cost / len(SENTENCES) * 1000:.3f} per 1000-char call)\n" ) async def main() -> None: out = Path(sys.argv[1] if len(sys.argv) > 1 else "./poc-tts-out") out.mkdir(parents=True, exist_ok=True) s = get_settings() if s.azure_speech_key: print("== Azure Neural ==") await bake("azure", AzureTTS(), AZURE_VOICES, out) else: print("(GOGO_AZURE_SPEECH_KEY not set — skipping Azure)") if s.elevenlabs_api_key: print("== ElevenLabs Flash ==") await bake("elevenlabs", ElevenLabsTTS(), ELEVEN_VOICES, out) else: print("(GOGO_ELEVENLABS_API_KEY not set — skipping ElevenLabs)") print(f"\nListen to the WAVs in {out}/ and pick 2–3 voices for the dashboard (§6.1).") if __name__ == "__main__": asyncio.run(main())