"""Thai STT — interface + backends (REQ-003). No model is hard-coded in the pipeline (MUST-NOT-001): the engine and any model/endpoint come from config. ``mock`` transcribes deterministically for tests; ``typhoon``/``faster-whisper`` are lazy adapters. """ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass @dataclass class STTResult: text: str language: str = "th" confidence: float = 1.0 class STTEngine(ABC): async def transcribe(self, audio: bytes) -> STTResult: """``audio`` is the utterance PCM. Subclasses implement it.""" raise NotImplementedError class MockSTT(STTEngine): """Deterministic: echoes a fixed Thai sentence. Lets the full pipeline be exercised on a CPU-only host (AC-005).""" async def transcribe(self, audio: bytes) -> STTResult: return STTResult(text="สวัสดีครับ", language="th", confidence=1.0) class TyphoonSTT(STTEngine): def __init__(self, endpoint: str, sample_rate: int = 16000): self.endpoint = endpoint self.sample_rate = sample_rate async def transcribe(self, audio: bytes) -> STTResult: import httpx # lazy async with httpx.AsyncClient() as client: r = await client.post( self.endpoint, content=audio, headers={"Content-Type": "application/octet-stream"}, ) r.raise_for_status() data = r.json() return STTResult( text=data.get("text", ""), language=data.get("language", "th"), confidence=float(data.get("confidence", 1.0)), ) class WhisperSTT(STTEngine): def __init__(self, model_name: str = "large-v3"): self.model_name = model_name self._model = None def _ensure(self): if self._model is None: from faster_whisper import WhisperModel # type: ignore # lazy self._model = WhisperModel(self.model_name, device="auto", compute_type="int8") return self._model async def transcribe(self, audio: bytes) -> STTResult: import numpy as np model = self._ensure() import io # Convert int16 PCM to float32 mono for faster-whisper. samples = np.frombuffer(audio, dtype=np.int16).astype(np.float32) / 32768.0 segments, _info = model.transcribe(io.BytesIO(samples.tobytes()), language="th") text = "".join(s.text for s in segments).strip() return STTResult(text=text, language="th", confidence=1.0) def make_stt(cfg) -> STTEngine: if cfg.engine == "mock": return MockSTT() if cfg.engine == "typhoon": return TyphoonSTT(cfg.typhoon_endpoint, cfg.sample_rate) if cfg.engine == "faster-whisper": return WhisperSTT(cfg.whisper_model) raise ValueError(f"unknown stt engine: {cfg.engine}")