import asyncio import httpx from app.providers.tts.base import TTSProvider # Preview-TTS-Modelle liefern gelegentlich HTTP 200 mit LEEREM Body oder ein # transientes 5xx. Solche Aussetzer kurz wiederholen, statt die Runde abzubrechen. _MAX_ATTEMPTS = 3 _RETRY_BACKOFF = 0.6 # Sekunden, linear ansteigend class OpenRouterTTSProvider(TTSProvider): def __init__(self, api_key: str, model: str, voice: str): self.api_key = (api_key or "").strip() self.model = (model or "").strip() self.voice = (voice or "").strip() async def synthesize( self, text: str, voice: str | None = None, audio_format: str = "pcm", ) -> bytes: if not self.api_key: raise ValueError("OPENROUTER_API_KEY is empty") if not self.model: raise ValueError("OPENROUTER_TTS_MODEL is empty") if not text or not text.strip(): raise ValueError("TTS input text is empty") effective_voice = (voice or self.voice).strip() if not effective_voice: raise ValueError("TTS voice is required for OpenRouter TTS") payload = { "model": self.model, "input": text.strip(), "voice": effective_voice, "response_format": audio_format, } timeout = httpx.Timeout(connect=10.0, read=120.0, write=30.0, pool=10.0) async with httpx.AsyncClient(timeout=timeout) as client: last_error = "OpenRouter TTS returned empty audio content" for attempt in range(_MAX_ATTEMPTS): try: response = await client.post( "https://openrouter.ai/api/v1/audio/speech", headers={ "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", }, json=payload, ) response.raise_for_status() except httpx.HTTPStatusError as exc: status = exc.response.status_code # 4xx (z. B. ungueltige Stimme) ist nicht transient -> sofort melden. if status < 500: raise RuntimeError( f"OpenRouter TTS error {status}: {exc.response.text}" ) from exc last_error = f"OpenRouter TTS error {status}: {exc.response.text}" except httpx.TimeoutException: last_error = "OpenRouter TTS timeout" except httpx.HTTPError as exc: raise RuntimeError(f"OpenRouter TTS transport error: {exc}") from exc else: if response.content: return response.content # HTTP 200 mit leerem Body -> transienter Aussetzer, erneut versuchen. if attempt < _MAX_ATTEMPTS - 1: await asyncio.sleep(_RETRY_BACKOFF * (attempt + 1)) raise RuntimeError(last_error)