# WebRTC / Voice AI

Короткая голосовая или текстовая сессия через собственную WebRTC-инфраструктуру. Транспорт и LangGraph работают реально, ASR/LLM/TTS заменены обозначенными моками.

Статус: passed

Версия: `b16c97886356285f0c0624d16803dd96edcef2e8`. История кода: с октября 2025. Публичный runtime и собственный TURN: 7 сентября 2026.

## Задача

Диалоговый runtime должен связать сигналинг, передачу медиа, маршрутизацию запроса и завершение сессии, не оставляя незакрытых соединений.

## Аудитория

Разработчики голосовых интерфейсов и real-time AI-систем.

## Личный вклад

- WebRTC-транспорт, обмен через DataChannel и оркестрация исходного LangGraph.

- Сценарии echo, маршрутизация в free_speech, отмена по таймауту и повтор.

- Собственный ограниченный TURN, короткие сессии и наблюдаемые счётчики без записи аудио.

## Решения

- Браузер использует relay через собственный coturn. SDP ограничен одним аудиопотоком и DataChannel.

- Исходный WorkflowEngine и handlers сохранены; MockIO заменяет внешние AI-вызовы.

- ASR возвращает фиксированную фразу только после фактического получения аудиокадров. Вместо TTS передаётся короткий тон.

- Сессия живёт 90 секунд и допускает десять сообщений. Старое закрытие соединения проверяет идентичность сессии и не завершает новую.

## Границы и ограничения

- Фиксированная ASR-фраза не является распознаванием речи. Короткий тон не является синтезированной речью.

- Одновременно разрешены две сессии; третья отклоняется. Аудио не записывается, контекст хранится в памяти и удаляется.

- TURN разрешает peer только на адресе собственной VPS, имеет квоты и короткие credentials.

- Микрофонные QA-потоки созданы синтетическим устройством Chromium; передача DTLS/SRTP, TURN и счётчики медиа реальны.

## Что попробовать

- Подключитесь без микрофона и отправьте echo: ответ проходит исходный граф.

- Проверьте шаблонный ответ, таймаут LLM и успешный запрос после отмены.

- Разрешите микрофон, дождитесь аудиокадров на сервере и примените мок ASR.

- Откройте stats, сбросьте контекст и дождитесь автоматического завершения через 90 секунд.

## Проверки

- Два unit-теста графа, 14 production-кадров, две одновременные аудиосессии и фактический TTL 90 секунд.

- Real relay/DataChannel, original echo, mock LLM, timeout then retry, context reset and close

- Two real simultaneous audio sessions; third rejected; closing other session preserves first

- Actual 90-second expiry closes RTC and deletes context

- Подтверждённого GitHub CI для этого SHA нет; перечислены выполненные локальные проверки и сценарии опубликованной версии. Исторические CI-ссылки, если есть, отмечены отдельно.

## Результаты

- Сохранено 14 содержательных кадров фактического интерфейса.

- Дата, сценарий и версия указаны у каждого кадра.

- Максимум RAM контейнеров в коротком тесте: 89.62 MiB. Общие службы учитываются отдельно; магазин включает свою БД.

- Измерено 2 повторений сценария; медиана 3660.5 мс, максимум 4226 мс. Время включает браузер и HTTPS; методика и ограничения приложены.

- Два аудиопотока подтверждены серверными кадрами и browser getStats. Третье подключение отклонено лимитом.

## Запуск и эксплуатация

- Исходная архитектура допускает внешние ASR/LLM/TTS. Публичный runtime использует только MockIO и не хранит аудио. Реальная транспортная часть проверена getStats и серверным счётчиком кадров.

- Dockerfile.portfolio собирается вне VPS. App слушает 127.0.0.1:18406, coturn — 3478 UDP/TCP. Host network нужен для ICE на публичном интерфейсе; служебные HTTP-порты наружу не открыты.

- TURN-конфигурация root-only содержит auth secret; её нельзя публиковать в документации. Разрешён только peer VPS, не произвольные внешние адреса. До 16 краткоживущих TURN allocations, но не более двух пользовательских RTC-сессий.

