Files
gogo-telefon/scripts/poc/tts_bakeoff.py
Senad Uka fd45ff84dc M6 + M0: hardening, deploy docs, PoC scripts
- Pipeline-down fallback: dialplan plays gogo-fallback after a failed
  AudioSocket (§15) + script to synthesize the Bosnian prompt via TTS
- Voice heartbeat file → /health/voice endpoint + live status in admin panel
- Retention job also purges stale call registrations; chat sessions metered;
  100%-usage super-admin alert email (GOGO_ADMIN_ALERT_EMAIL)
- .env.example, docs/DEPLOY.md (compose stack, GPU node, backups pg_dump,
  update procedure, security notes)
- §16 Definition of Done as an automated test: softphone-style call → fake
  partner availability → request pushed → webhook confirm → console SMS
- M0 PoC scripts: STT accuracy harness (CER/WER + spoken-digit check; dry-run
  verified with espeak-ng samples + faster-whisper small on CPU — phone number
  extracted exactly), TTS bake-off (Azure vs ElevenLabs, latency+cost), and
  end-to-end latency smoke test speaking real AudioSocket to the live pipeline

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:27:05 +02:00

83 lines
3.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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())