Мы делаем CRM для продаж в переписке. Клиенты наших магазинов любят голосовые: «Здравствуйте, а вот этот набор ещё есть, и сколько доставка до Самары?» Менеджеру такое сообщение нужно прослушать, а в шумном цеху или на созвоне это неудобно. Плюс по списку диалогов не понять, о чём голосовое, пока его не откроешь.
Решение очевидное — показывать под плеером текст. Расскажу, как мы это сделали без облачных API распознавания, на маленьком сервере в России, и на какие грабли наступили.
Почему не взять расшифровку из мессенджера
ВКонтакте умеет расшифровывать голосовые сам: у вложения audio_message в ответе API есть поле с текстом. Но для сообщений сообщества оно у нас оставалось пустым даже спустя несколько дней. У MAX, Telegram и WhatsApp, которые приходят через сервис-посредник, расшифровки нет совсем.
Облачные сервисы распознавания отпали по другой причине: голосовое клиента — это персональные данные. Отправлять их на чужие серверы за границу мы не хотели, поэтому всё должно работать на наших серверах в России, а файлы — не сохраняться.
Модель и железо
Взяли 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:
#!/opt/stt/bin/pythonimport subprocess, sysimport numpy as npfrom faster_whisper import WhisperModel# ffmpeg переводит любой формат в сырой PCM: 16 бит, моно, 16 кГцraw = subprocess.run( ["ffmpeg", "-v", "quiet", "-i", "pipe:0", "-f", "s16le", "-ac", "1", "-ar", "16000", "-"], input=sys.stdin.buffer.read(), capture_output=True,).stdoutif not raw: sys.exit(2)audio = np.frombuffer(raw, np.int16).astype(np.float32) / 32768model = WhisperModel("small", device="cpu", compute_type="int8", download_root="/opt/stt/models", cpu_threads=2)segs, _ = model.transcribe(audio, language="ru", beam_size=1, vad_filter=True)sys.stdout.write(" ".join(s.text.strip() for s in segs).strip()))
Интерфейс «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, и не храните файлы — для расшифровки они не нужны.
ссылка на оригинал статьи https://habr.com/ru/articles/1107746/