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

NATS: текущий путь Chat Agent

Контракт исходников на 04.11.2026. Архитектура, поставка, batch protocol 3. Исторические workflow, Core leases и Trickle ICE не являются рабочими путями. Их прежнее описание доступно в версии до удаления Core.

Веб-виджет Chat Agent

Модуль /widget/chat-agent.js открывает общую панель в документе принимающей страницы. Она получает UI- и аудиодоступ по POST /auth/nats с полем client: "widget". Gateway принимает профиль с HTTP(S)-origin за штатной авторизацией. Транспорт использует /chat-ws, команды и события конкретного client_id. Аудиодоступ открывается после включения голоса.

Команда browser_context регистрирует context_id и схему page_action по протоколу 2. Вызов приходит в chat.ui.<client_id>.events.tools, результат возвращается в chat.ui.<client_id>.cmd.browser_tool_result с теми же request_id, context_id и tool_call_id. Расширение продолжает передавать run_js по этому протоколу. Профиль веб-виджета не выдаёт права Monitor.

При TTS_ENABLED=1 озвучка доступна авторизованным веб-виджетам во всех сессиях. Для расширения список UUID задаёт TTS_PILOT_SESSION через запятую. При открытии панели веб-виджет регистрирует проигрыватель, если пользователь не отключил озвучку. При блокировке воспроизведения браузером панель предлагает включить звук вручную.

На странице Chat Agent, Forms и внутри iframe подключается один модуль:

<script type="module" src="https://chat-agent.gis-master.ru/widget/chat-agent.js"></script>

Все файлы пакета находятся под /widget/ и доступны публично с CORS. Модуль определяет backend по своему URL. Bootstrap /auth/nats и WSS /chat-ws проходят Slim Auth. Preflight OPTIONS возвращает только разрешения CORS. Для профиля widget допускается любой HTTP(S)-origin; cookies браузера и политика CSP принимающей страницы должны разрешать подключение.

Виджет создаёт свою сессию независимо от маршрута сайта. ID хранится в localStorage принимающего origin: chat-agent.widget.session.top для верхнего документа и chat-agent.widget.session.frame для iframe. Страницы одного origin разделяют соответствующую историю. Каждый открытый экземпляр имеет свои client/context/document ID и исполняет инструменты только своего документа. Если хранилище недоступно, ID живёт до перезагрузки и панель показывает предупреждение. В Monitor используются эти же session/request ID.

Для внешнего скрипта и аудио CSP разрешает origin Chat Agent в script-src, connect-src и worker-src, его WSS в connect-src, wasm-unsafe-eval в script-src, blob: в worker-src и media-src. Родитель iframe разрешает microphone и autoplay через allow и Permissions Policy. Микрофон требует действия пользователя и защищённого контекста. При блокировке сторонних cookies подключение остаётся недоступным до разрешённого входа браузера.

Транспорт и владельцы

Браузер получает защищённый HTTPS bootstrap /auth/nats или bootstrap расширения, затем использует NATS WebSocket /chat-ws (расширение — свой существующий маршрут). Capability, срок и per-client reply subject проверяет Gateway. Credentials здесь не приводятся. Backend использует TCP NATS: CHAT_NATS_URL для чата, ASR_NATS_URL для аудио. Production разделяет эти брокеры; локальная конфигурация может явно использовать один.

Откуда → куда Subject Назначение
UI → Gateway chat.ui.<client_id>.cmd.<command> Команды, история, Monitor и диагностика; JSON request/reply
Audio client → Gateway chat.audio.<client_id>.cmd.<command> start/phrase/end/abort; capability принадлежит владельцу
Audio client → Ingress chat.audio.<client_id>.pcm Бинарные Opus/PCM-пакеты существующего протокола
Gateway → Ingress chat.browser.audio Проверенная команда управления потоком
Gateway → Agent chat.internal.agent.widget Submit, tool result, cancel; ответ о приёме не является итогом
Agent → LLM nats.llm.<user_id> Один agent_step, подготовленные сообщения и инструменты
LLM → Agent chat.internal.model.capture Ограниченные диагностические входы/выходы; без повторной генерации
Agent / Ingress → Gateway nats.events.<user_id> Результат команды/инструмента или транскрипция исходному клиенту
Gateway → UI chat.ui.<client_id>.events.ui, .events.tools События и выполнение инструмента текущего документа
Ingress → Audio client chat.audio.<client_id>.events.audio READY, ошибки, состояние и диагностика захвата
Ingress → ASR bridge webrtc.komaroff.asr.stream.<session_id> Готовый mono PCM16 16 кГц: begin, блоки ≤128 КиБ, commit RPC
ASR bridge → Ingress webrtc.komaroff.asr.text.<session_id> final/rejected/error, измерения и исходные наблюдения worker
Bridge → worker → bridge to.inference.webrtc.asr.sample.<rpc_id> / from.inference.webrtc.asr.sample.result.<rpc_id> Существующий account import/remap во внешнем ASR
Delivery → Gateway / Ingress chat.internal.gateway.lifecycle / chat.internal.audio.lifecycle state/drain/resume каждого владельца; браузеру запрещены

