Files
esp32-server/app/stt/__init__.py
Macky b8950b38a4 feat: Phase 1 testable bridge skeleton (18 tests green)
Xiaozhi WebSocket endpoint with handshake/auth, mock STT/TTS/Hermes
backends, Thai chunker, barge-in queue, latency logger, GPU planner,
voice-profile guard (reasoning=none, session_search only).

CON-002: no GPU/audio libs loaded at import. MUST-NOT-001: all model
names/tokens from config.
2026-10-03 11:45:31 +07:00

90 lines
2.8 KiB
Python

"""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}")