Свой голосовой агент без внешних моделей
Разбор текущего кода, сравнение архитектуры и план автономного контура. Таблицы, кликабельные схемы, code-map и проверяемые критерии вместо обещанного ускорения.
01Что стоит перенять и что оставляем своим
Из Codex полезно взять разделение звука, управления разговором и работы агента. Для изолированной сети эту схему нужно собрать на собственных ASR, LLM и TTS. Перенос ChatGPT endpoint или токена автономности не даст.
У нас уже есть большая часть нужных границ: браузерный клиент, WebRTC в src_core, отдельное распознавание, src_agent, сценарии и Postgres. Я бы не начинал с замены всего стека. Сначала ограничил бы очереди и работу в полёте, замкнул зависимости внутри каждой площадки и подключил обратный аудиотрек. После этого - единые события сессии и перебивание ответа.
Этот учебник идёт от фактического пути речи к варианту автономного развёртывания, затем к изменениям и их проверке. Таблицы сравнивают конкретные обязанности. Числа в критериях приёмки - предложенные цели для испытаний, а не уже полученные показатели.
Какие версии сравниваются
| Система | Исследованный срез | Что это доказывает |
|---|---|---|
| webrtc-komaroff, GitLab main | 1fc1702859381062eb7d3ffec2d3b0e310b1ebef, получен через fetch без checkout | Основной источник сравнения. Не доказывает запущенный image или live-конфигурацию. |
| Локальный webrtc-komaroff | f0a6eac965c7d6be8a353265775b676248855905 | Старее main. Существующие untracked DOCS, presentations и videos сохранены. |
| ASR bridge py-faster-whisper | 21c85b0c7bc7a96497ce78ec2ead44b113f18db6, соседний локальный checkout | Подтверждает его реализацию. Совпадение с image в работающем стеке не проверено. |
| Codex CLI | Официальные Rust-исходники rust-v0.156.1, сохранённый срез из предыдущего исследования | Подтверждает устройство клиента, RPC и signaling. Серверные модели ChatGPT закрыты. |
| Community fork | 60cd00805b777bced0e6069d8bc52e529cc05cbb + незакоммиченные аудиодемо; Desktop-бандл 26.928.20755 повторно прочитан 2 октября | Фрагменты захвата проверены статически. Это не проверка настроек запущенного окна, качества звука или всего installed payload. |
Успешные обращения к ChatGPT из прежнего учебника - исторические результаты, здесь они не повторялись. Для этого сравнения достаточно клиентского контракта. Мы не знаем внутреннее устройство серверного ASR/TTS, аппаратные ресурсы ChatGPT и его задержки в одинаковых с нашим проектом условиях.
02Сначала отделим транспорт от распознавания
WebRTC доставляет звук, но не понимает слова. ASR, или STT, превращает звук в текст. LLM выбирает или формирует текстовый ответ. TTS превращает этот текст в звук. Агент отличается от обычной модели тем, что может выбрать сценарий, вызвать инструмент, сохранить состояние и вернуть результат действия.
| Термин | Простое объяснение | Зачем он здесь |
|---|---|---|
| SDP / signaling | Текстовое описание возможностей соединения и обмен им до разговора | POST /core/offer передаёт не запись голоса, а предложение WebRTC. |
| ICE, STUN, TURN | ICE выбирает путь; STUN помогает найти адрес; TURN ретранслирует медиа, когда прямой путь не получается | В автономной площадке эти службы либо местные, либо не нужны при доступных host candidates. |
| Opus / RTP / SRTP | Кодек сжимает звук; RTP переносит пакеты; SRTP защищает медиаданные | Это не WAV, не multipart и не JSON с Base64. |
| PCM16LE | Сырые знаковые 16-битные сэмплы с little-endian порядком байтов | После декодирования WebRTC у нас ASR получает mono 16 кГц. |
| VAD | Определение участков речи и тишины | Граница фразы влияет на момент запуска распознавания и задержку ответа. |
| Sideband | Отдельный канал событий и команд, рядом с медиапотоком | Распознанный текст, состояние, отмена и вызовы tools не обязаны ехать вместе со звуком. |
| Сессия / ход / item | Сессия - разговор; ход - одна работа; item - реплика или результат внутри хода | Идентификаторы не дают старому ответу попасть в новый разговор. |
| Backpressure | Что делаем, когда потребитель медленнее источника | Нужно ограничение памяти и явная политика: ждать, отбрасывать или завершать поток с ошибкой. |
| p95 / RTF | p95 - граница для 95% замеров; RTF - время вычисления, делённое на длительность аудио | RTF < 1 нужен для устойчивого непрерывного распознавания на выбранной нагрузке; это не гарантия малой задержки первого текста. |
Поэтому вопрос "у нас тоже WebRTC, почему ответ медленнее?" решается измерением всего пути. После быстрого транспорта могут идти ожидание конца фразы, файл, несколько вызовов модели и ожидание полного TTS. Сам WebRTC эти задержки не убирает.
03Как сейчас проходит речь в webrtc-komaroff
Браузер согласовывает соединение через Core, а затем отправляет микрофон по WebRTC. Core декодирует и нормализует аудио, передаёт кадры в NATS. Соседний ASR bridge находит законченные фразы, записывает временный WAV и отправляет файл в LinTO. Только после получения текста начинается работа агента.
Полученный ответ сценария сохраняется вместе с состоянием сессии и публикуется как текст или команды интерфейса. На схеме LinTO и LLM вынесены отдельно: текущая конфигурация использует соседний вычислительный контур, а не обязательно интернет. Для независимой площадки этот контур всё равно надо перенести внутрь её границы.
Обратный аудиотрек предусмотрен типом соединения sendrecv и обработчиком peer.ontrack в браузере. Но в проверенных handle_track и графе нет TTS producer и назначения его трека в sender Core. Наличие HTML audio element не доказывает, что агент говорит. Этот вывод относится к просмотренному voice-agent пути, а не ко всем экспериментам и TTS-демонстрациям репозитория.
04Что передаётся на каждом участке
У одного разговора несколько разных форматов. Между браузером и Core идут сжатые медиапакеты. Между Core и bridge идут сырые сэмплы. Между bridge и ASR снова файл с контейнером. Агент получает только текст. Ни один из этих форматов нельзя без преобразования подставить вместо другого.
Речь поступает кадрами, пока пользователь говорит. Распознавание одной фразы стартует после решения VAD, а агент - после final transcript. Pending показывается раньше текста, но не означает, что partial уже распознан. В этой последовательности результат Agent публикуется по NATS/WS: стрелка к браузеру не является новым прямым HTTP вызовом Agent.
| Участок / endpoint | Запрос | Ответ / следствие |
|---|---|---|
Browser → POST /core/offer | application/json: {sdp,type:"offer",session_id?}. SDP с ICE/DTLS параметрами, без аудио. | application/json: {sdp,type:"answer",session_id}. Core добавляет session_id после establish_connection. |
| Browser ↔ выбранный ICE peer Core | Opus в защищённом WebRTC RTP-потоке; кодек уточняется из SDP/getStats | Декодированные AudioFrame. Это не HTTP endpoint и не файл. |
Core → inference.whisper.stream.<session> | 4 байта BE длины JSON + UTF-8 JSON metadata + raw PCM16LE; mono, 16000 Гц, шаг 20 мс | seq считает пакеты, но producer не ждёт ASR acknowledgement каждого кадра. |
ASR bridge → LINTO_HTTP_URL | POST, multipart поле file, WAV PCM16 mono 16k; query language=ru, Accept: text/plain. В DRS URL заканчивается /transcribe. | Ответ читается целиком, извлекается текст. Через настроенный proxy используется CONNECT, без скрытого direct fallback. |
Bridge → inference.whisper.text.<session> | {pending:true,phrase_id,session_id}, затем {text,phrase_id,duration,session_id} либо error | Это pending и финальный текст, не доказанный поток partial transcript. |
Core → nats.agent.<USER_ID> | JSON TraceEnvelope + text + turn_id + session_id + edit + user_id | AgentInboundResponse; UI события публикуются отдельно. NATS request/reply не означает durable обработку. |
Workflow → nats.llm.<user> | JSON LlmRequest: mode, input, constraints и trace envelope | JSON LlmResponse. Ollama adapter вызывает POST /api/chat с stream:false, local adapter - llama.cpp. |
UI события → browser /ws | NATS по WebSocket; subject из конфигурации nats.events.user123 | ServerEvent: type, kind, data, uid, service, time. Payload некоторых событий содержит trace/session_id; не единый session-specific поток. |
Один кадр: 640 байт аудио, не считая метаданных
Иллюстрация текущей упаковки без отправки пакета. Нулевой звук взят вместо личной записи.
# Модуль нужен для UTF-8 представления метаданных перед сырыми сэмплами.
import json
# Производитель объявляет формат сырых сэмплов, а не MIME файла.
meta = {"type": "frame", "seq": 1, "sample_rate": 16000,
"sample_width": 2, "channels": 1, "session_id": "00000000-0000-4000-8000-000000000001"}
# 20 мс × 16000 сэмплов/с × 2 байта × 1 канал = 640 байт PCM.
raw = bytes(640)
meta_bytes = json.dumps(meta, ensure_ascii=False).encode("utf-8")
# Длина метаданных идёт big-endian, сами сэмплы - little-endian.
packet = len(meta_bytes).to_bytes(4, "big") + meta_bytes + raw
Пакет type:end заканчивает поток и не содержит аудио. 16 кГц × 2 байта дают 32 000 байт/с и 50 сообщений/с при шаге 20 мс на одного клиента, до NATS overhead. Это арифметика, не замер пропускной способности. Base64 здесь не используется и ради сходства с OpenAI добавлять его незачем.
Старые поля LINTO_WS_URL в DRS не отменяют код _transcribe_and_publish: в исследованном bridge он вызывает HTTP. Какой image сейчас работает в production, в этом исследовании не проверялось.
05Как устроен голос Codex и чем он отличается от диктовки
В CLI 0.156.1 команда /voice начинает разговор: helper работает с устройствами и WebRTC, app-server ведёт управляющую часть, а ядро согласует разговор с агентной работой. Локальный app-server - не удалённая нейросеть и не обязательный посредник для каждого аудиопакета.
App-server создаёт call по HTTPS и принимает текстовые события по WSS sideband. Helper отправляет и получает медиа непосредственно через WebRTC. Эти роли могут быть разделены логически, даже когда app-server встроен в процесс TUI: штатный embedded client использует InProcess/mpsc, а не строки JSON в stdin. STDIO относится к нашим отдельным RPC-демо.
| Путь | Endpoint / формат | Что можно взять |
|---|---|---|
| CLI /voice, ChatGPT backend V3 | POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas; JSON {sdp,session} → plain SDP answer + Location/call ID. Sideband wss://api.openai.com/v1/live/<call_id>; медиа Opus/SRTP по ICE. | Разделение сигнализации, событий и звука; call/thread/turn identities. Не сам закрытый endpoint. |
| Desktop диктовка | POST /backend-api/transcribe, multipart аудиофайл → JSON text; в бандле есть и отдельная потоковая ветка диктовки. | Отдельный файловый STT режим и строгая проверка MIME/контейнера. Не путать с voice conversation. |
| Desktop / ChatGPT voice conversation | В исследованном Desktop встречается иной маршрут /tbo/{tbo_id}/voice/calls; фактическая доступность режима для аккаунта не проверена. | Не утверждать, что весь Desktop использует CLI V3. |
| Read Aloud ChatGPT | GET /backend-api/synthesize?conversation_id=…&message_id=…&voice=…&format=mp3; ID существующего ответа → аудио. | Связь озвучки с конкретной сохранённой репликой; локальная реализация хранит собственный text/response_id. |
| Linux Read Aloud форка | Локальный Rust helper / CLI / MCP → Kokoro или custom command, Speech Dispatcher и другие backend; Piper подключается командой. | Provider adapter и локальные модели. MCP - оболочка инструмента, не медиа транспорт. |
| app-server appendAudio / appendSpeech | thread/realtime/appendAudio: Base64 audio + sampleRate/numChannels; appendSpeech: threadId + text. В WS-ветке outputAudio/delta возвращает PCM. | Типизированный контракт и события. Это альтернативный путь, не доказательство, что V3 WebRTC-аудио проходит через RPC. |
Границы того, что известно о ChatGPT
Клиентские исходники показывают отправленные поля и обработку событий. Они не показывают серверные ASR/TTS модели и не доказывают, что ChatGPT внутри собран как отдельные STT → LLM → TTS сервисы. В автономной системе мы сознательно предлагаем именно такой каскад: его компоненты можно поставить локально, заменить и проверить по отдельности. Сравнение не обещает разговорное качество ChatGPT на меньшем оборудовании.
Структура backend V3 в демо: session.model="gpt-live-1-codex", audio.output.voice="cove", delegation.type="client". Это исследованный версионный контракт, не публичная гарантия долговременной совместимости. Собственный сервис не должен эмулировать эту авторизацию или копировать cookies/attestation.
06Сопоставление обязанностей: что у нас уже есть
Сравнивать надо не имена библиотек, а границы работы. У нас есть аналог управляющей части app-server, но он распределён между Core, Agent и workflow runtime. Дополнительный сервис с названием app-server не нужен сам по себе: сначала достаточно согласованного интерфейса этих компонентов.
| Обязанность | Codex / fork | webrtc-komaroff main | Решение |
|---|---|---|---|
| Медиа отдельно от бизнес-логики | voice-host и Realtime service; app-server управляет | aiortc Core отдельно от Agent; после декодирования PCM идёт через NATS | Сохранить разделение. Замена PCM/NATS - только после нагрузочного доказательства узкого места. |
| Управление жизнью разговора | realtime start/stop, события, cancellation token | connect/disconnect и pc_lifecycle; voice blocked через UI | Добавить явные состояния и единый cancel. Cleanup при ошибке уже есть в main. |
| Текстовые события и прогресс | item/transcript delta и завершение хода | pending, transcription, message, thought, model statuses | Единый версионированный envelope, фильтр session/turn, resume. Partial STT не приписывать существующему bridge. |
| Ограничение незавершённой работы | Bounded channels в ядре; демо ограничивает input queue | FanOut queue без лимита; publish task на каждый кадр | Исправить лимиты и политику перегрузки прежде роста нагрузки. |
| Речь агента в ответ | Двусторонний WebRTC audio; локальный Read Aloud отдельно | Текст ответа есть, обратный TTS producer в voice path не найден | Добавить локальный TTS и обратный track, сохраняя text-only режим. |
| Идентификаторы и долговечное состояние | thread/call/realtime session/turn/item | session/phrase/turn/request/trace; Postgres | Не заменить IDs одной UUID; добавить generation/epoch и response_id. Транзакция уже исправлена. |
| Инструменты и делегация | Handoff соединяет голос с ядром агента | Workflow вызывает собственные tools по NATS | Перенять явную связь request/result/cancel/permission, а не shell-функции Codex. |
| Полная автономность площадки | ChatGPT voice зависит от удалённого сервиса; локальная озвучка - иной путь | Локальный LLM путь есть; DRS указывает центральные ASR/LLM и leafnode | Замкнуть media, control, модели, assets и управление внутри каждой площадки. |
Три уже выполненных улучшения исключены из нового плана: deadline ICE gathering, очистка front ресурсов при отказе signaling и транзакция runtime+history. Остаются вопросы порядка ходов, отмены и идемпотентности tools: SQL-транзакция не отменяет уже выполненное внешнее действие.
07Что означает независимая сеть на практике
Изоляция означает, что новая площадка может загрузиться и вести разговор без основной сети, интернет-DNS, центрального брокера и внешних моделей. "Всё в Docker" для этого недостаточно: контейнер может обратиться к основному H100, скачать модель или загрузить style.json снаружи.
| Зависимость | Что найдено | Что требуется внутри площадки |
|---|---|---|
| Медиа адреса | Браузер: stun.gis-master.ru; Core: iceServers=[] | Доступные host candidates либо свой STUN/TURN. При разных подсетях/запрете UDP нужен проверенный TURN; HTTPS ingress не ретранслирует RTP автоматически. |
| ASR | DRS: llm-models_linto_stt_whisper_http:80/transcribe через NATS proxy | Свой ASR с весами и VAD. Direct LAN HTTP или локальный proxy с контрактом CONNECT; не зависеть от main-network receiver. |
| LLM | LLM_MODE remote, nats-proxy-ollama.gis-master.ru; local llama.cpp путь существует | Свой Ollama в этой LAN либо локальный GGUF. Название remote в коде означает HTTP provider, а не обязательно интернет. |
| NATS topology | DRS: leafnode на 192.168.28.134:37422; локальный account + inference mappings | Самостоятельный broker и local responders. Удалённый leafnode в offline profile не нужен. Не смешивать изолированные площадки по user123. |
| Модели | LLM downloader обращается к Hugging Face при cache miss; VAD к GitHub при отсутствии ONNX | Предустановленные точные веса, лицензии, SHA256, проверка отсутствия файла до старта. Offline mode должен явно запрещать download, а не ждать DNS timeout. |
| Браузер и карта | MapLibre style: demotiles.maplibre.org/style.json | Локальные style, glyphs, sprites и tiles; отключаемый map fallback. Карта не должна блокировать голос. Это контекст UI, не замена исследования WebRTC. |
| HTTPS и устройства | getUserMedia требует безопасного контекста | Локальный доверенный CA, сертификаты, DNS и время. Для другого компьютера LAN HTTP обычно не подходит; localhost - специальное исключение. |
| Образы и пакеты | Образы и сборка используют registries и package sources | Заранее собранные images с digest, wheel/npm/model bundle; offline startup не запускает build/install/pull. |
| Состояние и наблюдаемость | Postgres, NATS events и OTel в проекте | Собственные DB, backup, collector, логи и secret store на каждой площадке; никакого обязательного центрального telemetry sink. |
Не предлагаю свести всё к одной машине. Независимая сеть может содержать несколько серверов, включая собственный GPU. Для одной CPU-машины нужны отдельные тесты качества и задержек. Если одновременно модели и их локальные аналоги отсутствуют, останется только заранее описанный сценарный помощник с записанными репликами; свободный разговор обещать нельзя.
08Как собрать автономный контур, сохранив существующий проект
Предлагаю оставить браузер, aiortc, Agent, workflow и Postgres. Вокруг них задаём local-only профиль и добавляем обратное аудио. Голосовой Gateway здесь - ответственность Core, а не требование написать новый продукт или поставить Codex app-server в корпоративную систему.
Браузер: звук и понятное состояние
Браузер отправляет микрофон и воспроизводит обратный трек. В отдельном событийном канале он видит текущие session/turn IDs, распознанный текст, источник ответа, отмену и деградацию. Локальные NATS WS можно сохранить на первом шаге, но credentials должны быть с узкими правами. Позже application WSS gateway может скрыть broker от браузера и проверять доступ к сессии.
На схеме первые два блока находятся на одном клиентском устройстве. Для браузера сохраняем его встроенную обработку. Sonora предлагаем как альтернативу для нативного клиента, не как второй обязательный фильтр после browser AEC. Возвратная стрелка в AEC означает локальную копию реально проигрываемого PCM и его задержку, а не повторную отправку TTS на ASR.
Core: медиа, согласование формата и границы жизни
Core владеет peer connection, аудиочасами, bounded queues и ресемплингом. Он переводит WebRTC в собственный ASR контракт и ответ TTS обратно в WebRTC. Бизнес-сценарий остаётся в Agent: не надо переносить туда aiortc, а в Core - prompts и права tools. Локальный брокер соединяет workers, но не делает их автоматически независимыми от центральных моделей.
Опциональное подключение к основной сети строится позже как явно разрешённый adapter. Его отказ не должен менять local-only сценарий. В строгом offline profile fallback наружу запрещён. Можно синхронизировать выбранные обезличенные события, когда сеть снова появилась, но модельный контекст и голос не должны уходить туда без отдельной политики.
09Звук на клиенте: Sonora и проверка Opus
Sonora: предлагаемая обработка до отправки
Opus сжимает звук для сети. AEC убирает из микрофона ответ, который звучит в динамиках; NS подавляет шум, AGC регулирует усиление. В CLI 0.156.1 Sonora включена внутри voice-host. Обработчик получает PCM микрофона и копию воспроизводимого звука с задержкой, обрабатывает блоки по 10 мс при 48 кГц mono и собирает Opus-пакеты по 20 мс.
У нас startMic уже запрашивает браузерные AEC/NS; defaults включены. Сначала сохраняем в диагностике фактические track.getSettings() исходного микрофона. Sonora 0.2.0 включаем в список улучшений как прототип нативного клиента. Она работает локально, не требует ChatGPT или внешней модели. Браузерная интеграция потребует отдельного прототипа: готовое подключение WASM/AudioWorklet здесь не подтверждено. Возможности и лицензия BSD-3-Clause описаны в документации Sonora.
| Что реализовать | Почему именно здесь | Как проверить |
|---|---|---|
| Нативный audio adapter + Sonora под feature flag | На клиенте доступны микрофон, реальный playout PCM и временные метки. Один серверный фильтр перед ASR не знает фактическую громкость и задержку динамиков. | 48 кГц mono, блоки 480 float-сэмплов, согласованный render/capture delay; после mute, gap и смены устройства нет старых кадров. Проверить callback latency и CPU. |
| Сравнение со встроенным browser AEC/NS | Запрошенный флаг не подтверждает качество. Не включать обе цепочки последовательно без проверки. | Тот же корпус и устройства: шум, только ответ из динамиков, одновременная речь. Остаточное эхо, ложные перебивания, WER, clipping, p95 задержки и CPU. Ни улучшение качества, ни экономия ресурсов пока не измерены. |
| Нативный reference test, затем акустический стенд | Синтетический сигнал проверяет алгоритм и временные метки, но не комнату и устройство. | Upstream rendered_reference_changes_decoded_echo_capture есть, здесь не запускался. Наушники не заменяют испытание с динамиками; при провале оставить half-duplex и отключить прототип. |
Opus: не добавлять заново, а проверить соединение
Мы используем browser WebRTC и aiortc 1.14.0, в котором Opus уже поддерживается и стоит первым в audio codecs. Это не доказывает выбор кодека в живом соединении. В исходниках собственного принудительного выбора Opus не найдено. Проверяем оба направления и отличаем SDP-предложение от фактически используемого codecId.
| Проверка | Что фиксировать | Как трактовать |
|---|---|---|
| Offer и answer SDP | Согласованный payload type, rtpmap audio/opus, fmtp, ptime/maxptime при наличии. | opus/48000/2 означает RTP-clock 48 кГц и объявленную поддержку каналов; не доказывает stereo речи или 48 кГц исходной TTS-записи. Не задавать payload type жёстко. |
| pc.getStats в обе стороны | inbound/outbound-rtp codecId → codec.mimeType/clockRate/channels/sdpFmtpLine; bytes/packets, loss, jitter, RTT и selected ICE pair, если доступны. | Проверять активные audio streams с положительными приращениями счётчиков. Само наличие codec record не доказывает передачу. Недоступную метрику обозначать неизвестной, не нулём. |
| Параметры и потери | Из согласованных параметров и измерений: bitrate, packet duration, mono/stereo, FEC/DTX при наличии. Для bitrate брать разность bytes за время, а не один накопленный счётчик. | Указанный в fmtp FEC/DTX не доказывает фактическое использование в каждом пакете. Проверить декодированный ответ и качество при контролируемых потерях и jitter. |
| Приоритет, только если нужен | setCodecPreferences из getCapabilities; сохранить совместимые fallback codecs. | Если согласуется нежелательный кодек, отдельно задать Opus первым и повторить negotiation/reconnect. Не пересжимать уже готовый WebRTC-поток; серверный ASR получает декодированный PCM. |
Пример только читает настройки и статистику. Не меняет кодек, микрофон или соединение и не выполняется автоматически учебником.
// Пример для будущей диагностики рядом с connectSession, не внедрённый код.
// micTrack - исходный микрофон до replaceTrack/VAD, peer - текущий peer connection.
console.log("Параметры микрофона", micTrack.getSettings());
const report = await peer.getStats(); // Читаем статистику уже работающего соединения.
for (const stream of report.values()) {
// Смотрим только локальные отчёты об отправленном или полученном аудио.
if (!["outbound-rtp", "inbound-rtp"].includes(stream.type)) continue;
if ((stream.kind ?? stream.mediaType) !== "audio" || stream.isRemote) continue;
const codec = report.get(stream.codecId); // Связываем поток с его кодеком.
const packets = stream.type === "outbound-rtp" ? stream.packetsSent : stream.packetsReceived;
console.log("Поток и кодек", stream.id, packets ?? "неизвестно", codec?.mimeType ?? "неизвестно");
// Полный отчёт нужен для clockRate/channels/fmtp и доступных метрик сети.
// Повторный снимок покажет активность; этот вывод не измеряет качество AEC.
}
Опоры для параметров: RFC 7587 и W3C WebRTC Stats. Обработка Sonora и Opus выполняется без внешних сервисов, но независимость всей системы всё равно требует локальных ASR/LLM/TTS и сетевой приёмки.
10Как очищается звук у нас, в CLI и в Desktop
В выдержке из бокового чата верно разделены Silero и Sonora. Но наш путь содержит ещё один этап: до отправки звука браузер может пропустить его через energy gate. Поэтому сравниваем не названия библиотек, а последовательность обработки и то, что остаётся на выходе каждого этапа.
AEC удаляет акустическое эхо ответа из микрофона, NS уменьшает шум, AGC регулирует уровень. Gate закрывает сигнал ниже порога, а VAD находит участки речи. Opus решает транспортную задачу: сжимает звук. Эти механизмы не взаимозаменяемы; Silero можно сохранить после Sonora или browser AEC/NS.
| Путь | Очистка / регулировка | Отбор речи | Что подтверждено и что нет |
|---|---|---|---|
| Наш браузер | getUserMedia запрашивает echoCancellation и noiseSuppression; defaults=true. AGC явно не задан. | Опциональный energy gate: RMS/dB → gain 0 или 1. | SOURCE. Реальные getSettings, акустическое качество и настройки конкретного устройства не измерены. |
| Наш Core → ASR bridge | Декодирование и приведение к PCM16LE mono 16k. В просмотренной цепочке отдельный denoiser/AEC не найден. | Silero ONNX VAD возвращает интервалы; bridge вырезает исходные сэмплы и пишет WAV. | SOURCE. Это сегментация, не очищенная нейросетью запись; внутренний preprocessing ASR provider не установлен. |
| Codex CLI 0.156.1 | Локальный voice-host включает Sonora AEC/NS/AGC2; получает playout reference и задержку. | Серверная сегментация голосового разговора не установлена этим разбором. | SOURCE. Качество на наших устройствах не проверено; app-server сам звук не очищает. |
| Desktop: Codex voice | В просмотренном web provider явно mono + noiseSuppression:true. AEC/AGC не заданы явно. | Не приравнивается к серверному VAD или Silero нашего bridge. | STATIC BUNDLE. Отсутствие параметра не означает false; фактическая обработка браузера неизвестна. |
| Desktop: ChatGPT voice | AEC запрашивается по device policy; код читает getSettings().echoCancellation. Есть ducking воспроизведения. | Монитор активности помогает управлять громкостью ответа, но не доказывает серверную сегментацию. | STATIC BUNDLE. NS/AGC в этом фрагменте явно не заданы. Ducking не очищает микрофон. |
| Desktop: диктовка | Отдельные browser и native capture ветки. В browser-вызове явно задан mono, не универсальный набор AEC/NS. | Запись или streaming adapter → удалённый STT. | STATIC BUNDLE. Обработка native capture и закрытого STT не установлена; это не вызов voice-host Sonora. |
У нас: AEC и NS запрашиваются на стороне браузера
startMic передаёт два флага из audio state в getUserMedia. Они включены в defaults, но браузер и устройство могут обработать запрос по-разному. В просмотренном коде нет отдельного вызова Sonora, RNNoise или SpeexDSP в этой точке. На выходе - MediaStream, уже после запрошенной браузерной обработки. Снять настройки нужно у исходного микрофонного трека, до замены его треком Web Audio.
Параметры из нашего mic.ts; diagnostics getSettings предложена выше, но в приложение здесь не добавлена.
// Фрагмент параметров startMic. Это запрос обработки, а не замер качества.
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: state.echoCancellation, // Просим убрать эхо динамиков.
noiseSuppression: state.noiseSuppression, // Просим уменьшить шум.
},
video: false, // Видео для этого захвата не нужно.
});
// autoGainControl здесь явно не задан; нельзя считать его выключенным.
Energy gate: тихий сигнал закрывается, речь не очищается
Если gate включён, connectSession вызывает createEnergyVad и заменяет sender track. Web Audio оценивает RMS - среднеквадратичный уровень сигнала - и переводит его в dB. Выше порога gain стремится к 1, ниже - к 0; переключение сглаживается attack/release. Это не нейросетевое выделение голоса и не AGC: во время открытого gate шум внутри речи может пройти вместе с ней. Тихие начала и окончания слов могут пропасть до Silero.
Упрощённый фрагмент порогового gate. Проверка - тихая речь, шум и WER при gate on/off на одинаковом корпусе.
// Смысл участка createEnergyVad; получение RMS/dB опущено.
const active = db > gate; // Порог сравнивается с общей энергией, не смыслом речи.
const target = active ? 1.0 : 0.0; // Пропускаем либо закрываем весь сигнал.
gainNode.gain.setTargetAtTime(target, ctx.currentTime, T); // Сглаживаем переход.
// Это gain 0/1, а не восстановление речи из смеси с шумом.
Core меняет формат аудио, а не устраняет эхо
WebRTC доставляет закодированное медиа; aiortc декодирует его. TrackSourceNode приводит аудио к mono PCM16LE 16 кГц и кадрам по 20 мс, WhisperStreamNode добавляет метаданные и публикует в NATS. Название normalization здесь относится к формату и длительности кадров. В этом коде не найден процессор очистки или автоматического выравнивания громкости. Детальный бинарный пример находится в главе о кадре NATS.
Silero выбирает фразы из накопленного PCM
На стороне ASR bridge _get_timestamps вызывает ONNX VAD и получает start/end. Затем код берёт audio[start_sample:end_sample] из исходной записи. Silero не возвращает заменённую очищенную волну: выбираются участки того же звука. Именно их bridge упаковывает в WAV и отправляет в HTTP ASR. Это соответствует контракту Silero VAD.
Распознавание выполняет следующий ASR provider. VAD задаёт границы, а не текст и не очищенный звук.
# Фрагмент существующего выделения фразы по интервалам VAD.
segment_audio = audio[start_sample:end_sample] # Берём исходные сэмплы речи.
# Далее bridge пишет этот сегмент в WAV и отправляет распознавателю.
# Denoised audio от Silero этот участок не получает.
Для изолированной поставки важна ещё загрузка VAD: при отсутствии ONNX-файла _ensure_vad_loaded может скачать модель. Её нужно включить в поставку с контрольной суммой и лицензией, проверить локальный путь и cold start с запрещённой внешней сетью. Silero оставляем и при добавлении Sonora; перенос сегментации и очистки в один блок только запутает диагностику.
Вернуться к схеме нашего тракта
CLI: обработка находится в voice-host перед Opus
В CLI 0.156.1 микрофон обслуживает helper, а не UI терминала или app-server. Processor::new включает в Sonora эхоподавление, шумоподавление и адаптивную цифровую регулировку усиления. Capture обрабатывается блоками по 10 мс при 48 кГц mono; затем собирается 20-мс пакет Opus. По сети идёт кодированное медиа, не ONNX VAD и не файл WAV. Никакой внешний API для самого Sonora-процессора не нужен.
Почему CLI передаёт в AEC ещё и проигранный ответ
render_output сохраняет реально проигранные сэмплы и временные данные. Capture worker сначала подаёт reference в процессор, затем обрабатывает микрофон; это позволяет сопоставить ответ из динамиков с его эхом. После mute или разрыва обработка сбрасывается, чтобы не использовать старую опору. Поставить Sonora на сервер только после получения PCM не равно этому пути: без синхронизированного render reference там нет сведений о реальном воспроизведении у пользователя.
App-server управляет сеансом, signaling, событиями и агентом. В этом V3-пути он не выполняет capture DSP и не несёт Opus между устройством и сервисом. Подробности - в архитектурном сравнении. В upstream есть синтетический тест влияния reference; здесь он не запускался и не заменяет акустический стенд.
Desktop Codex voice: собственный web provider с NS
В просмотренном provider Wbo содержит channelCount:1 и noiseSuppression:true. Класс Gbo открывает MediaStream, использует Web Audio/AudioWorklet для буферизации и mute и отдаёт трек WebRTC. В этом участке AEC и AGC явно не заданы; нельзя дописать из этого, что браузер их выключил. Отдельный процессор Sonora в этой ветке не подтверждён.
Это статический фрагмент Desktop, не команда для запуска и не измеренные settings текущего микрофона.
// Дословные поля provider из минифицированного бандла; оформление раскрыто.
const Wbo = { channelCount: 1, noiseSuppression: true };
// Gbo передаёт параметры в захват и строит трек Web Audio для WebRTC.
// Здесь нет явного echoCancellation или autoGainControl.
Desktop ChatGPT voice: AEC зависит от policy, результат читается из трека
Другой голосовой путь использует функцию, которая при device policy nowhere возвращает пустой объект, иначе - echoCancellation:true. Пустой объект не означает запрет AEC. Код создания сессии читает getSettings().echoCancellation и записывает наблюдаемое значение в состояние. Для текущего устройства это значение здесь не снято. NS и AGC в этом фрагменте не заданы явно, а возможная native/provider ветка имеет отдельные условия.
Связаны два места кода: policy захвата и чтение settings после получения трека. Это иллюстрация, не новый DSP.
// Смысл rt(device): точная минифицированная строка сохранена в evidence JSON.
const constraints = policy === "nowhere" ? {} : { echoCancellation: true };
// Пустой объект оставляет настройки реализации, а не принудительно выключает AEC.
const { echoCancellation } = microphoneTrack.getSettings(); // Наблюдаемое значение.
// Отсутствующий параметр нужно обозначать неизвестным, не заменять false.
Ducking уменьшает громкость ответа, но не очищает запись
В updateDucking при активности пользователя уменьшается playback volume; после паузы громкость восстанавливается. Монитор активности может использовать energy/Silero-ветку. Пользователю проще говорить поверх ответа, а эхо может стать тише. Но ducking не вычитает шум и эхо из микрофонных сэмплов. Это управление воспроизведением рядом с AEC, не его замена. Наличие Silero в таком мониторе не означает одинаковую с нашим ASR bridge сегментацию.
Диктовка Desktop не запускает тот же voice processor
У диктовки отдельный recorder/streaming adapter. В бандле есть выбор native microphoneCapture и browser capture; в просмотренном browser-вызове явно задаётся mono. Дальше адаптер отправляет запись или аудиопоток удалённому STT и получает текст. Этот код не доказывает Sonora, полный набор AEC/NS/AGC, обработку native capture или очистку внутри закрытого сервиса. Кнопку микрофона нельзя описывать как тот же путь, что разговор или CLI /voice.
Сохранённые фрагменты Desktop с SHA256 и byte offsets позволяют проверить эти выводы без абсолютных путей к компьютеру. Там только код обработки звука; credentials и пользовательские реплики не включены. Названия минифицированных функций относятся к этому бандлу и изменятся при пересборке.
Какие альтернативы сравнивать и что измерять
Для существующего браузерного клиента сначала проверяем встроенную обработку, gate и Opus. Для собственного native клиента начинаем с Sonora, поскольку в Codex виден пример всей цепочки capture/render. Остальные варианты нужны для сравнительного стенда, а не для автоматического включения всех фильтров подряд.
| Вариант | Назначение и условия | Статус для нашего проекта |
|---|---|---|
| Browser AEC/NS + Silero | Клиентская очистка и серверные границы фраз. Проверить settings исходного track, тихую речь, шум, динамики и double-talk. | Уже есть в коде; качество не измерено. Gate on/off - отдельный фактор. |
| Sonora | Локальные AEC/NS/AGC; native capture + render reference. API и лицензия. | Предложенный прототип, не внедрён. Silero сохраняется после очистки. |
| C++ WebRTC Audio Processing | Capture ProcessStream и render ProcessReverseStream. Контракт APM. | Альтернатива нативной интеграции; не подтверждение конкретного алгоритма браузера/Desktop. |
| SpeexDSP | Preprocessor и echo canceller для локального тракта; нужны reference и корректные параметры. Руководство. | Кандидат стенда. Сравнение качества и CPU на наших записях не выполнялось. |
| RNNoise | Нейросетевое шумоподавление, не самостоятельная замена AEC или VAD. Исходники и пример. | Кандидат для шума. Веса/сборку поставить заранее: autogen.sh upstream может скачивать модель. |
Одинаковый стенд должен включать обычную и тихую речь, фоновый шум, только ответ из динамиков и одновременную речь собеседника с ответом. Сравниваем WER, потерянные границы VAD, ложные перебивания, остаточное эхо, clipping, задержку и CPU. Остаточное эхо сначала измеряем при молчащем пользователе; double-talk оцениваем отдельно, чтобы не принять подавленную живую речь за хорошее AEC. Ни Sonora, ни другая библиотека ещё не показали у нас выигрыш; цели и порядок внедрения приведены в плане работ.
11STT: сначала управляем фразой, потом решаем, нужен ли streaming
У текущего bridge уже есть Silero VAD, phrase_id, pending и финальный текст. Это сохраняем. Первое изменение - локальный ASR provider с тем же HTTP-контрактом, заранее поставленными весами и явной readiness. Затем измеряем, сколько времени занимает ожидание границы фразы отдельно от распознавания.
| Изменение | Почему | Приёмка и измеримый эффект |
|---|---|---|
| Зафиксировать VAD/start/end policy | Energy gate в browser и Silero на сервере решают разные задачи. Жёсткий browser gate может срезать тихие слова до ASR. | Корпус обычной/тихой речи и шума; WER, false end, missed start, p95 конца фразы. Цель оптимизации: уменьшить p95 end→final на ≥20% относительно baseline без ухудшения WER >1 процентного пункта. |
| Накопитель PCM с правильным хвостом | TrackSourceNode дополняет хвост каждого resampled frame до 20 мс. Это не равнозначно накоплению remainder между кадрами. | Для входа 44100/48000 Гц сравнить входную и выходную длительность на 60 с. Цель: drift ≤20 мс после flush; padding только одного конечного неполного кадра. Искажение сейчас не измерено. |
| Дополнительно streaming ASR adapter | Первые partial можно показать до окончания длинной фразы; текущий HTTP final путь этого не даёт. | Поток partial/final привязан к utterance_id; только final запускает необратимые tools. Цель для локального RTF и t_first_partial задаётся после теста железа; пример: p95 partial ≤500 мс после начала речи. |
| Правила ошибки ASR | Ошибка модели не должна висеть как вечный pending или превращаться в вопрос агенту. | При error/timeout UI получает финальную ошибку по тому же phrase_id; ни одного agent call на пустой/ошибочный transcript. Offline fallback - ручной текстовый ввод. |
Цель в 500 мс - кандидат для будущего испытания, не утверждение о доступной нам модели. Если локальный ASR распознаёт только после конца фразы и этого хватает пользователю, streaming можно не добавлять. Файловый режим полезен для больших записей, но его очередь и таймауты отделяем от разговорных.
12TTS: подключаем недостающую половину разговора
Первый рабочий шаг - озвучить подтверждённый ответ сценария своим TTS и отправить его по обратному WebRTC-треку. Сначала можно синтезировать целую короткую реплику. Потоковая генерация и синтез предложениями появляются позже, когда есть cancellation, порядок chunks и контроль того, что пользователь действительно услышал.
| Вариант | Что переиспользовать | Ограничение |
|---|---|---|
| TTS worker на площадке | Идею backend adapter из Linux Read Aloud: text + voice/config → проверяемый audio result | MCP helper запускает локальное воспроизведение на своём компьютере. Его нельзя без адаптации назвать серверным WebRTC TTS. |
| Piper или другой локальный движок | Изолированное демо загрузки model+config и создания WAV, проверки ffprobe/полного decode | Выбор русского голоса, лицензии, CPU/GPU, качество терминов и RTF проверяются отдельно. Не обещаем равенство ChatGPT. |
| Предсинтезированные служебные реплики | Связь аудио с response_id + checksum и явной версией текста | Для "ASR недоступен" можно не ждать нейросеть. Нельзя выдать запись за свежий ответ инструмента. |
| Synthesis по завершённым фразам | Нумерация chunks и output lifecycle как в Realtime clients | Наш LLM adapter stream:false; mode возвращает структурированный JSON. Нельзя озвучивать токены JSON или ещё не проверенные результаты tools. |
| Browser speechSynthesis | Только дополнительный доступный режим | Голос браузера/ОС не всегда локальный или одинаковый на устройствах. Для строгой автономности не основной путь. |
Проектируемая граница TTS. Значение ru-local - placeholder, а не гарантированно установленный голос.
# Предложение собственного интерфейса, не существующий endpoint проекта.
job = {"session_id": "s-1", "turn_id": "t-1", "response_id": "r-1",
"generation": 4, "text": "Найден один аэропорт.", "voice": "ru-local"}
# TTS выдаёт сэмплы в своём формате, например PCM16LE mono 22050 Гц.
# Voice Gateway ресемплирует их, задаёт монотонный PTS и кодирует в Opus.
# RTP-clock Opus = 48000; он не объявляет исходные сэмплы TTS как 48000 Гц.
# Каждый chunk несёт response_id, generation и seq; отменённые chunks удаляются.
Цель первой реализации: для 100 коротких ответов text→decoded audio завершается 100/100 раз, у каждого ответ и waveform имеют один response_id. После отмены удаляем ещё не отправленный хвост и не принимаем новые chunks старого ответа. Уже отправленные RTP пакеты и буфер браузера отозвать нельзя; слышимый хвост измеряем отдельно. Затем проверяем p95 first audio, RTF TTS и задержку перебивания. Наличие WAV и успешный decode - необходимые проверки, но качество произношения и слышимость проверяются отдельно.
13Перебивание, mute и Stop - три разных действия
Сейчас Core на время запроса агента публикует voice blocked, а frontend выключает sender.track. Такой полудуплексный режим не даёт пользователю перебить работу голосом. Его можно оставить как явный fallback, но для разговорного режима микрофон остаётся активным, а новая речь отменяет старый ответ.
| Действие | Что должно произойти | Что проверяем |
|---|---|---|
| Mute | Не отправлять реальную запись; очистить capture queue. Если политика требует освобождения микрофона, stop capture и reacquire при unmute; медиа может продолжать синтетическую тишину. | После mute ни один кадр предыдущей generation не попадёт в сеть. Track.enabled сам по себе не отменяет Agent и не доказывает остановку устройства. |
| Перебивание (barge-in) | Обнаружить речь, прекратить playout, отменить TTS и незавершённую генерацию; открыть новый ход | p95 speech-start→тишина старого ответа ≤300 мс - предложенная цель; AEC обязателен к проверке, чтобы ответ из динамиков не считался речью пользователя. |
| Stop ответа | Cancel по конкретному turn_id/response_id; session и соединение могут остаться | Cancellation идемпотентна; поздние result/audio deltas не возвращают старый ответ в UI. |
| Завершить разговор | Остановить capture, workers и subscriptions, peer закрыть, сохранить остаток событий | 100 connect/disconnect циклов: tasks/subscriptions/peers возвращаются к baseline; никаких новых tools после закрытия. |
| Неразрывное действие tool | Не обещать физическую отмену уже совершённого действия | Указать в UI "ответ остановлен, действие выполнено" и сохранить результат. Cancel не равен rollback бизнес-операции. |
Псевдокод политики. Для реальной реализации нужны bounded queues, cancel propagation и тесты порядка.
# Предлагаемая защита от ответа предыдущего хода.
generation += 1
old_turn.cancel() # Отменяем вычисления, которые действительно поддерживают отмену.
playback_queue.clear() # Убираем ещё не услышанное аудио.
# Сетевой callback со старой generation завершается без показа и воспроизведения.
if chunk_generation != generation:
return
Демо WebRTC из предыдущей работы уже использует epoch и очистку входной очереди на mute. Это полезный образец, но не готовый production barge-in: там нет полноценного AEC и агентные delegation requests отклоняются. После перебивания в историю должен попасть факт, что часть озвучки не была услышана. Иначе следующий ответ опирается на реплику, которую пользователь пропустил.
14Ограничиваем не только очередь, но и задачи, байты и возраст аудио
В FanOutNode.subscribe стоит asyncio.Queue() без maxsize. Ветка drop-oldest есть, но QueueFull у такой очереди не возникнет. В WhisperStreamNode.handle_frame каждый кадр создаёт task, которая затем ждёт semaphore. Значение 3 ограничивает одновременно выполняемые publish, но не накопленные task.
Результат нашей локальной пробы 2 октября: publish заменён ожидающим mock, ни одного сетевого запроса.
{
"frames": 100,
"queue_maxsize": 0,
"queued_frames": 100,
"pending_publish_tasks": 100,
"publish_calls_entered": 3,
"network_requests": 0
}
Проба доказывает механизм роста, а не утечку или падение production. После восстановления потребителя очередь может уйти; время восстановления и рост памяти на реальной нагрузке ещё не измерены. Остановившийся потребитель способен оставить память пропорционально длине ожидания, поэтому одного semaphore недостаточно.
| Граница | Предлагаемая политика | Критерий |
|---|---|---|
| PCM frame queue | Начальный лимит 10 × 20 мс = 200 мс; одновременно maxbytes и age. При overflow - gap/overload и явное решение ASR reset или отказ | qsize ≤10 при stalled consumer; старый звук не догоняет разговор через секунды. Silent drop может испортить слова и не подходит автоматически для записи файла. |
| NATS publishing | Один или небольшой фиксированный набор worker, bounded вход; preserve seq order или contract gap detection | Число task не растёт с длительностью stall; память выходит на плато в 10-минутном тесте. |
| ASR phrase jobs | Лимит количества/байтов/возраста фраз до постановки task; конечный deadline | При недоступном ASR новая фраза получает overload, UI выходит из pending. Большие файлы - отдельная durable очередь. |
| TTS playback | Небольшой prebuffer, лимит будущего аудио и sequence continuity | Cancel очищает непроигранный хвост; underrun/dropped audio измеряются, не скрываются. |
| Graph shutdown | Cancel tasks и wait с deadline, повторное close безопасно | Shutdown завершается даже при блокированном NATS/ASR. End marker не ждёт бесконечно занятого транспорта. |
Повторяемая проба очередей. Ожидаемый результат относится к main 1fc1702; после исправления её проверки должны измениться.
# Запустить из корня webrtc-komaroff в окружении с numpy, av и nats-py.
# --source-root указывает на исследуемый checkout; сеть не используется.
python3 DOCS/codex-voice-comparison-2026-10-02/probe_audio_bounds.py --source-root .
Прямая экономия от лимита - ограниченное число удерживаемых кадров и задач, а не гарантированное ускорение ASR. В качестве исходной политики 200 мс годятся для разговора, но её надо проверить на packet loss и точности. Для обязательной полной записи сбрасывать кадры нельзя: нужна остановка с ошибкой либо иной надёжный канал.
15События сессии: из ответа целиком в наблюдаемый жизненный цикл
Сейчас есть trace envelope между сервисами и часть событий содержит session_id. Но browser подписан на общий events subject, а handleNatsPayload передаёт событие обработчику без единого фильтра активной сессии. Для двух окон или двух разговоров одного пользователя это граница, которую надо проверить. UUID сессии в payload ещё не является авторизацией.
Предложение событийного envelope, не нынешний контракт. JSON не поддерживает комментарии; поля объяснены ниже.
{
"schema_version": 1,
"event_id": "e-42",
"session_id": "s-1",
"call_id": "c-2",
"turn_id": "t-3",
"response_id": "r-4",
"generation": 7,
"seq": 42,
"type": "tts.started",
"ts_ms": 1790899200000,
"payload": {"voice": "ru-local", "sample_rate": 22050, "channels": 1}
}
| Поле | Значение и правило |
|---|---|
| session_id / call_id | Session владеет историей; call - одно медиасоединение. Reconnect создаёт новый call, но не должен обнулить историю. |
| turn_id / response_id | User turn и ответ связаны, но не равны. У отменённого ответа остаётся собственный status. |
| generation / seq | Generation отсеивает старую работу; seq задаёт порядок событий внутри договорённого потока. Нельзя сравнивать seq независимых producers без агрегатора. |
| event_id / schema_version | Event_id обеспечивает дедупликацию/возобновление; версия даёт совместимость старых consumers. |
| ts_ms | Время для журналов. Для latency измерять monotonic на одном процессе или учитывать синхронизацию часов. |
| payload | Только данные разрешённого типа. Для TTS format и voice; для transcript текст и final; credentials сюда не попадают. |
Состояния удобно держать раздельно: соединение connecting/connected/closed, вход listening/muted, работа transcribing/thinking/tool_running/speaking/cancelling. Mute и speaking могут быть одновременно, поэтому одна линейная строка статуса на всё будет давать ложные переходы. UI показывает наблюдаемое состояние, а не считает HTTP 200 доказательством слышимого ответа.
При reconnect клиент просит snapshot + события после cursor. Для media потерянное время не воспроизводится; для завершённых результатов допустим replay. Цель: в двух одновременных сессиях ни одного чужого сообщения/voice block; после reconnect - snapshot без дублирования финального ответа. Core NATS не хранит события для отсутствующего подписчика, это надо обеспечить на уровне приложения или отдельного durable stream.
16Сохранение хода, повтор запроса и права инструментов
В свежем main runtime_state и история сохраняются в одной транзакции. Тесты также подтверждают, что обычная ошибка сохранения не повторяет workflow. Но конфликт версии состояния ещё вызывает повтор workflow, а tool мог успеть выполнить действие до SQL commit. Для голоса с reconnect и повтором фразы это особенно чувствительно.
| Что добавить | Зачем | Как измерить |
|---|---|---|
| Idempotency на границе входного хода | Повтор того же final ASR/HTTP request не должен создавать второй результат | Десять повторов одного ключа → один принятый ход и один final result. Ключ scoped по site/user/session/turn, а не просто строка text. |
| Idempotency key для tool | Повтор workflow при конфликте не должен второй раз менять внешний объект | Crash after tool success before commit: при восстановлении одно действие. Инструмент либо поддерживает ключ, либо требует reconcile; exactly-once не обещать. |
| Очередь/lease по сессии | Два хода не должны одновременно менять последовательную память разговора | Две параллельные фразы → детерминированная политика: очередь, reject либо interrupt, без потери версии. |
| Outbox результата | После commit сервис может упасть до publish UI события | Crash after commit before publish: после restart/reconnect ответ появляется один раз, без повторного tool. |
| Разрешения и аудит | Голосовая модель сообщает намерение, но не является источником прав | Любой недопустимый tool отклоняется server-side; speech injection не меняет права. Отменённое действие имеет явный terminal status. |
| История слышимого ответа | Сохранённый полный текст не равен прослушанному | Для interrupted response сохранить playback position/статус. Не транслировать время в слова без aligner; контексту явно сообщать об усечении. |
В этом месте полезна идея Codex handoff: голосовой канал отдаёт агенту конкретную задачу и получает конкретный результат, сохраняя связь идентификаторов. Копировать терминал и unrestricted shell не надо. Нужны только доменные инструменты площадки с собственной проверкой прав и сроком жизни запроса.
17Локальные модели: совместимость интерфейса не гарантирует качество
В проекте уже два пути LLM: llama.cpp внутри процесса и Ollama через HTTP. Ollama можно поставить в той же изолированной сети. Для CPU-площадки GGUF путь полезен, но текущий init не содержит явной настройки GPU layers; установка GPU сама по себе не доказывает ускорение этого adapter.
| Решение | Почему именно так | Результат и цена |
|---|---|---|
| Сначала local-only profile на существующем adapter | Меньше изменений в workflow/NATS контрактах | Ноль обращений к основной сети при cold start и разговоре. Качество structured JSON и tools проверяется на локальной модели. |
| Фиксировать модель и формат ответа | Downloader без model_file может выбрать минимальный GGUF. Замена файла меняет качество и память. | SHA256/version/license и конкретный GGUF path в manifest. Нет автоматического выбора другой quantization в offline run. |
| Readiness после реального model warmup | Существующий ready после ensure_model_path ещё не доказывает загрузку в llama.cpp или успешный infer | Один локальный canary: JSON по схеме и простой ответ; отдельно время cold/warm. Дешёвый health не должен постоянно запускать большие inference. |
| Детерминированные доменные пути | Для разрешённых команд можно не вызывать модель на каждом шаге | В main уже есть guard явного поиска аэропорта до _infer_json. Продолжение требует extraction/tools и не означает полную работу без LLM. Считать число infer/turn и точность маршрутизации. |
| Streaming LLM - позже | Сейчас stream:false и структурированные modes. Голос не должен читать невалидированный JSON | Отдельный разговорный text stream с финальной валидацией. Текст tools и критичные факты озвучиваются только после результата. |
| Деградация при недоступной модели | Полная автономность не равна наличию любого inference | Предложить text-only/recorded prompts/детерминированные команды согласно профилю; UI честно сообщает ограничение. Не пробовать тайно внешний provider. |
Выбор конкретной ASR/LLM/TTS модели стоит сделать по одному русскому набору: тихая речь, имена аэропортов, номера рейсов, паузы, шум и попытки вызвать недопустимый инструмент. Без такого сравнения нельзя обосновать, что меньшая модель даст тот же результат быстрее. Ускорение измеряем на одном железе и одной нагрузке, качество - отдельно от синтаксической валидности JSON.
18Что можно переносить как код, а что только как приём
Совпадение архитектурной идеи не требует копирования большого куска Codex. Для нашего Python/TypeScript проекта дешевле собственный небольшой interface и тесты, чем зависимость от всего Rust-ядра с облачным разговорным контрактом. Перед переносом файлов отдельно проверить лицензии исходников, пакетов, весов и конкретного голоса.
| Источник | Перенос | Адаптация / проверка |
|---|---|---|
| WebRTC isolated demo: queue/epoch/mute/cleanup | Небольшие алгоритмы и сценарии тестов; Python, aiortc и PyAV совпадают по классу задачи | Не копировать ChatGPT auth/signaling. Демо не имеет полноценного AEC и production reconnect. |
| Codex bounded channels + cancellation | Приём управления временем жизни; реализовать asyncio equivalents | Проверить shutdown и late callbacks. Не переносить числа capacity без измерения нашего latency. |
| Codex protocol types / item lifecycle | Принцип start/delta/done + ID и completion | Наш envelope с schema_version; не объявлять совместимость OpenAI RPC, если семантика другая. |
| Локальные TTS demos / helper | Проверку файлов/формата и adapter подготовки модели | Backend должен вернуть PCM/audio в Gateway, а не проиграть звук на сервере. Установленные voices и лицензии не переносим по умолчанию. |
| Proxy observation demo | Раздельный учёт HTTPS/WSS и ICE/RTP | Для isolated LAN - allowlist фактических peers и отсутствие egress. HTTP proxy не закрывает UDP маршрут автоматически. |
| Штатные HTTP диктовка / synthesis | Контрактная дисциплина: точный MIME, correlation, полное декодирование результата | Не зависимость от ChatGPT endpoints. Собственный ASR/TTS контракт и локальная авторизация. |
| ChatGPT model/session internals | Не переносим | Закрытые модели/веса/сервис недоступны. Нельзя обещать on-prem ChatGPT voice из fork исходников. |
| MCP / CLI read_aloud | Возможный административный или тестовый интерфейс | MCP не заменяет WebRTC, VAD, сессию и потоковый playback. |
LiveKit/SFU не включаю в обязательный первый этап. Сейчас разговор один-на-один с сервером; aiortc уже используется. Рассматривать смену transport runtime стоит после проблем распределения peers, TURN эксплуатации или измеренной нагрузки. Новый framework добавит серверы, зависимости и offline packaging; само его имя не уменьшает latency модели.
19Какими числами проверяем пользу изменений
Базовая единица замера - один законченный пользовательский ход в конкретной сессии. Отдельно пишем длительность аудио, размер моделей/quantization, версии, CPU/GPU/RAM, число клиентов, сеть и cold/warm режим. Сравниваем одинаковые входы. Тест на наушниках не заменяет тест AEC с динамиками.
| Метрика | Точки времени / счётчик | Предлагаемая цель, не достигнутый результат |
|---|---|---|
| t_first_partial / t_final_STT | Начало речи→первый partial; конец акустической речи→final | Сначала baseline 100+ реплик; p50/p95. Для streaming-кандидата partial p95 ≤500 мс; конец→final снизить ≥20% без потери WER >1 п.п. |
| t_agent / TTFT | Final ASR→agent result; для stream - первый разрешённый текст | Считать infer/turn и tools duration. Снижение infer calls должно сопровождаться сохранением task success, не только speed. |
| t_first_audio | Конец пользовательской речи→первые декодированные сэмплы ответа у browser | Бюджет-предложение для короткого локального сценария p95 ≤2 с; подтвердить на целевом железе. HTTP 200/TTS accepted - не first audio. |
| t_interrupt | Речь пользователя во время ответа→прекращение слышимого старого аудио | p95 ≤300 мс как цель; старые chunks не принимаются после cancel generation. Уже переданные/буферизованные RTP пакеты нельзя отозвать: фактический слышимый хвост измеряется отдельно. |
| RTF ASR/TTS и вместимость | Compute time/audio duration, GPU memory и очередь при N clients | RTF <1 на N_accept, без бесконечного queue growth. N_accept устанавливается из испытания, не из MAX_CONCURRENCY. |
| Медиа | Selected candidate pair, RTP byte/packet counters, loss/jitter, underrun | Нет нежелательного peer вне LAN; формат декодируется. Норматив jitter зависит от сети; не выдумывать обещание zero-loss. |
| Bounded work | qsize, pending tasks, age_ms, bytes, RSS | Queue ≤10 frames как стартовый бюджет; task count фиксирован; stalled consumer 10 мин → память выходит на плато, overload виден. |
| Семантика | WER, task success, JSON validity, duplicate actions, session leak | 0 чужих сессий и дублированных tool side effects в fault-тестах; WER/task success сравниваются на неизменном корпусе. |
| Автономность | Процессы и трафик при отключённой основной сети | 10/10 cold starts + 100 диалогов без внешних успешных соединений и без попыток egress приложения; missing model даёт local error, не download. |
| Ресурсы и восстановление | 100 close/reconnect циклов, worker failure, CPU/RAM и task count | Ресурсы возвращаются к baseline; cancel/shutdown deadline соблюдается; результат сохранённого tool не повторяется. |
Предложение instrumentation. В общие журналы не выводить SDP, ICE credentials, tokens и личный transcript.
// Пример чтения медиаметрик в месте, где приложение владеет RTCPeerConnection.
// Это не готовая команда для браузерной консоли: pc берётся из кода приложения.
const report = await pc.getStats();
for (const item of report.values()) {
// Счётчики RTP и jitter относятся к медиа; их недостаточно для ASR latency.
if (item.type === "inbound-rtp" && item.kind === "audio") {
console.log({packetsReceived: item.packetsReceived,
packetsLost: item.packetsLost, jitter: item.jitter});
}
}
Сумма p95 отдельных этапов не равна p95 всей цепочки. Общую задержку измеряем непосредственно. При отсутствии синхронизации часов межпроцессная разность ts_ms может быть неверной; используем связанные spans, локальные monotonic интервалы и отмечаем точность clock sync. Рост RTP counters ещё не доказывает качество распознавания или полный слышимый ответ.
20Испытания: от контракта до холодного старта без сети
Для сравнения достаточно локальных тестов; сегодня они не доказывают работу всего стенда. Будущую приёмку проводим в отдельном namespace/VM площадки, а не отключением сети на рабочей машине. Любые firewall/netem изменения относятся к будущему стенду и в этом исследовании не выполнялись.
| Слой | Сценарий | Ожидаемый результат |
|---|---|---|
| Аудиоконтракт | PCM16 mono 16k; WAV в raw поле; odd byte length; неверная metadata length; channels/rate/width mismatch | Некорректная упаковка отвергается до ASR; ошибочная частота может дать искажённый звук, а не HTTP error. WAV header не интерпретируется как речь. |
| Согласование | Offer валиден/обрезан; ICE никогда не complete; peer disconnected; denied mic | Deadline и cleanup уже покрыты частью тестов main; добавить повторный connect и частично созданные ресурсы. |
| Входные кадры | Кадры 44.1/48 kHz с неполным remainder; 20 мс normalizer; stall publisher | Точная длительность после resample/flush; bounded queues и task count; gap policy проверяется на WER. |
| STT/agent | Partial меняется, final повторяется, pending→error, неверный phrase_id | Partial не запускает tool; один final принят один раз; ошибочный transcript не становится запросом. |
| Двустороннее media | Файл-фикстура → local ASR → agent echo/controlled reply → local TTS → browser capture | Соответствие каждому request ID; полный decode и параметры; отдельно ручная оценка текста, произношения и слышимости. |
| Сессии | Два browser клиента одного пользователя; reconnect с cursor; позднее событие old call | Ни одного чужого voice block/ответа; generation отсекает старый поток, snapshot не дублирует результат. |
| Инструменты | Повтор запроса; crash до/после tool/commit/publish; конфликт runtime | Нет повторного side effect; outbox/reconcile даёт финальный результат; права проверяются сервером. |
| Прерывание | Mute/unmute с queued capture; речь на фоне TTS; Stop во время ASR/LLM/tool | Отдельная политика каждого действия. Late callback не оживляет ответ; незавершённые задачи освобождены. |
| Сеть и ресурсы | Host candidates; разные VLAN; UDP forbidden; только локальный TURN; отказ DNS/ASR/NATS | Проверяем selected ICE candidate pair и все application peers. Отказ одного этапа виден как конкретная ошибка, не бесконечный spinner. |
| Offline cold start | Пакет предзагружен, internet/main-network отключены; отдельно отсутствует модель | В первом случае диалог работает; во втором отказ локальный и понятный. Блокировка egress без попыток сети - более строгий критерий, чем просто отсутствие успешного HTTP. |
Негативные тесты должны показать точную границу отказа: parser, ASR provider, Agent, TTS или media. Для load-теста увеличиваем число сессий, пока хотя бы один критерий не нарушится; последняя успешная нагрузка становится подтверждённым N_accept. Команду "docker compose up" нельзя объявить проверкой полной автономности.
21Порядок работ: сначала автономность и ограничения, затем разговор
Менять транспорт, модель, схему сессии и workflow сразу неудобно: при регрессии непонятно, где искать причину. Ниже этапы, после каждого остаётся рабочий режим. Это проект изменений, не внесённые этим документом изменения приложения.
| Этап | Область и конкретная работа | Как принимаем | Цена / откат |
|---|---|---|---|
| 0. Baseline и версия | Подтвердить deployed SHA/image отдельно от main; собрать 100 реплик и запись этапов. Использовать свежие ICE/cleanup/transaction исправления. | Воспроизводимый baseline с hardware/model config и p50/p95. | Только read-only сбор. Никакой обещанной экономии до baseline. |
| 0a. Клиентский звук и Opus | Фактические getSettings, SDP/getStats и активный codec в обе стороны. Sonora: отдельно нативный прототип с playout reference и задержкой, без двойной обработки по умолчанию. | Сравнить browser AEC и Sonora на динамиках: эхо, false barge-in, WER, p95 задержки/CPU; loss/jitter тест Opus. Выбор кодека подтверждён счётчиками. | Feature flag; browser path сохраняется. Sonora не заменяет ASR/TTS и не переносится вслепую на сервер. |
| 1. Изолированный профиль | Local DNS/CA/STUN/TURN по необходимости; свои NATS/DB/ASR/LLM; модели/assets/images заранее; startup без downloads. | 10/10 cold starts, внешний/основной network недоступен; fault missing model. | Новые config/packaging, не замена runtime. Online профиль отдельно, без hidden fallback. |
| 2. Bounded audio и jobs | Core queues, фиксированные publisher workers, ASR admission, timestamps/gap rules, shutdown deadlines. | Повтор queue probe после изменения; 10 мин stall и нагрузка без роста памяти; WER контролируется. | Меньше удерживаемого аудио, но overload надо честно показывать. File-mode не теряет кадры молча. |
| 3. Обратная озвучка | Локальный TTS provider → Core output track; response_id, полное decode, text-only fallback. | 100/100 коротких реплик сопоставлены тексту и audio; звук доходит до клиента. | Новый worker и ресурсы; выключаем feature - прежний text-only путь сохраняется. |
| 4. Session lifecycle | Schema-versioned events, фильтр/права, cursor+snapshot, generation/cancel, outbox и tool idempotency. | Две сессии, reconnect, crash matrix и duplicate request tests. | Межсервисные контракты: dual-read/migration, consumers обновляются согласованно. |
| 5. Полный разговор | Barge-in, AEC tests, правильный heard context, настройка VAD, при необходимости partial STT/TTS. | t_interrupt p95 ≤300 мс как цель; меньше задержка при неизменном task success и WER. | Full duplex feature flag; явный half-duplex fallback, если устройство/echo не прошли тест. |
| 6. Масштаб только по данным | Измерить CPU/GPU/NATS/peer load; если transport ownership мешает, отдельный media worker/SFU PoC. | N_accept и bottleneck подтверждены; сравнить до/после на одном корпусе. | LiveKit/новый runtime не обязательны. Realtime PCM не записывать в JetStream по умолчанию. |
Приоритетный набор для первой доработки: автономный профиль, bounded работа и локальная озвучка. Они закрывают зависимость от основной сети, предел ресурсов и отсутствие аудиоответа. Streaming и новая библиотека WebRTC могут быть следующими решениями, но только если измеренный сценарий этого требует.
22Куда идти в коде за каждым блоком схемы
Ссылки на webrtc-komaroff закреплены на GitLab main 1fc1702, а не на старом локальном checkout. В каждой записи указаны вход, результат и граница ответственности. Ссылки на незакоммиченные аудиодемо ведут в соседний локальный checkout; для чтения самого учебника этот каталог не нужен.
Browser: connectSession → negotiate
Вход - пользователь включает микрофон. connectSession открывает MediaStream, создаёт peer, назначает sender и при необходимости energy gate. negotiate собирает SDP и вызывает /core/offer. Выход - session_id и peer; при отказе signaling свежий main закрывает открытые ресурсы.
Core: offer → handle_track → AudioGraph
handle_offer_connect создаёт peer и UUID сессии; incoming audio запускает handle_track. Source нормализует звук, WhisperStreamNode упаковывает кадры и подписывается на текст. Выход - PCM через NATS и последующий agent request; TTS producer в этом графе не назначен.
Здесь NATS и incoming track отмечают асинхронные границы. Работа ASR не выполняется внутри Python call stack Core.
# Выборочное дерево вызовов, не временная диаграмма.
POST /core/offer
handle_offer → handle_offer_connect
_webrtc_pc_create → aiortc.RTCPeerConnection
on_track → handle_track
AudioGraph → TrackSourceNode._normalized_chunk_stream
WhisperStreamNode.handle_frame → _publish → NATS
_handle_whisper_message → handle_transcription
NATS request nats.agent.<user>
Bridge: кадры → VAD → WAV → HTTP
_process_message разбирает wire packet; _handle_frame накапливает сэмплы; VAD выбирает сегменты; _transcribe_and_publish пишет временный WAV, ждёт слот и вызывает transcribe_http_text. После обработки WAV удаляется в finally. Это не доказательство удаления всех данных сервера LinTO или журналов.
Agent: входной текст → workflow → result/UI
AgentServer.handle_request валидирует AgentInboundRequest, запускает runner и публикует UI события. Runner передаёт сценарную работу выбранному workflow через NATS, сохраняет состояние и историю, возвращает typed response. Голосовые identifiers полезны для корреляции, но нынешняя обработка остаётся текстовой.
Postgres: атомарное состояние и история
Свежий runner использует одну db.transaction() и передаёт один connection в save_runtime_state и persist_chat_turn. Это защищает два SQL эффекта вместе. Outbox, lease и идемпотентность внешних tools остаются отдельными предложениями.
LLM: local и HTTP - способы вызова, а не география
Local adapter вызывает llama.cpp; HTTP adapter делает Ollama POST /api/chat. Оба возвращают JSON message без потоковой озвучки. Локальный model downloader умеет использовать уже существующий путь; при отсутствии веса обращается к Hugging Face. Offline профиль обязан выбирать конкретный файл и проверять его наличие.
Codex: управление разговором и helper
TUI вызывает thread/realtime/start с WebRTC transport. RealtimeCallClient создаёт удалённый call; core ведёт session/events и handoff. voice-host владеет медиасоединением. Код сервера, выполняющего распознавание и генерацию аудио, здесь отсутствует; для него подтверждены клиентский контракт и исторические наблюдения, не внутреннее устройство.
Локальный Processor::new включает Sonora AEC/NS/AGC; render_output сохраняет реально воспроизводимые сэмплы, CaptureWorker передаёт их в обработчик перед capture. Это дополнение к исследованию транспорта, не функция app-server. Подробности и план переноса - в разделе Sonora, проверка кодека - в разделе Opus.
Реестр исходников
| Проект | Файл / строка | Роль |
|---|---|---|
| webrtc | src_core/handlers/handle_offer.py:63 | Создание aiortc peer и SDP answer |
| webrtc | src_front/src/webrtc/negotiate.ts:3 | POST /core/offer, SDP offer/answer |
| webrtc | src_front/src/webrtc/session.ts:19 | Микрофон, WebRTC, cleanup при ошибке |
| webrtc | src_front/src/config/appConfig.ts:54 | STUN и событийный subject |
| webrtc | src_front/src/audio/mic.ts:3 | Запрос браузерного эхоподавления и шумоподавления |
| webrtc | src_front/src/audio/vad.ts:5 | Energy gate: порог RMS/dB и gain 0/1, не нейросетевой denoiser |
| webrtc | src_core/handlers/handle_track.py:18 | Сборка графа входного аудио |
| webrtc | src_core/processors/track_source_node.py:122 | Resample, 20 мс и padding хвоста |
| webrtc | src_core/processors/base.py:16 | Очередь без maxsize |
| webrtc | src_core/processors/whisper_stream_node.py:76 | Бинарный NATS packet и задачи publish |
| asr | src/stt_whisper_to_nats.py:341 | Фраза в WAV, HTTP ASR, текст |
| asr | src/stt_whisper_to_nats.py:83 | Silero VAD, интервалы и настройка буфера |
| asr | src/stt_whisper_to_nats.py:303 | Выделение фраз из исходных сэмплов по интервалам Silero |
| asr | src/stt_whisper_to_nats.py:424 | Загрузка ONNX Silero; при отсутствии файла возможна загрузка модели |
| asr | src/utils/wire_packet.py:9 | Разбор метаданных и PCM16LE |
| asr | src/webui/file_mode.py:1202 | Multipart file, query language, ответ text/plain |
| asr | src/model_transport.py:6 | CONNECT или прямое соединение, без fallback |
| webrtc | src_core/handlers/handle_transcription.py:34 | Текст в agent request; voice blocked |
| webrtc | src_agent/agent.py:138 | Workflow и атомарное сохранение runtime + истории |
| webrtc | src_agent/service.py:151 | Валидация запроса, ответ и UI publisher |
| webrtc | src_agent/ui_events.py:15 | Ответ и команды UI с trace |
| webrtc | src_front/src/assistant/assistantApp.ts:249 | Обработка событий и управление микрофоном |
| webrtc | src_shared/contracts/common.py:22 | trace/request/correlation/session IDs |
| webrtc | src_langgraph/runtime_io.py:48 | Отдельные NATS-вызовы LLM и tools |
| webrtc | src_llm/clients/local_chat.py:8 | Локальный llama.cpp chat completion |
| webrtc | src_llm/utils/model_downloader.py:12 | Путь к GGUF или Hugging Face download |
| webrtc | src_llm/service.py:109 | Создание локальной модели без явного GPU placement |
| webrtc | src_llm/clients/ollama_chat.py:12 | POST /api/chat с stream:false |
| webrtc | stack/webrtc.drs:51 | Желаемые ASR, proxy, Ollama и leafnode связи |
| webrtc | src_front/src/map/mapController.ts:40 | Внешний style.json для карты |
| webrtc | src_core/utils/ice.py:6 | Ограниченный deadline ICE gathering |
| webrtc | src_core/utils/pc_lifecycle.py:22 | Остановка графа при disconnected/failed/closed |
| webrtc | tests/test_core_ice.py:12 | Три теста deadline ICE |
| webrtc | tests/test_agent_persistence.py:38 | Транзакция и повторы workflow |
| webrtc | src_front/src/webrtc/session.test.ts:62 | Очистка ресурсов при ошибке signaling |
| codex | codex-rs/codex-api/src/endpoint/realtime_call.rs:129 | Создание WebRTC call |
| codex | codex-rs/core/src/realtime_conversation.rs:623 | Bounded channels и cancellation |
| codex | codex-rs/app-server-protocol/src/protocol/v2/realtime.rs:143 | RPC audio chunk и realtime transport |
| codex | codex-rs/app-server-client/src/lib.rs:1 | InProcess, daemon и typed app-server client |
| codex | codex-rs/tui/src/app_server_session/realtime.rs:20 | TUI запускает V3 через app-server |
| codex | codex-rs/codex-api/src/endpoint/realtime_websocket/protocol_frameless_bidi.rs:20 | Transcript, turn.done и delegation |
| codex | codex-rs/voice-host/src/processing.rs:107 | Sonora: AEC/NS/AGC, PCM 48k mono и Opus |
| codex | codex-rs/voice-host/src/devices.rs:390 | Копия воспроизводимого звука и время вывода для AEC |
| codex | codex-rs/voice-host/src/capture_worker.rs:61 | Reference перед capture, сброс обработки на mute и разрыве |
| codex | codex-rs/voice-host/src/processing_tests.rs:3 | Тест удаления синтетического эха; здесь не запускался |
| aiortc | src/aiortc/codecs/__init__.py:27 | Поддерживаемые кодеки aiortc 1.14.0; Opus первый в списке |
| desktop | desktop-audio-processing-evidence.json:1 | Desktop 26.928.20755: фрагменты захвата, AEC и ducking с SHA256 |
| fork | scripts/webrtc-voice-isolated/voice-console.py:117 | Лимит кадров, mute, epoch и signaling |
| fork | read-aloud-linux/src/main.rs:204 | Локальные backend и custom command |
| fork | scripts/voice-probes/chatgpt-transcribe.sh:1 | HTTP диктовка вне Desktop |
| fork | scripts/webrtc-voice-proxy/README.md:1 | Раздельное наблюдение HTTP/WSS и ICE/RTP |
| fork | docs/codex-cli-voice-textbook.html:1 | Предыдущий учебник и срез Rust-исходников |
23Что подтверждено, что предложено и чего пока не знаем
VERIFIED / SOURCE означает, что связь найдена в коде или конфигурации. LOCAL TEST - что конкретная проверка выполнена без живых сервисов. INFERRED - механизм риска или ожидаемый эффект без нужного замера. PROPOSED - проект изменения. UNKNOWN - сведений недостаточно. Эти статусы не превращаются в production evidence от наличия хорошей схемы.
| ID | Утверждение | Статус | Опора | Граница |
|---|---|---|---|---|
| C01 | Core создаёт peer, media после SDP идёт по WebRTC, не HTTP upload | VERIFIED / SOURCE | offer, negotiate, track | ICE codec/peer конкретного runtime не измерен |
| C02 | Core передаёт PCM16LE mono 16k, 20 мс по NATS с metadata prefix | VERIFIED / SOURCE | track, pcm, publish | Подтверждено в main; deployed image не проверен |
| C03 | Исследованный ASR bridge пишет фразу WAV и вызывает HTTP | VERIFIED / SOURCE | asr, asr-http | Соседний checkout ASR, не runtime trace |
| C04 | Voice path возвращает текст; TTS producer не найден в graph | VERIFIED / SOURCE | track, ui-events, front-events | Не утверждает отсутствие TTS экспериментов во всём repository |
| C05 | Очередь без maxsize; pending publish tasks не ограничены semaphore | VERIFIED / SOURCE + LOCAL TEST | queue, publish | Mock stall: 100 frames → 100 tasks, network=0; не production RAM benchmark |
| C06 | Main исправляет ICE deadline, error cleanup и SQL transaction | VERIFIED / SOURCE + LOCAL TEST | ice, tests-core, front-session, tests-front, agent, tests-agent | 13 Python + 1 frontend unit tests; не e2e |
| C07 | Voice blocked выключает sender track во время agent request | VERIFIED / SOURCE | transcription, front-events | Не полный cancel и не остановка device capture |
| C08 | Browser не имеет единого session filter в dispatch событий | VERIFIED / SOURCE | front-events, front-config | Риск межсессионного смешения INFERRED; live leak не проверен |
| C09 | DRS зависит от центрального ASR/LLM/leafnode | VERIFIED / SOURCE CONFIG | stack | Desired config, не фактическое подключение production |
| C10 | Local GGUF путь есть, cache miss вызывает download | VERIFIED / SOURCE | llm-local, llm-model | Наличие файла/качество модели на offline host UNKNOWN |
| C11 | CLI V3 разделяет signaling/sideband и медиапуть | VERIFIED / SOURCE | codex-call, codex-tui, codex-events | CLI rust-v0.156.1; серверная реализация UNKNOWN |
| C12 | Код WebRTC демо использует bounded capture queue и epoch | VERIFIED / SOURCE | voice-demo | Наше демо, не автоматически поведение Desktop/CLI |
| C13 | Возможен padding на хвосте каждого resampled frame | VERIFIED / SOURCE; влияние INFERRED | pcm | Drift и влияние на ASR не измерены |
| C14 | Ожидаемое снижение latency и рост ёмкости | PROPOSED / NOT MEASURED | queue, publish, asr | Все процентные и временные цели - критерии будущего benchmark |
| C15 | CLI voice-host включает локальные Sonora AEC/NS/AGC и playout reference | VERIFIED / SOURCE | codex-processing, codex-render, codex-capture, codex-aec-test | Upstream тест найден, не запускался; акустическое качество не измерено; перенос в наш клиент PROPOSED |
| C16 | Browser запрашивает AEC/NS; aiortc поддерживает Opus | VERIFIED / SOURCE | front-mic, front-config, aiortc-codecs | Фактические getSettings, selected codec, FEC/DTX и качество конкретного runtime UNKNOWN |
| C17 | Energy gate закрывает тихий сигнал; Silero вырезает фразы из исходного PCM, не очищает его | VERIFIED / SOURCE | front-gate, front-session, asr-segments, asr-vad-load | Качество gate/VAD и внутренний preprocessing ASR provider UNKNOWN; ONNX надо заранее поставить для offline |
| C18 | Desktop имеет разные web capture настройки для Codex voice, ChatGPT voice и диктовки; ducking меняет playout volume | VERIFIED / STATIC BUNDLE | desktop-audio | Бандл 26.928.20755 повторно прочитан; настройки живого трека, DSP native capture и качество UNKNOWN; Sonora в этих web ветках не подтверждена |
Какие проверки выполнены в этой работе
Из remote main извлечена временная копия только нужных исходников; рабочая папка не переключалась. В отдельном venv установлены test requirements. Выполнены python -m unittest discover -s tests -v: 13/13 passed (3 persistence, 3 ICE, 7 routing). После подготовки зависимостей frontend vitest run: 1/1 passed, cleanup при отказе signaling. Модели не запускались, в тестах нет живого ASR/LLM/NATS.
Проба очередей импортирует настоящие классы Core и заменяет только publish ожидающим mock. Получены queue size=100, pending tasks=100, publish entered=3; network requests=0. Это отдельное доказательство роста незавершённой работы, а не тест качества звука.
Выполнены 12 статических проверок HTML и 8 тестов навигационного JS на mock DOM. Проверены внутренние anchors, ссылки схем, меню всех разделов, JSON-примеры, относительные ссылки и отсутствие сетевых загрузок. SVG отдельно отрисованы через ffmpeg/librsvg: это проверка схем, не всей страницы.
Дополнение Sonora/Opus и сравнение очистки проверены отдельно: 11 тестов пояснений, evidence, ссылок схем и краткости выжимки. Пример getStats исполнен на фиктивных потоках в обе стороны, включая неизвестный кодек. Пересобраны 9 SVG учебника и выжимки, просмотрены пять новых или изменённых схем. Микрофон, реальные RTP и обработка Sonora в этих тестах не запускались.
Пять сохранённых Desktop-фрагментов сверены непосредственно с локальным ASAR: версия package.json, SHA256 трёх бандлов, byte offsets и связанные фрагменты совпали. Данные входа не читались. Эта проверка подтверждает источник текста, но не настройку текущего устройства и не качество удаления эха.
Браузер отклонил открытие file:// по политике окружения. Обход через HTTP-сервер или headless browser не выполнялся. Поэтому реальный UI, мобильная раскладка, печать и клавиатурная навигация в браузере остаются непроверенными. Полный протокол - в verification.json; повторяемые команды - в README.txt.
Что требует отдельного стенда
Не проверены deployed SHA/images и live DRS, фактический selected ICE pair/codec, реальное качество ASR/TTS, RTF моделей на целевой CPU/GPU, AEC при перебивании, end-to-end задержки, конкурентные сессии и полностью offline cold start. Также не установлен внутренний STT/TTS стек ChatGPT. Для утверждения автономности нужен весь комплект локальных зависимостей и сетевой тест, а не только source review.
24Проверяем понимание и переходим к реализации
| Вопрос нового участника | Ответ |
|---|---|
| Если HTTPS работает, почему голос не проходит? | Signaling и медиа используют разные пути. Нужно проверить ICE candidate pair, доступность UDP/TURN и decoded audio; успешный /core/offer недостаточен. |
| Почему у нас потоковая передача, а распознавание ждёт? | Поток кадрами доходит до bridge, который ждёт границу фразы и отправляет WAV. Это file-based ASR внутри разговорного тракта. |
| Можно просто подключить Codex app-server и получить offline voice? | Нет. Его исследованный voice путь вызывает удалённый сервис. Полезны обязанности/контракты, но локальные модели и инфраструктуру надо поставить самим. |
| У нас уже есть session_id. Зачем ещё generation? | Session сохраняется при reconnect; generation различает работу до и после отмены. UUID сессии не отсеивает запоздалый ответ внутри той же сессии. |
| Семафор 3 разве не держит очередь маленькой? | Нет: задачи создаются до ожидания semaphore. Лимит нужен до постановки task или через fixed workers с bounded входом. |
| Зачем оставлять NATS, если в Codex медиа идёт напрямую? | У нас он уже соединяет Core с ASR workers. Codex тоже имеет сетевой медиасервер, просто удалённый. Удалять NATS стоит по bottleneck measurements, не по сходству схем. |
| Один PCM chunk можно положить в WAV и отправить как raw? | Нет. WAV содержит RIFF header, raw PCM - только сэмплы. Header станет шумом; неверная частота меняет время/тембр и не обязана вызвать ошибку. |
| Сколько именно мы выиграем? | Уже доказан механизм накопления и предложено его ограничение. Ускорение/ёмкость не измерены; их принимаем по baseline и одинаковому корпусу с метриками из главы о замерах. |
| Что потребуется перед первой доработкой? | Целевые CPU/GPU/RAM и N_accept, модельные файлы с лицензиями, доверенный CA/DNS, local network topology, выбранный TTS голос и допустимое качество. Отсутствующие данные не мешают читать документ, но мешают обещать конкретные SLA. |
Начать можно с двух независимых результатов: комплект для cold start без основной сети и ограниченные очереди на существующем transport. Затем локальный TTS закрывает аудиоответ. Когда эти части проверены, имеет смысл доводить полнодуплексный разговор, а не заменять весь проект ради сходства с ChatGPT.
Связанные проектные документы: текущая архитектура, NATS контракты, точки входа. Предыдущая работа: аудиоучебник Codex в соседнем checkout. Его ссылки необязательны для открытия этого автономного HTML.
Официальные источники для протоколов
Роли app-server и lifecycle операций: OpenAI app-server. Характер Core NATS и необходимость отдельного хранения завершённых событий: NATS Core. Opus RTP-clock 48 кГц при разных частотах входа: RFC 7587. Peer и getStats: W3C WebRTC. Условия доступа к микрофону: MDN getUserMedia. Ссылки не загружаются автоматически и не являются runtime dependencies учебника.