- Проверять /healthz, ICE relay, поступающие кадры и DataChannel. При сетевом сбое завершить сессию и повторить. TTL закрывает соединение через 90 секунд; таймаут графа через две секунды не мешает следующему запросу.

- Общие правила эксплуатации: готовые файлы и закреплённые образы собираются вне VPS; секреты не входят в артефакты. Общесерверный docker prune не применяется. Логи backend ограничены ротацией, изменения log options требуют пересоздания контейнера.

- Перед изменением пройти code-map от функции к экрану/API, модулю и данным. После изменения обновить карту и схемы, выполнить связанные сценарии, включая ошибку, повтор и сброс. Источник истины — конкретная версия кода, а не старая диаграмма.

- Откат проверен 2026-09-07T01:06:33.669720+00:00: 20260907-verified → 20260907e → 20260907-verified. После каждого переключения подтверждены HTTPS, readiness и SHA. Это проверка конфигурации и артефактов на одном проверенном SHA; обратная миграция БД не выполнялась.

## Стек и роль

- **WebRTC / aiortc**: Настоящие ICE, DTLS/SRTP, аудио и DataChannel.
- **coturn**: Собственный ограниченный relay, временные credentials и peer allowlist.
- **Python / aiohttp**: HTTPS-сигналинг за Nginx, жизненный цикл сессий и счётчики.
- **LangGraph**: Исходная маршрутизация echo/free_speech, память и отмена.

## Code-map

|Функция|Экран / API|Модуль|Данные / проверка|
|---|---|---|---|
|Сигналинг|GET config / POST offer|portfolio/server.py config, offer|HMAC cookie; SDP validate; production QA|
|Аудио|Микрофон / stats|receive_audio; MockTone|кадры без записи; два настоящих потока|
|Граф|DataChannel text/asr|portfolio/runtime.py; WorkflowEngine|echo/free_speech; два unit-теста|
|Таймаут / повтор|LLM timeout button|DemoRuntime.run / asyncio.wait_for|отмена через 2 с; успешный повтор|
|Срок сессии|status / DELETE session|expiry; close; state_change|память; 90-секундная production QA|

## Схемы

- Компоненты опубликованного демо — `diagrams/components.mmd`
- Основной сценарий — `diagrams/scenario.mmd`
- Размещение опубликованной версии — `diagrams/deployment.mmd`
- Исходный интеграционный контур — не развёрнут в демо — `diagrams/original.mmd`

## Индекс изображений

- 01-overview: Назначение, реальный транспорт и явные моки ASR/LLM/TTS; 2026-09-07T00:37:52.304Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 02-disconnected: Начальное состояние с выбором микрофонного или текстового режима; 2026-09-07T00:37:52.780Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 03-connected: Реальный WebRTC DataChannel и ICE relay через собственную VPS; 2026-09-07T00:37:55.095Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 04-echo: Исходный LangGraph echo без вызова модели; 2026-09-07T00:37:55.684Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 05-llm-mock: Ответ через исходный граф и детерминированный MockIO; 2026-09-07T00:37:56.199Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 06-timeout: Отмена графа по таймауту через две секунды; 2026-09-07T00:37:59.018Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 07-retry: Успешный запрос после отмены предыдущего графа; 2026-09-07T00:37:59.517Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 08-stats: Фактические getStats, принятые аудиобайты и серверные счётчики; 2026-09-07T00:37:59.988Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 09-audio-ingress: Настоящие микрофонные кадры на сервере; ASR отдаёт явно фиксированную фразу; 2026-09-07T00:38:03.574Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 10-second-audio-mobile: Вторая параллельная голосовая сессия на мобильном экране; 2026-09-07T00:38:07.508Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 11-session-limit: Третье соединение отклонено серверным лимитом двух сессий; 2026-09-07T00:38:08.479Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 12-mobile-overview: Мобильное описание транспорта и моков; 2026-09-07T00:38:08.930Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 13-mobile-dialog: Текстовый сценарий через WebRTC на узком экране; 2026-09-07T00:38:11.431Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8
- 14-expiry: Автоматическое закрытие и удаление контекста через 90 секунд; 2026-09-07T00:39:41.262Z; SHA b16c97886356285f0c0624d16803dd96edcef2e8

