Spaces:
Running
Running
| import asyncio | |
| import httpx | |
| from app.core.exceptions import GenerationError | |
| class TTSClient: | |
| def __init__(self, tts_space_url: str, timeout_seconds: float) -> None: | |
| self._tts_space_url = tts_space_url.rstrip("/") | |
| self._timeout_seconds = timeout_seconds | |
| # Persistent client — reuses connections | |
| self._http = httpx.AsyncClient( | |
| timeout=timeout_seconds, | |
| limits=httpx.Limits(max_keepalive_connections=5, max_connections=10), | |
| ) | |
| async def close(self): | |
| await self._http.aclose() | |
| def is_configured(self) -> bool: | |
| return bool(self._tts_space_url) | |
| async def ping(self) -> None: | |
| if not self.is_configured: | |
| return | |
| try: | |
| await self._http.post( | |
| f"{self._tts_space_url}/synthesize", | |
| json={"text": "hi", "voice": "bf_emma"}, | |
| headers={"Content-Type": "application/json"}, | |
| timeout=5.0, | |
| ) | |
| except Exception: | |
| pass | |
| async def synthesize(self, text: str, voice: str = "bf_emma") -> bytes: | |
| if not self.is_configured: | |
| raise GenerationError("TTS client is not configured") | |
| try: | |
| response = await self._http.post( | |
| f"{self._tts_space_url}/synthesize", | |
| json={"text": text, "voice": voice}, | |
| headers={"Content-Type": "application/json"}, | |
| ) | |
| response.raise_for_status() | |
| audio_bytes = response.content | |
| if not audio_bytes: | |
| raise GenerationError("TTS response was empty") | |
| return audio_bytes | |
| except httpx.TimeoutException as exc: | |
| raise GenerationError("TTS request timed out") from exc | |
| except httpx.HTTPStatusError as exc: | |
| raise GenerationError( | |
| "TTS upstream returned an error", | |
| context={"status_code": exc.response.status_code}, | |
| ) from exc | |
| except GenerationError: | |
| raise | |
| except Exception as exc: | |
| raise GenerationError("TTS synthesis failed", context={"error": str(exc)}) from exc | |
| async def synthesize_stream(self, text: str, voice: str = "bf_emma"): | |
| text = text.strip() | |
| if not text: | |
| raise GenerationError("TTS request text is empty") | |
| if not self.is_configured: | |
| raise GenerationError("TTS client is not configured") | |
| try: | |
| async with self._http.stream( | |
| "POST", | |
| f"{self._tts_space_url}/synthesize", | |
| json={"text": text, "voice": voice}, | |
| headers={"Content-Type": "application/json"}, | |
| ) as response: | |
| response.raise_for_status() | |
| async for chunk in response.aiter_bytes(): | |
| yield chunk | |
| except httpx.TimeoutException as exc: | |
| raise GenerationError("TTS request timed out") from exc | |
| except httpx.HTTPStatusError as exc: | |
| raise GenerationError( | |
| "TTS upstream returned an error", | |
| context={"status_code": exc.response.status_code}, | |
| ) from exc | |
| except Exception as exc: | |
| raise GenerationError("TTS synthesis stream failed", context={"error": str(exc)}) from exc | |