ASR-микросервис: whisper-large-v3-russian + Qwen3-ASR-1.7B
This commit is contained in:
commit
11a646604b
8 files changed
+247
No files matched your search
@@ -0,0 +1,13 @@
|
|||||||
|
# ASR-сервис: конфигурация
|
||||||
|
# Устройство распознавания
|
||||||
|
DEVICE=cuda
|
||||||
|
|
||||||
|
# Активность моделей
|
||||||
|
LOAD_WHISPER=true
|
||||||
|
LOAD_QWEN=true
|
||||||
|
|
||||||
|
# Путь к whisper-large-v3-russian (локальный checkpoint)
|
||||||
|
WHISPER_MODEL_PATH=/home/neo/models/whisper-russian
|
||||||
|
|
||||||
|
# Модель Qwen3-ASR (автоматическая загрузка с HuggingFace)
|
||||||
|
QWEN_MODEL_NAME=Qwen/Qwen3-ASR-1.7B
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.egg-info/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# ASR-сервис
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Модели (локальные, не коммитить)
|
||||||
|
models/
|
||||||
|
|
||||||
|
# Данные
|
||||||
|
*.wav
|
||||||
|
*.mp3
|
||||||
|
*.m4a
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# ASR-микросервис для calls24.ru
|
||||||
|
# Распознавание речи: whisper-large-v3-russian + Qwen3-ASR-1.7B
|
||||||
|
|
||||||
|
FastAPI-сервис распознавания речи для анализа записанных звонков.
|
||||||
|
Две модели: Qwen3-ASR-1.7B (новее, точнее) и whisper-large-v3-russian.
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── __init__.py
|
||||||
|
├── main.py # FastAPI app, роуты
|
||||||
|
├── engine.py # Движки ASR (whisper + qwen3)
|
||||||
|
└── config.py # Pydantic Settings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
```bash
|
||||||
|
cd /home/neo/workspace/asr-microservice
|
||||||
|
~/gigaam-env/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
- `GET /health` — статус моделей
|
||||||
|
- `POST /transcribe` — загрузка аудиофайла, параметр `model` (qwen3|whisper)
|
||||||
|
|
||||||
|
### Пример
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/transcribe?model=qwen3" \
|
||||||
|
-F "file=@/tmp/test_silero.wav"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Конфиг
|
||||||
|
Переменные окружения (см. .env.example):
|
||||||
|
- `DEVICE` — cuda / cpu
|
||||||
|
- `WHISPER_MODEL_PATH` — путь к checkpoint whisper
|
||||||
|
- `QWEN_MODEL_NAME` — модель Qwen3-ASR
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
```bash
|
||||||
|
~/gigaam-env/bin/python -m pip install fastapi uvicorn python-multipart librosa soundfile
|
||||||
|
```
|
||||||
|
Модели уже скачаны:
|
||||||
|
- whisper: `/home/neo/models/whisper-russian`
|
||||||
|
- qwen3: `Qwen/Qwen3-ASR-1.7B` (автоматическая загрузка)
|
||||||
Whitespace-only changes.
@@ -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()
|
||||||
@@ -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
@@ -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)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
python-multipart
|
||||||
|
pydantic-settings
|
||||||
|
transformers
|
||||||
|
torch
|
||||||
|
torchaudio
|
||||||
|
librosa
|
||||||
|
soundfile
|
||||||
|
qwen-asr
|
||||||
Reference in new issue
Block a user