Голосовой ассистент


Проблема

Ко мне часто обращаются с вопросами как к эксперту по ассемблеру PDP-11. Раньше я отвечал текстовыми сообщениями, затем перешёл на голосовые для экономии времени. Всё равно это было расточительно.

Задача

Встроить в мой сайт manwe.ru/bk0010 голосового ассистента, который будет консультировать людей, опираясь на накопленную мной базу знаний. Сайт – статический html, статьи в Markdown.

Архитектура

В качестве поставщика выбран x.ai. Подключение к нему из браузера требует API-ключа, но хранить его в статическом сайте небезопасно. Решение такое: браузер запрашивает токен у маленького php-скрипта, x.ai выдаёт временный токен на 5 минут. С ним браузер открывает WebSocket-соединение с агентом. Настоящий ключ лежит только на сервере с закрытым доступом. Скрипт проверяет, откуда пришёл запрос, и ограничивает число токенов с одного IP-адреса в час.

Цена вайбкодинга

Claude при реализации написал Cloudflare Worker для выдачи токенов, хотя я его об этом не просил. Пришлось объяснять ему как сделать на php. Over-engineering – типичная проблема ИИ-ассистентов.

Доработки и баги

Хоть виджет и подключается одной строкой кода, расшифровка голоса пользователя не работала. Помогло явное включение опции в настройках сессии.

Я хотел дать возможность пользователю говорить и печатать одновременно, но по умолчанию так не работало: текстовая реплика разбивала голосовую реплику на части. Пришлось привязать расшифровку голоса к ID реплики, а для напечатанного текста создавать отдельный ID.

Пауза в диктовке тоже разбивала фразу на части, причём с повтором: после паузы сервер создавал новую реплику, и её расшифровка почему-то содержала уже сказанное в прошлой реплике. Помогло сравнение содержания двух последних реплик: в случае совпадения они объединяются с одним ID.

Контекст

Сайт одностраничный, статьи подгружаются без перезагрузки. Агенту передаётся системное сообщение с содержанием страницы (благо, она изначально хранится в формате md). В конструкторе прописано отвечать в первую очередь по контексту текущей страницы, и лишь затем обращаться к базе знаний.

Это оказалось дешевле по расходу токенов, чем передавать агенту интернет-адрес страницы, чтобы он её парсил.

Каждая отправленная статья остаётся в истории разговора, контекст растёт. Но для коротких диалогов это даже плюс.

Ссылки и источники

Голосовой ассистент присылает обычный текст, а мой виджет находит а нём адреса (типа https://) и стилизует как нажимаемые ссылки.

В инструкции агенту сказано: если ответ взят с внешнего сайта, дописывать в конце «Source: адрес». Виджет оформляет ссылку на источник в виде тэга, а слово «Source» убирает из текста.

UX голосового ассистента

Я быстро понял, что UX виджета от x.ai никуда не годится. Например, почему нельзя сразу печатать текст? Поле ввода неактивно, пока не установлено соединение с сервером. Это ведь бессмыслица. В 99% случаев соединение установится раньше, чем пользователь закончит печатать свой вопрос. Я разрешил печатать и нажимать Enter для отправки сразу, но фактическая отправка произойдёт только после установки соединения.

Зачем в поле ввода писать «Send a message», а справа ставить кнопку микрофона? Пользователя сбивают с толку: что ему делать – писать или говорить? К тому же, справа обычно ставят кнопку отправки. Я максимально упростил интерфейс: в поле нет подсказки, зато всегда мигает курсор, а микрофон включен и отображает уровень сигнала. С первого взгляда понятно: система активна и одновременно принимает как голосовой, так и текстовый ввод. Сказанное сразу появляется в чате, а написанное – после нажатия клавиши Enter. Красный цвет подчёркивает, что идёт запись. Нажатие на красную кнопку – привычный паттерн, несложно догадаться как остановить запись.

Финансовые показатели

Работа заняла один день, не считая сбора базы знаний, которую я собирал и структурировал годами.

Один сеанс с обсуждением нескольких тем обходится примерно в 1$. Зато я больше не трачу своё время.

Что дальше

В одной модели сложно настроить разные правила для текста и голоса. Стало понятно, что нужны две модели: первая для генерации текста, вторая для его озвучивания. Тогда текст может содержать ссылки, а в голосовую модель они не попадут. Специфические термины можно преобразовывать в транскрипцию перед отправкой в голосовую модель. Возможностей для улучшения много.