Голосовые системы

webrtc-komaroff: от реплики до изменения карты

Архитектурный снимок от 16 сентября 2026 года. Исследован удалённый main 772de4a714ae6c5abea91946e85a1953ee919ec3 в отдельном чистом worktree. Локальный checkout, в котором лежит этот документ, оставлен на своей исходной версии. Существующие презентации и сгенерированный OpenWiki не использовались как доказательства.

Техническая презентация · Реестр доказательств · Восемь предложений по развитию · Проверка материалов

Что делает система

Браузер принимает текст или голос. Core завершает WebRTC-соединение, передаёт звук соседнему ASR-сервису и отправляет готовую реплику Agent. Agent загружает состояние разговора из Postgres и вызывает выбранный workflow. Workflow обращается к LLM и инструментам, после чего Agent сохраняет результат и публикует команды интерфейсу. Пользователь видит ответ и, для авиационных сценариев, аэропорты или маршрут на карте.

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

Везде ниже VERIFIED / SOURCE означает подтверждение кодом, тестом или декларацией. INFERRED — вывод из реализации без нагрузочного эксперимента. UNKNOWN — слой, который в этом исследовании не проверялся. Развёрнутые образы, число реплик и live-поведение не установлены.

Граница проходит между диалогом и распознаванием

Компоненты и владельцы

Браузер владеет микрофоном и состоянием отображения. Core владеет живым peer connection; его нельзя восстановить из записи диалога. Agent владеет постоянным состоянием сессии. LLM gateway изолирует вызов модели, а API gateway исполняет инструменты.

py_faster_whisper — соседний репозиторий: он получает PCM и возвращает текст. Его же образ обслуживает файловый WebUI, но публичные маршруты этого UI объявлены в stack/webrtc.drs. GPU backends и принимающая сторона межстековых proxy принадлежат внешнему rag-stack; его код в этот аудит не включён.

Источники: Core routes, AgentServer, владение ingress.

Где находится реализация

Структура модулей

Модуль Ответственность и вход Состояние / выход
src_front/src TypeScript UI, WebRTC, HTTP-команды, NATS WebSocket sessionStorage, сообщения, MapLibre
src_core aiohttp: /core/offer, /core/message, /core/history, /core/init_map peer connections и audio tasks в памяти
src_agent NATS nats.agent.<user_id>, история сессии sessions, runtime_state, events в Postgres
src_langgraph Python workflow, LangGraph runtime передаётся в запросе и ответе
src_langgraph_rb Ruby workflow через собственный DSL/адаптер альтернативная реализация сценариев
src_langgraph_rb_node Ruby workflow на AsyncGraph 0.1.2 граф, память и результаты внешних шагов
src_llm NATS LLM request → Ollama или локальная llama.cpp-модель ответ нормализуется под режим запроса
src_api_gateway FastAPI и NATS tools каталог аэропортов, демонстрационные данные
src_shared/contracts Pydantic DTO и subjects Python-сервисов граница совместимости JSON
src_postgres, docker, stack schema/migrations, build, желаемое развёртывание образы, volume, stack contract

Название ruby_node не означает отдельный процесс Node.js. Здесь «node» — узел графа; командой запуска служит Ruby, а зависимостью — async-graph.

Корни src_* соответствуют выбранному корпоративному профилю. Их перенос в api-gateway/src или front/src не сокращает legacy. Framework-каталог src_front/src имеет другую роль и остаётся внутри service root.

Текстовый запрос проходит через два уровня оркестрации

Последовательность текстового запроса

Core преобразует HTTP payload в сообщение с trace_id, request_id, session_id и turn_id. Agent создаёт или обновляет строку сессии, читает runtime и передаёт его workflow. Возвращённый next_runtime записывается по ожидаемой версии; история записывается отдельной транзакцией. После этого Agent публикует UI-события и отвечает Core.

Workflow не выбирается автоматически по готовности соседей. В DRS WORKFLOW_RUNTIME по умолчанию равен ruby_node, а Agent направляет запросы на nats.workflow.run.ruby.node. Альтернативы используют другие subjects. При отсутствии responder Agent делает ограниченные повторы, затем возвращает workflow_unavailable; перехода на другой движок в этой ветке нет.