Допуск и результат

Gateway проверяет каталог страницы в WidgetStore, передаёт Agent проверенный WidgetBinding и ограниченный admission deadline. Неизвестный исход не повторяет команду. Очереди, receipts, отмена и PostgreSQL fencing остаются у Agent.

Ingress владеет тремя живыми потоками и одной replay-резервацией. Повторный контекст, чужая сессия, истёкший grant, пропущенные пакеты и неподдерживаемый протокол отклоняются. Для batch до первой отправки регистрируется ожидаемый phrase_id. Единственный listener проверяет session/stream/phrase/revision и принимает терминальный результат один раз; архив и live-измерения получают тот же принятый результат. После end ожидание результата ограничено 20 секундами. Abort закрывает только владельца.

ASR не запускает Agent самостоятельно. Ingress возвращает транскрипцию проверенному widget_route; панель добавляет текст и отправляет обычную команду с тем же request ID. chat.browser.core, chat.agent.audio.control и Core lifecycle больше не имеют действующих consumers. Старый lifecycle используется лишь процедурой удаления Core до остановки старого контейнера и при её откате.

Replay остаётся на chat.internal.audio.replay. Gateway подготавливает архив, Ingress резервирует допуск и получает результаты через общий listener. Для полного монотонного Opus-повтора используется один decoder iterator; новый повтор начинает декодирование сначала. Резервация освобождается после подтверждения idle от ASR. Compare/replay не порождают пользовательские команды или новые архивные записи.

Наблюдения и readiness

nats_log хранит реальные передачи, связки ID и исходные часы. Бинарный звук туда не попадает. Producer сохраняет назначение операции; Monitor использует одну проекцию для журнала и схемы. Неизвестный отправитель и потерянные записи остаются неизвестными. Историческая метка bus означает отсутствие имени участника, а не сервис NATS. RPC-квитанция содержит reply_to_event_id исходной передачи: квитанция результата инструмента не является новым выполнением или окончательным ответом.

Расширение передаёт в существующей widget_client_receipt необязательные parent_event_id, subject, received_at, displayed_at вместе с client_ms. Четыре новых поля принимаются только вместе. Gateway сохраняет первую квитанцию в существующем receipt с серверным saved_at. Monitor проверяет владельца, сессию, запрос, subject и ссылку на отправку Gateway, затем показывает получение и локальное добавление ответа в чат. Тело ответа остаётся по payload_ref; время браузера не используется как время сервера или сетевая задержка. Старый client_ms без наблюдений не порождает выдуманные события получения.

Ingress readiness отдельно показывает доступность chat/ASR, server ID, один result listener и два ограниченных диагностических адаптера на отдельной ASR-шине: chat.agent.health.agent и nats.llm.user123. Адаптер LLM принимает только явный agent_step без инструментов и с deadline. Он не является общим прокси чата. Health RPC Agent/LLM — chat.agent.health.agent / chat.agent.health.llm. Отсутствующий ответ не считается нулевой очередью. Сбой ASR не выключает обычный чат.

Источник точных полей: browser API, Gateway, Ingress, results, batch.

