Промышленная транскрибация аудио на Whisper: масштабирование до тысяч часов | Кейс ЮMoney | AiManual
AiManual Logo Ai / Manual.
20 Мар 2026 Гайд

Промышленная транскрибация на Whisper: как ЮMoney масштабировали сервис для тысяч часов аудио

Разбираем архитектуру сервиса транскрибации ЮMoney. Чанкование, диаризация, голосовые эмбеддинги на Whisper v3. Технический гайд по обработке тысяч часов звонко

Запустить Whisper на одном файле — это уровень джуна. Пропустить через него десяток записей — уже мидл. А вот обрабатывать тысячи часов аудио ежедневно, чтобы потом аналитики ищем тренды, а юристы проверяют соблюдение скриптов — это совершенно другая лига.

Именно с такой задачей столкнулись в ЮMoney. Их сервис поддержки генерировал горы записей. Вручную их слушать — безумие. Нужен был промышленный конвейер, который не просто переводит звук в текст, а делает это точно, быстро и с пониманием, кто, когда и что сказал.

Они его построили. И я расскажу, как это работает изнутри, с такими деталями, от которых у классического DevOps потекут слезы умиления (или ужаса).

Проблема: не "если", а "когда" твой сервер сгорит

Первый соблазн — взять Whisper, обернуть его в Flask и радостно слать запросы. Сотня звонков в день? Легко. Потом приходит релиз новой функции, нагрузка вырастает в 50 раз, и твой красивый сервис падает под грузом 12-часовых аудиофайлов от клиентов.

Главная ошибка на старте: думать, что проблема — это точность транскрипции. Нет. Проблема — это предсказуемая задержка, управление памятью GPU и идемпотентность обработки одного и того же файла в сотне параллельных воркеров.

ЮMoney быстро это поняли. Их цели были конкретны:

  • Обрабатывать >5000 часов аудио в сутки.
  • Среднее время обработки (от загрузки до готового текста с метаданными) — не более 15 минут.
  • Точность (WER) не хуже 5-7% для русской речи в условиях типового телефонного шума.
  • Автоматически определять спикеров и склеивать их реплики в диалог.
  • Все должно работать внутри корпоративного контура, без выхода в публичное облако.

Архитектура: от монолита к конвейеру из микросервисов

Они убили монолит. Вместо него появился асинхронный пайплайн, где каждый этап — отдельный сервис, который можно масштабировать горизонтально.

Этап Инструмент/Модель Задача
Прием и валидация FastAPI, Celery Принять файл, проверить формат, создать таск в очереди.
Предобработка и чанкование FFmpeg, PyTorch Конвертация в wav, нормализация громкости, разбивка на сегменты по 30 сек.
Транскрибация Whisper v3 Large-v3 (актуально на 20.03.2026) Основное преобразование речи в текст для каждого чанка.
Диаризация PyAnnotate 2.1 + собственные дообученные эмбеддинги Определение границ реплик и кластеризация по спикерам.
Постобработка Кастомные правила, ASR-постпроцессинг Исправление доменных терминов ("юмани", "кошелек"), форматирование.
Сборка и сохранение PostgreSQL, Redis, MinIO Склейка текста, добавление меток времени и спикеров, сохранение в БД и object storage.

Ключевое здесь — разделение ответственности. Сервис транскрибации не должен думать о форматах файлов. Сервис диаризации получает уже чистый аудиопоток и текст. Если один этап лег, остальные могут продолжать работать с накопленными задачами из очереди.

1 Чанкование: почему 30 секунд — это новый black

Whisper v3 Large — монстр. Он жрет до 10 ГБ VRAM на полную длину контекста. А звонки длятся и по часу. Прогнать такой файл целиком — гарантировать OutOfMemory даже на A100.

Решение — резать. Но не абы как. Прямые разрезы по времени убивают слова на стыках. Нужны умные границы по silence detection.

import whisper
from pydub import AudioSegment, silence

