Modification of the STT structure timeout
This commit is contained in:
@@ -38,7 +38,7 @@ export class AiSttService {
|
||||
file.originalname || 'audio.wav',
|
||||
);
|
||||
|
||||
const timeoutMs = Number(this.config.get<string>('STT_TIMEOUT_MS') || 60000);
|
||||
const timeoutMs = Number(this.config.get<string>('STT_TIMEOUT_MS') || 180000);
|
||||
const headers: Record<string, string> = {};
|
||||
const token = this.config.get<string>('STT_INTERNAL_TOKEN')?.trim();
|
||||
if (token) headers['X-STT-Token'] = token;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Local Pipecat Whisper — no GapGPT / OpenAI cloud key required for STT.
|
||||
# Use a local size: tiny | base | small | medium | large-v3
|
||||
# Local Faster-Whisper (same engine as Pipecat local STT) — no GapGPT.
|
||||
# Sizes: tiny | base | small | medium | large-v3
|
||||
# Do NOT use cloud names like whisper-1
|
||||
STT_MODEL=small
|
||||
# On low-RAM servers prefer: base or tiny
|
||||
STT_MODEL=base
|
||||
STT_DEVICE=cpu
|
||||
STT_COMPUTE_TYPE=int8
|
||||
STT_PROMPT=یادداشت و مکالمهٔ فروش به زبان فارسی. اسامی مشتریان و اصطلاحات کسبوکار را دقیق بنویس.
|
||||
|
||||
+18
-32
@@ -1,22 +1,21 @@
|
||||
# Montact STT (Pipecat — Whisper محلی)
|
||||
# Montact STT — Whisper محلی (بدون GapGPT)
|
||||
|
||||
تبدیل صوت به متن — فقط STT. بدون TTS، بدون مکالمهٔ صوتی، **بدون GapGPT**.
|
||||
تبدیل صوت به متن روی همین سرور با **Faster-Whisper** (همان موتوری که [Pipecat](https://www.pipecat.ai/) برای STT محلی استفاده میکند).
|
||||
کلید ابری لازم نیست.
|
||||
|
||||
از [Pipecat](https://www.pipecat.ai/) سرویس `WhisperSTTService` استفاده میشود؛ مدل Faster-Whisper روی **همین سرور** اجرا میشود و کلید ابری لازم نیست.
|
||||
## نصب
|
||||
|
||||
## لوکال (ویندوز)
|
||||
Python 3.11+ و ترجیحاً `ffmpeg`.
|
||||
|
||||
Python 3.11+ و ترجیحاً `ffmpeg` در PATH.
|
||||
|
||||
```powershell
|
||||
```bash
|
||||
cd apps/stt
|
||||
py -3.11 -m venv .venv
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
python3.11 -m venv .venv
|
||||
source .venv/bin/activate # ویندوز: .\.venv\Scripts\Activate.ps1
|
||||
pip install -r requirements.txt
|
||||
copy .env.example .env
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
```powershell
|
||||
```bash
|
||||
uvicorn main:app --host 127.0.0.1 --port 3040
|
||||
```
|
||||
|
||||
@@ -25,31 +24,18 @@ uvicorn main:app --host 127.0.0.1 --port 3040
|
||||
```
|
||||
STT_ENABLED=true
|
||||
STT_URL=http://127.0.0.1:3040
|
||||
STT_TIMEOUT_MS=180000
|
||||
STT_INTERNAL_TOKEN=
|
||||
```
|
||||
|
||||
## تنظیمات `apps/stt/.env`
|
||||
## تنظیمات
|
||||
|
||||
| کلید | پیشفرض | توضیح |
|
||||
| کلید | پیشنهاد | توضیح |
|
||||
|------|---------|--------|
|
||||
| `STT_MODEL` | `small` | `tiny` / `base` / `small` / `medium` (فارسی: حداقل `small`) |
|
||||
| `STT_DEVICE` | `cpu` | یا `cuda` اگر GPU دارید |
|
||||
| `STT_COMPUTE_TYPE` | `int8` | برای CPU؛ روی GPU معمولاً `float16` |
|
||||
| `STT_PROMPT` | متن فارسی فروش | راهنمای املا/اصطلاحات |
|
||||
| `STT_INTERNAL_TOKEN` | خالی | اگر پر شود باید با API یکی باشد |
|
||||
| `STT_MODEL` | `base` | `tiny` سبکتر، `small` دقیقتر برای فارسی |
|
||||
| `STT_DEVICE` | `cpu` | یا `cuda` |
|
||||
| `STT_COMPUTE_TYPE` | `int8` | روی GPU معمولاً `float16` |
|
||||
|
||||
**دیگر به `OPENAI_API_KEY` / GapGPT برای دیکته نیاز نیست.**
|
||||
نامهای ابری مثل `whisper-1` را نگذارید (اگر بماند خودکار به `base` نگاشت میشود).
|
||||
|
||||
اولین اجرا مدل را از Hugging Face دانلود میکند (چند صد مگابایت تا چند گیگ و بسته به مدل).
|
||||
|
||||
## سرور
|
||||
|
||||
```bash
|
||||
cd /var/www/crm/apps/stt
|
||||
source .venv/bin/activate # یا مسیر venv فعلی
|
||||
pip install -r requirements.txt
|
||||
# OPENAI_* را از .env بردارید یا نادیده بگیرید
|
||||
pm2 restart mento-stt
|
||||
```
|
||||
|
||||
اگر RAM کم است، `STT_MODEL=base` یا `tiny` بگذارید.
|
||||
اولین اجرا مدل را دانلود میکند. وضعیت: `curl -s http://127.0.0.1:3040/health` — وقتی `"ready": true` شد آماده است.
|
||||
|
||||
+87
-100
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,5 +3,5 @@ uvicorn[standard]==0.35.0
|
||||
python-multipart==0.0.20
|
||||
python-dotenv==1.1.1
|
||||
numpy>=1.26,<3
|
||||
# Local Whisper via Pipecat (Faster-Whisper). No OpenAI/GapGPT cloud STT.
|
||||
pipecat-ai[whisper]==1.8.1
|
||||
# Same engine Pipecat uses for local STT — no GapGPT/OpenAI cloud.
|
||||
faster-whisper>=1.1.0,<2
|
||||
|
||||
+3
-2
@@ -153,9 +153,10 @@ cp .env.example .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
در `.env` فقط مدل محلی لازم است (`STT_MODEL=small`، `STT_DEVICE=cpu`).
|
||||
در `.env` فقط مدل محلی لازم است (`STT_MODEL=base`، `STT_DEVICE=cpu`).
|
||||
`OPENAI_API_KEY` برای دیکته لازم نیست.
|
||||
`STT_INTERNAL_TOKEN` اگر پر شد باید با `apps/api/.env` یکی باشد.
|
||||
`STT_INTERNAL_TOKEN` اگر پر شد باید با `apps/api/.env` یکی باشد.
|
||||
در API: `STT_TIMEOUT_MS=180000` پیشنهاد میشود (لود اول مدل طول میکشد).
|
||||
|
||||
### ۷) PM2 — فقط پروسههای جدید
|
||||
|
||||
|
||||
Reference in New Issue
Block a user