"""Speech-to-text: Speaches over HTTP (default) or embedded faster-whisper. The embedded import is deferred so the gateway runs without the heavy [speech] extras installed. """ import io import wave import httpx from .config import settings _client: httpx.Client | None = None _embedded_model = None def transcribe(pcm: bytes, sample_rate: int) -> str: """Transcribe raw s16le mono PCM to text.""" if settings.stt_backend == "speaches": return _transcribe_speaches(pcm, sample_rate) return _transcribe_embedded(pcm) def _wav_bytes(pcm: bytes, sample_rate: int) -> bytes: buf = io.BytesIO() with wave.open(buf, "wb") as w: w.setnchannels(1) w.setsampwidth(2) w.setframerate(sample_rate) w.writeframes(pcm) return buf.getvalue() def _transcribe_speaches(pcm: bytes, sample_rate: int) -> str: global _client if _client is None: _client = httpx.Client(base_url=settings.speaches_base_url, timeout=60.0) response = _client.post( "/v1/audio/transcriptions", files={"file": ("utterance.wav", _wav_bytes(pcm, sample_rate), "audio/wav")}, data={"model": settings.stt_model, "language": "en"}, ) response.raise_for_status() return response.json()["text"].strip() def _transcribe_embedded(pcm: bytes) -> str: global _embedded_model if _embedded_model is None: from faster_whisper import WhisperModel _embedded_model = WhisperModel( settings.embedded_stt_model, device=settings.embedded_stt_device ) import numpy as np audio = np.frombuffer(pcm, dtype=np.int16).astype(np.float32) / 32768.0 segments, _info = _embedded_model.transcribe(audio, language="en") return " ".join(segment.text.strip() for segment in segments).strip()