Сессии Monitor и переписка виджета

  • UUID страницы хранится в window.sessionStorage под ключом chat-agent.session_id. Переоткрытие панели и reload используют тот же UUID; старый localStorage не переносится.
  • Через существующую команду monitor (source=postgres) доступны action=sessions, requests, request_events, service_operations и operation_events. selected_session_id выбирает сессию журнала и не заменяет авторизованный session_id клиента. null выбирает события без сессии. Список отбирает сессии с активностью за окно; запросы читаются с начала выбранной сессии до фиксированного until. Сводки возвращаются страницами до 100 элементов (UI — по 25), новые сверху; доступны search (текст или точный ID), state и input=text|audio|unknown. Неизвестный итог обозначается unknown, отсутствующая длительность — null. request_events требует request_id, operation_events — operation_id; события читаются только после открытия выбранной цепочки, порциями до 100 и 192 KiB. В списках и цепочке нет JSON-тел. event_content читает их частями и разрешает payload_ref вне текущей страницы, сохраняя границы сессии и явного request_id. Владение событием определяется сохранённым request_id, однозначным operation_id или причинным родителем, без группировки по времени. Непривязанные наблюдения доступны в служебных операциях. Устаревшие exchanges, session_events и exchange_events удалены; используются requests / request_events / operation_events.
  • Monitor: в сессии «События / Аудиопотоки», в запросе «Данные / Диагностика / Последовательность событий». view=events открывает список; request_id или operation_id — отдельную цепочку, request_tab=lanes|events — её представление. Старый view=events&request_id=… открывает события запроса. Ссылки между Audio и событиями сохраняют request_id.
  • При явном чтении Monitor проверяет действующее подключение и срок grant; потерянный доступ восстанавливается через защищённый bootstrap с тем же UUID страницы. Timeout не вызывает повтор команды. Фонового опроса данных нет.
  • Журнал сохраняет структуру переданного JSON, включая каталог, кэш и маршрут; credentials удаляются только из полей протокола. payload_ref повторно использует тело только при совпадении переданных байтов. Метаданные payload_scope=message отличают новые записи от прежней проекции, которая могла удалять поля. Полные пакеты старых событий не восстанавливаются искусственно.
  • chat.ui.<client_id>.cmd.widget_history — служебное чтение переписки профилями расширения и веб-виджета. Gateway берёт UUID из проверенного grant, не из параметров запроса. action=list возвращает до 25 пар с next и фиксированным until; action=content, request_id, role=user|assistant, offset дочитывают большое значение. Ответы используют существующие cell/fragment, без незаметного обрезания.
  • Необязательный include_asr_outcomes: true в action=list включает завершённые asr_result с result_type=rejected|error из того же журнала. По умолчанию false: старые клиенты получают прежние пары. Карточка содержит request_id, created_at, asr (session_id, result_type, code и сохранённые stream_id/phrase_id/revision), question=null, answer=null. Хронология и пагинация общие, повторные события одного request_id не создают карточки. Это только представление истории; оно не вызывает модель и инструменты. Offscreen передаёт тот же отказ соседним панелям через существующий history_view без текста, PCM и аргументов инструментов.
  • Переписка — проекция исходных вопросов и терминальных ответов nats_log, а не история модели. Чтение не повторяет операции и не журналирует собственный результат. Потерянные диагностические события не восстанавливаются искусственно.
  • User-Agent bootstrap хранится в browser_access.user_agent и в метаданных событий Gateway; профиль клиента также сохраняется в журнале. Истёкшая привязка доступа не удаляет эти метаданные из nats_log. Старым событиям User-Agent не приписывается.

Аудиодиагностика расширения

Серверный monitor source:audio и три chat.internal.monitor.audio* RPC удалены. Monitor сохраняет source:postgres. Диагностика использует конечную owner-scoped команду audio_diagnostics и существующий .events.audio; описание действий, metadata, admission ACK и исключений журнала — AUDIO_DIAGNOSTICS.

Monitor: путь отдельной фразы

request_events с event_kind=transcription и stream_events читают одну проекцию nats_log и существующего аудиоархива. Принадлежность определяется сохранёнными session_id, stream_id, phrase_id и детерминированным request_id; время не используется для присоединения соседних фраз. Захват и допуск относятся к контексту потока и не входят в сводку обработки фразы. Пакеты одного диапазона показываются одной записью со ссылкой на существующие audio_chunks.packets/manifest; подробности читаются лениво, без бинарного звука и второй копии метаданных.

Bridge и worker отправляют диагностические конверты type=monitor_observation через существующий ответный ASR-канал. Конверт содержит исходные ID, автора, время наблюдения, назначение операции и причинного родителя. Он не является результатом ASR и не запускает команду. Audio Ingress проверяет идентичность и передаёт наблюдение в существующий ограниченный регистратор. Потеря наблюдений отмечается отдельно, без повторного распознавания. Ожидаемая фраза регистрируется локально в BatchCapture до отправки; фактическая очередь измеряется bridge. Исторический queued обозначал регистрацию в Core и не является измерением очереди ASR.

Заголовки X-Monitor-Transfer, X-Monitor-Source и X-Monitor-Subject связывают отправку с приёмом, включая account remap GPU. Клиентская квитанция архива может содержать parent_event_id, received_at и subject конкретной отправки Gateway. Проекция принимает эту связь только в той же сессии и запросе. displayed_at фиксируется сразу после добавления текста, до вычисления SHA-256. Разные часы сервисов сохраняются как есть; разность их timestamps не является измерением сети. Старые записи без этих наблюдений остаются неполными, без предполагаемых стрелок.

Monitor: уведомления о сохранении

Существующий общий subject chat.monitor.audio_live.streams передаёт также {version:1, kind:"monitor_audio", event:"changed", data:{session_id, request_id?, recording_id?}}. Gateway публикует его после commit запроса, ASR или архива. Текст, JSON событий и PCM в уведомление не включаются. Monitor объединяет изменения за 250 мс, выполняет одну сверку одновременно и хранит только признак повторной сверки. После повторной подписки выполняется одна полная сверка видимых данных. Скрытая вкладка и live=0 приостанавливают подписки; старый until ограничивает только снимок на паузе.

