""" Audio Service - Speech-to-Text and Text-to-Speech. """ import io import logging import tempfile import asyncio from typing import Optional, Union from huggingface_hub import InferenceClient from config.settings import Settings logger = logging.getLogger(__name__) class AudioService: """Audio service for STT and TTS.""" def __init__( self, api_key: str, stt_provider: str = "fal-ai", stt_model: str = "openai/whisper-large-v3", tts_model: str = "canopylabs/orpheus-3b-0.1-ft", ): """ Initialize audio service. Args: api_key: Hugging Face API token stt_provider: Provider for speech-to-text stt_model: ASR model ID tts_model: TTS model ID """ self.api_key = api_key self.stt_model = stt_model self.tts_model = tts_model # STT client logger.debug(f"Initializing ASR client with provider={stt_provider}") self.asr_client = InferenceClient( provider=stt_provider, api_key=self.api_key, ) # TTS client logger.debug(f"Initializing TTS client") self.tts_client = InferenceClient(token=self.api_key) logger.info(f"AudioService configured: ASR={self.stt_model}, TTS={self.tts_model}") async def speech_to_text(self, audio_input: Union[str, bytes, io.BytesIO]) -> str: """ Convert speech to text. Args: audio_input: File path, bytes, or BytesIO of audio Returns: Transcribed text """ # Prepare input path if isinstance(audio_input, str): input_path = audio_input logger.debug(f"Using existing file for ASR: {input_path}") else: data = audio_input.getvalue() if isinstance(audio_input, io.BytesIO) else audio_input tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav") tmp.write(data) tmp.close() input_path = tmp.name logger.debug(f"Wrote audio to temp file for ASR: {input_path}") try: logger.info(f"Calling ASR model={self.stt_model}") result = await asyncio.get_event_loop().run_in_executor( None, lambda: self.asr_client.automatic_speech_recognition( input_path, model=self.stt_model, ) ) transcript = result.get("text") if isinstance(result, dict) else getattr(result, "text", "") logger.info(f"ASR success, transcript length={len(transcript)}") return transcript or "" except Exception as e: logger.error(f"ASR error: {e}", exc_info=True) return "" async def text_to_speech(self, text: str) -> Optional[bytes]: """ Convert text to speech. Args: text: Text to synthesize Returns: Audio bytes or None """ if not text.strip(): logger.debug("Empty text input for TTS") return None def _call_tts(): try: return self.tts_client.text_to_speech(text, model=self.tts_model) except StopIteration as e: raise RuntimeError(f"StopIteration in TTS call: {e}") try: logger.info(f"Calling TTS model={self.tts_model}, text length={len(text)}") audio = await asyncio.get_event_loop().run_in_executor(None, _call_tts) logger.info(f"TTS success, received {len(audio)} bytes") return audio except Exception as e: logger.error(f"TTS error: {e}", exc_info=True) return None