ASR-микросервис: whisper-large-v3-russian + Qwen3-ASR-1.7B

This commit is contained in:
n1kt4r committed 2026-08-30 15:47:44 +05:00
commit 11a646604b
8 files changed
+247

No files matched your search

View File
Whitespace-only changes.
+26
View File
@@ -0,0 +1,26 @@
"""Конфигурация ASR-микросервиса."""
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
# Модель whisper-large-v3-russian
WHISPER_MODEL_PATH: str = "/home/neo/models/whisper-russian"
# Модель Qwen3-ASR-1.7B
QWEN_MODEL_NAME: str = "Qwen/Qwen3-ASR-1.7B"
# Устройство: cuda / cpu
DEVICE: str = "cuda"
# Режим загрузки моделей при старте
LOAD_WHISPER: bool = True
LOAD_QWEN: bool = True
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
settings = Settings()
+65
View File
@@ -0,0 +1,65 @@
"""Движки распознавания речи: whisper-large-v3-russian и Qwen3-ASR-1.7B."""
import warnings
import logging
import librosa
import numpy as np
import soundfile as sf
import torch
import torchaudio
from transformers import WhisperForConditionalGeneration, WhisperProcessor
from qwen_asr import Qwen3ASRModel
from .config import settings
logger = logging.getLogger(__name__)
class WhisperModel:
"""whisper-large-v3-russian (transformers)."""
def __init__(self, device):
self.model = WhisperForConditionalGeneration.from_pretrained(
settings.WHISPER_MODEL_PATH
)
self.processor = WhisperProcessor.from_pretrained(settings.WHISPER_MODEL_PATH)
self.device = device
self.model.to(device)
def transcribe(self, audio_path):
wav, sr = sf.read(audio_path)
wav = wav.mean(axis=1) if wav.ndim > 1 else wav
wav16 = torchaudio.functional.resample(
torch.from_numpy(np.asarray(wav, dtype=np.float32)), sr, 16000
)
inputs = self.processor(
wav16, sampling_rate=16000, return_tensors="pt"
)
with torch.no_grad():
ids = self.model.generate(inputs["input_features"].to(self.device))
return self.processor.batch_decode(ids.cpu().numpy(), skip_special_tokens=True)
class QwenModel:
"""Qwen3-ASR-1.7B (qwen-asr)."""
def __init__(self, device):
self.model = Qwen3ASRModel.from_pretrained(settings.QWEN_MODEL_NAME)
self.device = device
def transcribe(self, audio_path):
result = self.model.transcribe(audio_path)
return result.get("text") if isinstance(result, dict) else result
def load_models(device):
"""Загрузить все активные модели."""
models = {}
if settings.LOAD_WHISPER:
logger.info("Загрузка whisper-large-v3-russian...")
models["whisper"] = WhisperModel(device)
if settings.LOAD_QWEN:
logger.info("Загрузка Qwen3-ASR-1.7B...")
models["qwen3"] = QwenModel(device)
return models
+72
View File
@@ -0,0 +1,72 @@
"""ASR-микросервис: whisper-large-v3-russian + Qwen3-ASR-1.7B для calls24."""
import logging
from fastapi import FastAPI, UploadFile
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from starlette.middleware.cors import CORSMiddleware
from .config import settings
from .engine import load_models
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="ASR Microservice", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
_models = {}
@app.on_event("startup")
async def startup_event():
import torch
device = torch.device(settings.DEVICE)
global _models
_models = load_models(device)
logger.info(f"Модели загружены: {list(_models.keys())}")
@app.get("/health")
async def health():
return {"status": "ok", "models": list(_models.keys())}
@app.post("/transcribe")
async def transcribe(file: UploadFile, model: str = "qwen3"):
"""Распознать загруженный аудиофайл (model в query-параметре)."""
import os
import tempfile
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
content = await file.read()
tmp.write(content)
tmp.close()
if model not in _models:
return JSONResponse(
status_code=400,
content={"error": f"Неизвестная модель: {model}. Доступны: {list(_models.keys())}"},
)
try:
result = _models[model].transcribe(tmp.name)
return {"model": model, "text": result}
except Exception as e:
logger.error(f"Ошибка распознавания: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
finally:
os.unlink(tmp.name)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)