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.
90 lines
2.8 KiB
Python
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}")
|