"""ElevenLabs text-to-speech service.""" import logging import time _logger = logging.getLogger(__name__) try: import requests as _requests except ImportError: _requests = None ELEVENLABS_BASE = "https://api.elevenlabs.io/v1" DEFAULT_VOICES = { "female_british": "21m00Tcm4TlvDq8ikWAM", # Rachel "male_british": "VR6AewLTigWG4xSOukaG", # Arnold "female_american": "EXAVITQu4vr4xnSDxMaL", # Bella "male_american": "TxGEqnHWrfWFTfGW9XjX", # Josh } class ElevenLabsService: """ElevenLabs TTS — higher quality multilingual voices.""" def __init__(self, env): self.env = env self._get_param = env["ir.config_parameter"].sudo().get_param def _get_key(self): key = self._get_param("encoach_ai.elevenlabs_api_key", "") if not key: import os key = os.environ.get("ELEVENLABS_API_KEY", "") if not key: raise RuntimeError("ElevenLabs API key not configured — set in AI Settings") return key def _log(self, action, latency, status="success", error=None): try: self.env["encoach.ai.log"].sudo().create({ "service": "elevenlabs", "action": action, "latency_ms": latency, "status": status, "error_message": error, }) except Exception: pass def synthesize(self, text, *, voice_id=None, voice_key="female_british", model=None, output_format="mp3_44100_128"): """Convert text to speech using ElevenLabs. Returns: dict with 'audio' (bytes), 'content_type', 'voice_id', 'characters' """ if not _requests: raise RuntimeError("requests package not installed") key = self._get_key() voice_id = voice_id or DEFAULT_VOICES.get(voice_key, list(DEFAULT_VOICES.values())[0]) model = model or self._get_param("encoach_ai.elevenlabs_model", "eleven_multilingual_v2") url = f"{ELEVENLABS_BASE}/text-to-speech/{voice_id}" t0 = time.time() try: resp = _requests.post( url, json={ "text": text, "model_id": model, "voice_settings": {"stability": 0.5, "similarity_boost": 0.75}, }, headers={"xi-api-key": key, "Accept": "audio/mpeg"}, params={"output_format": output_format}, timeout=60, ) resp.raise_for_status() latency = int((time.time() - t0) * 1000) self._log("synthesize", latency) return { "audio": resp.content, "content_type": "audio/mpeg", "voice_id": voice_id, "characters": len(text), } except Exception as exc: self._log("synthesize", int((time.time() - t0) * 1000), "error", str(exc)) raise def list_voices(self): """List available ElevenLabs voices.""" key = self._get_key() resp = _requests.get( f"{ELEVENLABS_BASE}/voices", headers={"xi-api-key": key}, timeout=15, ) resp.raise_for_status() return [ {"voice_id": v["voice_id"], "name": v["name"], "labels": v.get("labels", {})} for v in resp.json().get("voices", []) ]