Как мы собирали собственного голосового AI-оператора — FreeSWITCH + Whisper + Alem.ai + LLM + Silero + DamuBPM
FreeSWITCH + Whisper + Alem.ai + локальные LLM + Silero TTS + DamuBPM.
История о том, как обычный телефонный звонок постепенно превратился
в полноценный интерфейс к бизнес-процессам.
Последние несколько дней у нас в B-Apps был довольно интересный эксперимент.
Ну да. Просто. 😄
На бумаге всё действительно красиво:
А потом начинаешь это реально собирать — и выясняется, что между этими
пятью стрелочками находится примерно миллион мелочей.
В итоге получился рабочий прототип голосового AI-консультанта,
который принимает обычный телефонный звонок, слушает человека,
понимает русский и казахский, обращается к бизнес-логике DamuBPM,
использует AI-инфраструктуру Alem.ai,
формирует ответ и проговаривает его обратно в телефонную линию.
FreeSWITCH отвечает непосредственно за телефонию:
принимает звонок, работает с SIP/RTP и управляет самим разговором.
Но обычной телефонии недостаточно.
Нам нужно было в реальном времени забирать голос звонящего
и отправлять его в систему распознавания.
Для этого подключили
На каждый звонок создаётся WebSocket:
UUID — идентификатор конкретного звонка.
FreeSWITCH начинает передавать туда PCM-аудио,
а Python-сервис постоянно его принимает.
Телефонный звук — это вам не студийный микрофон.
Реальный звонок у нас шёл через PCMA, 8000 Hz.
Потом поток преобразовывался для Whisper.
На первых тестах Whisper иногда слышал что-то совершенно своё.
Ну спасибо. 😄
Начали разбираться и быстро выяснили:
просто отправлять Whisper каждые три секунды аудио — не лучшая идея.
Фраза может начинаться в одном куске, заканчиваться в другом,
плюс тишина, шумы, дыхание и особенности телефонного кодека.
Каждый небольшой аудиофрейм анализируется по RMS.
То есть если человек сказал:
мы стараемся отправить Whisper именно всю реплику,
а не что-нибудь в духе:
«…ствуйте, меня интересует…»
После нескольких экспериментов пришли к
Whisper получает готовую реплику и возвращает уже нормальный текст:
Отдельно экспериментировали с русским и казахским.
Whisper может автоматически определить язык,
поэтому нам не обязательно заставлять человека нажимать:
«один — русский, два — казахский».
Человек просто говорит.
Следующая задача — AI.
Здесь мы принципиально не хотели делать архитектуру:
Нам хотелось, чтобы управление разговором оставалось
внутри нашей платформы.
Поэтому в схеме появился DamuBPM.
Whisper передаёт распознанную реплику в DamuBPM,
где находится бизнес-логика разговора:
Для AI-части мы используем в том числе
Alem.ai.
Для нас это важный архитектурный момент:
голосовой оператор не должен быть намертво связан
с одной конкретной моделью.
Сегодня одна модель лучше решает одну задачу,
завтра появляется другая.
Где-то нужна мощная модель.
Где-то маленькая и быстрая.
Для голосового интерфейса зачастую гораздо важнее
получить хороший ответ быстро,
чем заставить огромную модель несколько секунд
философствовать над вопросом:
Поэтому мы экспериментируем одновременно с AI через
Alem.ai и локальными моделями через Ollama.
Консультант постепенно должен выяснить:
Но при этом нельзя превращать разговор в допрос.
Так никто с роботом разговаривать не захочет.
Поэтому LLM получает историю последних реплик
и системный prompt.
Одно из важных правил:
Клиент говорит:
А через пару реплик AI:
Я уже думаю: ну приехали. 😄
Оказалось, проблема была не столько в самой модели,
сколько в истории сообщений.
Некоторые сообщения уходили без корректного
Для LLM есть большая разница между:
и просто:
Когда история стала передаваться нормально:
модель стала гораздо лучше держать контекст разговора.
Для обычного чата две-три секунды ожидания — нормально.
Для телефона — вечность.
Попробуйте во время разговора молчать секунд семь
после каждого вопроса.
Поэтому мы тестируем разные варианты.
Для задач, где нужен внешний AI-сервис, используем
Alem.ai.
Для локального контура — модели Qwen через Ollama.
В локальном варианте отключили thinking:
Ограничили контекст и максимальную длину ответа.
Голосовому консультанту не нужно писать диссертацию.
Ему нужно сказать:
Всё.
Некоторые запросы к бизнес-логике могут занимать
от трёх до десяти секунд.
Чтобы человек не думал, что связь пропала,
добавили человеческие fillers.
Если ответ быстрый — бот ничего лишнего не говорит.
Если ожидание затянулось:
Но здесь появилась интересная проблема.
Нужно отличать:
Если консультант уже спросил:
и клиент просто молчит,
нельзя внезапно отвечать:
Минуточку чего? Мы же его ждём. 😄
Так постепенно появилась маленькая state machine:
Fillers разрешены только в состоянии
Хотелось сделать синтез речи локально
и без оплаты за каждую фразу.
Для первого рабочего варианта подключили Silero TTS.
Он лёгкий, быстрый, работает локально
и нормально подходит для телефонного сценария.
Для воспроизведения сделали отдельный небольшой
FastAPI-сервис
Он принимает UUID звонка и WAV-файл,
сохраняет его во временный каталог
и через FreeSWITCH WebAPI вызывает:
Один из самых неприятных багов:
вопросы AI начали иногда накладываться друг на друга.
Например:
Причина оказалась логичной.
HTTP-запрос
Это НЕ означает:
Пришлось учитывать реальную продолжительность WAV
и сделать общий playback lock.
Теперь принцип жёсткий:
Это ещё одна классическая проблема голосовых роботов.
Пока AI говорит,
Whisper не должен воспринимать собственный TTS
как реплику клиента.
Иначе появляется почти философский эксперимент:
Можно оставить на ночь и утром посмотреть,
к чему они пришли. 😄
Поэтому состояния вроде
Как только звонок принят,
система сразу говорит:
Понадобилась возможность,
чтобы бизнес-логика сама решила:
разговор пора завершать.
Для этого используем простой маркер:
Например, DamuBPM/AI возвращает:
Python видит управляющий маркер,
но клиент его не слышит.
Для TTS остаётся:
Мы ждём полного окончания WAV,
и только потом FreeSWITCH корректно завершает звонок.
Потому что AI сам по себе умеет разговаривать.
Но когда за ним находится BPMS,
он уже может не только разговаривать,
но и запускать реальные действия.
AI понимает намерение → DamuBPM находит процесс →
получает статус → Alem.ai или другая модель формирует
человеческий ответ → клиент слышит его по телефону.
Или:
Дальше можно классифицировать обращение,
зарегистрировать тикет,
определить клиента,
проверить SLA,
найти ответ в базе знаний
и только при необходимости подключить человека.
Вот здесь AI перестаёт быть просто чат-ботом.
Он становится интерфейсом к бизнес-процессам.
При этом архитектура не завязана намертво
на одного AI-провайдера.
Мы можем использовать
Alem.ai,
локальные модели,
специализированные модели
или их комбинацию —
в зависимости от задачи,
требований к скорости,
безопасности и инфраструктуре.
То есть с машинами на фоне,
плохими гарнитурами,
эхом и знаменитым:
Но уже сейчас система реально разговаривает.
Когда мы начинали, задача звучала:
«Давайте заставим AI отвечать на телефон».
Теперь она звучит немного иначе:
И вот эта формулировка мне нравится намного больше.
Как мы собирали собственного голосового AI-оператора
Всё начиналось вроде бы просто
Первый слой — FreeSWITCH
mod_audio_stream.
/ws/{uuid}Телефонный звук — отдельная история
Whisper уверенно отвечает: «Беги».
Добавили Voice Activity Detection
Whisper на GPU
faster-whisper и Whisper large-v3
на NVIDIA GPU.
DamuBPM как оркестратор
Alem.ai и AI-слой
AI-консультант должен не просто болтать
— Город?
— Компания?
— Количество пользователей?
— Срок?
И тут появился смешной баг
role.
```
{
```
"role": "user",
"content": "Меня зовут Ельдар"
}
```
{
```
"content": "Меня зовут Ельдар"
}В телефонии latency чувствуется особенно сильно
"think": false«Минуточку…» оказалось отдельной задачей
PROCESSING.
Голос — Silero TTS
play.py.
uuid_broadcastКогда AI начал говорить одновременно с самим собой
и поверх:
«А какая у вас компания?»
/play уже вернул OK,
но это означает только:
AI не должен слушать самого себя
bot_speaking,
turn_busy
и блокировки оказались не менее важными,
чем сами нейросети.
Первое приветствие
HANGUP как управляющая команда
HANGUPСпасибо за обращение. Всего доброго! HANGUPЧто получилось в итоге
Самое интересное — AI + DamuBPM
AI становится интерфейсом
Что ещё предстоит сделать
Подписывайтесь на нас в соц сетях

