Зачем ещё один сервис для транскрибации?
Whisper от OpenAI уже стал стандартом. Но он требует GPU или жертвовать качеством на CPU. Whisper.cpp решает проблему производительности, но все равно остаются лицензионные ограничения и зависимость от конкретного формата модели.
Qwen3-ASR 1.7B (последняя версия на февраль 2026) - это open-source модель от Alibaba Cloud, которая поддерживает 52 языка, работает на CPU с приемлемой скоростью и дает качество, сравнимое с Whisper Medium. И главное - её можно законно использовать в коммерческих проектах без оглядки на OpenAI.
Что получаем на выходе?
Готовый Docker-образ с:
- Qwen3-ASR 1.7B (можно заменить на 2B или другие версии)
- FastAPI сервер с документацией Swagger
- Автоматическую генерацию SRT и VTT субтитров
- Поддержку длинных аудио (сегментация + объединение)
- Конфигурацию через переменные окружения
- Мониторинг через Prometheus метрики
Сравниваем с альтернативами: холодные цифры 2026
| Решение | Качество (WER*) | Скорость (x real-time) | Память (CPU) | Лицензия |
|---|---|---|---|---|
| Qwen3-ASR 1.7B | 5.8% (русский) | 0.6x | 8GB RAM | Apache 2.0 |
| Whisper Large v3 | 4.9% | 0.1x (CPU) | 12GB RAM | MIT |
| Whisper.cpp (medium) | 6.2% | 0.8x | 4GB RAM | MIT |
| LFM2-2.6B-Transcript | 7.1% | 0.4x | 10GB RAM | CC BY-NC 4.0 |
*WER (Word Error Rate) - процент ошибок на тестовом датасете русской речи, данные на февраль 2026.
Qwen3-ASR проигрывает Whisper Large в качестве, но выигрывает в лицензионной чистоте и проще интегрируется в коммерческие проекты. Если вы уже используете Whisper.cpp в продакшене, переход на Qwen3 даст вам больше гибкости в выборе модели.
Собираем сервис: от Dockerfile до первого запроса
1 Dockerfile - основа всего
FROM python:3.11-slim
WORKDIR /app
# Устанавливаем системные зависимости для аудио
RUN apt-get update && apt-get install -y \
ffmpeg \
libsndfile1 \
&& rm -rf /var/lib/apt/lists/*
# Копируем requirements до кода для кэширования слоев
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копируем код приложения
COPY . .
# Скачиваем модель при сборке (опционально)
# Лучше монтировать volume в runtime
# RUN python -c "from transformers import AutoModelForSpeechSeq2Seq; \
# AutoModelForSpeechSeq2Seq.from_pretrained('Qwen/Qwen3-ASR-1.7B')"
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Не скачивайте модель при сборке образа! Иначе каждый контейнер будет весить 7+ GB. Используйте volume или скачивайте при первом запуске. В 2026 году появились умные кэширующие прокси для моделей Hugging Face - используйте их в продакшене.
2 FastAPI сервер с логикой сегментации
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import JSONResponse, FileResponse
from pydantic import BaseModel
from typing import Optional, List
import torch
import torchaudio
from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor
import numpy as np
import tempfile
import os
from datetime import timedelta
app = FastAPI(title="Qwen3-ASR Transcription Service", version="2026.02")
# Глобальные переменные для модели и процессора
model = None
processor = None
device = "cuda" if torch.cuda.is_available() else "cpu"
torch_dtype = torch.float16 if device == "cuda" else torch.float32
class TranscriptionRequest(BaseModel):
language: str = "ru"
task: str = "transcribe"
beam_size: int = 5
chunk_length_s: int = 30 # Длина сегментов в секундах
class SubtitleSegment(BaseModel):
start: float
end: float
text: str
@app.on_event("startup")
async def load_model():
global model, processor
print(f"Loading Qwen3-ASR 1.7B on {device}...")
model_id = "Qwen/Qwen3-ASR-1.7B"
model = AutoModelForSpeechSeq2Seq.from_pretrained(
model_id,
torch_dtype=torch_dtype,
low_cpu_mem_usage=True,
use_safetensors=True
).to(device)
processor = AutoProcessor.from_pretrained(model_id)
print("Model loaded successfully")
@app.post("/transcribe", response_model=List[SubtitleSegment])
async def transcribe_audio(
file: UploadFile = File(...),
params: TranscriptionRequest = TranscriptionRequest()
):
if not file.content_type.startswith("audio/") and file.content_type != "video/mp4":
raise HTTPException(status_code=400, detail="Unsupported file type")
# Сохраняем временный файл
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
content = await file.read()
tmp_file.write(content)
audio_path = tmp_file.name
try:
# Загружаем аудио
waveform, sample_rate = torchaudio.load(audio_path)
# Конвертируем в mono если нужно
if waveform.shape[0] > 1:
waveform = torch.mean(waveform, dim=0, keepdim=True)
# Ресемплируем до 16kHz если нужно
if sample_rate != 16000:
resampler = torchaudio.transforms.Resample(sample_rate, 16000)
waveform = resampler(waveform)
sample_rate = 16000
# Сегментируем длинные аудио
chunk_samples = params.chunk_length_s * sample_rate
total_samples = waveform.shape[1]
segments = []
for start_sample in range(0, total_samples, chunk_samples):
end_sample = min(start_sample + chunk_samples, total_samples)
chunk = waveform[:, start_sample:end_sample]
# Пропускаем тишину (опционально)
if torch.max(torch.abs(chunk)) < 0.01:
continue
# Подготавливаем входные данные
inputs = processor(
raw_speech=chunk.numpy().squeeze(),
sampling_rate=sample_rate,
return_tensors="pt",
padding=True
).to(device)
# Генерируем транскрипцию
with torch.no_grad():
generated_ids = model.generate(
**inputs,
max_new_tokens=256,
language=params.language,
task=params.task,
num_beams=params.beam_size
)
transcription = processor.batch_decode(
generated_ids,
skip_special_tokens=True
)[0]
# Рассчитываем временные метки
start_time = start_sample / sample_rate
end_time = end_sample / sample_rate
segments.append(SubtitleSegment(
start=start_time,
end=end_time,
text=transcription.strip()
))
return segments
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
os.unlink(audio_path)
@app.post("/transcribe/srt")
async def transcribe_to_srt(
file: UploadFile = File(...),
params: TranscriptionRequest = TranscriptionRequest()
):
segments = await transcribe_audio(file, params)
# Генерируем SRT формат
srt_content = ""
for i, segment in enumerate(segments, 1):
start_str = str(timedelta(seconds=segment.start)).split(".")[0]
end_str = str(timedelta(seconds=segment.end)).split(".")[0]
srt_content += f"{i}\n"
srt_content += f"{start_str} --> {end_str}\n"
srt_content += f"{segment.text}\n\n"
# Возвращаем файл
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".srt") as tmp:
tmp.write(srt_content)
tmp_path = tmp.name
return FileResponse(
tmp_path,
media_type="text/plain",
filename=f"transcription_{file.filename}.srt"
)
Ключевой момент здесь - chunk_length_s. Для русского языка оптимально 25-30 секунд. Меньше - больше накладных расходов на обработку. Больше - модель может "потерять" контекст в середине длинной речи.
3 Docker Compose для продакшена
version: '3.8'
services:
qwen-asr:
build: .
container_name: qwen3-asr-service
ports:
- "8000:8000"
volumes:
# Монтируем кэш моделей
- qwen-models:/root/.cache/huggingface
# Для сохранения субтитров (опционально)
- ./subtitles:/app/subtitles
environment:
- MODEL_ID=Qwen/Qwen3-ASR-1.7B
- MAX_FILE_SIZE=500MB
- ENABLE_PROMETHEUS=true
- LOG_LEVEL=INFO
deploy:
resources:
limits:
memory: 12G
reservations:
memory: 8G
restart: unless-stopped
# Для GPU раскомментируйте:
# runtime: nvidia
# environment:
# - NVIDIA_VISIBLE_DEVICES=all
volumes:
qwen-models:
Используем API: примеры из реальной жизни
Допустим, вы запустили подкаст и получаете по 10 интервью в неделю. Вместо платить за транскрибацию или сидеть часами с аудиоредактором:
# Загружаем интервью
curl -X POST "http://localhost:8000/transcribe/srt" \
-H "accept: application/json" \
-F "file=@interview_podcast_45.mp3" \
-F "language=ru" \
-F "chunk_length_s=25" \
--output interview_subtitles.srt
Или интегрируем в Python-скрипт для автоматической обработки:
import requests
# Для пакетной обработки
files = ['episode1.mp3', 'episode2.mp3', 'meeting_recording.m4a']
for audio_file in files:
with open(audio_file, 'rb') as f:
response = requests.post(
'http://localhost:8000/transcribe',
files={'file': f},
data={'language': 'ru', 'task': 'transcribe'}
)
segments = response.json()
# Сохраняем в формате для редактора
with open(f'{audio_file}.json', 'w', encoding='utf-8') as out:
import json
json.dump(segments, out, ensure_ascii=False, indent=2)
print(f"Обработан {audio_file}: {len(segments)} сегментов")
А если нужны другие модели?
Архитектура сервиса позволяет легко менять модели. Хотите попробовать экспериментальную Qwen3-ASR 2B? Просто поменяйте MODEL_ID в docker-compose.yml:
environment:
- MODEL_ID=Qwen/Qwen3-ASR-2B-experimental
# или даже
- MODEL_ID=openai/whisper-large-v3-turbo-2026
Да, в 2026 году появилась Whisper Large v3 Turbo - оптимизированная версия для CPU. Но она все равно проигрывает в лицензионной свободе.
Для специализированных задач можно использовать LFM2-2.6B-Transcript - модель, обученную на деловых встречах с лучшим распознаванием бизнес-терминов.
Кому подойдет это решение?
Подойдет:
- Студиям подкастов, которые хотят автоматизировать создание субтитров
- Компаниям с требованиями к обработке данных внутри периметра (GDPR, ФЗ-152)
- Разработчикам, которые уже используют Qwen-модели (например, Qwen3-TTS на Rust для синтеза речи)
- Образовательным платформам с контентом на 52 языках
Не подойдет:
- Если нужна транскрибация в реальном времени (задержка 2-3 секунды на сегмент)
- Для устройств с менее 8GB RAM (попробуйте Whisper + Ollama)
- Когда критически важно качество выше 95% точности (тогда только Whisper Large + GPU)
Что дальше? Куда развивать сервис
Базовый сервис работает. Но в продакшене нужно добавить:
- Очередь задач - Redis + Celery для обработки десятков файлов параллельно
- Веб-интерфейс - простой редактор субтитров с ручной коррекцией
- Интеграцию с облачными хранилищами - автоматический импорт из S3/Google Drive
- Детектор говорящих - кто говорит в диалоге (можно дообучить модель)
Самое интересное - объединить с Qwen3 TTS для аудиокниг. Получается цикл: текст → аудио → транскрибация → коррекция → снова аудио. Идеально для локализации контента.
А если добавить AI-радиостанцию VibeCast в цепочку, можно автоматически генерировать и транскрибировать радиопередачи. Полная автономия от человеческого голоса.
Внимание: Qwen3-ASR иногда "галлюцинирует" на фоновом шуме или музыке. Всегда проверяйте автоматические транскрипции перед публикацией. Особенно если в аудио есть имена собственные или технические термины.
И последнее: не зацикливайтесь на одной модели. К середине 2026 года ожидаются новые open-source модели от Meta, Google и китайских лабораторий. Архитектура сервиса позволяет подключать любые модели через Hugging Face - меняйте их как перчатки, когда появятся более качественные варианты.
Сейчас самое время строить инфраструктуру, а не привязываться к конкретной нейросети. Qwen3-ASR - хороший старт, но завтра будет что-то лучше. Главное, что у вас уже есть рабочий пайплайн.