""" tts_engine.py — ElevenLabs TTS engine. Returns raw MP3 bytes for a spoken line given a voice_id. """ import os import requests class TTSEngine: """Wraps the ElevenLabs REST API for text-to-speech synthesis.""" _BASE_URL = "https://api.elevenlabs.io/v1/text-to-speech/{voice_id}" _MODEL = "eleven_multilingual_v2" def __init__(self, api_key: str): self.api_key = api_key if not self.api_key: print("[TTSEngine] WARNING: api_key is empty — TTS will be skipped.") def speak(self, text: str, voice_id: str) -> bytes: """ Synthesize `text` with `voice_id`. Returns raw MP3 bytes on success. Raises RuntimeError on API failure. Falls through to raise so callers can catch and skip audio gracefully. """ if not self.api_key: raise RuntimeError("ElevenLabs API key not configured.") text = text.strip() if not text: raise ValueError("speak() called with empty text.") url = self._BASE_URL.format(voice_id=voice_id) headers = { "xi-api-key": self.api_key, "Content-Type": "application/json", "Accept": "audio/mpeg", } payload = { "text": text, "model_id": self._MODEL, "voice_settings": { "stability": 0.35, "similarity_boost": 0.75, "style": 0.65, "use_speaker_boost": True }, } response = requests.post(url, json=payload, headers=headers, timeout=30) if response.status_code == 200: return response.content else: raise RuntimeError( f"ElevenLabs API error {response.status_code}: {response.text[:300]}" )