def chunk_audio_by_silence(file_path, chunk_duration_ms=30000, min_silence_len=500, silence_thresh=-40):
    audio = AudioSegment.from_wav(file_path)
    not_silence_ranges = silence.detect_nonsilent(audio, min_silence_len, silence_thresh, 1)
    
    chunks = []
    current_chunk = AudioSegment.empty()
    
    for start, end in not_silence_ranges:
        segment = audio[start:end]
        if len(current_chunk) + len(segment) <= chunk_duration_ms:
            current_chunk += segment
        else:
            if len(current_chunk) > 0:
                chunks.append(current_chunk)
            current_chunk = segment
    
    if len(current_chunk) > 0:
        chunks.append(current_chunk)
    
    return chunks

Этот код режет аудио на сегменты до 30 секунд, но не разрывает речь в местах, где пауза меньше полусекунды. Результат — чанки, которые Whisper обрабатывает стабильно, без скачков потребления памяти.

💡
Не используйте фиксированное чанкование. Всегда ищите моменты тишины. Это снижает Word Error Rate (WER) на 15-20% для длинных записей, потому что модель получает более цельные фразы для контекста.

2 Диаризация: кто сказал "алло"?

Текст есть. Но где начало реплики оператора, а где клиент перебивает? Это задача диаризации. ЮMoney отказались от простых энергетических методов (тише/громче) — они не работают при перекрытии речи.

Они используют комбинацию из двух моделей:

  1. Детектор смены спикера (Speaker Change Detection) — нейросеть, которая отмечает моменты, когда вероятно сменился говорящий.
  2. Голосовые эмбеддинги (Voice Embeddings) — модель преобразует короткий отрезок голоса в вектор. Векторы одного человека кучкуются рядом, векторы разных — далеко.

Для извлечения эмбеддингов они дообучили модель на внутренних данных — голосах своих операторов. Это дало сумасшедший прирост точности кластеризации для известных голосов.

# Упрощенный пример кластеризации спикеров
import numpy as np
from sklearn.cluster import DBSCAN

def cluster_speakers(audio_chunks, embedding_model):
    """
    audio_chunks: список аудиосегментов после детекции смены спикера.
    embedding_model: модель для получения голосового эмбеддинга (например, на базе ECAPA-TDNN).
    """
    embeddings = []
    for chunk in audio_chunks:
        # Конвертируем аудио в numpy массив, нормализуем
        audio_np = np.frombuffer(chunk.raw_data, dtype=np.int16)
        # Получаем эмбеддинг (условный вызов)
        emb = embedding_model.infer(audio_np)
        embeddings.append(emb)
    
    embeddings = np.array(embeddings)
    # DBSCAN сам определяет количество кластеров, что удобно для неизвестного числа спикеров
    clustering = DBSCAN(eps=0.3, min_samples=2, metric='cosine').fit(embeddings)
    
    return clustering.labels_  # Метки кластера для каждого чанка

Получается разметка: "0-10 сек: спикер А, 10-25 сек: спикер Б". Ее потом накладывают на транскрибированный текст.

3 Масштабирование: очередь как спасательный круг

5000 часов аудио. Это примерно 208 дней непрерывной речи. Делать это синхронно — смешно. В основе всего пайплайна — RabbitMQ (хотя сейчас многие переходят на Kafka, но RabbitMQ проще в эксплуатации для таких пайплайнов).

Каждый этап — отдельный воркер, который берет задачу из своей очереди, выполняет и кладет результат в следующую очередь. Если транскрибатор падает, задачи накапливаются перед ним, но не теряются. Поднять еще три инстанса — и очередь быстро рассасывается.

Самая частая ошибка при настройке таких очередей — забыть про идемпотентность. Что, если воркер транскрибации выполнил задачу, но умер перед тем, как отметить ее выполненной? Задача уйдет другому воркеру, и тот же файл обработается дважды. Решение — сохранять результат в общее хранилище (например, MinIO) по уникальному ID задачи сразу после обработки. Перед началом обработки проверять, нет ли уже результата.

Железо и деньги: как не разориться на GPU

