Скрайб: ИИ-ассистент, который ходит на мои Zoom-звонки
Бот-участник заходит в звонок по календарю, ведёт транскрипт «кто что сказал»,
отвечает голосом за ~2 секунды, понимает команды вроде «помолчи, пока я не скажу
"продолжаем"» и помнит историю каждого клиента. Всё крутится на моём собственном VPS -
записи звонков не уходят ни в какой чужой сервис. Собрано за 9 дней вечерами
в паре с Claude Code.
~2 сзадержка голосового ответа (realtime-диалог)
0.6 сот конца фразы до строки в live-транскрипте
1 VPS8 ГБ RAM, всё self-hosted, без подписок на бот-сервисы
9 днейот идеи до боевых звонков с клиентами
01Что он умеет
Сам приходит на звонки
Смотрит в календарь: фиолетовый ивент = коучинговая сессия, любой ивент с zoom-ссылкой = цель. За 2 минуты до старта бот уже в комнате, мне в Telegram падает ссылка на live-транскрипт.
Транскрипт по говорящим
Отдельная аудио-дорожка на каждого участника - «кто сказал» известно точно, без диаризации. На клиентских сессиях имена заменяются на «Коуч» и «Клиент» ещё до записи в базу.
Голосовой диалог
«Скрайб, ...» - и он отвечает голосом в звонок. После ответа 20 секунд слушает уточнения, потом сам выходит из беседы и ждёт следующего обращения.
Инструкции натуральным языком
«Помолчи, пока я не скажу "продолжаем"», «напомни через 5 минут», «отвечай только в чат», «когда заговорим про бюджет - скажи». Подтверждает своими словами и исполняет.
Видит экран и чат
Кадры демонстрации экрана (один на слайд) описываются vision-моделью и попадают в контекст. Сообщения чата Zoom сохраняются в общую ленту.
Память по историям общения
Память привязана не к встрече, а к истории общения с человеком: профиль (цели, динамика, договорённости) обновляется после каждой сессии, поверх архива работает полнотекстовый поиск. Но открывается всё это только там, где я явно подтвердил, чья это история. Автомат подбирает встречу к истории и предлагает - решаю я.
Спросить вне звонка
Личный Telegram-канал: спрашиваю обычным текстом - что решили на прошлой сессии, где обсуждали конкретную тему, какие звонки были на неделе, пришли транскрипт. Оттуда же отправляю его в звонок по ссылке. Канал закрыт списком: всем, кроме меня, бот просто не отвечает - ни ответа, ни отказа.
Чистый лист по умолчанию
Пока привязка не подтверждена, бот в звонке не знает ничего: пишет транскрипт и отвечает, но профиль, прошлые саммари и поиск ему недоступны. Ошибка автомата в календаре стоила бы чужой сессии в чужой комнате - в коучинге это дороже любого неудобства.
02Архитектура
Три сервиса на одном VPS: календарь-зеркало, self-hosted бот-платформа Attendee
и ядро «scribe-core», где живёт вся логика. Внешних зависимостей три: Groq для распознавания
речи, OpenAI для realtime-голоса и OpenRouter для остальных LLM-задач.
Внешние LLM-вызовы (Groq, OpenAI Realtime, OpenRouter) идут из scribe-core через прокси в Европе - об этом ниже, в граблях.
Почему такие блоки
Attendee вместо платных бот-сервисов. Recall.ai и аналоги берут ~$0.50/час и гоняют аудио звонков через своё облако. Attendee - открытая платформа, которая делает то же самое у меня на сервере: заходит в звонок официальным Zoom Meeting SDK, отдаёт отдельную дорожку на каждого участника, умеет писать в чат и проигрывать аудио в звонок.
Своя логика - отдельным сервисом. Attendee не трогаю вообще (ни одного патча): всё поведение - в scribe-core, который общается с ним по websocket и REST. Обновления Attendee не ломают мою логику, и наоборот.
Календарь как источник правды. Я живу по Google Calendar и раскрашиваю ивенты по смыслу. Зеркало-сервис читает цвет - и тип встречи определяется сам: фиолетовый = коучинг (бот придёт, имена скроются), любая встреча с zoom-ссылкой = бот постучится. Ноль ручной разметки.
Память - профиль-документ, а не RAG. На каждого клиента один живой текст (цели, динамика, договорённости), который LLM переписывает после каждой сессии. Для десятка клиентов это проще, дешевле и предсказуемее векторной базы. Пересмотрю, когда клиентов станет больше двадцати.
Память открывается только по подтверждённой привязке. Раньше «чей это звонок» решал матчинг со встречей в календаре - и ошибался молча: не тот ивент, сдвинутая серия, перенос в чужой слот. Теперь единица памяти - история общения, а связка «этот звонок - эта история» становится рабочей только после моего подтверждения; серию можно подтвердить один раз на все будущие встречи. Всё остальное - предложение в очереди на разбор. Цена ошибки несимметрична: молчаливый бот - неудобство, болтливый - утечка чужой сессии.
История - не только человек. Есть истории-темы: проект или направление, к которому звонки цепляются по ключевым словам в названии встречи. У истории несколько наборов идентификаторов - почты, имена в Zoom, ключевые слова; правила предлагают привязку по любому из них, но личное совпадение всегда сильнее тематического, а неоднозначное совпадение молчит. Истории собираются в группы для порядка в списке, каждое принятое решение остаётся в архиве - и его можно поменять тем же движением, память при этом пересчитывается.
03Жизнь голосового обращения
Самое интересное - как бот понимает, что говорят именно ему, и не лезет в чужой
разговор. Обращение по имени сначала проходит через «диспетчер», и только потом решается,
что это было: вопрос, команда замолчать, поведенческая инструкция - или вообще разговор
О боте в третьем лице, на который отвечать не нужно.
Перебить бота можно голосом в любой момент - barge-in отменяет генерацию на полуслове.
04Стек и цифры
Слой
Чем сделано
Заметка
Бот в звонке
Attendee (open source, ELv2) + Zoom Meeting SDK
General App в Zoom Marketplace, ревью не нужно для своих встреч
Распознавание
Каскад: Groq Whisper large-v3, при rate-limit - фолбэк OpenAI Whisper; чанки по VAD
0.6-0.7 с на фразу; чанк теряется только при отказе обоих движков, под нагрузкой сегменты автоматически укрупняются
Realtime-голос
OpenAI gpt-realtime по websocket
семантический VAD на их стороне, barge-in из коробки
без фреймворков на фронте - страницы просто быстрые
Инфраструктура
Docker на VPS 8 ГБ, nginx, git-деплой
бот в звонке ест 1-2 ГБ - это главный лимит параллельности
Качество держат ~400 автотестов и smoke-прогон по проду после каждого деплоя:
проверяется всё вплоть до инварианта «в коучинговых транскриптах нет ни одного настоящего имени».
05Грабли, которые сэкономят вам дни
Из России половина API просто не отвечает. OpenAI, OpenRouter и Telegram Bot API ходят через маленький прокси на европейском VPS (tinyproxy, порт закрыт по IP). Приятный сюрприз: websocket до OpenAI Realtime через тот же CONNECT-прокси завёлся с первого раза. Groq работает и напрямую.
Платные бот-сервисы отпали не из-за цены. Recall.ai в условиях использования прямо запрещает обход экспортных ограничений, оплата - только зарубежной картой. Self-hosted Attendee снял оба вопроса и дал бонус: данные звонков вообще не покидают мой сервер.
Whisper галлюцинирует на тишине. В транскрипте появлялись «Субтитры сделал DimaTorzok» и «Продолжение следует...» - модель училась на видео с титрами. Лечится фильтром стоп-фраз на коротких сегментах плюс порогом громкости.
Бот на чужую встречу не попадёт. С марта 2026 Zoom пускает неопубликованные приложения только на встречи своего аккаунта (unpublished_zoom_app). На свои - без ограничений. Планируйте: либо вы хост, либо проходите ревью приложения.
Красный индикатор записи - обязателен. Когда бот получает доступ к аудио, Zoom показывает всем «идёт запись», и хост должен нажать Allow. Это не облачная запись Zoom - но участникам стоит объяснить заранее, чтобы не пугались.
Celery + Redis: зомби-задачи. Дефолтный visibility_timeout у Redis - час; задача бота живёт дольше звонка, брокер «передоставляет» её второй копией, и оба слота воркера заняты мертвецами. Лечение: timeout 12 часов + фоновая чистка ботов, чей звонок давно кончился.
Realtime-модель считает, что говорят с ней. Пока сессия открыта, она слышит весь звонок и встревает в живой разговор. Понадобилось «окно диалога»: 20 секунд после ответа - и модель отключается от аудио до следующего обращения по имени.
Люди говорят О боте, а не С ним. На групповом звонке участники обсуждали бота в третьем лице - классификатор с двумя классами (вопрос/инструкция) принял это за команду «молчи до фразы» и замьютил бота до конца звонка. Лечение в три слоя: третий класс «это не обращение», валидация директивы до принятия (бессмысленное условие снятия = отказ) и страховки от вечного молчания - лимит 30 минут плюс подсказка в чат, если бота зовут по имени и не понимают, почему он молчит.
06Как это строилось
Весь проект - парная работа с Claude Code: я формулирую требования голосом
и текстом, агенты пишут код, деплоят и сами проверяют результат в браузере. Что сработало
в процессе:
Живые звонки как полигон. Почти каждая фича рождалась из реального инцидента: клиент назвался настоящим именем - через 10 минут анонимизация работала на живом звонке; бот встревал в разговор - к вечеру появилось окно диалога.
Решения - артефактами. Каждый серьёзный выбор (бот-платформа, realtime-архитектура, память без RAG) записан отдельным документом с условиями пересмотра. Через месяц не придётся вспоминать, «почему мы так сделали».
Smoke-тесты против прода. Деплой не считается успешным, пока скрипт не прогнал реальные проверки по боевому URL - вплоть до «бот видит завтрашний звонок в календаре».
Красные линии. Несколько правил, которые нельзя нарушать никакому агенту: записи не уходят во внешние сервисы, имена клиентов не попадают в базу, саммари получаю только я.