Многие помнят статью «Мошенники позвонили моему ИИ-деду. Он продержал их 31 минуту и записал всё». Статья быстро набрала популярность, плюсы и комментарии. К сожалению, позже выяснилось, что автор немного «пофантазировал» и описал гипотетический сценарий реализации ии-бота, за что справедливо подвергся критике.
Тем не менее, я (как и многие другие) вполне уверен, что предложенный сценарий использования LLM реален, реализуем на текущем железе и доступных моделях. Что ж, посмотрим, можно ли дома собрать фреймворк, позволяющий ИИ беседовать с мошенниками по телефону без мгновенного раскрытия.
Небольшое отступление, задача выложить аудио на Хабр оказалась нетривиальной. Что-то заблокировано в России, что-то в мире, а аудиотеги я так и не победил. Поэтому в статье аудио выложено через rutube, а в конце в спойлере продублировано через soundcloud.
Автор оригинальной статьи описывал правдоподобный локальный пайплайн: Asterisk + SIP-провайдер + выделенный сервер, Silero VAD за 20 миллисекунд определяет конец фразы, локальный Whisper транскрибирует за 400мс, Llama-3 70B генерирует ответ за 500мс, Piper озвучивает за 150мс. Итого — 1.1 секунды от конца фразы мошенника до ответа деда. Вполне производственные показатели, можно масштабировать.
Сомнения вызвали — скорость работы (практически на всех этапах скорость завышена раза в два). И возможность локальных моделей более или менее натурально генерировать голос деда.
❯ Реализация
Начнём по порядку. Для начала на практике всё упёрлось в железо. У меня старый компьютер с видеокартой на 6GB — ни Llama-3 70B, ни нормальный локальный Whisper туда не влезают, хотя в облаке виспер распознаёт речь не так чётко, как мне нужно — хочется большей точности.
Регулярно проскальзывали знаменитые галлюцинации про субтитры. Впрочем, для реализации проекта потребности в собственном железе нет совершенно. И на скорость отклика это влияет только положительно.
Для экспериментов я не стал связываться с «Астериском». Помню, как в 2010 для настройки IP-телефонии мы в областном центре не смогли найти специалиста, пришлось головному офису отправлять инженера в командировку.
Вместо Asterisk и SIP-провайдера использовался ngrok и браузер. Генерируется ссылка вида https://xxxx.ngrok-free.app, по ней браузер устанавливает WebRTC-соединение напрямую с моим компьютером. Никакой телефонной инфраструктуры и аренды номера. Голос идёт через UDP поверх обычного интернета.
Итак, вместо Silero VAD — простой RMS-детектор тишины: ждём 0.5 секунды без звука выше порога, считаем, что мошенник закончил фразу. Грубее, но работает надёжно. Вместо локального Whisper взял OpenAI gpt-4o-transcribe через API: точнее распознаёт русскую речь с фоновым шумом, понимает контекст из подсказки, меньше галлюцинаций. Вместо Llama — Claude Haiku: хорошо играет роль, устойчив под давлением, относительно хорошо держит промпт. Вместо Piper — ElevenLabs v3 с генерированным голосом деда и стримингом для снижения задержки. Пожалуй, лучший на сегодня вариант для генерации естественного голоса, поддерживает api-команды, хоть и не очень быстр, но думаю решат в ближайшее время.
Суммарная пауза между концом фразы мошенника и началом ответа Геннадия — 6-8 секунд. Можно попробовать ужать задержку, но трудозатраты слишком велики для эксперимента на выходных.
В тестах паузы не выглядели совсем неестественными, местами работали на образ. Пожилой человек не отвечает мгновенно, думает, теряет нить разговора, готовит фразу. Семь секунд тишины в трубке мошенник воспринимает как размышления собеседника.
❯ Промпт
Меньше всего проблем было с промптом. Возникали нюансы с включением в ответы LLM служебных команд (типа паузы или интонаций), но всё решалось достаточно быстро. Для купирования галлюцинаций распознавания голоса пришлось включить правило запрета повторения непонятных слов и фраз, выпадающих из контекста. Вместо этого переспрашивать. В дальнейшем примеры ответов в промпте стоит либо расширить, либо вообще убрать. ИИ часто повторяется в разных беседах. Но можно и потребовать не повторять примеры, а генерировать свои фразы, тоже вариант.
Служебные команды (вздох, кашель) ИИ берет в теги. В коде их обработка настраивается под соответствующее API генератора голоса.
❯ Крик мошенника
Использовал самый простой подход. Никакого анализа интонации или частот, просто RMS, среднеквадратичное значение амплитуды аудиосигнала за фразу. Если мошенник говорит спокойно — RMS в районе 2000-4000. Начинает повышать голос RMS уходит за 7000. Порог эмпирический, подобран по результатам экспериментов. Да, и подбирать приходится индивидуально в зависимости от способа записи голоса мошенника, зависит от его микрофона.
Когда порог превышен, Claude получает системную пометку: [собеседник резко повысил голос] и реагирует соответственно промпту.
❯ Код
Для экспериментов запускался отдельно ngrok для приема звонков, отдельно сам код. Фоном было наложена запись «Деревни дураков» с ютуба. Запись звонков реализована, но с поджатием пауз, поэтому для статьи записал общение на диктофон, это соответствует реальному таймингу и вполне достаточно.
Для приветствия было сгенерировано несколько фраз деда, которые код случайно вставляет в начало беседы.
❯ Проблемы при реализации
Галлюцинации при распознавании голоса. На коротких и тихих фрагментах нейросеть выдавала несуществующий текст — «Субтитры делал DimaTorzok», «Спасибо за просмотр». Поднял порог тишины (RMS) чтобы не отправлять фоновый шум в транскрипцию, и минимальную длину буфера до 0.8 сек. Дополнительно сменил модель на gpt-4o-transcribe — она устойчивее к шуму и лучше понимает русский. Добавил в промпт контекст, чтобы правильно распознавал имена и термины.
Искажение имён. Стабильно коверкалось «Соколов» — «Высоковолк», «Гостоколов», «Выстрекалов». Claude воспринимал это как факт и поправлял мошенника, вслух называя неправильную версию: «фамилия не Высоковолк — Соколов». Решение через правку промпта: Геннадий при искажении просто представляется заново не цитируя чужую версию.
Диалог разваливался в монолог. Геннадий отвечал на первую реплику мошенника, игнорируя все последующие. Причина — pending буфер: пока Геннадий говорил, новые фразы мошенника копились и отправлялись в Claude все разом после ответа. Claude получал устаревший контекст и отвечал на него. Решение: pending буфер сбрасывается после ответа, ждём новую фразу мошенника.
Фраза мошенника резалась на куски. Детектор тишины срабатывал на паузы внутри фразы и отправлял куски в Whisper отдельно. «Служба безопасности» и «Центрального банка» приходили как два разных сообщения. Решение, ждём 0.5 секунды тишины прежде чем считать фразу законченной.
Геннадий отвечал на устаревший контекст. Полная цепочка Whisper+Claude+TTS занимает 6-7 секунд. За это время мошенник успевал сказать ещё 2-3 фразы. Геннадий заканчивал ответ на первую и сразу начинал отвечать на накопившееся — разговор сдвигался на фазу назад. Решение то же: сброс pending после каждого ответа.
Геннадий возвращался к уже сказанному. «Не кричите пожалуйста» повторялось на каждой громкой реплике мошенника. Зафиксировал в промпте: если реакция уже была — тема закрыта, в следующий раз реагируем только на содержание.
Задержка ответа. Проблема не решена. Можно выиграть секунду на локальном виспере. Но основная проблема генерация живого голоса. Переход на более простые версии приводил к тому, что голос однозначно распознавался как синтезированный. Но судя по прогрессу в этой области в течение года эта проблема будет решена. Будет доступ к генерации достаточно живого голоса с минимальной задержкой. Чем, конечно, в первую очередь воспользуются мошенники.
Была забавная попытка решить эту проблему буферизацией и озвучиванием ответов Геннадия на тезисы мошенника из базового сообщения. В результате диалог стал слегка шизофреничным и больше напоминал какую-то кавээновскую сценку. Но как человек, исполняющий роль мошенника, скажу, что раздражало это весьма и весьма.
Есть вариант со вставкой в паузы заранее сгенерированных вздохов, кашлей, мычаний и бормотаний, но не стал заморачиваться.
Остальные проблемы, думаю, не стоят внимания.
❯ Заключение
Вот и два примера аудиозаписи диалога с ИИ-дедом. Сразу скажу, я не актер, и запись более или менее приличных диалогов, стоила массу времени (в основном по моей вине). Для проведения экспериментов закинул по $5 на openAi, Anthropic и Elevenlabs (основные траты). Лимит на доступ к топовой V3 Elevenlabs на тарифе $5, включает в себя всего 30 минут генерации, так что записывать больше вариантов не было никакого резона, а использовать более быстрые и дешёвые нейросетки нет смысла из-за качества голоса.