Whisper v3 Large на GPU в 10 раз быстрее, чем на CPU. Но одна карта A100 стоит как неплохая иномарка. ЮMoney пошли по пути гибридного кластера:

  • Горячий пул: серверы с A100 или H100 для обработки в реальном времени (звонки, которые требуют быстрой транскрипции для live-аналитики).
  • Холодный пул: инстансы с несколькими RTX 4090 для фоновой обработки архивных записей. Дешевле, но все еще с GPU.
  • Резервный CPU-пул: на случай пиковых нагрузок или проблем с GPU. Используют оптимизированные реализации Whisper.cpp, которые работают на CPU, но медленнее.

Они написали свой шедулер, который распределяет задачи по пулам в зависимости от приоритета и наличия свободных ресурсов. Задача с пометкой "realtime" летит в горячий пул. Архивная обработка — в холодный.

Если своих мощностей не хватает, они используют облачные GPU инстансы от партнеров (партнерская ссылка) для кратковременного масштабирования. Но основная нагрузка — на своем железе.

Мониторинг: что пахнет горелым в дата-центре?

Тысячи задач в очередях. Десятки воркеров. Как понять, что все идет не так? Они собирают метрики по каждому этапу:

  • Время обработки на чанк (перцентили 50, 95, 99). Внезапный рост p99 — кто-то подал 8-часовой файл без пауз, и чанкование сломалось.
  • Загрузка VRAM на GPU. Если она постоянно выше 90% — скоро будет OOM.
  • Длина каждой очереди. Растет очередь перед диаризацией? Значит, там бутылочное горлышко.
  • Качество (WER) на отложенном тестовом наборе, который прогоняется раз в сутки.

Все это летит в Prometheus, дашборды в Grafana. Плюс кастомные алерты: "Если среднее время обработки превысило 20 минут — разбудить инженера" (да, даже ночью).

Финальный спринт: постпроцессинг, который знает бизнес

Whisper выдает "ю мани", а нужно "ЮMoney". Он пишет "кошелек", а в компании говорят "кошелек ЮMoney". Поэтому после всей магии нейросетей идет слой простых, но жизненно важных правил.

Они используют комбинацию словарей замен и небольшую языковую модель (типа MumbleFlow), которая исправляет опечатки в профессиональной лексике. Это дает еще 2-3% улучшения точности с точки зрения бизнес-пользователя, который ищет в тексте конкретные термины.

Итоговая схема пайплайна

Аудиофайл -> Приемный сервис (создание задачи с ID) -> Очередь "to_preprocess" -> Воркер предобработки (чанкование) -> Очередь "to_transcribe" -> Воркер транскрибации (Whisper v3) -> Очередь "to_diarize" -> Воркер диаризации и эмбеддингов -> Очередь "to_postprocess" -> Воркер постобработки -> Сохранение в БД и object storage -> Уведомление о завершении.

Каждый воркер пишет результат в общее хранилище (MinIO) под ID задачи. Если воркер умирает, задача забирается другим воркером, который сначала проверяет, нет ли уже результата в хранилище.

💡
Не зашивайте в архитектуру только Whisper. Мир меняется. Следите за открытыми моделями, такими как Qwen3-ASR 1.7B. Возможно, через полгода появится что-то быстрее и точнее для русского языка. Ваш пайплайн должен позволять подключать новые модели транскрибации как плагины.

Что дальше? Не останавливайтесь на тексте

Текст — это только сырье. Следующий шаг, который уже делают в ЮMoney — запуск LLM (например, локально через Ollama) для суммаризации диалогов, определения интента клиента, выявления эмоциональной окраски. И все это в том же асинхронном пайплайне, где следующий этап берет текст и выдает анализ.

И последний совет, который не даст вам наступить на те же грабли: начинайте с мониторинга и логирования. Не с модели, не с кода. Сначала настройте сбор метрик и логов так, чтобы вы в любой момент видели, где застревают задачи, сколько памяти ест каждый воркер и какова точность на свежих данных. Без этого вы будете масштабировать вслепую, а это дорого и больно.

Подписаться на канал