feat: Add handler-base library for NATS AI/ML services
- Handler base class with graceful shutdown and signal handling - NATSClient with JetStream and msgpack serialization - Pydantic Settings for environment configuration - HealthServer for Kubernetes probes - OpenTelemetry telemetry setup - Service clients: STT, TTS, LLM, Embeddings, Reranker, Milvus
This commit is contained in:
113
handler_base/clients/tts.py
Normal file
113
handler_base/clients/tts.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
TTS service client (Coqui XTTS).
|
||||
"""
|
||||
import io
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from handler_base.config import TTSSettings
|
||||
from handler_base.telemetry import create_span
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TTSClient:
|
||||
"""
|
||||
Client for the TTS service (Coqui XTTS).
|
||||
|
||||
Usage:
|
||||
client = TTSClient()
|
||||
audio_bytes = await client.synthesize("Hello world")
|
||||
"""
|
||||
|
||||
def __init__(self, settings: Optional[TTSSettings] = None):
|
||||
self.settings = settings or TTSSettings()
|
||||
self._client = httpx.AsyncClient(
|
||||
base_url=self.settings.tts_url,
|
||||
timeout=120.0, # TTS can be slow
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close the HTTP client."""
|
||||
await self._client.aclose()
|
||||
|
||||
async def synthesize(
|
||||
self,
|
||||
text: str,
|
||||
language: Optional[str] = None,
|
||||
speaker: Optional[str] = None,
|
||||
) -> bytes:
|
||||
"""
|
||||
Synthesize speech from text.
|
||||
|
||||
Args:
|
||||
text: Text to synthesize
|
||||
language: Language code (e.g., "en", "es", "fr")
|
||||
speaker: Speaker ID or reference
|
||||
|
||||
Returns:
|
||||
WAV audio bytes
|
||||
"""
|
||||
language = language or self.settings.tts_language
|
||||
|
||||
with create_span("tts.synthesize") as span:
|
||||
if span:
|
||||
span.set_attribute("tts.language", language)
|
||||
span.set_attribute("tts.text_length", len(text))
|
||||
|
||||
params = {
|
||||
"text": text,
|
||||
"language_id": language,
|
||||
}
|
||||
if speaker:
|
||||
params["speaker_id"] = speaker
|
||||
|
||||
response = await self._client.get("/api/tts", params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
audio_bytes = response.content
|
||||
|
||||
if span:
|
||||
span.set_attribute("tts.audio_size", len(audio_bytes))
|
||||
|
||||
return audio_bytes
|
||||
|
||||
async def synthesize_to_file(
|
||||
self,
|
||||
text: str,
|
||||
output_path: str,
|
||||
language: Optional[str] = None,
|
||||
speaker: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Synthesize speech and save to a file.
|
||||
|
||||
Args:
|
||||
text: Text to synthesize
|
||||
output_path: Path to save the audio file
|
||||
language: Language code
|
||||
speaker: Speaker ID
|
||||
"""
|
||||
audio_bytes = await self.synthesize(text, language, speaker)
|
||||
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(audio_bytes)
|
||||
|
||||
async def get_speakers(self) -> list[dict]:
|
||||
"""Get available speakers/voices."""
|
||||
try:
|
||||
response = await self._client.get("/api/speakers")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
async def health(self) -> bool:
|
||||
"""Check if the TTS service is healthy."""
|
||||
try:
|
||||
response = await self._client.get("/health")
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
Reference in New Issue
Block a user