Голосовые клиентов в CRM теперь показываются текстом: расшифровка запускается на сервере с двумя ядрами и без видеокарты, а звук не уходит за пределы российских серверов.
Магазины в переписке часто получают голосовые сообщения — клиент уточняет наличие набора или стоимость доставки в Самару. Менеджеру такое приходится слушать, а в шумном помещении или во время созвона это неудобно; кроме того, из списка диалогов не видно, о чём запись, пока её не откроешь. Логичное решение — выводить текст под плеером. Рассказываем, как мы реализовали это без облачных сервисов распознавания на небольшом сервере в России и какие трудности встретили.
Расшифровка силами мессенджеров не подошла. ВКонтакте сам распознаёт голосовые: у вложения audio_message в ответе API есть поле с текстом, но для сообщений сообщества оно оставалось пустым даже через несколько дней. У MAX, Telegram и WhatsApp, приходящих через сервис-посредник, расшифровки нет вовсе. Облачные API отпали по другой причине: голосовое клиента — персональные данные, отправлять их на зарубежные серверы не хотелось. Значит, всё должно работать на нашей инфраструктуре в России, а файлы — не сохраняться.
Выбор пал на faster-whisper — версию Whisper на CTranslate2, заметно быстрее оригинала на CPU. Взяли модель small с квантованием int8: на русской разговорной речи она путает отдельные слова, но смысл передаёт уверенно. Сервер облачный, 2 ядра, без видеокарты. Распознавание вынесли на отдельную машину, чтобы не отбирать процессор у CRM. Голосовое длиной 35 секунд расшифровывается примерно за 9 секунд. Помогли настройки: compute_type=»int8″ (квантование весов — основной выигрыш по скорости на CPU), beam_size=1 (жадное декодирование вместо поиска по лучу, на коротких фразах качество почти не страдает), vad_filter=True (детектор голоса отрезает тишину и паузы, которых в голосовых много) и language=»ru» (без автоопределения — быстрее и меньше ошибок на коротких записях).
Основные грабли — декодирование звука. faster-whisper умеет открывать файл через PyAV, но на голосовых из мессенджеров это ломалось: часть файлов PyAV не читал из-за ошибки в метаданных, а форматы приходят разные — ogg, m4a, mp3. Проще отдать декодирование ffmpeg и передать модели готовый массив: моно, 16 кГц, float32 от −1 до 1. Тогда исходный формат перестаёт иметь значение. Скрипт принимает звук на stdin и отдаёт текст в stdout: ffmpeg переводит любой формат в сырой PCM (16 бит, моно, 16 кГц), numpy превращает его в float32, модель «small» на CPU с compute_type=»int8″, download_root=»/opt/stt/models», cpu_threads=2 транскрибирует с language=»ru», beam_size=1 и vad_filter=True, а результат склеивается в строку. Интерфейс «stdin → stdout» удобен тем, что скрипт ничего не знает ни про CRM, ни про мессенджеры, а файл нигде не ложится на диск. Основной сервер вызывает его так: curl -s «$VOICE_URL» | ssh stt-server /opt/stt/stt.py. Модель загружается при каждом вызове — для нашего потока это приемлемо, но при большой нагрузке скрипт стоит превратить в постоянно работающую службу с очередью.
В CRM на основном сервере раз в минуту запускается задача: * * * * * flock -n /tmp/stt_fill.lock timeout 600 python3 /opt/kohorta/stt_fill.py —limit 5. Ключ flock -n не даёт новому запуску стартовать, пока работает предыдущий, и задачи не копятся; timeout 600 страхует от зависшего ssh или ffmpeg; —limit 5 ограничивает проход пятью голосовыми, чтобы всплеск сообщений не забил сервер распознавания. Сам stt_fill.py находит входящие голосовые за последние 2 дня без текста, по очереди скачивает звук по ссылке мессенджера, отправляет на распознавание и записывает текст во вложение. CRM показывает его под плеером, а в списке диалогов вместо «Голосовое сообщение» появляется «🎤 Здравствуйте, а вот этот набор…». Для каждого сообщения ведётся счётчик попыток: если файл битый или ссылка протухла, после трёх неудач сообщение больше не трогают, а окно в 2 дня не даёт перебирать всю историю при каждом запуске.
В итоге голосовое становится текстом примерно через минуту после поступления — до ближайшего запуска задачи плюс несколько секунд на распознавание. Менеджер видит суть записи прямо в списке диалогов, не открывая её. Внешних API и оплаты за минуты нет: модель, звук и текст остаются на наших серверах в России. Если будете повторять — начните с small int8 и beam_size=1 на CPU, декодируйте звук через ffmpeg, а не через PyAV, и не храните файлы: для расшифровки они не нужны.