Новый запрос в сценарии аэропортов обычно вызывает модель дважды: routing_decision, затем tool_params. Далее инструменты возвращают позицию и аэропорты. build_route вызывается только при запросе маршрута; координаты и финальная фраза уже формируются без дополнительных LLM-вызовов. Это закреплено в тестах сценария.

Голос добавляет поток с собственным жизненным циклом

Голосовой путь

После SDP offer Core создаёт RTCPeerConnection. Полученный audio track проходит TrackSourceNode: выход — mono PCM s16, 16 кГц, кадры по 20 мс. WhisperStreamNode публикует их на inference.whisper.stream.<token> и слушает inference.whisper.text.<token>.

Граница между репозиториями — бинарный пакет:

[длина metadata: u32 big-endian][metadata JSON UTF-8][PCM int16 little-endian]
metadata: type=frame|end, seq, sample_rate, sample_width, channels, session_id

Whisper делает VAD — выделение речи среди тишины — и отдаёт pending, затем текст с phrase_id. Core преобразует этот идентификатор в стабильный turn_id и вызывает тот же обработчик диалога. Наличие поля seq у отправителя не означает проверки последовательности у получателя: в исследованном bridge эта проверка не найдена.

PCM при этих настройках занимает 32 000 байт/с на поток, без metadata и транспортных накладных расходов; каждый 20-мс кадр несёт 640 байт звука. Это расчёт формата, а не измеренная пропускная способность.

Как пройти код до побочного эффекта

Выборочное дерево вызовов

message_handler вызывает handle_transcription; тот переходит через NATS к AgentServer.handle_request. AgentRunner.run читает состояние, а _run_with_optimistic_lock вызывает workflow и сохраняет ответ. В выбранном Ruby runtime Engine.run выбирает сценарий, GraphRunner.run доходит до внешнего await, а RequestExecutor.call направляет его в RuntimeIo.call_llm или call_tool.

Это дерево описывает конкретную реализацию, а не обещание durable execution. GraphRunner хранит request_results в локальном hash на время вызова. Python StateGraph компилируется без checkpointer. Сохранённый между репликами runtime не равен восстановлению незавершённого внешнего шага после падения процесса.

Постоянная история и runtime записываются раздельно

Данные и точки фиксации

sessions связывает диалог с пользователем. runtime_state хранит версию, активный сценарий, pending и JSON-контекст. events хранит сообщения, а уникальность (session_id, turn_id) защищает строки истории от одинаковых ключей.

Agent сначала фиксирует runtime условным UPDATE, затем вызывает persist_chat_turn. Две записи истории внутри этой функции атомарны между собой, но не с предшествующим UPDATE runtime. Если история не запишется после успешного UPDATE, общий retry повторит workflow. Внешние обращения уже могли выполниться. INFERRED: при конфликте или отказе в этой точке возможны лишние LLM/tool-вызовы и расхождение runtime с историей. Частота в production неизвестна.

Редактирование реплики удаляет историю от найденного event_id и сбрасывает активный pending workflow перед пересчётом. Это контракт ветвления разговора, который нельзя потерять при замене движка.

Источники: AgentRunner, runtime repository, история и транзакции, schema.

Контракты, которые связывают сервисы

Контракт Производитель → потребитель Что нельзя менять отдельно
POST /core/offer браузер → Core SDP и session identity
POST /core/message браузер → Core text, session_id, turn_id, edit
GET /core/history браузер → Core → Agent порядок сообщений и runtime_context
nats.agent.<user_id> Core → Agent envelope и Agent response
nats.workflow.run.{python,ruby,ruby.node} Agent → выбранный runtime runtime version, status, errors, client_events
nats.llm.<user_id> workflow → LLM mode и нормализованный data/error
nats.tools.<tool_name> workflow → API gateway args и tool result
nats.events.<user_id> Agent/Core → браузер command, artifacts, trace, порядок UI
inference.whisper.stream/text.<token> Core ↔ Whisper бинарный PCM и JSON результата

Pydantic использует extra="forbid", а Ruby поддерживает свои validators. Даже добавление поля в JSON может потребовать согласованного rollout. Общей автоматически проверяемой схемы для Python, Ruby и TypeScript в исследованном наборе нет.

В браузере eventsSubject зафиксирован как nats.events.user123. Это не доказательство межпользовательской изоляции. Для многопользовательского режима нужны отдельная модель identity, server-side разрешения на сессию и проверка subscriptions.

