Перейти к содержанию
Новое AiManual теперь в MAX Подписаться
Публикация AiManual

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

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

Коротко

Что будет в материале

  1. 01

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

  2. 02

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

  3. 03

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

  4. 04

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

Запустить 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) для суммаризации диалогов, определения интента клиента, выявления эмоциональной окраски. И все это в том же асинхронном пайплайне, где следующий этап берет текст и выдает анализ.

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

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