Onur Kansoy
Update: Fixed AI Playwright generation logic with Gemma, resolved UI input box freezing, and added dynamic voice assignment for new characters.
39f0786 verified | """ | |
| 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]}" | |
| ) | |