Voice Router
Голосовой AI-оператор страхового контакт-центра: речь на русском или казахском распознаётся двумя STT-движками, LLM выбирает сценарий из каталога в…
О проекте
Команда: TalkIt Team
Кейс HackAlem.ai: Коммуникации — Halyk Bank · кейс 2
Стадия: Прототип
Задача кейса: «Voice Router»: голосовой AI, маршрутизирующий звонки в контакт-центре страховой компании
Суть
Голосовой AI-оператор страхового контакт-центра: речь на русском или казахском распознаётся двумя STT-движками, LLM выбирает сценарий из каталога в 40 сценариев, ответ строится по проверенным данным и озвучивается. Супервизор видит трассировку решения. По собственному README реализация не завершена: сквозной голосовой продукт пока не готов.
Как устроено
PCM16 16 кГц из браузера по WebSocket идёт параллельно в OpenAI STT/VAD и Yandex SpeechKit. Модуль DSR сводит две гипотезы в один текст. Модель gpt-realtime через tool call route_turn выбирает сценарии с учётом описаний, not_this_if, примеров RU/KK и слотов. Сервер проверяет результат по Zod-схеме и каталогу, при ошибке делает одну попытку исправления, затем политика уверенности выбирает исход: respond, clarify, handoff, out_of_scope или goodbye. Ответ собирается из фактов и озвучивается ElevenLabs V3. Контекст сессии и слоты хранятся в памяти, повторы хода идемпотентны.
Ключевые решения
- LLM routing via tool call
- Zod schema validation + retry
- confidence policy
- dual STT reconciliation (DSR)
- VAD
- idempotent turns
- slot tracking
Особенности
Две STT-системы (OpenAI и Yandex) работают параллельно, и их гипотезы сводятся в одну перед маршрутизацией. Выбор сценария делает LLM с серверной валидацией по каталогу и политикой уверенности, классификатора намерений нет. README прямо разделяет реализованное и непроверенное с живыми провайдерами и ведёт журнал прогресса по фазам.
Стек
TypeScript · Node.js 24 · Vue · H3 · Zod · shadcn-vue · pnpm · Docker · gRPC
Модели
gpt-realtime · OpenAI STT · Yandex SpeechKit · ElevenLabs V3
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-b269965b-talkit-team.
README
README.mdVoice Router
Голосовой AI-оператор для маршрутизации обращений в контакт-центре вымышленной страховой компании Saqta Insurance.
Кейс: HackAlem AI 2026, Voice Router
Владелец задачи: Halyk Bank / АО «Народный Банк Казахстана»
Дата симуляции данных: 2026-10-01
Voice Router должен принимать речь на русском и казахском языках, передавать согласованный транскрипт в LLM, выбирать один или несколько сценариев из каталога и отвечать по проверенным данным. Супервизор должен видеть выбранный маршрут, альтернативы, краткую причину решения и фактическое время этапов.
Главный критерий кейса — качество LLM-маршрутизации. Качество STT/TTS важно для рабочего голосового демо, но не заменяет корректный выбор сценария.
Текущий статус
Репозиторий содержит работающий starter и незавершённую реализацию Voice Router. Сквозной голосовой продукт пока не готов.
| Область | Фактическое состояние |
|---|---|
| Исходный набор | Подключены неизменённые 40 сценариев, 3 системных намерения, слоты, действия, база знаний, mock backend, примеры и dev-набор. |
| Контракты и каталог | Реализованы Zod-схемы входа/выхода, загрузка и перекрёстная проверка каталога, допустимых ID и слотов. |
| Текстовая маршрутизация | Есть серверный POST /api/voice-router/turn: вызов gpt-realtime, tool call route_turn, валидация результата, одна попытка исправления, политика уверенности и генерация ответа по выбранным фактам. |
| Контекст | В памяти процесса сохраняются активные сценарии, извлечённые слоты, язык ответа и серия низкой уверенности. Повторы одного turnId идемпотентны; конфликтующий повтор отклоняется. |
| Веб-интерфейс | Сейчас приложение открывает только starter-экран Echo. Клиентский экран Voice Router и панель супервизора ещё не подключены. |
| Голос | В src/web/features/voice-router/realtime/ подготовлены модули PCM-захвата, воспроизведения и WebSocket-клиента. Серверного WebSocket-маршрута, DSR, Yandex STT и ElevenLabs TTS в рабочем контуре ещё нет. |
| Действия | Контракты действий загружаются, но исполнитель над mock_backend.json, подтверждения и handoff ещё не реализованы. |
| Оценивание | Исходный evaluate.py присутствует, но runner реального маршрутизатора и подтверждённые метрики ещё не добавлены. |
| Deployment | Docker-сборка starter ранее проверялась локально. Публичный URL и сквозной голосовой deployment не зафиксированы. |
Актуальные свидетельства, ограничения и следующий шаг ведутся в docs/STATUS.md и docs/progress/. Наличие кода провайдера не означает, что живой внешний API уже проверен.
Целевой пользовательский путь
Микрофон
→ OpenAI STT + VAD ─┐
├→ DSR → согласованный текст
→ Yandex SpeechKit ─┘
→ gpt-realtime: выбор сценария
→ серверная валидация и политика
→ mock-действие / данные Saqta
→ текст ответа по фактам
→ ElevenLabs V3
→ звук в браузере + трассировка для супервизора
Текстовый резервный ввод должен входить в тот же обработчик после DSR. На текущем этапе реализована только серверная часть текстового пути до ответа; голосовой и пользовательский контуры ещё соединяются.
Почему нужен LLM-router
Ключевые слова недостаточны. Фраза «Я попал в аварию вчера, заявление уже подал; когда придут деньги?» относится к статусу страхового случая, а не к сценарию ДТП, которое произошло только что. Одна реплика также может содержать несколько намерений, смену темы или смешение русского и казахского языков.
LLM получает:
- текущую реплику и ограниченный контекст сессии;
- описания всех 40 сценариев и 3 системных намерений;
- границы
not_this_if, приоритеты и примеры RU/KK; - словарь слотов.
LLM возвращает структурированное решение. Сервер повторно проверяет его по Zod-схеме и каталогу, определяет исход respond, clarify, handoff, out_of_scope или goodbye и только затем формирует ответ.
Структура проекта
data/voice-router/ исходный набор организатора, без правок
data/voice-router-app/ производные привязки приложения к данным
src/shared/ браузер-безопасные Zod-контракты
src/server/
features/voice-router/ каталог, LLM-router, policy, сессии и HTTP API
features/echo/ инфраструктурная проверка starter
src/web/
features/voice-router/realtime/ подготовленный браузерный аудиотранспорт
features/echo/ текущий экран проверки Vue → H3
tests/ unit, API и SSR-регрессии
scripts/ setup, doctor, test, build smoke и snapshot
docs/ требования, фазы, runbooks и журнал прогресса
Ограничения архитектуры и правила разработки находятся в AGENTS.md. Полные требования и критерии приёмки — в TASK.md.
Требования
Для итоговой сдачи должны работать:
- голосовой ввод и голосовой ответ в браузере;
- содержательный выбор сценария через LLM, без encoder intent classifier;
- весь исходный каталог из 40 сценариев;
- русский, казахский и смешанная речь;
- несколько намерений и смена темы без потери контекста;
- трассировка после каждого хода: транскрипт, решение, причина, альтернативы и реальные задержки;
- ответы и mock-действия только по предоставленным данным;
- уточнение при неуверенности и handoff без выдуманного успеха;
- серверное подтверждение перед необратимым действием;
- воспроизводимый запуск и доступный deployment.
Ориентиры по скорости — около 500 мс на выбор сценария и до 1,5 секунды от конца речи до начала воспроизведения. Это бонусные показатели, а не основание скрывать ошибку или подменять реальный замер константой.
Локальный запуск
Требования
- Node.js
24.21+в ветке24.x; - pnpm
10.28.0; - ключ OpenAI — только для живого текстового LLM-маршрута.
Установка и настройка
В Phase 2 добавлен управляемый браузерный звонок на существующем /api/voice-router/ws: PCM16/16 кГц подаётся в OpenAI STT/VAD и Yandex SpeechKit, DSR сохраняет обе гипотезы и передаёт один подтверждённый текст тому же handleTurn, ответ озвучивается серверным ElevenLabs V3. Если STT, LLM или TTS недоступен, интерфейс показывает это явно. Локальные тесты не означают, что сквозной звонок с реальными провайдерами уже принят: платные API и микрофон в живом браузере не проверялись.
pnpm install --frozen-lockfile
pnpm run setup
setup создаёт .env из .env.example, только если .env ещё не существует. Секреты не должны попадать в Git или переменные VITE_*.
Для текущего серверного LLM-маршрута заполните:
OPENAI_API_KEY=...
Переменные Yandex и ElevenLabs уже перечислены в .env.example, но их наличие пока не включает рабочий DSR или TTS: соответствующие серверные интеграции ещё не подключены.
Development
pnpm dev
- Web:
http://127.0.0.1:5173 - API:
http://127.0.0.1:3000 - Health:
http://127.0.0.1:3000/api/health
Текущий web-экран проверяет только starter Echo. Текстовый Voice Router API можно вызвать отдельно:
curl -X POST http://127.0.0.1:3000/api/voice-router/turn \
-H "Content-Type: application/json" \
-d '{"sessionId":"123e4567-e89b-42d3-a456-426614174000","turnId":"123e4567-e89b-42d3-a456-426614174001","text":"Деньги списались, а полис не пришёл","source":"text"}'
Без OPENAI_API_KEY endpoint отвечает контролируемым статусом unavailable; фиктивный успешный маршрут не создаётся.
Сброс состояния сессии:
curl -X POST http://127.0.0.1:3000/api/voice-router/reset \
-H "Content-Type: application/json" \
-d '{"sessionId":"123e4567-e89b-42d3-a456-426614174000"}'
Проверка
pnpm run check
pnpm run verify
checkзапускает TypeScript/Vue typecheck и тесты Node.verifyпоследовательно запускает doctor, проверку форматирования,check, production build и smoke собранного приложения.- Обычные тесты не требуют платных API-ключей.
- Живые проверки OpenAI, Yandex, ElevenLabs, микрофона и deployment должны фиксироваться отдельно; unit-тесты не доказывают их работоспособность.
Другие полезные команды:
| Команда | Назначение |
|---|---|
pnpm run doctor | Проверить версию Node, lockfile, локальный .env и H3 1.x. |
pnpm run build | Собрать сервер в dist/server и Vue-приложение в dist/web. |
pnpm start | Запустить production-сборку. |
pnpm run smoke -- <URL> | Проверить health, API и web уже запущенного экземпляра. |
pnpm run format | Отформатировать репозиторий; команда изменяет файлы. |
Подробный порядок проверки: docs/runbooks/VALIDATION.md.
Docker
docker compose up --build -d
pnpm run smoke -- http://127.0.0.1:3000
Compose публикует порт только на localhost. Это не является публичным deployment. Ключи провайдеров нужно передавать серверу безопасным способом; в текущем compose.yaml они намеренно не заданы.
Данные и безопасность
- Saqta Insurance, клиенты, полисы, цены, клиники и операции полностью синтетические.
- Данные не описывают реальные продукты Halyk и не являются юридической консультацией.
- Относительные даты вычисляются от
2026-10-01, а не от даты запуска приложения. - Исходные файлы
data/voice-router/нельзя менять ради улучшения метрик. - Приложение не должно выполнять реальные звонки, платежи, отправку SMS или изменение production-систем.
- API-ключи остаются только на сервере и не попадают в публичные DTO, трассировку или клиентский bundle.
Документация
TASK.md— полные требования и финальная приёмка.docs/README.md— карта документации.docs/STATUS.md— текущий рабочий снимок.docs/implementation/README.md— фазы реализации.docs/runbooks/DEMO.md— сценарий демонстрации.docs/runbooks/SUBMISSION.md— контроль перед сдачей.data/voice-router/README.ru.md— оригинальное описание набора на русском языке.
Происхождение
Starter, комплект организатора и возможные компоненты TalkIt не считаются результатом конкурсных пяти часов. Их происхождение, ограничения использования и новый вклад раскрываются в docs/compliance/PREEXISTING.md. Историю, результаты тестов, метрики и разрешения нельзя восстанавливать или отмечать задним числом.
Настройка голосового звонка после интеграции
Для голосового звонка задайте серверные ключи нужных провайдеров из .env.example. Нажатие на визуализатор устанавливает WebSocket-соединение и запускает запись после разрешения микрофона; повторное нажатие завершает звонок. «Закончить реплику» отправляет записанный звук на обработку. Микрофон требует localhost или HTTPS. Локальному запуску нужен ffmpeg в PATH; runtime Docker-образ устанавливает его. Ручного кода доступа нет: перед публичным развёртыванием обеспечьте внешнее ограничение доступа и расходов как для голоса, так и для текстового API. Исполнение действий относится к следующей фазе и не подключено.
Авторы коммитов
История коммитов
- 2026-09-23 17:59Update README.mdTemirlan Zhangazy
- 2026-09-23 17:58Restore repository state to 60f8954Temirlan Zhangazy
- 2026-09-23 17:54Revert "Merge upstream voice updates and reconcile README documentation"Temirlan Zhangazy
- 2026-09-23 17:53Revert "Merge latest upstream, keep web implementation and finish README sync"Temirlan Zhangazy
- 2026-09-23 17:52dockerfile doneNIMARS
- 2026-09-23 17:51Merge latest upstream, keep web implementation and finish README syncNIMARS
- 2026-09-23 17:48Merge upstream voice updates and reconcile README documentationNIMARS
- 2026-09-23 17:47final polishesTemirlan Zhangazy
Все коммиты (33)
- 2026-09-23 17:44docs: align all READMEs with Voice Router implementationNIMARS
- 2026-09-23 17:42test: cover Voice Router spec conversation and tracetagayevafarida
- 2026-09-23 17:42Merge branch 'main' of https://github.com/BAITC-Hacks/hack-b269965b-talkit-teamTemirlan Zhangazy
- 2026-09-23 17:41Improve voice STT/DSR/TTS orchestration and diagnosticsTemirlan Zhangazy
- 2026-09-23 17:22Integrate upstream voice transport with local audio fixesNIMARS
- 2026-09-23 17:10Add browser voice WS, Yandex STT, DSR & UITemirlan Zhangazy
- 2026-09-23 16:59Merge branch 'main' of https://github.com/BAITC-Hacks/hack-b269965b-talkit-teamTemirlan Zhangazy
- 2026-09-23 16:46voice steck v2NIMARS
- 2026-09-23 16:03Merge branch 'main' of https://github.com/BAITC-Hacks/hack-b269965b-talkit-teamTemirlan Zhangazy
- 2026-09-23 16:03Add Phase 1 voice-router text pipelineTemirlan Zhangazy
- 2026-09-23 16:00Merge branch 'main' of https://github.com/BAITC-Hacks/hack-b269965b-talkit-teamtagayevafarida
- 2026-09-23 15:59init main audio protocols and socketsNIMARS
- 2026-09-23 15:44Merge branch 'main' of https://github.com/BAITC-Hacks/hack-b269965b-talkit-teamtagayevafarida
- 2026-09-23 15:44Create VoiceVisualizer.vuetagayevafarida
- 2026-09-23 15:32Add voice-router feature and docs updatesTemirlan Zhangazy
- 2026-09-23 15:09checked state before voice imp startedNIMARS
- 2026-09-23 14:54Add voice-router dataset, schemas, docs, testsTemirlan Zhangazy
- 2026-09-23 14:45Merge branch 'main' of https://github.com/BAITC-Hacks/hack-b269965b-talkit-teamtagayevafarida
- 2026-09-23 14:44voip + websocket todoNIMARS
- 2026-09-23 14:41Merge branch 'main' of https://github.com/BAITC-Hacks/hack-b269965b-talkit-teamtagayevafarida
- 2026-09-23 14:41Add shadcn-vue Button and Textarea to the existing echo screen.tagayevafarida
- 2026-09-23 14:09DOCS: Implementation phasesTemirlan Zhangazy
- 2026-09-23 13:54Implemented the Web folder structuretagayevafarida
- 2026-09-23 13:35Add starter scaffold (server, web, tests)Temirlan Zhangazy
- 2026-09-23 13:31README.md v1 by tech taskNIMARS
Структура репозитория (210 файлов)
.dockerignore .editorconfig .env.example .gitattributes .gitignore .node-version .npmrc .nvmrc .prettierignore .prettierrc.json AGENTS.md Dockerfile README.md TASK.md components.json compose.yaml data/protos/yandexcloudapis/LICENSE.google data/protos/yandexcloudapis/LICENSE.yandex data/protos/yandexcloudapis/README.md data/protos/yandexcloudapis/source/google/api/annotations.proto data/protos/yandexcloudapis/source/google/api/http.proto data/protos/yandexcloudapis/source/google/rpc/status.proto data/protos/yandexcloudapis/source/yandex/cloud/ai/stt/v3/stt.proto data/protos/yandexcloudapis/source/yandex/cloud/ai/stt/v3/stt_service.proto data/protos/yandexcloudapis/source/yandex/cloud/api/operation.proto data/protos/yandexcloudapis/source/yandex/cloud/operation/operation.proto data/protos/yandexcloudapis/source/yandex/cloud/validation.proto data/voice-router-app/knowledge-bindings.json data/voice-router/README.kz.md data/voice-router/README.md data/voice-router/README.ru.md data/voice-router/actions.json data/voice-router/dev_utterances.json data/voice-router/dialogs_sample.json data/voice-router/evaluate.py data/voice-router/knowledge_base.json data/voice-router/mock_backend.json data/voice-router/scenarios.json data/voice-router/slots.json docs/DECISIONS.md docs/README.md docs/STATUS.md docs/compliance/IMPORT.md docs/compliance/PREEXISTING.md docs/implementation/OWNERSHIP.md docs/implementation/README.md docs/implementation/phases/00_BASELINE.md docs/implementation/phases/01_TEXT_PIPELINE.md docs/implementation/phases/02_DSR_VOICE.md docs/implementation/phases/03_DIALOG_ACTIONS.md docs/implementation/phases/04_TRACE_UI.md docs/implementation/phases/05_EVALUATION.md docs/implementation/phases/06_SUBMISSION.md docs/progress/2026-09-22_starter-validation.md docs/progress/2026-09-23_13-54_web-folder-organization.md docs/progress/2026-09-23_14-40_integrator_phase0.md docs/progress/2026-09-23_14-41_web-shadcn-components.md docs/progress/2026-09-23_14-59_integrator_docker-recheck.md docs/progress/2026-09-23_15-05_codex_todo-review.md docs/progress/2026-09-23_15-57_codex_phase1-text-pipeline.md docs/progress/2026-09-23_16-29_codex_phase1-latency.md docs/progress/2026-09-23_16-34_codex_voice-router-ui.md docs/progress/2026-09-23_16-58_codex_readme-refresh.md docs/progress/2026-09-23_17-16_codex_vad-autoend.md docs/progress/2026-09-23_17-20_codex_pull-integration.md docs/progress/2026-09-23_17-24_codex_tts-failure-diagnostics.md docs/progress/2026-09-23_17-32_codex_stt-orchestration.md docs/progress/2026-09-23_17-37_codex_voice-router-trace.md docs/progress/2026-09-23_17-46_codex_early-route-result.md docs/reference/API_NOTES.md docs/reference/BRIEF.template.md docs/runbooks/DEMO.md docs/runbooks/DEPLOY.md docs/runbooks/SUBMISSION.md docs/runbooks/VALIDATION.md package.json pnpm-lock.yaml pnpm-workspace.yaml roman_sol_medium_todo.md scripts/clean.mjs scripts/doctor.mjs scripts/eval-router.mjs scripts/setup.mjs scripts/smoke-built.mjs scripts/smoke-lib.mjs scripts/smoke.mjs scripts/snapshot.mjs scripts/test.mjs src/server/app.ts src/server/env.ts src/server/features/echo/routes.ts src/server/features/voice-router/catalog.ts src/server/features/voice-router/controller.ts src/server/features/voice-router/model.ts src/server/features/voice-router/openai-realtime.ts src/server/features/voice-router/policy.ts src/server/features/voice-router/provider-config.ts src/server/features/voice-router/router.ts src/server/features/voice-router/routes.ts src/server/features/voice-router/state.ts src/server/features/voice/dsr.ts src/server/features/voice/elevenlabs-tts.ts src/server/features/voice/openai-stt.ts src/server/features/voice/transport.ts src/server/features/voice/yandex-grpc.ts src/server/features/voice/yandex-stt.ts src/server/http/json.ts src/server/index.ts src/server/server.ts src/shared/contracts.ts src/shared/voice-router.ts src/shared/voice.ts src/web/App.vue src/web/README.md src/web/components/layout/AppHeader.vue src/web/components/ui/BaseButton.vue src/web/components/ui/VoiceVisualizer.vue src/web/components/ui/badge/Badge.vue src/web/components/ui/badge/index.ts src/web/components/ui/bubble/Bubble.vue src/web/components/ui/bubble/BubbleContent.vue src/web/components/ui/bubble/BubbleGroup.vue src/web/components/ui/bubble/BubbleReactions.vue src/web/components/ui/bubble/index.ts src/web/components/ui/button/Button.vue src/web/components/ui/button/index.ts src/web/components/ui/card/Card.vue src/web/components/ui/card/CardAction.vue src/web/components/ui/card/CardContent.vue src/web/components/ui/card/CardDescription.vue src/web/components/ui/card/CardFooter.vue src/web/components/ui/card/CardHeader.vue src/web/components/ui/card/CardTitle.vue src/web/components/ui/card/index.ts src/web/components/ui/field/Field.vue src/web/components/ui/field/FieldContent.vue src/web/components/ui/field/FieldDescription.vue src/web/components/ui/field/FieldError.vue src/web/components/ui/field/FieldGroup.vue src/web/components/ui/field/FieldLabel.vue src/web/components/ui/field/FieldLegend.vue src/web/components/ui/field/FieldSeparator.vue src/web/components/ui/field/FieldSet.vue src/web/components/ui/field/FieldTitle.vue src/web/components/ui/field/index.ts src/web/components/ui/label/Label.vue src/web/components/ui/label/index.ts src/web/components/ui/separator/Separator.vue src/web/components/ui/separator/index.ts src/web/components/ui/slider/Slider.vue src/web/components/ui/slider/index.ts src/web/components/ui/switch/Switch.vue src/web/components/ui/switch/index.ts src/web/components/ui/textarea/Textarea.vue src/web/components/ui/textarea/index.ts src/web/components/ui/toggle-group/ToggleGroup.vue src/web/components/ui/toggle-group/ToggleGroupItem.vue src/web/components/ui/toggle-group/index.ts src/web/components/ui/toggle/Toggle.vue src/web/components/ui/toggle/index.ts src/web/features/echo/api.ts src/web/features/echo/components/EchoPanel.vue src/web/features/echo/useEcho.ts src/web/features/voice-router/api.ts src/web/features/voice-router/components/VoiceCallPanel.vue src/web/features/voice-router/components/VoiceChatPanel.vue src/web/features/voice-router/components/VoiceRouterPanel.vue src/web/features/voice-router/index.ts src/web/features/voice-router/realtime/audio-capture.ts src/web/features/voice-router/realtime/audio-playback.ts src/web/features/voice-router/realtime/client.ts src/web/features/voice-router/realtime/errors.ts src/web/features/voice-router/realtime/index.ts src/web/features/voice-router/realtime/pcm-capture.worklet.js src/web/features/voice-router/realtime/protocol.ts src/web/features/voice-router/realtime/socket.ts src/web/features/voice-router/useVoiceRouter.ts src/web/index.html src/web/lib/health.ts src/web/lib/http.ts src/web/lib/utils.ts src/web/main.ts src/web/pages/EchoPage.vue src/web/pages/VoiceRouterPage.vue src/web/styles/base.css src/web/styles/index.css src/web/styles/tokens.css tests/api.test.ts tests/config.test.ts tests/json.test.ts tests/shared-contracts.test.ts tests/voice-router-baseline.test.ts tests/voice-router-phase1.test.ts tests/voice-router-tz-acceptance.test.ts tests/voice/dsr.test.ts tests/voice/elevenlabs-tts.test.ts tests/voice/realtime-client.test.ts tests/voice/realtime-level.test.ts tests/voice/transport.test.ts tests/voice/voice-router-cancel.test.ts tests/voice/yandex-stt.test.ts tests/web-api-client.test.ts tests/web-app-ssr.test.ts tsconfig.base.json tsconfig.json tsconfig.server.build.json tsconfig.server.json tsconfig.tools.json tsconfig.web.json vite.config.ts
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.