Files
gogo-telefon/scripts/poc/tts_bakeoff.py

83 lines
3.4 KiB
Python
Raw Permalink Normal View History

"""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 23 voices for the dashboard (§6.1).")
if __name__ == "__main__":
asyncio.run(main())