Инструменты пока ограничивают смысл продукта

API gateway использует локальный список аэропортов, фиксированную текущую позицию, демонстрационные сведения о рейсах и погоде. Маршрут строится геометрически по дуге большого круга. Он не подтверждает дорожный маршрут, время проезда или актуальную авиационную информацию.

Поэтому архитектуру разумно оценивать как стенд голосового интерфейса и сценарной оркестрации. Вынос инструментов в отдельный сервис оправдан будущей интеграцией с реальными источниками, но сами эти источники пока не реализованы в проверенных handlers. Перед продуктовым использованием нужны явные provenance, freshness и обработка отсутствующих данных.

Источник: API gateway, функции _current_position, _tool_get_weather, _lookup_flight, _build_route_data.

Сборка и развёртывание описаны отдельно от исполнения

Слои поставки

Compose содержит производимые образы и локальные runtime-настройки. Python build contexts указывают на src_*; Ruby contexts сейчас охватывают корень репозитория. DRS объявляет все три workflow-сервиса, а значение WORKFLOW_RUNTIME определяет путь Agent. Объявленный невыбранный движок нельзя автоматически считать удаляемым: сначала нужен учёт его consumers.

В корневом CI отдельная job tests закомментирована. В этом исследовании не подтверждён обязательный gate всех тестов перед публикацией. Успех build/deploy job сам по себе это не восполняет. Pipeline #4640 ранее прошёл для этого же commit; это историческое build/deploy evidence, не новый live smoke.

Слой Результат этого исследования
Source VERIFIED: точный commit, source anchors, clean worktree
Локальные tests 25 Ruby examples и 7 Python tests прошли 16.09.2026
Desired stack VERIFIED: декларации DRS; не исполнялись
Running image / replicas / health UNKNOWN: runtime не перепроверялся
Голос/карта в live environment UNKNOWN: новый end-to-end прогон не выполнялся

Проверки покрывают сценарии, но оставляют транспортные пробелы

В src_langgraph_rb_node/spec есть проверки registry, engine, memory, графов и сценариев. Они подтверждают, например, что вопрос о названии аэропорта не строит маршрут и явный радиус сохраняется при неудаче модели. tests/test_llm_routing.py проверяет логику нормализации маршрутизации. Входы LLM/tools в Ruby suite подменены: эти результаты не измеряют GPU, NATS или браузер.

Для принятия архитектурных изменений нужны ещё испытания двух реплик workflow, одновременных turn одной сессии, обрыва клиента, перегруженного ASR и reconnect UI. Предлагаемые метрики и условия приёмки приведены отдельно для каждого изменения. Сейчас p95, стоимость реплики, частота повторов и максимальное число сессий не измерены.

Куда идти при изменении поведения

Задача Реализация Что проверить вместе
Новый инструмент API gateway + tool DTO + RuntimeIo schema, NATS subject, ошибки, fixtures
Новый сценарий registry, definition, graph, nodes routing entries, pending, history, UI events
Изменить звук TrackSourceNode + WhisperStreamNode wire packet, VAD bridge, end, отмена
Заменить workflow Agent subjects + DRS + Compose parity всех сценариев, edit/replay, время ожидания
Добавить реплики subscriptions + state ownership queue groups, идемпотентность, порядок сессии
Изменить UI-события UiEventPublisher + frontend handlers session scope, reconnect, дубли, snapshot
Поменять хранение runtime repository + db.py + migrations атомарность turn и совместимость истории

Оценка подхода

Разделение медиапотока, GPU и диалога стоит сохранить: у них разные требования к задержке, состоянию и ресурсам. Самое спорное место — два последовательных оркестратора и три поддерживаемых реализации одних сценариев. Они увеличивают объём согласуемых contracts, не давая доказанного выигрыша на текущей нагрузке.

Начать стоит с исправления повторного исполнения и ограничения очередей, затем сокращать обращения к модели и выбирать один runtime по измеренному parity. Объединение Agent с выбранным workflow — обоснованный кандидат на упрощение. Полное переписывание нужно рассматривать только при подтверждённой потребности в восстановлении длительных процессов, многопользовательской изоляции или другой модели исполнения; уменьшение числа файлов само по себе его не оправдывает.

Исходник

Скачать Markdown · К материалам исследования