Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/services/voice/stt.py
2026-04-14 19:34:55 +03:30

97 lines
2.7 KiB
Python
Executable file

from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
import asyncio
@dataclass(frozen=True)
class STTConfig:
language: str = "fa"
model_size_or_path: str = "small" # قابل تنظیم (small/medium/large-v3 یا مسیر لوکال)
device: str = "auto" # auto/cpu/cuda
compute_type: str = "int8" # برای CPU مناسب است
class WhisperSTT:
"""
STT بر پایه Whisper (ترجیحاً faster-whisper).
- ورودی: PCM16 mono bytes
- خروجی: متن
"""
def __init__(self, cfg: STTConfig) -> None:
self.cfg = cfg
self._model = None # lazy load
self._lock = asyncio.Lock()
def _load_model(self):
try:
from faster_whisper import WhisperModel # type: ignore
except Exception as exc:
raise RuntimeError(
"faster-whisper نصب نیست. برای STT رایگان با کیفیت فارسی، optional-deps صوت را نصب کنید."
) from exc
device = self.cfg.device
if device == "auto":
device = "cuda" if _has_cuda() else "cpu"
self._model = WhisperModel(
self.cfg.model_size_or_path,
device=device,
compute_type=self.cfg.compute_type,
)
return self._model
async def transcribe_pcm16(self, pcm16: bytes, sample_rate_hz: int) -> str:
"""
تبدیل PCM16 به متن.
نکته: مدل Whisper عموماً روی 16kHz بهترین عملکرد را دارد.
"""
# Lazy load model (یکبار)
async with self._lock:
if self._model is None:
self._load_model()
model = self._model
# تبدیل به float32 numpy و resample در صورت نیاز (فعلاً فرض 16kHz)
try:
import numpy as np # type: ignore
except Exception as exc:
raise RuntimeError("numpy نصب نیست (برای STT لازم است).") from exc
audio_i16 = np.frombuffer(pcm16, dtype=np.int16)
audio = (audio_i16.astype(np.float32)) / 32768.0
# اگر sample_rate غیر 16k باشد، فعلاً خطا می‌دهیم (فاز بعد: resample)
if sample_rate_hz != 16000:
raise RuntimeError("در فاز اول فقط sample_rate=16000 پشتیبانی می‌شود.")
# اجرای transcribe در thread جدا چون CPU-bound است
loop = asyncio.get_event_loop()
text = await loop.run_in_executor(
None,
lambda: _transcribe_sync(model, audio, self.cfg.language),
)
return text or ""
def _transcribe_sync(model, audio, language: str) -> str:
segments, _info = model.transcribe(
audio,
language=language,
beam_size=5,
vad_filter=False, # ما VAD خودمان را داریم
)
return "".join([seg.text for seg in segments]).strip()
def _has_cuda() -> bool:
try:
import torch # type: ignore
return bool(torch.cuda.is_available())
except Exception:
return False