## Подробности runtime из репозитория

# Voice AI: один Python runtime и собственный WebRTC relay

Адрес https://voice.komaroff-dev.ru/. Демо подготовлено 7 сентября 2026.
Дата подготовки не является датой начала разработки исходной системы.

## Что работает реально

Браузер создаёт RTCPeerConnection с audio + DataChannel. HTTPS используется только
для config/SDP/status; сообщения сценария идут через настоящий DataChannel.
Собственный coturn на этой VPS принудительно используется как relay. SDP фильтруется
до публичного адреса VPS; TURN может обращаться только к этой же VPS, без произвольного
проксирования. Краткоживущие TURN credentials действуют 120 секунд. Медиа защищены DTLS/SRTP.

`portfolio.runtime.DemoRuntime` вызывает исходный `src_langgraph.engine.WorkflowEngine`.
Используются исходные dispatch, echo, free_speech, contracts и bounded conversation memory.
RuntimeIO заменён MockIO; отдельные NATS/LLM/ASR/TTS/Ruby процессы в демо не запускаются.
Это консолидация публичного сценария, а не исходная распределённая топология проекта.

ASR отдаёт фиксированную фразу только после приёма аудиокадров; это не транскрипция речи.
LLM router/response детерминированы. TTS заменён коротким синусоидальным тоном и текстом.
По команде «таймаут» мок задерживается, asyncio.wait_for отменяет граф через две секунды.
Сценарий echo проходит настоящий граф без LLM-вызова.

## Изоляция и ограничения

Два одновременных соединения, одно на подписанную cookie, до 90 секунд и 10 сообщений.
Текст до 280 символов, один audio stream и один DataChannel. Произвольные uploads,
внешние tools, API и запись аудио отключены. Сервер считает кадры и сразу отбрасывает их.
Контекст хранится только в памяти процесса, удаляется при завершении/таймауте/рестарте.
Сброс очищает контекст, сохраняя лимит сообщений и время текущей сессии.
Микрофон активируется только соответствующей кнопкой. Есть полноценный текстовый режим
без разрешения на микрофон; транспорт остаётся WebRTC.

## Code-map

| Функция | Экран/API | Модуль | Данные / проверка |
|---|---|---|---|
| Подключение | кнопка → config/offer | portfolio/server.py, исходный validate_sdp | SDP + короткая TURN auth; live ICE QA |
| Аудио | getUserMedia → RTCPeerConnection | receive_audio / MockTone | только счётчики, без записи; media byte/frame QA |
| Текст | DataChannel portfolio | handle_message → DemoRuntime | память одной сессии; runtime tests |
| Оркестрация | события LangGraph | src_langgraph.engine + scenarios | исходные trace/runtime contracts |
| Timeout/retry | «Таймаут LLM» | MockIO + wait_for | отмена графа; timeout then echo test |
| Завершение | DELETE session / 90 s | close + expiry | освобождение PC/track/tasks/memory |

Перед изменением проследить сигналинг, ICE, медиа, DataChannel, граф и ответ. После правки
обновить карту/схемы и проверить два соединения, таймаут, повтор и очистку. Рабочую и
демонстрационную архитектуру показывать раздельно.

## Сборка / эксплуатация

Dockerfile.portfolio: Python3.12 digest + полный requirements.lock. Сборка вне VPS.
Runtime 384 MiB / 0.8 CPU, coturn 96 MiB / 0.3 CPU, logs 3×5 MB.
Host networking нужен для ICE публичного интерфейса; HTTP привязан только к 127.0.0.1:18406.
Открыт TURN3478 UDP/TCP, relay ports 49160–49179 используются локальными peers этой VPS.
TURN quota 2 на пользователя / 8 всего, bandwidth 128000 B/s на сессию и 512000 B/s всего.
Healthcheck приложения проверяет процесс; фактическое прохождение медиа проверяется отдельно.
LangSmith tracing отключён. Секреты только в серверном .env, не в Git или образе.

Источники технических контрактов: https://aiortc.readthedocs.io/en/latest/api.html,
https://github.com/coturn/coturn/blob/master/README.turnserver.