Параметр Monitor batch_bytes совместим с прежним чтением: по умолчанию 24000, максимум 131072 байт PCM. Чтение диапазона возвращает несколько исходных блоков с проверкой каждого хеша, манифеста, последовательности и непрерывности; next остаётся индексом следующего исходного блока. Фраза активной записи доступна после сохранения её блоков и правой секунды контекста. Полный replay требует полной записи. Если манифест пришёл раньше PCM, вставка блока атомарно переносит его из ожидающих событий в audio_chunks. Повтор фразы не ждёт завершения всего потока.

finish и finalize идемпотентно сохраняют итоговые счётчики и завершение владельца. Полная проверка хешей выполняется существующей сверкой архива (до 8 записей каждые 10 секунд), вне двухсекундного RPC записи. На проверку одной записи отведено до 60 секунд; курсор держит не более 32 исходных блоков PCM. До проверки статус — finalizing. Блокировки строк изолируют записи: завершение одного потока не останавливает запись соседнего. Поздние блоки и манифесты получают прежние 30 секунд на доставку; конфликт итоговых счётчиков сохраняется как finish_conflict и автоматически не очищается.

Browser audio protocol 4 (Opus)

Клиентские subjects и соединение сохранены: START принимает audio_protocol=4, format=opus; protocol 3/PCM отклоняется с audio_upgrade_required. Бинарный пакет и точные координаты описаны в BROWSER_BATCH_ASR. Ingress декодирует один раз; внутренний binary batch по-прежнему передаёт PCM16LE 16 кГц. Архив хранит Opus-пакеты, а playback/replay использует общий декодер. Текущий повтор Monitor читает диапазон на сервере и сразу отправляет batch; описанная выше историческая загрузка PCM в браузер с возвратом 1× больше не применяется.

Ручная очистка Monitor

chat.ui.<client_id>.cmd.monitor_cleanup доступен только профилю web с действующей авторизацией. Профили extension, widget и replay его не получают. preview возвращает числа, защищённые объекты и подписанную фиксированную выборку; ничего не удаляет. apply требует command_id, selected_session_id, before, snapshot, proof и номер порции step. Повтор номера не запускает новую порцию. cancel сохраняет уже выполненное и останавливает следующие порции.

Удаляются только завершённые запросы и закрытые потоки, не пересекающие границу. Используемый replay, активные операции, каталоги подключённой панели и изменённые после preview строки защищены. Ссылочные тела остающихся событий, квитанции, владельцы, доступы, заметки и прикладные данные сохраняются. Квитанция в существующей таблице фиксирует выборку и прогресс. Тайм-аут откатывает только текущую порцию. Установка версии не запускает очистку. Старые PCM-архивы сохраняют метаданные, но воспроизведение и повтор не поддерживаются.

Термины диагностического ASR

audio_replay с action: terms возвращает фиксированный каталог ASR bridge через существующий chat.internal.audio_replay_probe. start принимает необязательный terms: {mode, profile, version, sha256}; off — отсутствие поля. Режимы hotwords для двух Whisper и LinTO, replacements для GigaAM/Nemotron. Профили — maps, weather, aviation. Произвольные слова, regex и эталон не принимаются.

Gateway проверяет версию/хеш и capabilities worker, связывает профиль с replay. Ingress берёт профиль из этой reservation, а не из обычного аудио START. batch_begin.terms входит в идентичность задания. Bridge проверяет реальный parameters.hotwords либо применяет точные замены один раз после модели. diagnostic.text и оценки остаются исходными; text — итог, terms содержит профиль, applied, список замен и отдельное postprocess_ms. Старые клиенты без поля работают без подсказок. Режим не меняет голосовой чат и каталог инструментов.

Compare загружает тот же каталог, независимые флажки A/B по умолчанию выключены. Переименование модели снимает её флажок. Эталон применяется только в браузере для WER/CER. Ошибка несовпадения профиля не переключает модель или режим.

Редактор ASR/TTS

chat.ui.<client_id>.cmd.text_editor — диагностика только для web-профиля. Результаты: существующий .events.model_test; preview: .events.speech. В существующей команде synthesize появился необязательный prepared: boolean: true запрещает повторную нормализацию чисел в worker, отсутствие сохраняет прежнее поведение. Живой ASR final может содержать original_text, text_editor; квитанция дополнительно поддерживает source_text_sha256. Проверки владельца и совместимость описаны в VOICE_EDITOR.md.

Исходник

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