Каждый день в российском бизнесе происходят миллионы телефонных звонков. Колл-центры, клиники, юридические конторы, отделы продаж — везде, где есть телефон, есть поток неструктурированных данных, который никто не обрабатывает. Менеджер повесил трубку, записал в CRM «клиент интересовался» — и 80% информации из разговора потерялось.
Я потратил полгода на то, чтобы построить пайплайн, который берёт аудиозапись телефонного звонка и выдаёт структурированный JSON: кто звонил, чего хотел, какие суммы называл, что договорились делать дальше. В процессе набил достаточно шишек, чтобы написать эту статью.
Здесь не будет теории из документации. Будут конкретные решения, рабочий код на Python и грабли, на которые я наступил, чтобы вам не пришлось.
Архитектура: четыре этапа, один сюрприз
Пайплайн выглядит просто:
На практике «просто» заканчивается после первогоpip install. Каждый этап имеет свои подводные камни, а самый неожиданный — взаимодействие между этапами. Ошибка STT на 3% каскадно снижает точность LLM-извлечения на 10-15%.
Этап 1. Аудио: почему 8 кГц — это боль
Источник аудио — SIP-транк облачной АТС. Большинство провайдеров (Mango, Zadarma, UIS) отдают записи через API или webhook. Формат на входе — обычно PCM 16kHz mono или G.711 (8kHz).
И вот тут первые грабли.
G.711 (8 кГц).Телефонный стандарт, придуманный в 1972 году. Частотный диапазон — 300-3400 Гц. Человеческая речь содержит информативные компоненты до 8000 Гц. Итог: STT-модели, обученные на широкополосном аудио, на 8 кГц показывают WER на 5-8 процентных пунктов хуже.
Апсемплинг.Наивныйlibrosa.resample(audio, orig_sr=8000, target_sr=16000)не добавляет информации — он просто интерполирует сэмплы. Но! Модели типа Whisper обучены на 16 кГц, и подача 8 кГц напрямую ломает их внутренние фильтры. Апсемплинг даёт +2-3% к точности просто за счёт корректного формата.
Моно vs стерео.Если АТС отдаёт стерео (левый канал = оператор, правый = клиент) — считайте, что вам повезло. Задача диаризации решена бесплатно. На практике 70% АТС отдают моно.
Минимальный код для приёма записи:
Грабля, на которую я наступил:webhook может прийти раньше, чем запись дозаписалась на стороне АТС. Файл будет обрезан. Решение: retry с проверкой длительности черезffprobe— если аудио короче 5 секунд, ждём 10 секунд и скачиваем повторно.
Этап 2. Speech-to-Text: Whisper, SpeechKit и честные бенчмарки
Я протестировал четыре варианта на корпусе из 200 телефонных записей на русском языке (средняя длительность 3.5 минуты, качество — типичный мобильный звонок):
WER (телефон, рус.)
Latency (3 мин)
Whisper large-v3 (локально, A100)
Whisper large-v3 (локально, RTX 4090)
Yandex SpeechKit
Deepgram Nova-2
~$0.0043/мин
Несколько неочевидных наблюдений:
Whisper врёт красиво.Когда Whisper не уверен, он не ставит[inaudible]— он генерирует правдоподобный, но неверный текст. Фраза «давайте встретимся в среду в три» может превратиться в «давайте встретимся в среду утри». Выглядит похоже, но LLM на следующем этапе не поймёт «утри» и потеряет время встречи.
SpeechKit выигрывает на русском.Это ожидаемо — модель дообучена именно на русской речи. Разница в WER (8.1% vs 14.2%) выглядит небольшой, но на практике это означает, что SpeechKit правильно распознаёт «двадцать третье» как дату, а Whisper — как «двадцать третья».
Deepgram — компромисс.Latency 4 секунды на трёхминутный звонок — это почти реалтайм. Для сценариев, где важна скорость (уведомления, алерты), Deepgram вне конкуренции.
Конфиг Whisper для телефонного аудио (не дефолтный!):
Почемуno_speech_threshold=0.45, а не дефолтные0.6?На телефонных записях фоновый шум (улица, машина, кафе) создаёт высокий no_speech probability. С дефолтным порогом Whisper пропускает 15-20% реплик, считая их шумом. С 0.45 — пропускает 3-5%, но появляется 2-3% ложных распознаваний тишины. Трейдофф в пользу полноты.
Этап 3. Диаризация: кто это сказал?
Если аудио в моно — нужна speaker diarization. Я использую pyannote-audio 3.1:
Проблема перехлёстов.Люди перебивают друг друга. pyannote честно размечает overlapping speech — но как совместить это с транскриптом, где Whisper выдаёт один поток текста?
Мой подход — привязка по средней точке сегмента:
Это работает в 85% случаев. В оставшихся 15% — когда оба говорят одновременно — спикер определяется неверно. Я пробовал более сложные алгоритмы (взвешенное пересечение, voting по субсегментам), но выигрыш — 3-4%, а сложность кода растёт кратно. Для продакшена оставил простой вариант.
Грабля:pyannote иногда разбивает одного спикера на два, если человек меняет тон (начал спокойно, потом стал говорить громче). Решение — постобработка: если SPEAKER_00 и SPEAKER_02 никогда не пересекаются по времени, это скорее всего один человек.
Этап 4. LLM-извлечение сущностей: prompt engineering на стероидах
Самая интересная часть. Берём размеченный транскрипт и просим LLM извлечь структурированные данные.
Промпт, который работает (после 40 итераций)
Первый промпт был наивный: «Извлеки из диалога контактные данные и суть обращения». LLM радостно галлюцинировал — додумывал имена, суммы и даты, которых в разговоре не было.
Вот версия, к которой я пришёл:
Почему confidence — это спасение
Полеconfidence— не декорация. Я использую его для автоматической фильтрации:
Выбор модели: не всегда нужен GPT-4
Тестировал на 500 размеченных звонках (ручная разметка — золотой стандарт):
F1 (извлечение)
Claude 3.5 Sonnet
GPT-4o-mini
Llama 3.1 70B (vLLM)
GPT-4o-mini— мой выбор для продакшена. F1 0.84 — значит 84% сущностей извлечены верно. Оставшиеся 16% — это в основном неявные данные (клиент не назвал сумму прямо, но она вычисляется из контекста). Для таких случаев есть поле confidence < 0.6.
Llama 3.1 70B— если данные не должны покидать контур. Self-hosted на 2×A100 через vLLM. Точность чуть ниже, но нулевые затраты на API и полный контроль.
Собираем пайплайн
Оптимизация: с 40 секунд до 8
Первая версия обрабатывала 3-минутный звонок за 40 секунд. Вот что помогло:
Параллелизация STT + диаризация.Они независимы — запускаем одновременно. Whisper: 18 сек, pyannote: 12 сек. Параллельно: 18 сек (вместо 30). Экономия: 12 секунд.
Кэширование моделей в памяти.Whisper large-v3 загружается ~30 секунд. Держим модель в GPU memory, переиспользуем между запросами. То же для pyannote.
Оптимизация промпта.Первый промпт: 800 токенов. Финальный: 350 токенов. Меньше токенов → быстрее ответ LLM. Убрал многословные инструкции, оставил чёткие правила.
Chunking для длинных звонков.Звонки > 10 минут разбиваем на чанки по 5 минут с перекрытием 30 секунд. Каждый чанк обрабатывается отдельно, результаты мержатся. Без этого LLM начинает «забывать» начало разговора.
Итог:8-12 секундна 3-минутный звонок (RTX 4090 + GPT-4o-mini API).
Грабли, которые сэкономят вам время
1. LLM генерирует невалидный JSON.В ~3% случаев модель возвращает JSON с незакрытой скобкой или markdown-обёрткой```json...```. Решение — неjson.loads(), а парсер с fallback:
2. Whisper повторяет фразы.На тихих участках записи Whisper large-v3 иногда зацикливается: «да да да да да да да да». Это известный баг. Детектирую через compression_ratio:
3. Диаризация путает спикеров между звонками.pyannote присваиваетSPEAKER_00иSPEAKER_01произвольно — в одном звонке оператор = SPEAKER_00, в другом = SPEAKER_01. Решение — эвристика: спикер, который говорит первым и произносит приветственную формулу («добрый день», «алло, компания…»), — оператор.
4. Кодировка номеров телефонов.«Плюс семь девятьсот пять триста двадцать один сорок два двенадцать» — STT может выдать как текст, а может как +79053214212. Нужен нормализатор, который обрабатывает оба варианта.
Мониторинг: как понять, что всё сломалось
Пайплайн без мониторинга — бомба замедленного действия.
Ключевые алерты:
- avg_confidence < 0.6за последний час → промпт деградировал или изменился формат звонков
- processing_time > 30 сек→ GPU под нагрузкой или API тормозит
- error_rate > 5%→ что-то сломалось, нужна ручная проверка
Раз в неделю — ручная выборка 20-30 звонков, сравнение с золотым стандартом. Если F1 падает ниже 0.80 — пересматриваем промпт или дообучаем STT.
Весь пайплайн — ~400 строк Python без учёта инфраструктуры. Ключевые решения:
- STT:Whisper large-v3 для self-hosted, Yandex SpeechKit если нужна точность на русском, Deepgram если нужна скорость
- Диаризация:pyannote-audio 3.1 — лучший open-source вариант
- LLM:GPT-4o-mini для продакшена (цена/качество), GPT-4o для критичных данных
- Валидация:обязательна — LLM галлюцинирует в ~5% случаев
Подход не привязан к домену. Замените JSON-схему в промпте — и пайплайн заработает для колл-центра, юридической консультации, медицинского приёма или любого бизнеса с телефонными переговорами.
Если есть вопросы по конкретному этапу — STT на русском, борьба с галлюцинациями, streaming-обработка — пишите в комментариях.