Modification of the STT structure timeout

This commit is contained in:
2026-09-22 11:37:59 +03:30
parent d53136d91c
commit 42db5919e1
6 changed files with 115 additions and 140 deletions
+87 -100
View File
@@ -1,29 +1,35 @@
"""Speech-to-text sidecar — Pipecat local Whisper only (no GapGPT / cloud STT)."""
"""Speech-to-text sidecar — local Faster-Whisper (Pipecat stack), no GapGPT."""
from __future__ import annotations
import io
import logging
import os
import shutil
import subprocess
import tempfile
import threading
import wave
from contextlib import asynccontextmanager
from pathlib import Path
import numpy as np
from dotenv import load_dotenv
from fastapi import FastAPI, File, Header, HTTPException, UploadFile
from fastapi.responses import JSONResponse
from pipecat.services.whisper.stt import WhisperSTTService
from pipecat.transcriptions.language import Language
from faster_whisper import WhisperModel
load_dotenv(Path(__file__).resolve().parent / ".env")
logger = logging.getLogger("mento-stt")
logging.basicConfig(level=logging.INFO)
MAX_BYTES = 8 * 1024 * 1024
WAV_MAGIC = (b"RIFF", b"WAVE")
app = FastAPI(title="Mento STT", docs_url=None, redoc_url=None)
_stt: WhisperSTTService | None = None
_model: WhisperModel | None = None
_model_lock = threading.Lock()
_model_error: str | None = None
def _env(name: str, default: str = "") -> str:
@@ -40,11 +46,10 @@ def _require_token(header_token: str | None) -> None:
def _model_name() -> str:
"""Map env / legacy cloud names to Faster-Whisper local sizes."""
raw = (_env("STT_MODEL", "small") or "small").lower()
# Old GapGPT/OpenAI cloud id — not a local Whisper size
raw = (_env("STT_MODEL", "base") or "base").lower()
legacy = {
"whisper-1": "small",
"whisper": "small",
"whisper-1": "base",
"whisper": "base",
"gpt-4o-transcribe": "small",
"gpt-4o-mini-transcribe": "base",
}
@@ -68,70 +73,46 @@ def _model_name() -> str:
"distil-large-v3",
"large-v3-turbo",
}
if raw in allowed:
return raw
# unknown → safe default for Persian
return "small"
return raw if raw in allowed else "base"
def _stt_service() -> WhisperSTTService:
"""Local Faster-Whisper via Pipecat — no OPENAI_API_KEY / GapGPT."""
global _stt
if _stt is not None:
return _stt
def _get_model() -> WhisperModel:
"""Load once, thread-safe. Only cache after success."""
global _model, _model_error
if _model is not None:
return _model
device = _env("STT_DEVICE", "cpu") or "cpu"
compute = _env("STT_COMPUTE_TYPE", "int8") or "int8"
try:
# Settings fields differ by Pipecat version — keep to model + language only
settings_kwargs: dict = {"model": _model_name()}
try:
settings_kwargs["language"] = Language.FA
settings = WhisperSTTService.Settings(**settings_kwargs)
except TypeError:
settings_kwargs.pop("language", None)
settings = WhisperSTTService.Settings(**settings_kwargs)
_stt = WhisperSTTService(
device=device,
compute_type=compute,
settings=settings,
)
except TypeError:
# Older constructor: model=/language= kwargs instead of settings=
try:
_stt = WhisperSTTService(
model=_model_name(),
device=device,
compute_type=compute,
language=Language.FA,
)
except Exception as exc:
with _model_lock:
if _model is not None:
return _model
if _model_error:
raise HTTPException(
status_code=503,
detail=f"بارگذاری مدل Whisper محلی ناموفق بود: {exc}",
) from exc
except Exception as exc:
raise HTTPException(
status_code=503,
detail=f"بارگذاری مدل Whisper محلی ناموفق بود: {exc}",
) from exc
detail=f"مدل Whisper آماده نیست: {_model_error}",
)
# force model load at startup path (first request otherwise downloads silently)
if getattr(_stt, "_model", None) is None and hasattr(_stt, "_load"):
name = _model_name()
device = _env("STT_DEVICE", "cpu") or "cpu"
compute = _env("STT_COMPUTE_TYPE", "int8") or "int8"
logger.info("Loading Faster-Whisper model=%s device=%s compute=%s", name, device, compute)
try:
_stt._load()
loaded = WhisperModel(name, device=device, compute_type=compute)
except Exception as exc:
_model_error = str(exc)
logger.exception("Whisper model load failed")
raise HTTPException(
status_code=503,
detail=(
"مدل Whisper دانلود/بارگذاری نشد. "
f"اینترنت سرور و فضای دیسک را چک کنید. جزئیات: {exc}"
"بارگذاری مدل Whisper محلی ناموفق بود. "
"اگر اولین بار است، دانلود طول می‌کشد؛ یا STT_MODEL=tiny/base بگذارید. "
f"جزئیات: {exc}"
),
) from exc
return _stt
_model = loaded
_model_error = None
logger.info("Whisper model ready: %s", name)
return _model
def _is_wav(data: bytes) -> bool:
@@ -196,44 +177,32 @@ def _wav_to_float32(wav_bytes: bytes) -> tuple[np.ndarray, int]:
return audio, rate
def _transcribe_local(wav_bytes: bytes) -> str:
service = _stt_service()
model = getattr(service, "_model", None)
if model is None:
raise HTTPException(status_code=503, detail="مدل Whisper محلی آماده نیست")
def _ensure_16k(wav_bytes: bytes) -> np.ndarray:
audio, rate = _wav_to_float32(wav_bytes)
# faster-whisper expects 16k mono float32 ideally
if rate != 16000:
# simple resample via ffmpeg path already targets 16k for non-wav;
# for wav at other rates, re-encode quickly
ffmpeg = shutil.which("ffmpeg")
if not ffmpeg:
raise HTTPException(status_code=400, detail="نرخ نمونه‌برداری صوت پشتیبانی نشد")
with tempfile.TemporaryDirectory() as tmp:
src = Path(tmp) / "in.wav"
dst = Path(tmp) / "out.wav"
src.write_bytes(wav_bytes)
result = subprocess.run(
[ffmpeg, "-y", "-i", str(src), "-ac", "1", "-ar", "16000", "-f", "wav", str(dst)],
capture_output=True,
check=False,
)
if result.returncode != 0 or not dst.exists():
raise HTTPException(status_code=400, detail="نرمال‌سازی صوت ناموفق بود")
audio, rate = _wav_to_float32(dst.read_bytes())
if rate == 16000:
return audio
settings = getattr(service, "_settings", None)
language = getattr(settings, "language", None) or Language.FA
lang_code = getattr(language, "value", language) if language is not None else "fa"
if isinstance(lang_code, str) and "-" in lang_code:
lang_code = lang_code.split("-")[0]
# Language enum may be "fa" or "FA" or full name — faster-whisper wants ISO code
if isinstance(lang_code, str):
lang_code = lang_code.lower()
if lang_code in ("persian", "farsi"):
lang_code = "fa"
ffmpeg = shutil.which("ffmpeg")
if not ffmpeg:
raise HTTPException(status_code=400, detail="نرخ نمونه‌برداری صوت پشتیبانی نشد")
with tempfile.TemporaryDirectory() as tmp:
src = Path(tmp) / "in.wav"
dst = Path(tmp) / "out.wav"
src.write_bytes(wav_bytes)
result = subprocess.run(
[ffmpeg, "-y", "-i", str(src), "-ac", "1", "-ar", "16000", "-f", "wav", str(dst)],
capture_output=True,
check=False,
)
if result.returncode != 0 or not dst.exists():
raise HTTPException(status_code=400, detail="نرمال‌سازی صوت ناموفق بود")
audio, _ = _wav_to_float32(dst.read_bytes())
return audio
def _transcribe_local(wav_bytes: bytes) -> str:
model = _get_model()
audio = _ensure_16k(wav_bytes)
initial_prompt = _env(
"STT_PROMPT",
"یادداشت و مکالمهٔ فروش به زبان فارسی. اسامی مشتریان و اصطلاحات کسب‌وکار را دقیق بنویس.",
@@ -241,7 +210,7 @@ def _transcribe_local(wav_bytes: bytes) -> str:
segments, _info = model.transcribe(
audio,
language=lang_code or "fa",
language="fa",
initial_prompt=initial_prompt,
vad_filter=True,
)
@@ -255,23 +224,41 @@ def _friendly_stt_error(exc: BaseException) -> tuple[int, str]:
if "timeout" in lowered or "timed out" in lowered:
return 504, "زمان تبدیل صوت به پایان رسید. دوباره با صدای کوتاه‌تر تلاش کنید."
if "memory" in lowered or "oom" in lowered:
return 503, "حافظهٔ سرور برای مدل Whisper کافی نیست. مدل کوچک‌تر (tiny/base) را امتحان کنید."
if len(text) > 180:
return 503, "حافظهٔ سرور برای مدل Whisper کافی نیست. STT_MODEL=tiny یا base بگذارید."
if len(text) > 220:
return 502, "تبدیل صوت به متن محلی ناموفق بود. لاگ mento-stt را بررسی کنید."
return 502, f"تبدیل صوت به متن ناموفق بود: {text}"
@asynccontextmanager
async def lifespan(_app: FastAPI):
# Warm model in background thread so first user request is faster
def warm() -> None:
try:
_get_model()
except Exception:
logger.exception("Startup model warm failed (will retry on first request)")
threading.Thread(target=warm, name="stt-warm", daemon=True).start()
yield
app = FastAPI(title="Mento STT", docs_url=None, redoc_url=None, lifespan=lifespan)
@app.get("/health")
async def health() -> dict[str, object]:
return {
"ok": True,
"engine": "pipecat",
"service": "WhisperSTTService",
"engine": "faster-whisper",
"service": "local",
"mode": "local",
"model": _env("STT_MODEL", "small"),
"model": _model_name(),
"device": _env("STT_DEVICE", "cpu") or "cpu",
"ready": _model is not None,
"cloud": False,
"gapgpt": False,
"lastError": _model_error,
}