From 11a646604bd44d3471ea85846ca0efef9935c503 Mon Sep 17 00:00:00 2001 From: n1kt4r Date: Sun, 30 Aug 2026 15:47:44 +0500 Subject: [PATCH] =?UTF-8?q?ASR-=D0=BC=D0=B8=D0=BA=D1=80=D0=BE=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B8=D1=81:=20whisper-large-v3-russian=20+=20Qwen?= =?UTF-8?q?3-ASR-1.7B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 13 +++++++++ .gitignore | 17 ++++++++++++ README.md | 44 +++++++++++++++++++++++++++++ app/__init__.py | 0 app/config.py | 26 +++++++++++++++++ app/engine.py | 65 +++++++++++++++++++++++++++++++++++++++++++ app/main.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 10 +++++++ 8 files changed, 247 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/config.py create mode 100644 app/engine.py create mode 100644 app/main.py create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ff7dab1 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13d1785 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Python +__pycache__/ +*.pyc +*.egg-info/ +.venv/ +venv/ + +# ASR-сервис +.env + +# Модели (локальные, не коммитить) +models/ + +# Данные +*.wav +*.mp3 +*.m4a diff --git a/README.md b/README.md new file mode 100644 index 0000000..4df2aee --- /dev/null +++ b/README.md @@ -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` (автоматическая загрузка) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..a085415 --- /dev/null +++ b/app/config.py @@ -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() diff --git a/app/engine.py b/app/engine.py new file mode 100644 index 0000000..a40f4a0 --- /dev/null +++ b/app/engine.py @@ -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 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..1ab4408 --- /dev/null +++ b/app/main.py @@ -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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8ab2927 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +fastapi +uvicorn +python-multipart +pydantic-settings +transformers +torch +torchaudio +librosa +soundfile +qwen-asr