От микрофона до агента

Codex CLI / ChatGPT / Community и webrtc-komaroff: аудио, сеть, состояние и проверяемые улучшения.

Один голосовой тракт, несколько независимых каналов

Голосовой интерфейс не выбирает один универсальный API. Диктовка возвращает текст, озвучка возвращает звук, а разговор держит двусторонний сеанс. В Codex и в webrtc-komaroff эти задачи соединены разными способами. Здесь обе реализации разобраны в одном учебнике, вместе с демо, исходниками и результатами диагностики.

Сначала отделяем звук от управления. Затем разбираем форматы и воспроизводимые примеры Codex, переходим к нашему Core и NATS, проверяем подключение и только после этого обсуждаем точность распознавания. Такой порядок помогает не лечить модель, когда до неё ещё не дошёл ни один аудиокадр.

Как связаны транспорт, очистка звука и агентPCM → OpusICE / DTLS / SRTPрезультат ASR → событиятекст / событияЗахват и очисткаBrowser AEC/NS или SonoraМедиатранспортWebRTC: согласованный OpusCore и ASR bridge16 kHz PCM → Silero → ASRУправление разговоромHTTP / WSS / JSON-RPCАгент и интерфейсLLM, инструменты, состояние
Это карта ролей, не одна общая реализация. У Codex удалённый Realtime заменяет наш Core/ASR bridge; app-server координирует разговор. У нас NATS связывает отдельные локальные сервисы.

Микрофон отдаёт отсчёты звука. Клиент очищает и кодирует их, а WebRTC доставляет медиа собеседнику. Управляющие каналы создают сеанс и передают события. NATS в нашей реализации дополнительно переносит декодированное аудио между Core и ASR bridge. У каждого перехода свой контракт, и успех одного канала ничего не говорит о готовности соседнего.

Как читать без прыжков между книгами

ВопросРаздел
Что вызывают кнопки и /voice?Интерфейс, роль app-server
Как повторить запрос без Desktop?Подготовка, HTTP STT, WebRTC V3, TTS
Что идёт через proxy?Маршруты HTTPS/WSS и отдельное медиа
Что передаём у нас?Текущий тракт, бинарный PCM-контракт
Почему при HTTP 200 нет звука?ICE и DTLS, случай 701, проверки live
Что менять и как проверить выигрыш?Sonora / Opus, порядок работ, испытания

Что означает отметка проверки

SOURCE - связь найдена в закреплённом коде; RUNTIME - поведение наблюдали в названной пробе; INFERRED - объяснение следует из наблюдений, но точный узел отказа ещё не установлен; PROPOSED - предлагаемое изменение. Исторический успех API не обещает доступ тому же аккаунту сегодня. Улучшения ниже не внедрялись при сборке этой книги.

Читать можно без сети. Ссылки на наши файлы и исходники относительные и остаются внутри комплекта. Ссылки на RFC, GitHub и GitLab - дополнительные первоисточники; переход к ним требует сети. В архиве нет входа пользователя, cookies, личных аудиозаписей, моделей или установленного Desktop.

Приёмка этой объединённой редакции: ссылки, повторная сборка, тесты и ограничение браузерного просмотра. Исторические отчёты отдельных демо остаются в своих главах.

Переносимый комплект: подготовка и запуск. Ссылки и код рядом с этим файлом; команды выполняются из корня после подключения scripts/audio-textbook/env.sh. Вход и модели не входят в архив.

Как голос становится текстом

Здесь собраны разные задачи, которые легко спутать из-за похожих кнопок. Диктовка даёт текст для поля ввода, озвучка даёт звук, а разговор держит двусторонний сеанс. Сначала выберите результат, который нужен вам. Затем проследите путь от действия пользователя до конкретного запроса и проверки ответа.

Открыть демо

Helper - локальный помощник, который обслуживает звук. App-server управляет разговором и событиями Codex. По WebRTC передаётся звук, по HTTPS/WSS - управление и текстовые события. SDP описывает соединение, а не аудиозапись; подробности дальше в форматах и словаре.

Голосовой режим Codex CLI: звук отдельно от управления Микрофон передаёт звук codex-voice-host. Helper отправляет Opus напрямую удалённому сервису через WebRTC. CLI и app-server согласуют соединение и получают текст; через них этот аудиопоток не проходит. Ваш компьютер Удалённый сервис ◉ Микрофон codex-voice-host CLI + app-server SDP / IPC Голосовой сервис WebRTC · звук Opus Расшифровка Создание и управление сеансом Endpoint и формат · нажмите для полных примеров ↓Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/doneФормат: Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демоПример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}Локальный IPC codex-voice-host · SDP / mute / stopФормат: Команды управления + SDP; аудио между устройствами и helper локальноеПример: SDP offer/answer; mute → отключить захват; stop → закрыть сеансPOST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avasФормат: application/json: sdp + session → plain SDP answer + Location/call IDПример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}WSS wss://api.openai.com/v1/live/<call_id>Формат: WebSocket UTF-8 JSON: тексты реплик, завершение, координацияПример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}WebRTC · выбранный SDP/ICE peer · не HTTP endpointФормат: Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файлПример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц
ЗвукУправлениеТекст

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

CLI ↔ локальный app-server

Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done

Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо

Пример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}

Полный запрос, ответ, код и ограничения

CLI ↔ voice helper

Локальный IPC codex-voice-host · SDP / mute / stop

Команды управления + SDP; аудио между устройствами и helper локальное

Пример: SDP offer/answer; mute → отключить захват; stop → закрыть сеанс

Полный запрос, ответ, код и ограничения

Создать WebRTC V3 call

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas

application/json: sdp + session → plain SDP answer + Location/call ID

Пример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}

Полный запрос, ответ, код и ограничения

События WebRTC V3

WSS wss://api.openai.com/v1/live/<call_id>

WebSocket UTF-8 JSON: тексты реплик, завершение, координация

Пример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}

Полный запрос, ответ, код и ограничения

Звук разговора

WebRTC · выбранный SDP/ICE peer · не HTTP endpoint

Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файл

Пример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Звук идёт потоком

Не нужно сначала сохранять WAV или FLAC. В CLI микрофоном и WebRTC занимается отдельный helper.

App-server организует сеанс

Авторизация, согласование соединения и события текста. Он не выполняет локальное распознавание речи.

Куда идти дальше

Для готовой записи начните с /transcribe или PCM Realtime STT. Для чтения своего текста есть HTTP pronunciation, GA Realtime, appendSpeech и локальные движки. Сохранённый ChatGPT-ответ читается через /synthesize. Самостоятельный разговор собран в WebRTC /voice. У каждого способа своя глава, команда и доказательный статус.

Отказы Speech429, summary404 и непроверенные режимы вынесены отдельно. Сравнение wire форматов не обещает обход распознавания клиента.

Кнопки, команды и их обработчики

Выбор аудиопути начинается с обработчика кнопки или команды, а не с расширения файла. Микрофон Desktop может выбрать файловую либо потоковую диктовку; команда /voice начинает разговор. Снимки помогают найти элемент, а код показывает, какой путь он способен вызвать. Снимок сам по себе не доказывает сетевой маршрут.

Открыть карту: где и как вызываются все способы

Настоящий снимок поля ввода Codex: микрофон и остановка выполняющегося хода
Снято 30.09.2026 из уже открытого Codex. Это app-gpt-fork, не доказательство наличия Linux Read Aloud в Community. Квадрат справа - остановка ответа, не старт разговора. Переписка и список чатов не включены.

Маленький микрофон превращает речь в текст. Разговор создаёт отдельный realtime-сеанс. Штатный ChatGPT Read Aloud запрашивает аудио по ID ответа, а Linux-кнопка расширения передаёт сам текст выбранному backend. Для GA Realtime и самостоятельных демо кнопки в Desktop нет: контракт задаёт команда запуска.

ВходОбработчик / путьПример и код
МикрофонФайл или PCM-поток → удалённая STTОткрыть разбор
Разговор DesktopRPC start → WebRTC / удалённый сеансОткрыть разбор
CLI /voiceTUI → app-server + helper → WebRTCОткрыть разбор
ПроизношениеТекст → pronunciation/synthesize → MP3Открыть разбор
Read Aloud ChatGPTID ответа → /synthesize → AAC / MP3Открыть разбор
Read Aloud LinuxТекст → IPC → локальный / свой backendОткрыть разбор
MCP / speakТекст → Rust helper → выбранный backendОткрыть разбор
Изолированные APIЯвная команда → выбранный контрактОткрыть разбор

Снимки микрофона получены. Read Aloud, настройки и экран разговора описаны по коду, но не сняты из установленного Community. Сетевые успешные пробы не подменяют GUI-проверку.

Пример: тот же pronunciation API вне интерфейса

Здесь используется демонстрационный текст. --dry-run не обращается к серверу и не читает токен. Подготовка остальных команд находится на отдельной странице.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
repo="$AUDIO_ROOT"
audio_out="$(mktemp -d "$audio_out/codex-audio-ui.XXXXXX")"
python3 "$repo/scripts/tts-demos/http-tts.py" chatgpt-text   "$audio_out/voice.mp3" --text-file "$repo/scripts/tts-demos/sample-ru.txt" --dry-run --auth-file "$auth_file"

Контракт pronunciation · Вызов из языкового виджета и код · Что действительно проверено

Текущий Codex: микрофон справа от модели; справа от него квадрат остановки выполняющегося хода
Настоящий снимок уже открытого Codex, 30.09.2026. Взята только область ввода, без переписки и списка чатов. Это окно из app-gpt-fork/chatgpt-app, а не подтверждение установки Community из codex-desktop-linux. В момент снимка шёл ответ: круглая кнопка с квадратом останавливает ход, а не начинает голосовой разговор.

Удалось снять поле ввода и его кнопки. Снимки разговора, меню удалённого Read Aloud, кнопки Linux Read Aloud и её настроек в этом проходе не получены. Ниже их положение описано по коду; нарисованных "скриншотов" вместо них нет. Рабочий сетевой контракт и видимость кнопки в конкретном экземпляре - разные проверки.

Карта входов в аудиофункцииКаждая строка - самостоятельный путь. Нажмите на блок, чтобы открыть объяснение и код.МикрофонФайл или PCM-поток → удалённая STTРазговор DesktopRPC start → WebRTC / удалённый сеансCLI /voiceTUI → app-server + helper → WebRTCПроизношениеТекст → pronunciation/synthesize → MP3Read Aloud ChatGPTID ответа → /synthesize → AAC / MP3Read Aloud LinuxТекст → IPC → локальный / свой backendMCP / speakТекст → Rust helper → выбранный backendИзолированные APIЯвная команда → выбранный контрактEndpoint и формат · нажмите для полных примеров ↓POST https://chatgpt.com/backend-api/transcribeФормат: multipart/form-data: file бинарно, MIME по контейнеру → JSON textПример: file=raven.flac; Content-Type: audio/flac → {"text":"Привет"}POST https://chatgpt.com/backend-api/codex/dictation-stream-connect-info → выданный WSS URLФормат: JSON connect-info → session.start + audio.append: Base64 raw PCM16 monoПример: {type:"audio.append",audio:"AQD//w=="}; sample_rate_hz в session.startPOST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avasФормат: application/json: sdp + session → plain SDP answer + Location/call IDПример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}WSS wss://api.openai.com/v1/live/<call_id>Формат: WebSocket UTF-8 JSON: тексты реплик, завершение, координацияПример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}WebRTC · выбранный SDP/ICE peer · не HTTP endpointФормат: Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файлПример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГцPOST https://chatgpt.com/backend-api/pronunciation/synthesize?format=mp3Формат: application/json: text / pronunciation_language / source → MP3 bytesПример: {text:"Привет",pronunciation_language:"ru",source:"translation"}GET https://chatgpt.com/backend-api/synthesize?conversation_id=<id>&message_id=<id>&voice=<voice>&format=mp3Формат: Query-параметры, тела нет → MP3 / AAC; не отправка текста ответаПример: conversation_id=demo; message_id=answer; voice=juniper; format=mp3Локальный IPC linux-read-aloud · action=speakФормат: Объект с text → backend result; сетевой endpoint по умолчанию отсутствуетПример: {action:"speak",source:"button",text:"Привет"}stdio:// · tools/call(name=read_aloud) → Rust helperФормат: JSON-RPC 2.0 на строку; arguments.text UTF-8 → результат запускаПример: tools/call {name:"read_aloud",arguments:{text:"Привет",interrupt:true}}WSS wss://api.openai.com/v1/realtime?intent=transcriptionФормат: UTF-8 JSON; Base64 raw PCM16LE mono 24000; WAV header не отправляетсяПример: {type:"input_audio_buffer.append",audio:"AQD//w=="} → commit → transcriptWSS wss://api.openai.com/v1/realtime?model=gpt-realtimeФормат: UTF-8 JSON input_text → response.output_audio.delta: Base64 PCM16 mono 24 кГцПример: {type:"response.output_audio.delta",response_id:"demo",delta:"AQD//w=="}stdio:// · thread/realtime/start → thread/realtime/appendSpeech → outputAudio/deltaФормат: JSON-RPC text → audio.data (Base64 PCM) + sampleRate / numChannelsПример: appendSpeech {threadId:"demo",text:"Привет"} → audio.data="AQD//w=="
Здесь нет универсального переключателя STT/TTS: путь задают выбранная функция, доступность сервиса и её конфигурация.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Файловая диктовка /transcribe

POST https://chatgpt.com/backend-api/transcribe

multipart/form-data: file бинарно, MIME по контейнеру → JSON text

Пример: file=raven.flac; Content-Type: audio/flac → {"text":"Привет"}

Полный запрос, ответ, код и ограничения

Потоковая диктовка Desktop

POST https://chatgpt.com/backend-api/codex/dictation-stream-connect-info → выданный WSS URL

JSON connect-info → session.start + audio.append: Base64 raw PCM16 mono

Пример: {type:"audio.append",audio:"AQD//w=="}; sample_rate_hz в session.start

Полный запрос, ответ, код и ограничения

Создать WebRTC V3 call

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas

application/json: sdp + session → plain SDP answer + Location/call ID

Пример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}

Полный запрос, ответ, код и ограничения

События WebRTC V3

WSS wss://api.openai.com/v1/live/<call_id>

WebSocket UTF-8 JSON: тексты реплик, завершение, координация

Пример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}

Полный запрос, ответ, код и ограничения

Звук разговора

WebRTC · выбранный SDP/ICE peer · не HTTP endpoint

Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файл

Пример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц

Полный запрос, ответ, код и ограничения

Произвольный текст → MP3

POST https://chatgpt.com/backend-api/pronunciation/synthesize?format=mp3

application/json: text / pronunciation_language / source → MP3 bytes

Пример: {text:"Привет",pronunciation_language:"ru",source:"translation"}

Полный запрос, ответ, код и ограничения

Read Aloud существующего ответа

GET https://chatgpt.com/backend-api/synthesize?conversation_id=<id>&message_id=<id>&voice=<voice>&format=mp3

Query-параметры, тела нет → MP3 / AAC; не отправка текста ответа

Пример: conversation_id=demo; message_id=answer; voice=juniper; format=mp3

Полный запрос, ответ, код и ограничения

Кнопка Linux Read Aloud

Локальный IPC linux-read-aloud · action=speak

Объект с text → backend result; сетевой endpoint по умолчанию отсутствует

Пример: {action:"speak",source:"button",text:"Привет"}

Полный запрос, ответ, код и ограничения

Read Aloud MCP STDIO

stdio:// · tools/call(name=read_aloud) → Rust helper

JSON-RPC 2.0 на строку; arguments.text UTF-8 → результат запуска

Пример: tools/call {name:"read_aloud",arguments:{text:"Привет",interrupt:true}}

Полный запрос, ответ, код и ограничения

Наше Realtime STT

WSS wss://api.openai.com/v1/realtime?intent=transcription

UTF-8 JSON; Base64 raw PCM16LE mono 24000; WAV header не отправляется

Пример: {type:"input_audio_buffer.append",audio:"AQD//w=="} → commit → transcript

Полный запрос, ответ, код и ограничения

Наше GA Realtime TTS

WSS wss://api.openai.com/v1/realtime?model=gpt-realtime

UTF-8 JSON input_text → response.output_audio.delta: Base64 PCM16 mono 24 кГц

Пример: {type:"response.output_audio.delta",response_id:"demo",delta:"AQD//w=="}

Полный запрос, ответ, код и ограничения

app-server appendSpeech

stdio:// · thread/realtime/start → thread/realtime/appendSpeech → outputAudio/delta

JSON-RPC text → audio.data (Base64 PCM) + sampleRate / numChannels

Пример: appendSpeech {threadId:"demo",text:"Привет"} → audio.data="AQD//w=="

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

1. Микрофон: записать речь в поле ввода

Значок микрофона в настоящем поле ввода Codex

Нажимаете маленький микрофон справа от названия модели, произносите текст и завершаете запись. Результат появляется в поле ввода; это диктовка, а не разговор с аудиоответом. В проверенном бандле PPc → FPc → startDictation выбирает запись и способ отправки.

Потоковая ветка включается только при разрешённом streaming и отсутствии явно заданного языка: C && ge.current == null. Она получает connect-info, передаёт PCM16 mono с указанной фактической частотой AudioContext и принимает текст. При невозможности начать или завершить поток предусмотрена файловая транскрипция. У прикреплённого аудиофайла streamingSession: null, поэтому это сразу файловый путь.

Файловая ветка Fki отправляет multipart с file и, при наличии, language на POST /transcribe. В ChatGPT-контексте HPc добавляет идентификаторы записи, попытки и сообщения. Контейнер зависит от записывающего клиента: файл FLAC в нашем примере не доказывает, что Desktop записывает микрофон в FLAC. Распознаёт удалённый сервер; исходники серверной STT нам недоступны.

Фрагмент проверенного Desktop-бандла

webview/assets/app-initial-41d268a90bf8.js · смещение 10426510 символов · 30.09.2026. Это выдержка из минифицированного кода, не самостоятельный скрипт.

Выдержка JavaScript клиента. Это не Rust и не готовый самостоятельный скрипт.

// Обработчик выбирает потоковую диктовку, если она разрешена и сеанс ещё не подготовлен.
// Комментарии добавлены для учебника; minified-выдержка может быть неполной.
let t=C&&ge.current==null;if(l.timings!=null&&(l.timings.streamingAttempted=t),le.current=null,F(!1),w!==`composer`&&Oe(),t){let e=Ce();try{d=r.prepareStreaming(`composer`,l.performance,e.onTranscriptUpdate==null?void 0:(t,n)=>{oe.current!==`abort`&&e.onTranscriptUpdate?.(t,n)},n.chatgpt==null?void 0:{dictationSessionId:l.dictationSessionId,attemptId:l.attemptId,finishTimeoutMs:6e4}),re.current=d}catch(e){let t=e;d?.close(),d=null,Jd.warning(`[Composer] unable to prepare streaming dictation`,{safe:{},sensitive:{error:t}})}}n.chatgpt;let i=!1,c=d!=null,p=IPc,m=await r.start(()=>{i=!0,c=!1,p()});u=!0;let h=performance.now();if(d!=null&&re.current!==d&&(d=null),f())return;l.
Фрагмент проверенного Desktop-бандла

webview/assets/app-initial-41d268a90bf8.js · смещение 4053407 символов · 30.09.2026. Это выдержка из минифицированного кода, не самостоятельный скрипт.

Выдержка JavaScript клиента. Это не Rust и не готовый самостоятельный скрипт.

// Запрос получает сведения для соединения диктовки; session.start отдельно задаёт PCM и VAD.
// Комментарии добавлены для учебника; minified-выдержка может быть неполной.
async function tti(){return(await ol.getInstance().post(`/codex/dictation-stream-connect-info`,void 0)).body}function nti(){return(nti=e((()=>{vs()})))()}function rti(e,t,n){return{type:`session.start`,dictation_session_id:n.dictationSessionId,attempt_id:n.attemptId,config:{language:n.language,input_audio_format:`pcm16`,sample_rate_hz:e,num_channels:1,max_buffer_size_bytes:4194304,max_utterance_duration_ms:3e4,session_ttl_ms:3e5,provider_mode:`streaming_sse`,transcript_delivery_mode:t?`segment`:`final_only`,vad:{type:`server_vad`,threshold:.5,prefix_padding_ms:300,silence_duration_ms:500}}}}function iti(e,t){return e.reason.length>0?new aU(t?`Dictation stream closed with reason '${e.reason}' and code ${e.code}.`:`Dictation stream closed before session.start completed with reason '${e.reason}' and code ${e.cod
Фрагмент проверенного Desktop-бандла

webview/assets/app-initial-41d268a90bf8.js · смещение 4596422 символов · 30.09.2026. Это выдержка из минифицированного кода, не самостоятельный скрипт.

Выдержка JavaScript клиента. Это не Rust и не готовый самостоятельный скрипт.

// Файловый STT собирает multipart с исходным Blob, MIME и именем, затем вызывает /transcribe.
// Комментарии добавлены для учебника; minified-выдержка может быть неполной.
async function Fki(e,t={}){let n=t.contentType??(e.type&&e.type.trim().length>0?e.type:`audio/webm`),r=n.split(/[/;]/)[1]??`webm`,i=zki(t.filename??`codex.${r}`),a={language:t.language},o=Bki(),s=await Rki({blob:e,boundary:o,filename:i,contentType:n,fields:a});return Lki(()=>ol.getInstance().fetch(`/transcribe`,{method:`POST`,body:s,headers:{"Content-Type":`multipart/form-data; boundary=${o}`,...Mo()},signal:t.signal,retry:!1}).then(e=>e.json()).then(Iki),t.signal)}function Iki(e){return Hki.parse(e)}async function Lki(e,t){t?.throwIfAborted();for(let n=0;;n++){t?.throwIfAborted();try{return await e()}catch(e){if(t?.throwIfAborted(),n===2||!(e instance
Фрагмент проверенного Desktop-бандла

webview/assets/app-initial-41d268a90bf8.js · смещение 10432015 символов · 30.09.2026. Это выдержка из минифицированного кода, не самостоятельный скрипт.

Выдержка JavaScript клиента. Это не Rust и не готовый самостоятельный скрипт.

// ChatGPT-ветка добавляет file и необязательные metadata в FormData.
// Комментарии добавлены для учебника; minified-выдержка может быть неполной.
async function HPc(e,t){let{Request:n}=await Y(async()=>{let{Request:e}=await import(`./chatgpt-request-26b6ea5d126d.js`);return{Request:e}},__vite__mapDeps([802,410,1,411]),import.meta.url),r=t.contentType??(e.type&&e.type.trim().length>0?e.type:`audio/webm`),i=r.split(/[/;]/)[1]??`webm`,a=(t.filename??`codex.${i}`).replace(/"/g,``),o=new FormData;o.append(`file`,new File([e],a,{type:r}));for(let[e,n]of Object.entries({language:t.language,dictation_session_id:t.dictationSessionId,attempt_id:t.attemptId,duration_ms:t.durationMs?.toString(),conversation_id:t.conversationContext?.conversationId,parent_message_id:t.conversationContext?.parentMessageId}))n&&o.append(e,n);t.signal.throwIfAborted();let s=performance.now();t.onRequestStart({audioSentTs:performance.timeOrigin+s});let c=await Lki(()=>n.safePost(`/transcribe`,{requestBody:o,signal:t.signal,retry:!1}).then(Iki),t.signal),l=performance.now();return t.onRequestSuccess({transcriptReceivedTs:performance.timeOrigin+l,transcriptLatencyMs:l-s,transcriptLength:c.text.length}),c}function UPc(e){return new Promise(t=>{let n=URL.createObjectURL(e),r=new Audio(n),i=()=>{URL.revokeObjectURL(n),r.removeEventListener(`loadedmetadata`,a),r.removeEventListener(`error`,o)},a=()=>{let e=r.duration;i(),t(Number.isFinite(e)&&e>=0?e*1e3:void 0)},o=()=>{i(),t(void 0)};r.addEventListener(`loadedmetadata`,a,{once:!0}),r.addEventListener(`error`,o,{once:!0}),r.load()})}var WPc,O7,GPc;function KPc(){return(KPc=e((()=>{WPc=Q(),BL(),q(),O7=nf(),rGn(),_re(),ak(),ds(),$N(),nc(),Vti(),iu(),sa(),X(),fNc(),NNc(),INc(),Pki(),sf(),KNc(),Uki(),iNc(),ud(),qm(),Ju(),NPc(),em(),GPc=250})))()}function qPc(){return Le(ZT,`composer.startDictation`)}function 

Пример вызова

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
python3 "$repo/scripts/voice-probes/chatgpt-dictation-file.py"   "$INPUT_AUDIO" --auth-file "$auth_file" --dry-run
# Уже проверенная файловая транскрипция (это живой запрос):
# bash "$repo/scripts/voice-probes/chatgpt-transcribe.sh" "$INPUT_AUDIO" --auth-file "$auth_file"

Контракт /transcribe · Потоковая STT в независимом демо · Bash-клиент: отправка файла

Вернуться к карте вызовов ↑

2. Разговор Desktop: отдельный сеанс

Кнопка с квадратом: остановка текущего ответа, не кнопка голосового разговора

Когда интерфейс предлагает Start voice chat, открывается двусторонний голосовой сеанс. Не путайте его с микрофоном диктовки и с квадратом на снимке. Саму кнопку старта разговора в этом проходе не снимали; наличие соответствующего кода не гарантирует её доступность аккаунту.

В Codex-ветке XGi отправляет thread/realtime/start локальному app-server: thread ID, модальность ответа, голос, транспорт и настройки сеанса. App-server управляет агентным сеансом и согласованием соединения. При WebRTC медиа идут по RTC-трекам, а не как файл через этот RPC. appendSpeech, appendText и WebSocket appendAudio - отдельные операции, не названия кнопок микрофона.

В бандле также есть облачная ветка Your Dot с /tbo/{tbo_id}/voice/calls. Её нельзя выдавать за наш воспроизведённый Codex V3: интерфейс и контекст определяют, какая ветка вообще доступна.

Фрагмент проверенного Desktop-бандла

webview/assets/app-initial-41d268a90bf8.js · смещение 4940389 символов · 30.09.2026. Это выдержка из минифицированного кода, не самостоятельный скрипт.

Выдержка JavaScript клиента. Это не Rust и не готовый самостоятельный скрипт.

// Этот обработчик вызывает локальный thread/realtime/start; media-байты здесь не показаны.
// Комментарии добавлены для учебника; minified-выдержка может быть неполной.
async function XGi({codexResponseHandoffPrefix:e=``,codexResponseItemPrefix:t=null,codexResponsesAsItems:n=!1,initialItems:r,manager:i,conversationId:a,outputModality:o,prompt:s=null,transport:c,realtimeEndInstructions:l,realtimeSessionId:u=null,realtimeSessionOverrides:d,realtimeStartInstructions:f,voice:p}){if(c.type===`existingCall`){await i.sendRequest(`thread/realtime/start`,{codexResponseHandoffChannelPrefixes:d?.codexResponseHandoffChannelPrefixes,codexResponseHandoffMode:d?.codexResponseHandoffMode,codexResponseItemPrefix:t,codexResponsesAsItems:n,flushTranscriptTailOnSessionEnd:!0,includeStartupContext:!1,outputModality:o,realtimeEndInstructions:l,realtimeSessionId:u,realtimeStartInstructions:f,threadId:a,transport:c,version:d?.version});return}let m={includeStartupContext:!1,flushTranscriptTailOnSessionEnd:!0,codexResponseHandoffPrefix:e,threadId:a,codexResponsesAsItems:n,codexResponseItemPrefix:t,initialItems:r,outputModality:o,realtimeSessionId:u,voice:p,...l==null?{}:{realtimeEndInstructions:l},...f==null?{}:{realtimeStartInstructions:f},...d,...s==null?{}:{prompt:s},transport:c};await i.sendRequest(`thread/realtime/start`,m)}async function ZGi(e,t){return(await Vu(e,t).sendRequest(`thread/realtime/listVoices`,{})).voices}async function QGi({manager:e,conversationId:t}){await e.sendRequest(`thread/realtime/stop`,{threadId:t})}async function $Gi({manager:e,conversationId:t,role:n,text:r}){await e.sendRequest(`thread/realtime/appendText`,{threadId:t,role:n,text:r})}async function eKi({manager:e,conversationId:t,text:n}){await e.sendRequest(`thread/realtime/appendSpeech`,{threadId:t,text:n})}function tKi(){return(tKi=e((()=>{fp(),xli()})))()}var nKi;function rKi

Роль app-server · Наш самостоятельный WebRTC V3 · Запросы и события app-server

Вернуться к карте вызовов ↑

3. CLI /voice: команда вместо кнопки

В штатном TUI вводите /voice. TUI управляет состоянием разговора через локальную сессию app-server. Отдельный codex-voice-host захватывает микрофон, воспроизводит ответ и передаёт WebRTC-медиа. App-server нужен для thread/turn, разрешений, signaling, sideband и связи речи с агентом, а не для пересылки каждого Opus-пакета.

Наша консоль похожа командой, но не запускает CLI, Electron или app-server и не выполняет инструменты агента. Python сам создаёт V3 call, поднимает aiortc и sideband. В живом запуске после /voice микрофон работает постоянно; /voice mute прекращает захват и оставляет синтетическую тишину, /quit закрывает соединения.

Пример вызова

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
"$voice_python" "$repo/scripts/webrtc-voice-isolated/voice-console.py"   --auth-file "$auth_file" --voice cove --dry-run
# Живой разговор: уберите --dry-run, затем в консоли введите /voice.

Код штатного клиента: TUI realtime → app-server session → аудиотрек helper.

Код демо: voice-console.py: main, command_loop, медиатрек · Подготовка и команды консоли · Полная схема CLI

Вернуться к карте вызовов ↑

4. Произношение в языковом виджете → HTTP TTS

В ChatGPT-языковом виджете озвучивается конкретный текст или произношение. Это не кнопка Linux Read Aloud. Проверенный обработчик виджета формирует text, pronunciation_language, source: translation; ixt делает POST /pronunciation/synthesize?format=mp3 и получает аудиоблоб. Виджет зависит от содержимого ответа; в текущем Codex-чате его снимка нет.

В демо та же форма запроса выбрана явно режимом chatgpt-text. Можно озвучить собственный UTF-8 файл, не создавая ChatGPT-разговор. Доступ к этому приватному API зависит от входа и сервиса; удачная старая проба не обещает бессрочную поддержку.

Фрагмент проверенного Desktop-бандла

webview/assets/translation-c611e48b1267.js · смещение 23686 символов · 30.09.2026. Это выдержка из минифицированного кода, не самостоятельный скрипт.

Выдержка JavaScript клиента. Это не Rust и не готовый самостоятельный скрипт.

// Языковой виджет передаёт pronunciation_language, source=translation и свой текст.
// Комментарии добавлены для учебника; minified-выдержка может быть неполной.
==g?(_={loading:p,play:m,stop:g},t[15]=p,t[16]=m,t[17]=g,t[18]=_):_=t[18];let v=`translation:${r??`auto`}:${n}`,y;t[19]!==n||t[20]!==r?(y=e=>je({pronunciation_language:r,source:`translation`,text:n},{signal:e}),t[19]=n,t[20]=r,t[21]=y):y=t[21];let b;t[22]!==f||t[23]!==_||t[24]!==v||t[25]!==y?(b=(0,$.jsx)(Ne,{errorMessage:f,iconClassName:`icon-sm`,labels:_,playbackId:v,synthesiz
Фрагмент проверенного Desktop-бандла

webview/assets/app-primary-1a4f67814ed6.js · смещение 1833610 символов · 30.09.2026. Это выдержка из минифицированного кода, не самостоятельный скрипт.

Выдержка JavaScript клиента. Это не Rust и не готовый самостоятельный скрипт.

// HTTP pronunciation отправляет JSON и получает Blob; выбранный формат запроса - MP3.
// Комментарии добавлены для учебника; minified-выдержка может быть неполной.
async function ixt(e,{signal:t}={}){return(await gd.getInstance().fetch(`/pronunciation/synthesize?format=mp3`,{method:`POST`,body:JSON.stringify(e),headers:{...pg(),"Content-Type":`application/json`},signal:t})).blob()}var u6,d6,f6,axt;function p6(){return(p6=e((()=>{Y_e(),X(),ge(),_f(),ym(),If(),Is(),u6=new Weak

Пример вызова

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
python3 "$repo/scripts/tts-demos/http-tts.py" chatgpt-text   "$audio_out/pronunciation.mp3" --text-file "$repo/scripts/tts-demos/sample-ru.txt"   --auth-file "$auth_file" --dry-run

Код: build_request → download_audio · Схемы и контракт pronunciation · Подробный HTTP-разбор

Вернуться к карте вызовов ↑

5. Штатный Read Aloud ChatGPT: прочитать существующий ответ

В ChatGPT-действиях под ответом команда Read Aloud передаёт не весь текст, а conversation_id и message_id. lxt выбирает готовый аудиоasset либо GET /synthesize с голосом и форматом. dxt находит последний финальный ответ в текущей ветке. Zbt проверяет поддержку воспроизведения: AAC, затем MP3, затем Opus; конкретный формат зависит от MediaSource и браузера.

Это ID ChatGPT-разговора, а не thread ID локального Codex. Демо работает с собственным существующим ChatGPT-ответом и сохраняет MP3/AAC. Учебник фиксирует живые результаты прежних проб; меню в текущем экземпляре на этой итерации не проверено.

Фрагмент проверенного Desktop-бандла

webview/assets/app-primary-1a4f67814ed6.js · смещение 1834856 символов · 30.09.2026. Это выдержка из минифицированного кода, не самостоятельный скрипт.

Выдержка JavaScript клиента. Это не Rust и не готовый самостоятельный скрипт.

// Read Aloud выбирает существующий audio asset либо /synthesize по IDs сообщения.
// Комментарии добавлены для учебника; minified-выдержка может быть неполной.
function lxt(e,{conversationId:t,messageId:n}){let r=e.get(yS,t);if(r==null)return;let i=gae(e.get(eE,t)?.[n]?.message);if(i==null&&n.startsWith(`request-placeholder-`))return;let a=i==null?t6.CHATGPT_VOICE_READ_ALOUD_AUDIO_SOURCE_SYNTHESIZED:t6.CHATGPT_VOICE_READ_ALOUD_AUDIO_SOURCE_AUDIO_ASSET;Fg(e,Gbt,{audioSource:a});let o=e.get(Gm).formatMessage({id:`chatgptConversations.readAloud.error`,defaultMessage:`Unable to read this response aloud`,description:`Error toast shown when Codex cannot synthesize or play a completed ChatGPT assistant response after the user selects Read aloud`}),s=`conversation:${r}:${n}`,c={playbackId:crypto.randomUUID(),conversationId:r,messageId:n,audioSource:a},l=()=>{Fg(e,Kbt,c)};if(i!=null){s6(e,{conversationId:r,playbackId:s,onPlaybackRequested:l,onError:(t,n)=>{n===`play`&&sxt(e,t)||e.get(hp).danger(o,{errorAnalytics:{toastId:`useVoiceReadAloudAudio_onStreamingError`}})},synthesizeAudio:async e=>{let{downloadUrl:t,requestHeaders:n}=i.startsWith(`https://cdn.openai.com/`)||i.startsWith(`/backend-api/`)||i.startsWith(`https://chatgpt.com/backend-api/`)?WOe({status:`success`,download_url:i}):await fge(i.replace(/^file-service:\/\//,``).replace(/^sediment:\/\//,``),{conversationId:r,downloadIntent:!1,showInline:!1,signal:e});return n==null?t:gd.getInstance().fetch(t,{headers:n,signal:e}).then(e=>e.blob())}});return}let u=uxt(e),d=e.get(Sn),f=e.get(it);s6(e,{conversationId:r,cacheKey:u.then(e=>JSON.stringify([d,f,r,n,e])),errorMessage:o,playbackId:s,onPlaybackRequested:l,onPlaybackFailed:(t,n)=>oxt(e,c,t,n),synthesizeAudio:async e=>{let t={conversation_id:r,message_id:n,voice:await u,format:Zbt()},i=new URLSearchParams;for(let[e,n]of Object.entries(t))n!=null&&i.set(e,n);return gd.getInstance().fetch(`/synthesize?${i.toString()}`,{headers:pg(),signal:e})}})}async function uxt(e){let t=e.query.snapshot(Za),n=t.getData()??await t.fetch().catch((
Фрагмент проверенного Desktop-бандла

webview/assets/app-primary-1a4f67814ed6.js · смещение 1837006 символов · 30.09.2026. Это выдержка из минифицированного кода, не самостоятельный скрипт.

Выдержка JavaScript клиента. Это не Rust и не готовый самостоятельный скрипт.

// Выбирается последний законченный ответ ассистента, не любая запись mapping.
// Комментарии добавлены для учебника; minified-выдержка может быть неполной.
function dxt(e,t){let n=e.get(eE,t);if(n==null||e.get(VC,t))return;let r=Bf({current_node:e.get(Gg,t),mapping:n}),i=r.findLast(e=>e.author.role===`assistant`&&(e.channel==null||e.channel===`final`)&&Zhe(e)!=null);i?.id==null||e.get(qD,t)===`streaming`&&r.findLastIndex(e=>e.author.role===`user`)>r.indexOf(i)||lxt(e,{conversationId:t,messageId:i.id})}var fxt;function m6(){return(m6=e((()=>{n6(),X(),OOe(),ge(),ym(),of(),tf(),a6(),If(),ec(),Is(),p6()
Фрагмент проверенного Desktop-бандла

webview/assets/app-primary-1a4f67814ed6.js · смещение 1828486 символов · 30.09.2026. Это выдержка из минифицированного кода, не самостоятельный скрипт.

Выдержка JavaScript клиента. Это не Rust и не готовый самостоятельный скрипт.

// Клиент выбирает воспроизводимый MIME/формат; порядок AAC, MP3, Opus не доказывает текущую ветку.
// Комментарии добавлены для учебника; minified-выдержка может быть неполной.
function Zbt(){let e=Xbt();if(e==null)return;let t=new Audio;for(let[n,r]of[[`audio/aac`,`aac`],[`audio/mpeg`,`mp3`],[`audio/ogg`,`opus`]])if(t.canPlayType(n)&&e.isTypeSupported(n))return r}function Qbt(){return(Qbt=e((()=>{})))()}async function $bt({response:e,mediaSource:t,audioElement:n,form

Пример вызова

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
python3 "$repo/scripts/remote-tts-isolated/chatgpt-read-aloud.py"   "$audio_out/message.mp3" --auth-file "$auth_file" --format mp3 --dry-run
# Для живого запуска задайте свои --conversation-id и --message-id.
# Либо используйте выбор собственного разговора, описанный в README демо.

Код: assistant_message, voice_catalog, main · Удалённый Read Aloud: ID и аудиоформат · Запуск с собственными ID

Вернуться к карте вызовов ↑

6. Кнопка Linux Read Aloud: расширение Community

При включённой функции read-aloud патч добавляет отдельный динамик под сообщением ассистента с подсказкой Read assistant response aloud. Щелчок вызывает codexLinuxReadAloudClick, очищает скопированный текст и отправляет внутреннее сообщение linux-read-aloud с action: speak, source: button и текстом. Backend - codexLinuxReadAloudSpeak. Сама кнопка не обращается к ChatGPT /synthesize.

Backend сначала учитывает свою команду. Без неё использует настроенный движок: Kokoro по умолчанию, либо явно выбранный Piper, затем доступный системный fallback согласно настройкам. Своя команда может вызвать наш HTTP TTS-адаптер, тогда синтез удалённый. Это настройка оболочки, а не автоматическое решение модели. Rust MCP-helper имеет собственный выбор backend; не следует переносить его приоритеты на JS-кнопку.

Кнопка и настройки описаны по текущему исходнику патча. Их снимков из установленного Community нет: уже открыто другое приложение, и запускать второй экземпляр с тем же профилем нельзя. Демо backend проверяет вызов вне Electron, но не доказывает появление кнопки.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

onClick → codexLinuxReadAloudClick(item, copyText, conversationId, button)
post({action: "speak", source: "button", text})
→ codexLinuxReadAloudSpeak → Kokoro / Piper / system / custom command

Пример вызова

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
# Это выполнение backend без Desktop, не GUI-тест; при запуске возможен звук.
env -u NODE_OPTIONS node "$repo/scripts/tts-demos/button-backend.cjs"   "$repo/scripts/tts-demos/sample-ru.txt"

Код: readAloudIconButtonSource, click, codexLinuxReadAloudSpeak · Включение функции и настройки · Локальные движки и удалённый адаптер

Вернуться к карте вызовов ↑

7. Настройки Read Aloud: подготовка, а не синтез

Дополнение добавляет в настройки раздел Read aloud. В нём доступны Choose folder, Download voice и Speech pace. Выбор папки указывает существующие Kokoro-модели; загрузка подготавливает runtime и модель; скорость сохраняется в настройках. Эти действия сами по себе не читают ответ и не делают запрос /synthesize.

UI-класс LinuxReadAloudSettings читает global state и сериализует запись параметров; codexLinuxReadAloudSetup отправляет action: setup с режимом choose/download. Где лежат модели и как не менять основной профиль, описано в README. Новых загрузок ради снимка мы не запускали.

linuxDesktopReadAloudSettingsSource, setup · Раздел Enable the feature · Движки и пути

Вернуться к карте вызовов ↑

8. MCP и speak: интерфейс инструмента, не новая модель

У MCP нет обязательного графического окна. Агент или отдельный STDIO-клиент вызывает read_aloud с текстом, doctor проверяет окружение, stop останавливает речь. Rust-helper read_aloud_inner → select_backend → spawn_backend выбирает и запускает движок. Команда speak использует тот же helper и принимает текст аргументом либо через stdin. Сеть появляется лишь при собственной команде, которая обращается к удалённому сервису; обычный Kokoro/Piper/system синтезирует локально.

Пример вызова

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
# Диагностика без чтения текста вслух.
python3 "$repo/scripts/tts-demos/mcp-client.py" doctor   --binary "$repo/target/debug/codex-read-aloud-linux"
# По явному запросу пользователя: замените doctor на read и добавьте --text-file.
# Прямая CLI-озвучка (может включить звук):
# "$repo/target/debug/codex-read-aloud-linux" speak < "$repo/scripts/tts-demos/sample-ru.txt"

Код: main, read_aloud_inner, select_backend, spawn_backend · McpClient: initialize → tools/call · Подключение MCP · Схема MCP и локальной озвучки

Вернуться к карте вызовов ↑

9. Воспроизведённые API без отдельной кнопки

Realtime STT и GA Realtime TTS в наших скриптах выбираются командой запуска. Это не доказательство, что Desktop использует те же GA endpoints. Для STT нужен WAV PCM16 mono 24 кГц, из которого клиент берёт raw PCM и кодирует чанки в Base64. Для TTS клиент создаёт сеанс, передаёт текст и собирает аудиодельты в WAV. В appendSpeech-демо сначала запускается отдельный app-server с временным профилем: текст отправляется RPC, а аудио приходит событиями. Основной профиль не меняется.

Пример вызова

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
"$voice_python" "$repo/scripts/remote-tts-isolated/realtime-tts.py"   "$repo/scripts/remote-tts-isolated/sample-ru.txt" "$audio_out/realtime.wav"   --auth-file "$auth_file" --voice cedar --dry-run

Пример вызова

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
python3 "$repo/scripts/remote-tts-isolated/appserver-tts.py"   "$repo/scripts/remote-tts-isolated/sample-ru.txt" "$audio_out/appserver.wav"   --codex "$(command -v codex)" --auth-file "$auth_file" --voice cedar --dry-run

Пример вызова

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
# Перекодирование - не локальная STT; распознаёт удалённый сервис.
ffmpeg -nostdin -v error -n -i "$INPUT_AUDIO"   -ar 24000 -ac 1 -c:a pcm_s16le "$audio_out/stt-input.wav"
"$voice_python" "$repo/scripts/realtime-stt-isolated/realtime-transcribe.py"   "$audio_out/stt-input.wav" --auth-file "$auth_file" --dry-run

Realtime STT · GA Realtime TTS · appendSpeech · STT-клиент · TTS-клиенты

Speech API с отказом 429 и audio summary с 404 остаются в ограничениях. Для них нет нового заявления "работает" или выдуманного скриншота успешного вызова.

Вернуться к карте вызовов ↑

10. Консоль через proxy: тот же V3, другая маршрутизация

У отдельного proxy-демо нет своей кнопки в Desktop: оно начинается из терминала. Выбор proxy меняет путь HTTPS signaling и WSS sideband, но не превращает WebRTC UDP в HTTP CONNECT. Поэтому нельзя подписать один снимок интерфейса "весь голос через proxy". Нужны отчёт маршрутов, соединения relay и медиасчётчики.

Пример вызова

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
"$voice_python" "$repo/scripts/webrtc-voice-proxy/proxy-voice.py" --dry-run --auth-file "$auth_file"
# Живой разговор и отчёт маршрутов: команды находятся в README ниже.

Proxy: запуск, endpoint и граница UDP · Исходное direct-демо · Сравнение форматов клиентов

Вернуться к карте вызовов ↑

Что проверено и что пока нельзя считать проверенным

Снимки получены системным захватом уже открытого окна; пиксели нужных областей считаны через XGetImage. Переписка, названия чатов и исходный полный кадр в документ не включены. Скриншот показывает положение микрофона и состояние остановки хода, но не маршрут сети и не установленную Linux-функцию.

Code-map проверен по codex-app/resources/app.asar и исходникам дополнения 30.09.2026. Имена минифицированных файлов, символы и смещения относятся только к этому снимку бандла. При обновлении их нужно проверять заново. Серверные алгоритмы STT/TTS закрыты: описаны наблюдаемые контракты, а не выдуманный серверный исходник.

Автотесты проверяют изображения, источники снимков, ссылки, якоря, SVG и команды. Они не заменяют открытие документа в браузере или щелчок по каждой кнопке Community. Просмотр учебника в разрешённом браузерном окружении ранее заблокирован; через другой сервер или браузер это ограничение не обходилось.

Происхождение снимков и SHA-256 · Выдержки кода, SHA-256 бандлов и смещения · Общий отчёт о проверке · Структурные проверки

Вернуться к карте вызовов ↑

Архитектура

Зачем вообще нужен app-server

Чтобы читать следующие схемы, сначала разделим роли. Интерфейс принимает действие пользователя. App-server предоставляет клиенту операции ядра Codex: вход, чаты, ходы, разрешения и события. Ядро ведёт агентную работу. В выбранном CLI V3 медиапроцесс обслуживает звук, а app-server создаёт разговор и держит канал событий. Поэтому часть обязанностей можно перенести в самостоятельное демо, не перенося весь Codex.

Одна операция, разные локальные транспорты

В embedded-режиме CLI 0.156.1 TUI создаёт AppServerClient::InProcess. Клиент передаёт типизированный ClientRequest по каналу mpsc внутри процесса; это не JSON-строка в stdin. Unix daemon и remote-режим используют другие соединения. Наш appendSpeech-демо, напротив, действительно запускает codex app-server и пишет JSON-RPC строки в STDIO.

Поэтому JSON start в учебнике - читаемое представление полей операции. Он не доказывает сериализацию этих полей в JSON на каждом внутреннем участке.

Выдержка Rust. Описывает штатный вызов; upstream-снимок рядом сохранён без изменений.

// Одна из разновидностей команды внутри процесса.
// Остальные варианты enum и импорты остаются в полном исходнике.
enum ClientCommand {
    Request {
        request: Box<ClientRequest>,
        response_tx: oneshot::Sender<IoResult<RequestResult>>,
    },
}

Карта кода: start_embedded_app_server, выбор InProcess; ClientCommand и request_typed. Проверка: test_native_transport_source сверяет эти определения независимо от текста книги.

Так разные интерфейсы используют одно ядро, не дублируя агентную логику. На схеме показаны роли, а не обязательное число процессов: интерфейс и app-server могут связываться внутри одного процесса.

Клиент → локальный мост → ядро → модель / toolsКлиент → локальный мост → ядро → модель / toolsTUI / CLIинтерфейсЛокальный app-serverRPC и координацияЯдро Codexсессия, модель, toolsМодели и toolsудалённая модель / средаEndpoint и формат · нажмите для полных примеров ↓Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/doneФормат: Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демоПример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}Локальные thread/start, turn/start; удалённый URL зависит от model providerФормат: JSON-RPC input[] → события; модельные запросы не Opus-медиаканалПример: turn/start {threadId:"demo",input:[{type:"text",text:"Привет"}]}POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avasФормат: application/json: sdp + session → plain SDP answer + Location/call IDПример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}WSS wss://api.openai.com/v1/live/<call_id>Формат: WebSocket UTF-8 JSON: тексты реплик, завершение, координацияПример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}WebRTC · выбранный SDP/ICE peer · не HTTP endpointФормат: Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файлПример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц
Это управляющая цепочка. В voice/WebRTC медиа helper → сервис обходят app-server, как показано в главе CLI.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

CLI ↔ локальный app-server

Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done

Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо

Пример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}

Полный запрос, ответ, код и ограничения

Клиент ↔ ядро / модель

Локальные thread/start, turn/start; удалённый URL зависит от model provider

JSON-RPC input[] → события; модельные запросы не Opus-медиаканал

Пример: turn/start {threadId:"demo",input:[{type:"text",text:"Привет"}]}

Полный запрос, ответ, код и ограничения

Создать WebRTC V3 call

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas

application/json: sdp + session → plain SDP answer + Location/call ID

Пример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}

Полный запрос, ответ, код и ограничения

События WebRTC V3

WSS wss://api.openai.com/v1/live/<call_id>

WebSocket UTF-8 JSON: тексты реплик, завершение, координация

Пример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}

Полный запрос, ответ, код и ограничения

Звук разговора

WebRTC · выбранный SDP/ICE peer · не HTTP endpoint

Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файл

Пример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Для аудио роль зависит от пути. В файловой диктовке Desktop получает token через app-server, но Electron сам отправляет multipart-файл. В CLI /voice app-server создаёт call, возвращает SDP и слушает sideband, а звук отправляет helper напрямую по WebRTC. В WebSocket appendAudio сэмплы, наоборот, действительно проходят через RPC. Нельзя объединить эти три случая словом "прокси".

СценарийГде идёт звукЧто делает app-server
Desktop mic dictationRecorder Blob → Electron HTTPS /transcribe; иной streaming путь есть в кодеДаёт актуальный вход. Не принимает файл через thread/realtime RPC
Desktop voice conversationWebRTC media к голосовому сервису; детали конкретного live Desktop call здесь не провереныУправляет клиентской сессией/входом и агентной координацией там, где выбран Codex voice путь; не переносим CLI контракт на каждый Desktop экран
CLI /voice, 0.156.1 V3codex-voice-host → Opus/SRTP → serviceHTTPS signaling, WSS sideband, transcript notifications и agent handoff
RPC appendAudio/WebSocketКлиент Base64 PCM → RPC → ядро → удалённый WSВ этом пути аудиочанки идут через app-server
Наш WebRTC voice-consoleInputTrack → aiortc → serviceApp-server отсутствует. Перенесены auth read, signaling, sideband, lifecycle; не перенесены threads/turns/tools/permissions/handoffs

Подмена model base URL или HTTP-прокси не делает его RTP-реле. Для голоса отдельно нужны маршруты HTTPS call, WSS sideband и ICE/media. RTP может идти по выбранным ICE-кандидатам напрямую или через разрешённый TURN, если такой сервер настроен. Наше демо не настраивает TURN; среда с закрытым UDP может не пройти media даже после успешного HTTPS.

thread/realtime: что именно принимает RPC

ОперацияВходОтвет / событие
startthreadId, prompt?, model?, version?, voice?, outputModality; transport Webrtc{sdp}, Websocket или ExistingCall{callId}RPC {}. SDP answer, started/error и captions приходят уведомлениями отдельно
appendAudiothreadId, audio: data(Base64), sampleRate, numChannels, samplesPerChannel?, itemId?RPC acknowledgement; аудио/текст приходит последующими событиями. Это chunk, не filesystem path
appendTextthreadId, text, roleДобавление текста; не upload файла
appendSpeechthreadId, textТекст для голосового вывода; outputAudio/delta, не готовый MP3 в RPC result
listVoicesБез начала разговораvoice catalogs/defaults версий; доступ к каждому voice не гарантирован
stopthreadIdЗавершение принадлежащего thread realtime-сеанса

Контракт: app-server-protocol/src/protocol/v2/realtime.rs. Здесь описаны клиентские типы. Какие сочетания transport/version поддерживает сервер, проверяется отдельно; наличие поля в схеме ещё не доказывает живой вызов.

Роли схемы

Ваш компьютер

На вашей машине находятся интерфейс, файлы записи и сетевые клиенты. Локальное размещение app-server не переносит сюда распознавание: STT и удалённый TTS выполняет сервис.

Вход и выход. Файл/микрофон/текст → клиентские процессы → сеть.

Code-map. scripts/webrtc-voice-isolated/voice-console.py; tui/src/chatwidget/realtime.rs. В демо main выбирает console или file_probe. Ни Codex, ни Electron в новом демо не запускаются.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

main → console | file_probe
asyncio.run(...)

Проверка. test_copied_script_dry_run_no_auth_or_fork

← Вернуться к схеме

Микрофон и динамики

Устройства получают или воспроизводят звук. Они не распознают речь. В Desktop браузерный MediaStream попадает в recorder; CLI использует отдельный voice helper.

Вход и выход. Звуковое давление → локальные сэмплы; полученные сэмплы → звук в наушниках.

Code-map. voice-host/src/audio_track.rs; MediaRecorder в Desktop. CLI audio track отправляет EncodedAudio; захват и live-кодирование не установлены этим файлом. Точное MIME записи MediaRecorder конкретного живого UI-запуска не установлено.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

микрофон → захват → сэмплы
полученный аудиопоток → декодирование → динамики

Проверка. Пользователь подтвердил диктовку raven через Desktop 29.09.2026. Новая файловая проба не проверяет физический микрофон.

← Вернуться к схеме

codex-voice-host

Это медиапроцесс CLI. Он создаёт offer, получает answer через IPC, кодирует запись Opus и отправляет её напрямую сервису. App-server не пересылает эти RTP-пакеты.

Вход и выход. PCM с устройства + SDP answer → Opus/SRTP; обратный поток → воспроизведение.

Code-map. voice-host/src/audio_track.rs; voice-host/src/transport.rs. Synthetic silence в audio track кодируется mono; формат live capture здесь не установлен. Transport создаёт oai-events и peer connection. Данные локального data channel не используются как источник captions.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

audio/opus; RTP clock=48000; channels=2 в SDP
synthetic silence: mono; 960 samples / 20 ms; live capture здесь не доказан
create_data_channel("oai-events")

Проверка. Проверено по тегу CLI 0.156.1; живой helper этого CLI здесь не запускали. Наш отдельный peer-тест проверяет aiortc, не бинарник helper.

← Вернуться к схеме

Локальный app-server

Это интерфейс к ядру Codex для Desktop/TUI. Он ведёт клиентские запросы, вход, thread/turn, события, разрешения и работу агента. В WebRTC voice он согласует call и держит sideband; не выполняет локальную STT.

Вход и выход. Типизированная операция start с SDP → удалённый call; SDP answer и transcript → уведомления клиенту.

Code-map. tui/src/app_server_session/realtime.rs; core/src/realtime_conversation.rs. start_realtime_conversation передаёт Webrtc/V3. Ядро build_realtime_session_config подставляет model=gpt-live-1-codex, затем realtime_request_headers задаёт alpha.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

thread/realtime/start { transport: { type: "webrtc", sdp }, version: "v3" }
HTTP headers: OpenAI-Alpha: quicksilver=v2
session.model: gpt-live-1-codex

Проверка. test_outgoing_http_body_and_failure_cleanup проверяет перенесённую форму запроса. AppServerTests - отдельный WebSocket RPC путь.

← Вернуться к схеме

TUI / CLI

Интерфейс начинает или останавливает сеанс и показывает transcript. Он не является удалённой моделью. /voice в CLI запускает разговор, а не загрузку готового аудиофайла.

Вход и выход. Действие пользователя → IPC/RPC; Delta/Done → субтитры и состояние.

Code-map. tui/src/chatwidget/realtime.rs; tui/src/app_server_session/realtime.rs. Обработчики realtime управляют helper и получают SDP-уведомления. /voice mute меняет захват, а не историю чата.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

/voice → helper offer → RPC start
SDP notification → helper apply answer
transcript notifications → TUI

Проверка. Проверено по исходникам. Физическая запись и навигация UI штатного клиента в этом этапе не проверялись.

← Вернуться к схеме

Realtime service

Сервис принимает media и проводит голосовой разговор. Расшифровки V3 приходят по WSS sideband, звук - по WebRTC. Исходников сервера у нас нет. Здесь описаны контракт и полученные ответы, а не внутренняя реализация или название скрытой модели.

Вход и выход. Запрос по описанному ниже контракту → текст либо аудио. Авторизация и доступ проверяются сервисом.

Code-map. Клиентский контракт Codex; HTTP-контракты. Серверную функцию назвать по исходникам нельзя. Пример ниже показывает только внешний обмен.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

клиент → запрос → удалённый сервис
клиент ← ответ/события ← удалённый сервис

Проверка. Живая V3-проба 30.09.2026: оба RTP-счётчика положительные, две финальные реплики и декодированный WAV.

← Вернуться к схеме

Ядро Codex

Ядро содержит логику агента. Оно хранит состояние turn, готовит контекст модели, координирует tools и policy. App-server выставляет эту логику клиентам, а не является отдельной альтернативной моделью.

Вход и выход. Запросы клиента → Session/turn/model client → удалённые модели и инструменты → события.

Code-map. core/src/realtime_conversation.rs; core/src/client.rs. build_realtime_session_config готовит голосовой session; ModelClient создаёт call с auth provider. Общая агентная цепочка шире аудио.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

UI → app-server → core Session → ModelClient
voice: Session → create call → sideband / handoff

Проверка. Исходники CLI 0.156.1; RPC тесты демо не проверяют полный агентный turn. Новый voice-console сознательно не импортирует это ядро.

← Вернуться к схеме

Модели и tools

Модель генерирует ответ удалённо. Инструменты Codex могут работать в локальной среде или подключённых сервисах; слово tools не означает, что всё выполняет голосовой сервер.

Вход и выход. Контекст/медиа → ответ, tool request; разрешённый tool → результат для модели.

Code-map. core/src/client.rs; scripts/webrtc-voice-isolated/README.md. Исходников удалённой модели нет; доступен клиентский контракт. В демо ни одного tool executor нет.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

model → request tool → core policy → execution
voice demo: delegation.created → error + close

Проверка. test_delegation_closes_without_running_tools

← Вернуться к схеме

Что именно передаётся по сети

Сначала определяем байты, затем выбираем способ отправки. Имя файла, MIME и звук внутри него - три разных вещи.

Контейнер и кодек

Контейнер хранит данные и описание дорожек. Кодек задаёт представление звука. WAV - контейнер, он не всегда содержит PCM; в наших WAV лежат несжатые PCM16-отсчёты. FLAC и MP3 нельзя превратить в WAV переименованием. HTTP-диктовка отправляет контейнер целиком, WSS-демо извлекает PCM, WebRTC кодирует отсчёты в Opus.

PCM16LE и размер

PCM - последовательность чисел, описывающих звук. Signed PCM16 занимает два байта на отсчёт; LE означает, что младший байт идёт первым. Mono 24 кГц даёт 24000 × 1 × 2 = 48000 байт в секунду. У V3-демо 48 кГц: 20 мс дают 960 отсчётов, то есть 1920 байт до Opus.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

01 00 FF FF
# Два учебных signed PCM16LE отсчёта: +1 и -1.
# Это не WAV и не осмысленная запись речи.

Base64 и JSON

Base64 записывает бинарные байты текстом для JSON. Он не сжимает и не шифрует звук. Для предыдущих четырёх байтов получится AQD//w==. Формат декодированных байтов определяется контрактом, а не самим Base64.

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{"type":"input_audio_buffer.append","audio":"AQD//w=="}

Это корректная форма учебного сообщения, но два отсчёта недостаточны для транскрипции. WAV-заголовок RIFF в поле audio нашего WSS-клиента не попадает.

Частота записи и RTP clock

Sample rate задаёт число PCM-отсчётов в секунду. RTP clock задаёт шкалу временных меток пакетов; для Opus здесь это 48 кГц. Из RTP clock нельзя вывести частоту микрофона или число его каналов. SDP описывает соединение, а ICE выбирает рабочую пару адресов; сами они аудиозаписью не являются.

Почему V3, v2 и GA не противоречат друг другу

GA означает General Availability - общедоступное семейство API, а не версию нашего WebRTC-сеанса. CLI voice V3, выбранный WS v2 в appendSpeech, заголовок OpenAI-Alpha: quicksilver=v2 и V1-каталог голосов принадлежат разным уровням. Их номера не взаимозаменяемы; голос выбирается из каталога конкретного endpoint и версии.

Какие ID нельзя менять местами

Codex thread/turn относятся к чату и ходу агента. ChatGPT conversation/message нужны для сохранённого ответа. Call ID соединяет signaling, media и sideband звонка. STT item_id связывает committed-вход с транскрипцией, GA response_id связывает дельты с конкретным ответом. Похожий вид строки не делает эти ID взаимозаменяемыми.

Точные контракты и словарь доступны из каждой практической главы.

Подготовка переносимого комплекта

Книга и исходники переезжают вместе. Вход, устройства и зависимости принадлежат новой машине и в архив не входят.

Распакуйте архив и откройте index.html. Команды выполняйте из корня audio-textbook. env.sh вычисляет корень по собственному расположению и помещает результаты в .runtime/output. Venv после переноса создаётся заново: его внутренние пути не являются переносимой частью.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте в корне распакованного audio-textbook.
source ./scripts/audio-textbook/env.sh
# Эта проверка не читает credential и не обращается к сервису.
python3 ./tooling/verify.py

Для живой пробы укажите AUDIO_AUTH_FILE перед source или задайте auth_file в этом терминале. Во всех командах выбранный файл передаётся явно. Если вход хранится только в keyring, файл не появится автоматически. Истёкший вход обновляется штатным клиентом отдельно; демо не делает refresh основного профиля.

Зависимости, venv и сборка локального helper. Начинайте с dry-run. Сетевой доступ к приватному ChatGPT endpoint зависит от сервиса и аккаунта; исторический успех не является обещанием доступа на другой машине.

Аудиоспособ · аудиофайл принят

Готовый файл → текст: ChatGPT /transcribe

Начнём с самого прямого способа: у нас уже есть запись, и нужен текст. Клиент отправляет целый файл через multipart POST. Контейнер остаётся в запросе, а распознавание происходит на удалённой стороне. Здесь не нужны SDP, WebRTC и управление ходами агента.

Для этой задачи не нужны ни разговор, ни thread/realtime/start. В Desktop файл собирается из записи микрофона. Наш скрипт пропускает захват и берёт уже существующий файл. В обоих случаях распознавание выполняется удалённо; app-server помогает Desktop получить вход, но не является STT-движком.

Результат и дата. HTTP STT и UI-диктовка воспроизведены 29.09.2026. Предыдущий 403 challenge и последующий успешный запрос относятся к разному сетевому контексту; конкретную решающую причину заголовков мы не установили.

Штатная система · диктовка микрофономШтатная система · диктовка микрофономDesktop recorderMediaStream → BlobBlob / multipartСетевой мост Electrontoken из app-serverHTTPS, файл целикомChatGPT STTзакрытый удалённый сервисEndpoint и формат · нажмите для полных примеров ↓POST https://chatgpt.com/backend-api/transcribeФормат: multipart/form-data: file бинарно, MIME по контейнеру → JSON textПример: file=raven.flac; Content-Type: audio/flac → {"text":"Привет"}POST https://chatgpt.com/backend-api/codex/dictation-stream-connect-info → выданный WSS URLФормат: JSON connect-info → session.start + audio.append: Base64 raw PCM16 monoПример: {type:"audio.append",audio:"AQD//w=="}; sample_rate_hz в session.start
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Файловая диктовка /transcribe

POST https://chatgpt.com/backend-api/transcribe

multipart/form-data: file бинарно, MIME по контейнеру → JSON text

Пример: file=raven.flac; Content-Type: audio/flac → {"text":"Привет"}

Полный запрос, ответ, код и ограничения

Потоковая диктовка Desktop

POST https://chatgpt.com/backend-api/codex/dictation-stream-connect-info → выданный WSS URL

JSON connect-info → session.start + audio.append: Base64 raw PCM16 mono

Пример: {type:"audio.append",audio:"AQD//w=="}; sample_rate_hz в session.start

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Наше демо · готовый файл без DesktopНаше демо · готовый файл без DesktopВаш аудиофайлне меняем его байтыбинарные байтыchatgpt-transcribe.shmultipart POSTHTTPS multipartChatGPT STTзакрытый удалённый сервисEndpoint и формат · нажмите для полных примеров ↓POST https://chatgpt.com/backend-api/transcribeФормат: multipart/form-data: file бинарно, MIME по контейнеру → JSON textПример: file=raven.flac; Content-Type: audio/flac → {"text":"Привет"}
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Файловая диктовка /transcribe

POST https://chatgpt.com/backend-api/transcribe

multipart/form-data: file бинарно, MIME по контейнеру → JSON text

Пример: file=raven.flac; Content-Type: audio/flac → {"text":"Привет"}

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Что проходит по сети

Часть обменаDesktop / CLIНаш клиент
EndpointPOST /backend-api/transcribeТот же URL, HTTPS через urllib/HTTP 1.1; Desktop использует Electron stack, NetLog показывал HTTP/2.
Телоfile; в отдельных вызовах language, dictation_session_id, attempt_id, duration_ms, conversation_id, parent_message_idОдин file; без model/language/ID. Имя файла и Content-Type внутри части.
ДанныеBlob recorder; точный codec живого UI не зафиксированИсходный FLAC raven: контейнер FLAC, audio/flac. Не raw PCM, не Base64.
РезультатJSON text → поле вводаJSON text → stdout; не создаёт chat/turn.
Вход и metadataToken из app-server; Electron может refreshToken/account ID из файла, static наблюдавшийся UA; нет refresh/cookies/integrity. Подробно - в сравнении клиентов.

Boundary разделяет части multipart, а не аудиочанки. Для FLAC частота/каналы описаны внутри файла; клиент не объявляет raw PCM rate. Одна запись может содержать много кодированных FLAC-фреймов, но это по-прежнему один file в запросе.

Как запустить

Каталог: открыть локальную папку. Инструкция и требования.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
bash "$repo/scripts/voice-probes/chatgpt-transcribe.sh" --auth-file "$auth_file"    "$INPUT_AUDIO"

Как проверять и где возможна ошибка

Сравниваем извлечённую multipart file с исходным файлом побайтово. MIME выбирается по расширению. Для известных контейнеров перед сетью сверяется сигнатура: MP3 или WAV с именем .flac получают локальный отказ. Это не полное декодирование: повреждённые фреймы внутри правильной сигнатуры ещё может обнаружить сервис. Конкретный серверный HTTP-код для mismatch не проверяли.

Блоки схемы и code-map

Desktop recorder

После нажатия на микрофон Desktop собирает запись из MediaRecorder chunks в Blob. Это файловая диктовка; альтернативный streaming dictation в коде не надо смешивать с этим вызовом.

Вход и выход. MediaStream → chunks → Blob → multipart file.

Code-map. Карта renderer ASAR. MediaRecorder / ondataavailable / new Blob; Fki/Rki/Bki/Vki и HPc собирают multipart или FormData.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

recorder.ondataavailable → chunks
new Blob(chunks) → FormData.set("file", blob)

Проверка. Virtual-mic опыт: текст соответствовал raven; позднее NetLog подтвердил файловый endpoint. Конкретный recorder codec в том сеансе не доказан.

← Вернуться к схеме

Сетевой мост Electron

Renderer формирует запрос, Electron main получает текущий token через app-server и отправляет HTTP. Сам файл через RPC app-server не переносится. Допустимый 401 может вызвать штатный refresh и один повтор.

Вход и выход. Renderer fetch + attach-маркеры → HTTP с авторизацией; HTTP response → renderer.

Code-map. Карта ASAR payload 26.928.20755. Якоря: applicationNetwork.fetch, getAuthToken, host.auth_refresh, retriedAfter401. Имена минифицированных файлов привязаны к снимку, не являются стабильным API.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

renderer request → getAuthToken → applicationNetwork.fetch
eligible 401 → auth_refresh → one retry

Проверка. Поиск контрактов в ASAR; ранее NetLog подтвердил POST /transcribe 200. Сырые auth-логи не опубликованы.

← Вернуться к схеме

Ваш аудиофайл

В автономном Bash-клиенте берётся готовый FLAC/WAV/другая разрешённая запись. Его не проигрывают в микрофон и не распознают локально.

Вход и выход. Файл на диске → исходные бинарные байты и MIME по расширению.

Code-map. scripts/voice-probes/chatgpt-transcribe.sh. Встроенный Python читает исходный файл; 25 МБ - лимит этого демо, не установленный серверный предел.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

audio_bytes = audio_file.read_bytes()
multipart file = audio_bytes

Проверка. test_request_contains_flac_and_keeps_token_out_of_url относится к соседнему Python-клиенту; отдельный тест Bash-упаковки добавлен в audio-textbook.

← Вернуться к схеме

chatgpt-transcribe.sh

Скрипт читает собственный ChatGPT access_token прямо из auth.json, собирает один multipart file и делает HTTPS-запрос. Cookies и основной профиль не меняются.

Вход и выход. Байты файла + filename/MIME → POST /transcribe → JSON text → терминал.

Code-map. scripts/voice-probes/chatgpt-transcribe.sh. Встроенный Python задаёт boundary, NoRedirect и urllib ProxyHandler({}). Нет model, language или dictation IDs в этой версии.

Форма HTTP-запроса или ответа. Учебные значения не заменяют вход и целый аудиофайл.

POST /backend-api/transcribe
Content-Disposition: form-data; name="file"; filename="raven.flac"
Content-Type: audio/flac
<исходные FLAC-байты>

Проверка. HTTP multipart interceptor; Python-контракт

← Вернуться к схеме

Учебный клиент

Один файл. Без форка и app-server.

Когда вместо готового файла сервису нужны отсчёты звука, меняется упаковка. Наш клиент читает WAV локально и отправляет его PCM-содержимое в Base64 JSON по WSS. RIFF-заголовок остаётся на машине. Эта файловая STT-проба не является уменьшенной копией CLI /voice: у них разные контракты и результаты.

./scripts/realtime-stt-isolated/

Ссылки рассчитаны на открытие этого HTML как локального файла. Если браузер запрещает переход в каталог, скопируйте путь и откройте его в файловом менеджере. На другой машине задайте свой вход и пересоздайте venv.

Штатный CLI /voice: Opus, не наш PCM JSONШтатный CLI /voice: Opus, не наш PCM JSONTUI / CLIинтерфейсcodex-voice-hostOpus и WebRTCRealtime serviceзакрытый удалённый сервисEndpoint и формат · нажмите для полных примеров ↓Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/doneФормат: Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демоПример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}Локальный IPC codex-voice-host · SDP / mute / stopФормат: Команды управления + SDP; аудио между устройствами и helper локальноеПример: SDP offer/answer; mute → отключить захват; stop → закрыть сеансPOST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avasФормат: application/json: sdp + session → plain SDP answer + Location/call IDПример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}WSS wss://api.openai.com/v1/live/<call_id>Формат: WebSocket UTF-8 JSON: тексты реплик, завершение, координацияПример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}WebRTC · выбранный SDP/ICE peer · не HTTP endpointФормат: Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файлПример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц
App-server ведёт signaling и sideband; полный управляющий тракт показан в схеме CLI.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

CLI ↔ локальный app-server

Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done

Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо

Пример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}

Полный запрос, ответ, код и ограничения

CLI ↔ voice helper

Локальный IPC codex-voice-host · SDP / mute / stop

Команды управления + SDP; аудио между устройствами и helper локальное

Пример: SDP offer/answer; mute → отключить захват; stop → закрыть сеанс

Полный запрос, ответ, код и ограничения

Создать WebRTC V3 call

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas

application/json: sdp + session → plain SDP answer + Location/call ID

Пример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}

Полный запрос, ответ, код и ограничения

События WebRTC V3

WSS wss://api.openai.com/v1/live/<call_id>

WebSocket UTF-8 JSON: тексты реплик, завершение, координация

Пример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}

Полный запрос, ответ, код и ограничения

Звук разговора

WebRTC · выбранный SDP/ICE peer · не HTTP endpoint

Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файл

Пример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Это сопоставление двух способов распознавания, не обещание одинакового wire-формата. В /voice идёт Opus по WebRTC и возвращается также аудиоответ. Следующее WSS-демо использует raw PCM и возвращает только финальный текст.

Изолированная транскрипция: передача PCM и возврат текстаWAV читается локально, его заголовок не передаётся. PCM16 кодируется Base64 в JSON append через WSS. Commit завершает turn. Удалённая модель возвращает completed transcript, клиент проверяет item_id и печатает текст. Python на вашей машинеУдалённая модель input.wavmono · 24 кГц PCM16без WAV-заголовка Base64чанки в JSON WSS gpt-transcribesession: transcriptionПосле commit:распознавание → текст commit: конец turn Проверить item_id→ transcript в stdout completed: item_id + transcript Нет CLI · нет Electron · нет app-server · нет локального STT · нет response.create Endpoint и формат · нажмите для полных примеров ↓WSS wss://api.openai.com/v1/realtime?intent=transcriptionФормат: UTF-8 JSON; Base64 raw PCM16LE mono 24000; WAV header не отправляетсяПример: {type:"input_audio_buffer.append",audio:"AQD//w=="} → commit → transcript

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Наше Realtime STT

WSS wss://api.openai.com/v1/realtime?intent=transcription

UTF-8 JSON; Base64 raw PCM16LE mono 24000; WAV header не отправляется

Пример: {type:"input_audio_buffer.append",audio:"AQD//w=="} → commit → transcript

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Почему это работает без app-server

Наш клиент сам выполняет минимум его сетевых обязанностей: читает credential, открывает защищённое соединение и задаёт session. Для выделенного STT не нужны thread, SDP, голосовой helper, handoff и ответы голосом. Это другой режим API с той же идеей удалённого распознавания, а не воспроизведение внутренних запросов TUI.

Реальный CLI 0.156.1Наш изолированный пример
ВходМикрофон через helperГотовый WAV; извлекается PCM
АудиотранспортWebRTC, Opus/RTPWSS, JSON + Base64 PCM16
РежимРазговорный WebRTC V3Отдельная session type transcription
Модель STTНе установлена из исследованных исходниковgpt-transcribe явно в запросе
АвторизацияШтатный auth manager app-serverЧтение файла; без refresh и записи
РезультатСубтитры, голосовой диалог, handoffФинальный transcript в stdout

Какие события посылает скрипт

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

← session.created
→ session.update: type=transcription, PCM16 24 кГц,
                  transcription.model=gpt-transcribe, turn_detection=null
← session.updated
→ input_audio_buffer.append: {audio: "Base64 PCM…"}  # несколько чанков
→ input_audio_buffer.commit                           # завершить вход
← input_audio_buffer.committed: item_id
← conversation.item.input_audio_transcription.completed:
    item_id, transcript
→ stdout: только финальный текст

Чанк - до 48 000 PCM-байтов (1 секунда записи). Чанки уходят подряд, без ожидания секунды между отправками. Для файла длиной 7,21 с получилось 8 чанков и 346 070 аудиобайтов. Base64 увеличивает размер примерно на треть; JSON и TLS добавляют служебные данные. Демо проверяет финальный item_id, даже если события пришли в другом порядке.

Изолированно ≠ локально. Распознавание делает удалённый сервис. Base64 - не шифрование: канал защищён TLS, а сервис видит аудио. Не передавайте записи с секретами без разрешения.

Протокол: официальный Realtime transcription. ChatGPT OAuth в этом API - проверенный здесь эксперимент, не обещанная замена Platform key для всех аккаунтов.

input.wav

WAV нужен для хранения и проверки параметров до отправки. После чтения контейнер отбрасывается: сервер не получает RIFF-заголовок.

Вход и выход. WAV PCM16 mono 24000 Гц → raw PCM байты.

Code-map. scripts/realtime-stt-isolated/realtime-transcribe.py. Функция read_audio проверяет параметры, длину и обрезанный payload.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

pcm = audio.readframes(frames)
len(pcm) == frames * 2

Проверка. test_pcm_validation, test_truncated_wav_rejected

← Вернуться к схеме

PCM16

PCM - последовательность signed 16-bit little-endian сэмплов. У неё нет собственного заголовка с частотой; 24000 Гц задаётся отдельно в session. Неправильное заявление частоты может изменить скорость, не вызвав сетевого отказа.

Вход и выход. Raw mono PCM16 → куски по 48000 байт, последний короче.

Code-map. scripts/realtime-stt-isolated/realtime-transcribe.py. transcribe режет pcm по CHUNK_BYTES. WAV-заголовок в эти куски не входит.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

for offset in range(0, len(pcm), 48000):
    chunk = pcm[offset:offset + 48000]

Проверка. test_flow_chunks_and_completed_transcript

← Вернуться к схеме

Base64 / WSS

Base64 позволяет поместить бинарные сэмплы в JSON. Это не кодек и не шифрование. TLS защищает соединение; Base64 увеличивает объём примерно на треть.

Вход и выход. PCM chunk → input_audio_buffer.append.audio; после последнего чанка → commit.

Code-map. scripts/realtime-stt-isolated/realtime-transcribe.py. transcribe ждёт session.created и session.updated до отправки аудио.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

{"type":"input_audio_buffer.append","audio":"<base64 raw PCM>"}
{"type":"input_audio_buffer.commit"}

Проверка. test_flow_chunks_and_completed_transcript, test_common_deadline_closes_socket

← Вернуться к схеме

Проверка item_id

Commit создаёт item. Итоговая транскрипция принимается только для этого item_id, иначе чужой результат можно ошибочно выдать за наш.

Вход и выход. input_audio_buffer.committed.item_id + completed.item_id/transcript → stdout.

Code-map. scripts/realtime-stt-isolated/realtime-transcribe.py. transcribe сохраняет ранний completed и сопоставляет его с подтверждённым commit.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

if committed in completed:
    return completed[committed].strip()

Проверка. test_completed_before_commit_ack_preserved, test_failure_and_empty_transcript_close_socket

← Вернуться к схеме

gpt-transcribe

Сервис распознаёт PCM после commit. Исходников сервера у нас нет. Здесь описаны контракт и полученные ответы, а не внутренняя реализация или название скрытой модели.

Вход и выход. Запрос по описанному ниже контракту → текст либо аудио. Авторизация и доступ проверяются сервисом.

Code-map. Клиентский контракт Codex; HTTP-контракты. Серверную функцию назвать по исходникам нельзя. Пример ниже показывает только внешний обмен.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

клиент → запрос → удалённый сервис
клиент ← ответ/события ← удалённый сервис

Проверка. Живая STT-проба 30.09.2026: 7,21 с аудио, 8 чанков, финальный текст 96 символов; точность автоматически не оценивалась.

← Вернуться к схеме

Практика

Запуск шаг за шагом

Контракт уже разобран; теперь воспроизведём его. Сначала подготовим WAV нужного формата и выполним dry-run без входа и сети. Затем явно выберем собственный auth-файл. Успешная транскрипция должна принадлежать item_id именно нашего отправленного буфера, а не соседнему событию в сокете.

1. Изолировать клиент и зависимости

Выполняйте блоки в одном терминале: переменная demo_dir сохраняется в этом shell. Нужны Python 3 с venv, ffmpeg и доступ к сети.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
# Создаём новый каталог, не трогая репозиторий или настройки Codex.
demo_dir=$(mktemp -d "$audio_out/realtime-stt-demo-XXXXXX")
# Переносим единственный клиент: он не импортирует код форка.
cp "$repo/scripts/realtime-stt-isolated/realtime-transcribe.py" "$demo_dir/"
# Python-пакеты будут только в этом каталоге.
python3 -m venv "$demo_dir/venv"
# Сетевой клиент, НЕ локальная модель распознавания.
"$demo_dir/venv/bin/pip" install websocket-client==1.9.2

2. Подготовить файл и проверить без сети

Конвертация не распознаёт речь. Она приводит FLAC к формату API. -n запрещает перезапись; -t 60 берёт не более 60 секунд.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
# Вместо raven.flac можно указать свою разрешённую к передаче запись.
ffmpeg -v error -n -i "$INPUT_AUDIO" -t 60    -ac 1 -ar 24000 -c:a pcm_s16le "$demo_dir/input.wav"
# Только проверка WAV и расчёт передачи: нет сети и чтения токена.
"$demo_dir/venv/bin/python" "$demo_dir/realtime-transcribe.py" --auth-file "$auth_file"    "$demo_dir/input.wav" --dry-run

Ожидается JSON с network: false, credentials_read: false, размером PCM и количеством чанков. Пример требует mono PCM16, 24 кГц, 0,2-60 с. Более длинные записи надо отдельно разбивать; автоматического разбиения здесь нет.

3. Сделать удалённую транскрипцию

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
# Теперь запись действительно отправится на api.openai.com через WSS.
# Секрет читается из tokens.access_token в указанном auth.json.
"$demo_dir/venv/bin/python" "$demo_dir/realtime-transcribe.py"    "$demo_dir/input.wav"    --auth-file "$auth_file"    --model gpt-transcribe --timeout 90

В stdout - только итоговый текст. Ошибка - в stderr и ненулевой exit code. Токен не вставляется в исходник, argv или отчёт; файл входа не изменяется. Скрипт не умеет обновлять истёкший токен. Для proxy-профиля с маркером 0000 этот вариант не подходит.

Варианты запуска

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
# Потоковая STT-модель; данный скрипт всё равно печатает только финал.
"$demo_dir/venv/bin/python" "$demo_dir/realtime-transcribe.py"    "$demo_dir/input.wav" --model gpt-live-transcribe --auth-file "$auth_file"
# Официальная API-key альтернатива, если у вас есть собственный Platform key.
# Укажите существующий приватный файл с ключом, не значение ключа.
"$demo_dir/venv/bin/python" "$demo_dir/realtime-transcribe.py"    "$demo_dir/input.wav" --api-key-file "${AUDIO_API_KEY_FILE:?Укажите свой файл ключа}"

Эти два варианта приведены по протоколу и интерфейсу клиента; живой запрос в этом исследовании проверен только для gpt-transcribe + текущий ChatGPT OAuth. Не приписываем эту проверку всем моделям и способам входа.

Сохранить текст и не принять сбой за успех

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
# Новые файлы доступны только вам. Перенаправление создаёт файл даже при сбое.
umask 077
if "$demo_dir/venv/bin/python" "$demo_dir/realtime-transcribe.py"    "$demo_dir/input.wav" --timeout 90 > "$demo_dir/transcript.txt" --auth-file "$auth_file"; then
  printf 'Текст сохранён: %s\n' "$demo_dir/transcript.txt"
else
  printf 'Транскрипция не получена; не используйте пустой файл как результат.\n' >&2
fi

А можно curl?

Этот конкретный режим - полноценный двусторонний WebSocket: handshake, JSON-события, аудиочанки, commit и ожидание результата. Обычный curl -F file=… не воспроизводит его. Для готового аудиофайла есть отдельный HTTP transcription API; наш ранее проверенный ChatGPT-файловый пример тоже работает иначе.

Не смешивайте HTTP-диктовку Desktop, CLI WebRTC v3 и учебный WSS STT. У них разная упаковка данных и разный жизненный цикл.

Аудиоспособ · локальные движки проверены

Локальная озвучка и оболочки кнопки, CLI, MCP

Теперь решим обратную задачу на этой машине. Текст передаётся выбранному движку, движок создаёт звук, проигрыватель выводит его в динамики. Кнопка, CLI и MCP лишь вызывают этот путь. Скачивание модели и локальный синтез - разные этапы; после подготовки локальный движок не обязан обращаться к сервису.

Piper, Kokoro и eSpeak уже дали аудиофайлы. Speech Dispatcher проверен как системное воспроизведение. Кнопка, команда speak и MCP-инструмент read_aloud только передают текст движку. Через custom command можно подключить HTTP TTS, но тогда удалённым становится выбранный backend, а не MCP-протокол.

Результат и дата. Образцы 29.09.2026: Piper 22050 Гц mono PCM16, 4,98 с; Kokoro 24000 Гц mono PCM16, 4,99 с; eSpeak - декодируемый WAV. Backend кнопки/CLI/MCP проверены отдельно. В снимке установленной сборки 30.09.2026 patch кнопки и MCP gate не обнаружены; этот учебник их не включает.

Штатная система · опциональные Linux featuresШтатная система · опциональные Linux featuresКнопка / CLI / MCPоболочки одного TTSтекстJS / Rust helperPiper / Kokoro / eSpeakпроцесс / ONNXЛокальный движоксетевого сервиса нетEndpoint и формат · нажмите для полных примеров ↓Локальный IPC linux-read-aloud · action=speakФормат: Объект с text → backend result; сетевой endpoint по умолчанию отсутствуетПример: {action:"speak",source:"button",text:"Привет"}stdio:// · tools/call(name=read_aloud) → Rust helperФормат: JSON-RPC 2.0 на строку; arguments.text UTF-8 → результат запускаПример: tools/call {name:"read_aloud",arguments:{text:"Привет",interrupt:true}}stdin / локальный процесс / ONNX · нет HTTP endpointФормат: UTF-8 text → локальные PCM-сэмплы / WAV / проигрываниеПример: printf 'Привет' | helper speak; файл демо → Piper / Kokoro / eSpeak
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Кнопка Linux Read Aloud

Локальный IPC linux-read-aloud · action=speak

Объект с text → backend result; сетевой endpoint по умолчанию отсутствует

Пример: {action:"speak",source:"button",text:"Привет"}

Полный запрос, ответ, код и ограничения

Read Aloud MCP STDIO

stdio:// · tools/call(name=read_aloud) → Rust helper

JSON-RPC 2.0 на строку; arguments.text UTF-8 → результат запуска

Пример: tools/call {name:"read_aloud",arguments:{text:"Привет",interrupt:true}}

Полный запрос, ответ, код и ограничения

Локальный синтез / проигрывание

stdin / локальный процесс / ONNX · нет HTTP endpoint

UTF-8 text → локальные PCM-сэмплы / WAV / проигрывание

Пример: printf 'Привет' | helper speak; файл демо → Piper / Kokoro / eSpeak

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Наше демо · прямой запуск движкаНаше демо · прямой запуск движкаUTF-8 текстне аудиоUTF-8local-file.pyPiper / Kokoro / eSpeakлокальный WAVЛокальный движоксетевого сервиса нетEndpoint и формат · нажмите для полных примеров ↓stdin / локальный процесс / ONNX · нет HTTP endpointФормат: UTF-8 text → локальные PCM-сэмплы / WAV / проигрываниеПример: printf 'Привет' | helper speak; файл демо → Piper / Kokoro / eSpeak
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Локальный синтез / проигрывание

stdin / локальный процесс / ONNX · нет HTTP endpoint

UTF-8 text → локальные PCM-сэмплы / WAV / проигрывание

Пример: printf 'Привет' | helper speak; файл демо → Piper / Kokoro / eSpeak

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Что проходит по сети

Часть обменаDesktop / CLIНаш клиент
Сетевой endpointНет для локального backendНет; Python/процесс/ONNX на машине
КнопкаRead Aloud patch + JS backendbutton-backend.cjs тестирует backend без запуска Desktop; не доказывает наличие кнопки в установленном UI
CLI/MCPspeak; STDIO read_aloud/stop/doctormcp-client.py запускает свой Rust helper. MCP payload - JSON text, не PCM и не file upload
ФорматДвижок → player/PCMPiper/Kokoro → WAV PCM16. eSpeak --stdout → WAV; Speech Dispatcher → воспроизведение без output WAV
АвторизацияНе нужна для локальных движковНе нужна; для custom HTTP команды - её отдельный собственный вход.

Частоту выбирает модель/движок. Kokoro samples приводятся к signed PCM16 LE, Piper заполняет WAV через библиотеку. ONNX-файлы - веса модели, не аудио. started=true у MCP означает запуск процесса, а не успешное декодирование всей речи.

Как запустить

Каталог: открыть локальную папку. Инструкция и требования.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
local_tts_out="$(mktemp -d "$audio_out/local-tts.XXXXXX")"
python3 "$repo/scripts/tts-demos/local-file.py" \
  espeak "$repo/scripts/tts-demos/sample-ru.txt" \
  "$local_tts_out/espeak.wav" --language ru
ffmpeg -nostdin -v error -i "$local_tts_out/espeak.wav" -f null -
# Piper/Kokoro используют уже установленные модели в cache; команды ниже.
"$TTS_DEMO_CACHE/venv/bin/python" "$repo/scripts/tts-demos/local-file.py" \
  piper "$repo/scripts/tts-demos/sample-ru.txt" \
  "$local_tts_out/piper.wav" --model "$TTS_DEMO_CACHE/models/ru_RU-denis-medium.onnx"
"$TTS_DEMO_CACHE/venv/bin/python" "$repo/scripts/tts-demos/local-file.py" \
  kokoro "$repo/scripts/tts-demos/sample-en.txt" \
  "$local_tts_out/kokoro.wav" --model "$TTS_DEMO_CACHE/models/kokoro-v1.0.onnx" \
  --voices "$TTS_DEMO_CACHE/models/voices-v1.0.bin" --voice bm_george --language en-us
python3 "$repo/scripts/tts-demos/mcp-client.py" doctor

На этой машине espeak-ng не установлен в PATH: команда выше использует извлечённый бинарник в cache. Если он установлен системно, задайте --espeak-bin espeak-ng. Piper/Kokoro используют существующее изолированное окружение; на другой машине сначала нужен setup-local.sh с загрузкой моделей, около398МиБ.

Кнопка, speak и MCP: отдельные точки входа

Следующие команды могут воспроизвести звук. Это не запуск Desktop: первая проверяет JS-backend кнопки в Node, вторая вызывает Rust CLI, третья - тот же helper по STDIO MCP. Для Speech Dispatcher модели ONNX не нужны. Они проверены отдельными backend/STDIO тестами; started=true означает запуск, а не завершённую речь.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
local_demo="$repo/scripts/tts-demos"
# Системная озвучка через Speech Dispatcher.
bash "$local_demo/spd-stdin.sh" < "$local_demo/sample-ru.txt"
# CLI speak, текст приходит в stdin.
CODEX_LINUX_READ_ALOUD_COMMAND="$local_demo/spd-stdin.sh" \
  "$repo/target/debug/codex-read-aloud-linux" speak \
  < "$local_demo/sample-ru.txt"
# MCP read_aloud: сервер создаёт только эта команда.
CODEX_LINUX_READ_ALOUD_COMMAND="$local_demo/spd-stdin.sh" \
  python3 "$local_demo/mcp-client.py" read --text-file "$local_demo/sample-ru.txt"
# Тест JS-backend кнопки без UI приложения.
env -u NODE_OPTIONS CODEX_LINUX_READ_ALOUD_COMMAND="$local_demo/spd-stdin.sh" \
  node "$local_demo/button-backend.cjs" "$local_demo/sample-ru.txt"

Если Rust helper не собран, вместо пути target/debug задайте существующий бинарник через --binary у MCP-клиента. Инструкция сборки есть в общем TTS-документе. Не надо включать patch кнопки или менять установленный payload ради этого теста.

Как проверять и где возможна ошибка

Сверяются непустой сигнал, WAV header и полное декодирование. MCP tests используют контролируемую команду, проверяют stdin, busy, stop и socketpair. Команды для настоящего backend не требуют UI, но воспроизведение может дать звук в помещении. Здесь Desktop не запускается.

Блоки схемы и code-map

Кнопка / CLI / MCP

Кнопка отправляет выбранный текст JS backend. CLI speak и MCP read_aloud используют Rust helper. Ни одна оболочка сама по себе не означает удалённый синтез.

Вход и выход. Текст/параметры → локальный движок или явно заданная команда.

Code-map. linux-features/read-aloud/README.md; read-aloud-linux/src/main.rs. Rust main / serve_mcp; JS backend обрабатывает запрос speak. MCP - STDIO protocol, не HTTP TTS endpoint.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

button → JS backend → engine
CLI speak → Rust helper → engine
MCP read_aloud → Rust helper → engine

Проверка. MCP и socketpair tests; backend harness

← Вернуться к схеме

local-file.py

Демо напрямую вызывает установленный движок и сохраняет WAV. Это не локальная STT: входом всегда является текст.

Вход и выход. UTF-8 строка → модель/команда → PCM WAV.

Code-map. scripts/tts-demos/local-file.py. main: PiperVoice.load/synthesize_wav, Kokoro.create или espeak-ng --stdout --stdin.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

PiperVoice.load(model).synthesize_wav(text, audio)
Kokoro(model, voices).create(text, voice=...)
espeak-ng --stdout -v ru --stdin

Проверка. 29-30.09.2026 все три создали декодируемый WAV; параметры образцов проверяются ffprobe. Speech Dispatcher отдельно проигрывает, не сохраняет WAV.

← Вернуться к схеме

Локальный движок

Piper и Kokoro выполняют inference модели на машине; eSpeak синтезирует речь локально. Speech Dispatcher маршрутизирует текст системному синтезатору и управляет воспроизведением. Это не удалённый сервер ChatGPT.

Вход и выход. Текст → локальный звук. Для Piper нужен ONNX; для Kokoro ONNX и voices.bin.

Code-map. linux-features/read-aloud/README.md; linux-features/read-aloud-mcp/README.md. Priority зависит от оболочки. Rust MCP: custom command → Kokoro → spd-say → espeak-ng. Piper подключается через команду, не как отдельный MCP engine enum.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

text → selected local engine → PCM → player
custom command может отдельно вызвать HTTP TTS

Проверка. test_cleaned_text_forwarded_to_real_process, test_busy_interrupt_and_stop

← Вернуться к схеме

Аудиоспособ · MP3 получен

Текст → MP3: /pronunciation/synthesize

Для удалённой озвучки собственной строки в исследованном Desktop найден pronunciation endpoint. В него передаётся текст, язык и source, а обратно приходит MP3. У нашего примера нет параметра выбора голоса; возможности соседних TTS API на этот контракт переносить нельзя.

Результат и дата. 30.09.2026 - 200 и декодируемый MP3. Ранний образец 29.09.2026: 24 кГц mono, 7,488 с. Новый размер ответа - 155520 байт.

Штатная система · языковой виджет DesktopШтатная система · языковой виджет DesktopЯзыковой виджетpronunciation / translationJSON payloadСетевой мост Electrontoken из app-serverHTTPSChatGPT pronunciationзакрытый удалённый сервисEndpoint и формат · нажмите для полных примеров ↓POST https://chatgpt.com/backend-api/pronunciation/synthesize?format=mp3Формат: application/json: text / pronunciation_language / source → MP3 bytesПример: {text:"Привет",pronunciation_language:"ru",source:"translation"}
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Произвольный текст → MP3

POST https://chatgpt.com/backend-api/pronunciation/synthesize?format=mp3

application/json: text / pronunciation_language / source → MP3 bytes

Пример: {text:"Привет",pronunciation_language:"ru",source:"translation"}

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Наше демо · строка из файлаНаше демо · строка из файлаUTF-8 текстне аудиоUTF-8 → JSONhttp-tts.pyJSON → MP3JSON → MP3ChatGPT pronunciationзакрытый удалённый сервисEndpoint и формат · нажмите для полных примеров ↓POST https://chatgpt.com/backend-api/pronunciation/synthesize?format=mp3Формат: application/json: text / pronunciation_language / source → MP3 bytesПример: {text:"Привет",pronunciation_language:"ru",source:"translation"}
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Произвольный текст → MP3

POST https://chatgpt.com/backend-api/pronunciation/synthesize?format=mp3

application/json: text / pronunciation_language / source → MP3 bytes

Пример: {text:"Привет",pronunciation_language:"ru",source:"translation"}

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Что проходит по сети

Часть обменаDesktop / CLIНаш клиент
EndpointPOST /pronunciation/synthesize?format=mp3POST https://chatgpt.com/backend-api/pronunciation/synthesize?format=mp3
JSONtext; необязательные hint/language/speed/source/version зависят от виджетаtext, pronunciation_language=ru, source=translation; hint/speed/version не добавляем
Аудио на входНетНет. Только JSON UTF-8, Content-Type application/json
Аудио на выходBlob MP3audio/mpeg, MP3 целиком → новый файл. Не WAV и не PCM delta
Голос/модельТочный выбор закрытого сервиса не установленПараметров voice/model в этой ветке нет.
АвторизацияElectron/auth_refresh/attach metadataФайл auth.json, Bearer/account ID и наблюдавшиеся HTTP headers; без refresh. urllib учитывает env HTTP proxy.

Частота и каналы принадлежат полученному MP3, а не входной строке. Нельзя объявить mp3-файл WAV сменой расширения. Если вместо MP3 приходит JSON/HTML или пустой ответ, демо отказывает; audio/mpeg само по себе ещё не доказывает декодируемость.

Как запустить

Каталог: открыть локальную папку. Инструкция и требования.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
tts_http_out="$(mktemp -d "$audio_out/tts-http.XXXXXX")"
python3 "$repo/scripts/tts-demos/http-tts.py" --auth-file "$auth_file"    chatgpt-text "$tts_http_out/voice.mp3"    --text-file "$repo/scripts/tts-demos/sample-ru.txt"
ffmpeg -nostdin -v error -i "$tts_http_out/voice.mp3" -f null -

Как проверять и где возможна ошибка

Тест перехватывает JSON до сети и сверяет endpoint, метод и поля. Отдельные тесты не дают сохранить пустой ответ, JSON/HTML с audio MIME, redirect или перезаписать существующий файл. ffmpeg проверяет весь поток. Ошибка HTTP и ошибка декодера - разные результаты.

Блоки схемы и code-map

Языковой виджет

Desktop умеет озвучивать произвольный текст в языковых виджетах. Это не Linux-кнопка с Piper/Kokoro и не чтение ответа по message ID.

Вход и выход. Текст + параметры виджета → synthesizeAudio → HTTP.

Code-map. Карта ASAR. app-primary: ixt; start-voice: synthesizeAudio, pronunciation_hint/language, speed/source/version. Снимок 26.928.20755; точный набор необязательных полей зависит от виджета.

Выдержка JavaScript клиента. Это не Rust и не готовый самостоятельный скрипт.

// Это форма вызова endpoint; авторизация и обработка ответа показаны в полном демо.
// Комментарии добавлены для учебника; minified-выдержка может быть неполной.
fetch("/pronunciation/synthesize?format=mp3", {
  method: "POST", body: JSON.stringify(payload)
}).then(response => response.blob())

Проверка. Поиск endpoint в payload; живой Python HTTP-запрос 30.09.2026 дал MP3.

← Вернуться к схеме

UTF-8 текст

Это исходный текст для чтения. В pronunciation-ветке никакие PCM/Opus/MP3-байты на вход не отправляются.

Вход и выход. Файл UTF-8 → строка text.

Code-map. scripts/tts-demos/http-tts.py. main читает --text-file. build_request собирает JSON без credentials, поэтому dry-run безопасен.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

text = text_file.read_text(encoding="utf-8").strip()
{"text": text, "pronunciation_language": "ru", "source": "translation"}

Проверка. test_arbitrary_chatgpt_text_contract

← Вернуться к схеме

http-tts.py

Этот клиент делает ту же основную операцию произвольного текста, но не воспроизводит весь Electron network stack. Он получает бинарный ответ и сохраняет новый файл.

Вход и выход. JSON text/language/source → MP3; output создаётся эксклюзивно.

Code-map. scripts/tts-demos/http-tts.py. build_request, chatgpt_headers, download_audio. MIME/размер/JSON вместо аудио проверяются до записи; полное декодирование делает ffmpeg.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

url, method, body = build_request(args, text)
download_audio(url, method, body, headers, output, timeout)

Проверка. HttpTests, RequestTests

← Вернуться к схеме

Аудиоспособ · оба формата получены

Существующий ответ → MP3 или AAC: /synthesize

Озвучка сохранённого ответа устроена иначе: клиент передаёт ID разговора и сообщения, а не сам текст. Сервис находит ответ и возвращает аудио. Нужны ChatGPT conversation/message IDs; Codex thread ID относится к другой сущности. Начните с явных собственных IDs, автоматический выбор закреплённого чата оставьте как дополнительный режим.

Этот способ полезен, когда ответ уже существует в ChatGPT. Сначала надо найти правильный assistant message активной ветки. Ошибка 404 для другого ID не означает, что весь endpoint не работает. Для произвольной строки используйте pronunciation или Realtime, а не подменяйте ID своим текстом.

Результат и дата. 30.09.2026: MP3 и AAC 200; повторный MP3 Juniper - 3732000 байт. Ранее проверенный MP3 3735840 байт, 186,792 с, 24 кГц mono. Это разные получения, их размеры не надо объединять.

Штатная система · чтение ответа ChatGPTШтатная система · чтение ответа ChatGPTОтвет ChatGPTconversation ID + message IDIDs + voiceChatGPT Read Aloudкнопка существующего ответаGET → BlobChatGPT synthesizeзакрытый удалённый сервисEndpoint и формат · нажмите для полных примеров ↓GET https://chatgpt.com/backend-api/synthesize?conversation_id=<id>&message_id=<id>&voice=<voice>&format=mp3Формат: Query-параметры, тела нет → MP3 / AAC; не отправка текста ответаПример: conversation_id=demo; message_id=answer; voice=juniper; format=mp3
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Read Aloud существующего ответа

GET https://chatgpt.com/backend-api/synthesize?conversation_id=<id>&message_id=<id>&voice=<voice>&format=mp3

Query-параметры, тела нет → MP3 / AAC; не отправка текста ответа

Пример: conversation_id=demo; message_id=answer; voice=juniper; format=mp3

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Наше демо · GET для своего сообщенияНаше демо · GET для своего сообщенияОтвет ChatGPTconversation ID + message IDвыбор активной веткиchatgpt-read-aloud.pyкаталог, IDs, GETGET → MP3 / AACChatGPT synthesizeзакрытый удалённый сервисEndpoint и формат · нажмите для полных примеров ↓GET https://chatgpt.com/backend-api/synthesize?conversation_id=<id>&message_id=<id>&voice=<voice>&format=mp3Формат: Query-параметры, тела нет → MP3 / AAC; не отправка текста ответаПример: conversation_id=demo; message_id=answer; voice=juniper; format=mp3
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Read Aloud существующего ответа

GET https://chatgpt.com/backend-api/synthesize?conversation_id=<id>&message_id=<id>&voice=<voice>&format=mp3

Query-параметры, тела нет → MP3 / AAC; не отправка текста ответа

Пример: conversation_id=demo; message_id=answer; voice=juniper; format=mp3

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Что проходит по сети

Часть обменаDesktop / CLIНаш клиент
EndpointGET /backend-api/synthesizeТот же endpoint; query conversation_id/message_id/voice/format
Тело запросаНетНет text, нет audio, нет file. Дополнительные GET каталога/чата выполняются отдельно
ГолосаКаталог текущего аккаунта--list-voices делает read-only GET. Название Sol может иметь backend ID glimmer; ID не равен CLI sol
ОтветКодированное MP3/AACformat=mp3 либо aac. AAC - полученный AAC/ADTS поток, не обещание контейнера M4A
АвторизацияШтатный сетевой мостСвой token из файла, без cookie/refresh/HTTP proxy; чужой account state не читается.

Для MP3/AAC частота, каналы и длительность определяются декодером ответа. Base64 здесь не используется. MIME и сигнатура помогают отвергнуть JSON/HTML, но окончательно поток проверяет ffmpeg. AAC-байты нельзя подавать как raw PCM.

Как запустить

Каталог: открыть локальную папку. Инструкция и требования.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
read_out="$(mktemp -d "$audio_out/chatgpt-read.XXXXXX")"
python3 "$repo/scripts/remote-tts-isolated/chatgpt-read-aloud.py" --list-voices --auth-file "$auth_file"
# Без явных IDs нужен закреплённый ChatGPT-чат текущего account ID.
python3 "$repo/scripts/remote-tts-isolated/chatgpt-read-aloud.py" --auth-file "$auth_file"    "$read_out/answer.mp3" --voice Juniper --format mp3 --conversation-id "${CHATGPT_CONVERSATION_ID:?Укажите свой conversation ID}" --message-id "${CHATGPT_MESSAGE_ID:?Укажите свой message ID}"
# Для AAC задайте новый путь answer.aac и --format aac.
ffmpeg -nostdin -v error -i "$read_out/answer.mp3" -f null -

Как проверять и где возможна ошибка

Проверяются account boundary, активная ветка, кодирование query, выбор backend voice ID, отказ redirect и новая запись 0600. Личные message ID и ответ не опубликованы. Демо может требовать --conversation-id и --message-id, если закреплённого собственного чата нет.

Блоки схемы и code-map

Ответ ChatGPT

Озвучивается сохранённый ответ ChatGPT, а не переданная строка. Codex thread ID и turn ID для этого запроса не подходят.

Вход и выход. Собственный conversation ID + ID видимого assistant ответа → query.

Code-map. scripts/remote-tts-isolated/chatgpt-read-aloud.py. pinned_conversation выбирает account; assistant_message идёт по current_node/parent активной ветки и пропускает hidden/analysis/tool.

Форма HTTP-запроса или ответа. Учебные значения не заменяют вход и целый аудиофайл.

current_node → parent → видимый assistant message
GET /synthesize?conversation_id=…&message_id=…

Проверка. test_current_account_only, test_active_branch_ignores_hidden_analysis_and_tool_messages

← Вернуться к схеме

ChatGPT Read Aloud

Штатное чтение сохранённого ChatGPT-ответа вызывает /synthesize с ID сообщения и голоса. Это другой механизм, чем добавленная Linux-функция Read Aloud.

Вход и выход. IDs ответа и голос → GET /synthesize → Blob/плеер.

Code-map. Карта Desktop read-aloud. Якорь /synthesize; query conversation_id, message_id, voice, format. Закрытые детали обработчика сервера неизвестны.

Форма HTTP-запроса или ответа. Учебные значения не заменяют вход и целый аудиофайл.

GET /synthesize?conversation_id=…&message_id=…&voice=juniper&format=mp3

Проверка. Живые MP3 и AAC 30.09.2026; выбран старый неподходящий ID ранее дал 404.

← Вернуться к схеме

chatgpt-read-aloud.py

Клиент получает каталог голосов, при необходимости читает свой закреплённый чат и выбирает подходящее сообщение. Затем запрашивает аудио. Личный текст/IDs в итог проверки не печатаются.

Вход и выход. Auth/state или явные IDs → GET → MP3/AAC.

Code-map. scripts/remote-tts-isolated/chatgpt-read-aloud.py. voice_catalog, assistant_message, get, save_audio. ID голоса берётся из живого каталога; display name преобразуется в backend ID.

Форма HTTP-запроса или ответа. Учебные значения не заменяют вход и целый аудиофайл.

GET /settings/voices?spoken_language=ru&voice_mode=advanced
GET /synthesize?...&voice=<backend_id>&format=aac

Проверка. ChatTests, FileTests

← Вернуться к схеме

Аудиоспособ · WAV получен

Текст → аудиочанки: GA Realtime WebSocket

GA Realtime позволяет передать текст и собрать звуковой ответ из событий. Клиент принимает Base64 PCM-чанки своего response_id, ждёт completed и только после этого создаёт WAV. Это генеративный ответ: он не обещает дословно повторить каждую букву. GA WSS и WebRTC V3 - разные способы связи.

Realtime полезен, если дальше нужен потоковый разговор, но для простого чтения он остаётся моделью, которая может изменить текст. В штатном CLI 0.156.1 /voice выбран WebRTC V3; показанный здесь GA WSS - другой воспроизведённый способ. На схемах он сопоставлен со штатным голосовым трактом, но не назван его копией.

Результат и дата. 30.09.2026 аудио cedar получено и декодировано; ранее удалённая STT подтвердила строку. Каталог GA: alloy, ash, ballad, coral, echo, sage, shimmer, verse, marin, cedar. Все голоса синтезом не проверялись.

Штатная система · CLI /voice, другой транспортШтатная система · CLI /voice, другой транспортTUI / CLIинтерфейсIPC / SDPcodex-voice-hostOpus и WebRTCWebRTC Opus + sidebandRealtime serviceзакрытый удалённый сервисEndpoint и формат · нажмите для полных примеров ↓Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/doneФормат: Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демоПример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}Локальный IPC codex-voice-host · SDP / mute / stopФормат: Команды управления + SDP; аудио между устройствами и helper локальноеПример: SDP offer/answer; mute → отключить захват; stop → закрыть сеансPOST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avasФормат: application/json: sdp + session → plain SDP answer + Location/call IDПример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}WSS wss://api.openai.com/v1/live/<call_id>Формат: WebSocket UTF-8 JSON: тексты реплик, завершение, координацияПример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}WebRTC · выбранный SDP/ICE peer · не HTTP endpointФормат: Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файлПример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

CLI ↔ локальный app-server

Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done

Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо

Пример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}

Полный запрос, ответ, код и ограничения

CLI ↔ voice helper

Локальный IPC codex-voice-host · SDP / mute / stop

Команды управления + SDP; аудио между устройствами и helper локальное

Пример: SDP offer/answer; mute → отключить захват; stop → закрыть сеанс

Полный запрос, ответ, код и ограничения

Создать WebRTC V3 call

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas

application/json: sdp + session → plain SDP answer + Location/call ID

Пример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}

Полный запрос, ответ, код и ограничения

События WebRTC V3

WSS wss://api.openai.com/v1/live/<call_id>

WebSocket UTF-8 JSON: тексты реплик, завершение, координация

Пример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}

Полный запрос, ответ, код и ограничения

Звук разговора

WebRTC · выбранный SDP/ICE peer · не HTTP endpoint

Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файл

Пример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Наше демо · GA text → audioНаше демо · GA text → audioТекст для Realtimeinput_text, не PCMJSON input_textrealtime-tts.pyGA WSS и PCM deltaWSS JSON PCM deltaGA Realtimeзакрытый удалённый сервисEndpoint и формат · нажмите для полных примеров ↓WSS wss://api.openai.com/v1/realtime?model=gpt-realtimeФормат: UTF-8 JSON input_text → response.output_audio.delta: Base64 PCM16 mono 24 кГцПример: {type:"response.output_audio.delta",response_id:"demo",delta:"AQD//w=="}
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Наше GA Realtime TTS

WSS wss://api.openai.com/v1/realtime?model=gpt-realtime

UTF-8 JSON input_text → response.output_audio.delta: Base64 PCM16 mono 24 кГц

Пример: {type:"response.output_audio.delta",response_id:"demo",delta:"AQD//w=="}

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Что проходит по сети

Часть обменаDesktop / CLIНаш клиент
ТранспортCLI V3: HTTPS call, WebRTC Opus, WSS /v1/live/call_idwss://api.openai.com/v1/realtime?model=gpt-realtime
SessionV3 session в call JSON, model=gpt-live-1-codexsession.update type=realtime, model=gpt-realtime, output_modalities=[audio], output PCM24000, voice
ВходCLI: Opus/RTP с микрофонаТолько input_text; файловых аудиобайтов и PCM на входе нет
ВыходOpus/SRTP; V3 captions по sidebandresponse.output_audio.delta: Base64 raw PCM16 mono 24 кГц LE; в чанке нет WAV header
Конец ответаV3 turn.done и lifecycle callresponse.done.status=completed; ID должен соответствовать response.created
ВходЯдро auth manager, alpha, optional genuine attestationСвой OAuth из файла; Bearer в WSS handshake, без refresh/cookies/env proxy.

Чанки delta - транспортные порции raw PCM, их длина не равна обязательно одной секунде или фразе. Клиент склеивает байты и проверяет выравнивание PCM16. WAV header записывается только локально после completed. У CLI RTP-clock 48 кГц, у этого raw PCM contract 24 кГц - это разные величины.

Как запустить

Каталог: открыть локальную папку. Инструкция и требования.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
rt_tts_out="$(mktemp -d "$audio_out/realtime-tts.XXXXXX")"
"$voice_python"    "$repo/scripts/remote-tts-isolated/realtime-tts.py" --auth-file "$auth_file"    "$repo/scripts/remote-tts-isolated/sample-ru.txt"    "$rt_tts_out/answer.wav" --voice cedar
ffmpeg -nostdin -v error -i "$rt_tts_out/answer.wav" -f null -

Как проверять и где возможна ошибка

Тесты проверяют порядок created/update/create, чужой response_id, ошибочный Base64, нечётный PCM, пустой и незавершённый ответ. Пустой done или только звук без правильного завершения - не успех. Параметры output format проверяются в отправленном session.update.

Блоки схемы и code-map

Текст для Realtime

В этой пробе Realtime просят прочитать строку. Это генеративный ответ с инструкцией чтения, а не гарантированно дословный специализированный TTS.

Вход и выход. UTF-8 text → conversation.item.create → response.create.

Code-map. scripts/remote-tts-isolated/realtime-tts.py. synthesize задаёт voice и output PCM в session.update, затем создаёт user message.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

{"type":"conversation.item.create","item":{"type":"message","role":"user","content":[{"type":"input_text","text":"…"}]}}
{"type":"response.create"}

Проверка. test_chosen_voice_and_completed_pcm

← Вернуться к схеме

realtime-tts.py

Это самостоятельный GA Realtime WebSocket. Он не эквивалентен CLI V3/WebRTC: другой endpoint, события и формат медиа.

Вход и выход. Строка → WSS JSON; response.output_audio.delta → PCM16 → WAV.

Code-map. scripts/remote-tts-isolated/realtime-tts.py. synthesize коррелирует response.created.id, delta.response_id и response.done.response.id; save_wav публикует готовый файл.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

session.update: audio.output.format={type:'audio/pcm',rate:24000}
base64(delta) → raw PCM16 mono → WAV
created.id == delta.response_id == done.response.id

Проверка. test_bad_base64_odd_pcm_empty_and_foreign_done_rejected

← Вернуться к схеме

Аудиоспособ · эксперимент воспроизведён

Текст → голос через app-server appendSpeech

Теперь вызовем озвучку через локальный интерфейс app-server. Наш опыт передаёт текст в appendSpeech и получает аудиодельты уведомлениями. Он запускает отдельный временный профиль и использует WS v2. OAuth, переданный в поле apiKey этого локального опыта, не становится ключом OpenAI Platform.

Прежняя проба с обычным ChatGPT auth не прошла локальную проверку API-key режима. Временный app-server получил тот же собственный OAuth через apiKey login; вход Desktop не переключался. Этот способ остаётся экспериментом.

Результат и дата. 30.09.2026 - аудио получено. ash: WAV 5,2 с, 24 кГц mono; обратная STT соответствует строке. Проба standalone CLI 0.156.1 также дала PCM. Старый обычный ChatGPT auth отказ остаётся отдельным результатом.

Штатная система · официальный RPC, не доказанная кнопка UIШтатная система · официальный RPC, не доказанная кнопка UIRPC клиентappendSpeech textJSON-RPCЛокальный app-serverRPC и координацияядро → WSSRealtime WebSocket serviceзакрытый удалённый сервисEndpoint и формат · нажмите для полных примеров ↓stdio:// · thread/realtime/start → thread/realtime/appendSpeech → outputAudio/deltaФормат: JSON-RPC text → audio.data (Base64 PCM) + sampleRate / numChannelsПример: appendSpeech {threadId:"demo",text:"Привет"} → audio.data="AQD//w=="WSS provider URL + /realtime?model=<model> (не /live/<call_id>)Формат: JSON session.update / input_text → JSON-события аудио и репликПример: Обычный OpenAI provider: wss://api.openai.com/v1/realtime?model=<model>
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

app-server appendSpeech

stdio:// · thread/realtime/start → thread/realtime/appendSpeech → outputAudio/delta

JSON-RPC text → audio.data (Base64 PCM) + sampleRate / numChannels

Пример: appendSpeech {threadId:"demo",text:"Привет"} → audio.data="AQD//w=="

Полный запрос, ответ, код и ограничения

Ядро Codex → Realtime WS v2

WSS provider URL + /realtime?model=<model> (не /live/<call_id>)

JSON session.update / input_text → JSON-события аудио и реплик

Пример: Обычный OpenAI provider: wss://api.openai.com/v1/realtime?model=<model>

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Наше демо · ephemeral WebSocket v2Наше демо · ephemeral WebSocket v2RPC клиентappendSpeech textтекст / temporary profileappserver-tts.pyсвой временный app-serverWSS → PCM → RPCRealtime WebSocket serviceзакрытый удалённый сервисEndpoint и формат · нажмите для полных примеров ↓stdio:// · thread/realtime/start → thread/realtime/appendSpeech → outputAudio/deltaФормат: JSON-RPC text → audio.data (Base64 PCM) + sampleRate / numChannelsПример: appendSpeech {threadId:"demo",text:"Привет"} → audio.data="AQD//w=="WSS provider URL + /realtime?model=<model> (не /live/<call_id>)Формат: JSON session.update / input_text → JSON-события аудио и репликПример: Обычный OpenAI provider: wss://api.openai.com/v1/realtime?model=<model>
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

app-server appendSpeech

stdio:// · thread/realtime/start → thread/realtime/appendSpeech → outputAudio/delta

JSON-RPC text → audio.data (Base64 PCM) + sampleRate / numChannels

Пример: appendSpeech {threadId:"demo",text:"Привет"} → audio.data="AQD//w=="

Полный запрос, ответ, код и ограничения

Ядро Codex → Realtime WS v2

WSS provider URL + /realtime?model=<model> (не /live/<call_id>)

JSON session.update / input_text → JSON-события аудио и реплик

Пример: Обычный OpenAI provider: wss://api.openai.com/v1/realtime?model=<model>

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Что проходит по сети

Часть обменаDesktop / CLIНаш клиент
Локальный вызовthread/realtime/start + appendSpeechinitialize, login apiKey во временном профиле, thread/start ephemeral, realtime/start websocket/v2, appendSpeech
ТекстappendSpeech.textИнструкция читать дословно + text; генеративную дословность не гарантируем
Аудио на входВ appendSpeech его нетНет файла или PCM. appendAudio - отдельная RPC операция
Аудио на выходoutputAudio/delta.audio: data/sampleRate/numChannelsBase64 raw PCM16; shape берётся из уведомления и не может меняться посреди ответа
Завершениеrealtime stop / session lifecycleПосле 3 секунд паузы сбор прекращается; это эвристика, не гарантия полного ответа по done
АвторизацияШтатный auth managerТот же OAuth в apiKey поле временного входа. Основной auth.json не меняется; временный credential удаляется после своего процесса.

В проверенных ответах sampleRate=24000 и numChannels=1. Это PCM16, без контейнера в data. Ширина сэмпла подтверждена живым декодированием, а в схеме RPC не задаётся отдельным sampleWidth. Нельзя считать любой Base64 из уведомления WAV-файлом.

Как запустить

Каталог: открыть локальную папку. Инструкция и требования.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
app_tts_out="$(mktemp -d "$audio_out/appserver-tts.XXXXXX")"
python3 "$repo/scripts/remote-tts-isolated/appserver-tts.py" --auth-file "$auth_file"    "$repo/scripts/remote-tts-isolated/sample-ru.txt"    "$app_tts_out/answer.wav" --codex "$(command -v codex)"    --version v2 --voice ash
ffmpeg -nostdin -v error -i "$app_tts_out/answer.wav" -f null -

Как проверять и где возможна ошибка

Тесты запускают только fake STDIO сервер: подтверждают выбор голоса, первую delta до RPC-ack, очистку профиля при success/error/timeout и отсутствие чужого env. Для живого результата использован настоящий отдельный app-server. SIGKILL/отключение питания не дают Python выполнить finally: временный профиль может остаться.

Блоки схемы и code-map

RPC клиент

Клиент app-server передаёт текст для голосового вывода. appendSpeech - не загрузка микрофона, не appendAudio и не файловая STT.

Вход и выход. thread ID + text → thread/realtime/appendSpeech.

Code-map. app-server-protocol/src/protocol/v2/realtime.rs. ThreadRealtimeAppendSpeechParams содержит thread_id и text; realtime/start выбирает version и transport.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

thread/realtime/appendSpeech {threadId:"…", text:"…"}

Проверка. AppServerTests

← Вернуться к схеме

appserver-tts.py

В отличие от нового WebRTC-демо, этот скрипт всё-таки запускает Codex app-server. Но это отдельный процесс с временным CODEX_HOME, без Desktop и исходников форка.

Вход и выход. Auth token в памяти → temporary account/login/start type=apiKey → ephemeral thread → appendSpeech → PCM WAV.

Code-map. scripts/remote-tts-isolated/appserver-tts.py. synthesize создаёт профиль 0700, отфильтрованное env и рабочую папку, останавливает только свой процесс. Обычный ChatGPT WebSocket вход ранее дал requires API key auth.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

initialize → account/login/start {type:"apiKey",apiKey:<тот же OAuth>}
thread/start ephemeral → realtime/start websocket/v2
appendSpeech → outputAudio/delta

Проверка. test_separate_home_cwd_selected_voice_and_clean_environment, test_timeout_always_clean_home

← Вернуться к схеме

Реальная система

Что запускает команда codex

Теперь вернёмся к разговору. В CLI 0.156.1 TUI, app-server и codex-voice-host выполняют разные роли, но не обязательно живут в трёх отдельных процессах. Встроенный app-server связывается с TUI через InProcess; возможны daemon по Unix-сокету и remote-режим. STDIO относится к нашим демо, которые действительно запускают отдельный процесс.

Историческое наблюдение 30.09.2026Команда codex из PATH ведёт в standalone CLI 0.156.1, а не в Electron-форк Desktop. Статус входа: ChatGPT. В пакете присутствуют codex-voice-host и GStreamer; voice-функции включены.

Наличие файлов и флагов не доказывает работу устройства записи. В этом исследовании не запускался живой голосовой сеанс CLI.

  1. 1. Интерфейс TUIВы вводите /voice или нажимаете F8. CLI запускает helper и получает предложение соединения - SDP.
  2. 2. Локальный app-serverПолучает thread/realtime/start, использует вход Codex и создаёт удалённый разговорный сеанс WebRTC v3.
  3. 3. Звук и текстHelper передаёт аудио по WebRTC. App-server получает текстовые события через отдельный sideband и возвращает их TUI.
Три независимых потока голосового CLITUI связывается с helper локально и с app-server по JSON-RPC. App-server отправляет HTTPS signaling и подключается к WSS sideband. Микрофон и динамики через helper работают с удалённым WebRTC напрямую. ЛокальноУдалённо Микрофони динамики codex-voice-hostзахват · Opus · WebRTC TUI / CLIкоманды, субтитрыи история app-serverauth · signaling · sidebandконтекст · handoff · события Realtime serviceПринимает звукРаспознаёт речьВедёт голосовой диалогТочная STT-модель CLIне установлена Opus / RTPаудиоответ IPC: SDP, mute, stopTyped RPCHTTPS: SDPWSS: sideband transcript → app-server → TUIDelta / Done: роль, текст, завершённость Endpoint и формат · нажмите для полных примеров ↓Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/doneФормат: Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демоПример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}Локальный IPC codex-voice-host · SDP / mute / stopФормат: Команды управления + SDP; аудио между устройствами и helper локальноеПример: SDP offer/answer; mute → отключить захват; stop → закрыть сеансPOST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avasФормат: application/json: sdp + session → plain SDP answer + Location/call IDПример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}WSS wss://api.openai.com/v1/live/<call_id>Формат: WebSocket UTF-8 JSON: тексты реплик, завершение, координацияПример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}WebRTC · выбранный SDP/ICE peer · не HTTP endpointФормат: Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файлПример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц
Схема по исходникам версии 0.156.1, не запись фактического сетевого трафика. Стрелки упрощены; signaling и управление имеют ответы.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

CLI ↔ локальный app-server

Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done

Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо

Пример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}

Полный запрос, ответ, код и ограничения

CLI ↔ voice helper

Локальный IPC codex-voice-host · SDP / mute / stop

Команды управления + SDP; аудио между устройствами и helper локальное

Пример: SDP offer/answer; mute → отключить захват; stop → закрыть сеанс

Полный запрос, ответ, код и ограничения

Создать WebRTC V3 call

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas

application/json: sdp + session → plain SDP answer + Location/call ID

Пример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}

Полный запрос, ответ, код и ограничения

События WebRTC V3

WSS wss://api.openai.com/v1/live/<call_id>

WebSocket UTF-8 JSON: тексты реплик, завершение, координация

Пример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}

Полный запрос, ответ, код и ограничения

Звук разговора

WebRTC · выбранный SDP/ICE peer · не HTTP endpoint

Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файл

Пример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Что именно делает app-server

App-server не является локальным Whisper. Его можно использовать без Electron-клиента, но он не заменяет WebRTC/helper и не превращает thread/realtime/start в endpoint загрузки FLAC.

Как включить в самом CLI

В терминале запустите codex. Внутри существующего основного чата: /voice - начать/остановить, /voice mute - выключить/включить микрофон, /voice settings - параметры. Стандартные клавиши: F8 и Ctrl + X; пользовательская раскладка может их менять. Voice не запускается в side conversation.

Основание: TUI / realtime, TUI → app-server, клавиши.

Обмен данными

Не один запрос, а несколько каналов

Разговор создаётся в несколько шагов. Локальный start задаёт WebRTC и version=v3. HTTPS-запрос передаёт SDP offer и получает answer; затем открываются WebRTC-медиа и WSS sideband. RPC result подтверждает вызов, уведомления приносят SDP и реплики, медиапоток приносит звук. Эти события нельзя объединять в один ответ start.

КаналЧто передаётсяЧто это не такое
TUI ↔ app-server
локальный JSON-RPC
thread/realtime/start: threadId, transport WebRTC, SDP, version V3; затем события transcript, stop.Не HTTP-загрузка WAV. В выбранном TUI-пути start не содержит исходный аудиофайл.
app-server ↔ service
HTTPS signaling
Авторизация, SDP offer и описание session; назад SDP answer и идентификатор call.SDP - параметры медиа, кодеков и транспорта, а не запись речи.
voice-host ↔ service
WebRTC / RTP
Поток Opus: микрофон наружу, голосовой ответ обратно. Защищённый media-канал WebRTC.Не JSON Base64 и не REST /audio/transcriptions.
app-server ↔ service
WSS sideband
События управления, transcript, завершения хода и handoff. V3: input_transcript.added, output_transcript.added, turn.done.Не единственный канал аудио и не локальное распознавание.
app-server → TUI
уведомления
ThreadRealtimeTranscriptDelta / Done; роль user/assistant, текст и завершённость.Частичный текст не равен окончательному. Caption не надо считать отдельным новым запросом модели.
Порядок создания сеанса и получения текстаHelper создаёт SDP; TUI передаёт его app-server; тот создаёт call и возвращает ответ. Затем работают WebRTC аудио и sideband текст. Stop закрывает сеанс. TUI + helperapp-serverRealtime service 1 · start: SDP offer + WebRTC V32 · HTTPS: auth + session + SDP3 · SDP answer + call ID4 · SDP-уведомление → helper 5 · WebRTC: звук напрямую, минуя app-server 6 · WSS sideband: transcript7 · Уведомление: текст в интерфейсе Старт/ответ показаны последовательно. Звук и служебные события затем идут параллельно. Endpoint и формат · нажмите для полных примеров ↓Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/doneФормат: Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демоПример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}Локальный IPC codex-voice-host · SDP / mute / stopФормат: Команды управления + SDP; аудио между устройствами и helper локальноеПример: SDP offer/answer; mute → отключить захват; stop → закрыть сеансPOST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avasФормат: application/json: sdp + session → plain SDP answer + Location/call IDПример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}WSS wss://api.openai.com/v1/live/<call_id>Формат: WebSocket UTF-8 JSON: тексты реплик, завершение, координацияПример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}WebRTC · выбранный SDP/ICE peer · не HTTP endpointФормат: Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файлПример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

CLI ↔ локальный app-server

Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done

Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо

Пример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}

Полный запрос, ответ, код и ограничения

CLI ↔ voice helper

Локальный IPC codex-voice-host · SDP / mute / stop

Команды управления + SDP; аудио между устройствами и helper локальное

Пример: SDP offer/answer; mute → отключить захват; stop → закрыть сеанс

Полный запрос, ответ, код и ограничения

Создать WebRTC V3 call

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas

application/json: sdp + session → plain SDP answer + Location/call ID

Пример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}

Полный запрос, ответ, код и ограничения

События WebRTC V3

WSS wss://api.openai.com/v1/live/<call_id>

WebSocket UTF-8 JSON: тексты реплик, завершение, координация

Пример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}

Полный запрос, ответ, код и ограничения

Звук разговора

WebRTC · выбранный SDP/ICE peer · не HTTP endpoint

Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файл

Пример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Ответ на RPC thread/realtime/start - пустой объект. SDP answer доставляется отдельно через ThreadRealtimeSdpNotification; принятие старта - через started-уведомление. Клиент должен обрабатывать события, а не ждать аудио или SDP в одном синхронном ответе.

Адреса: что подтверждено кодом

Для ChatGPT provider путь создания call - realtime/calls относительно настроенной backend base URL. Пример дефолтного маршрута из исходников:

Форма HTTP-запроса или ответа. Учебные значения не заменяют вход и целый аудиофайл.

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas
JSON: { "sdp": "…", "session": { … } }

WSS sideband V3 по умолчанию:
wss://api.openai.com/v1/live/<call_id>

Это маршруты по коду версии 0.156.1, не захваченный URL нашего живого CLI. Provider/base URL и experimental overrides могут их изменить. Публичный API-режим использует иной маршрут создания V3 call (/v1/live) и multipart, поэтому нельзя механически подменить только домен.

А что с thread/realtime/appendAudio?

В app-server существуют и другие realtime-операции, в том числе передача аудиочанков в WebSocket-пути. Но исследуемый CLI выбрал WebRTC: его микрофонный поток не идёт через appendAudio. API надо читать вместе с выбранными transport и version. appendSpeech также не означает "загрузить файл и распознать": это отдельная операция голосового вывода.

appendAudio принимает threadId и объект audio: data, sampleRate, numChannels, необязательные samplesPerChannel и itemId. Это аудиочанк, не путь к файлу на диске. appendText передаёт текст и роль, appendSpeech - текст для произнесения, stop закрывает сеанс, listVoices возвращает список голосов. Эти операции не нужны нашему выделенному STT-клиенту.

Поля и уведомления: app-server protocol / v2 / realtime.rs. Поддержку конкретной операции нужно проверять для выбранной версии и транспорта; наличие типа в схеме не является проверкой живого вызова.

Почему обычного proxy base URL недостаточно

Подмена backend для вызовов модели не гарантирует проксирование голоса. Нужно отдельно учитывать HTTPS signaling, sideband WSS и WebRTC media/ICE. По исходникам V3 sideband по умолчанию использует публичный api.openai.com независимо от provider base URL. Прокси только для JSON-RPC не становится RTP-реле.

Основание: создание call, sideband и маршруты, V3 transcript-события, Opus/RTP.

Аудиоспособ · живой V3 разговор подтверждён

Самостоятельная консоль /voice по WebRTC V3

Самостоятельный клиент переносит только необходимые обязанности: создаёт call, держит WSS и обслуживает WebRTC через aiortc. Он не запускает Codex и не выполняет инструменты. Если сервис просит делегацию агентной работы, клиент закрывает сеанс. Файловая проба проверяет одну реплику и получение звука; фиксированный хвост не доказывает полный media drain.

В CLI интерфейс передаёт SDP через app-server, а helper отправляет звук. В нашем варианте один процесс делает обе эти работы: aiortc держит медиа, aiohttp создаёт call и sideband. Так мы убрали зависимость от Codex, но сохранили V3 signaling. Агентную часть намеренно не переносили: запрос delegation.created завершает разговор, никакая команда модели не выполняется.

Результат и дата. 30.09.2026 файловый raven: V3 соединён, 682 исходящих/610 входящих RTP-пакетов, финальные реплики 99 и 63 символа, ненулевые декодированные сэмплы. WAV 48000 Гц mono PCM16, 12,12 с, 1163564 байта, ffmpeg exit0. Личные captions и WAV остались в закрытом /tmp, не в учебнике. Финальная версия повторно прошла V3: 722RTP отправлено/651принято, обе финальные реплики и полностью декодируемый WAV; вход не изменён.

Что делает app-server при открытии соединения

В штатном CLI app-server - организатор голосового сеанса. Helper создаёт SDP offer, CLI отправляет его вместе с thread ID методом thread/realtime/start. App-server использует авторизацию и настройки разговора, создаёт удалённый call, возвращает SDP answer helper’у через CLI и держит отдельное соединение событий. Сам звук проходит другим путём.

Создать call: HTTPS и JSON, не аудиофайл

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas. Тело содержит sdp и session - модель, голос и инструкции. Авторизация передаётся в заголовке. Ответ - текст SDP answer и call ID из заголовка Location. Call ID нужен для присоединения к этому же сеансу. Полный пример ниже в блоке endpoint.

Вернуть answer: app-server → CLI → helper

SDP - описание кодеков, сетевого соединения и параметров защиты, а не запись голоса. App-server сообщает answer клиенту событием thread/realtime/sdp; CLI передаёт его helper’у. Helper применяет answer, согласует WebRTC-соединение и начинает работу с медиапотоком. Схема сокращает внутренние шаги ICE/DTLS, но не смешивает их с транскрипцией.

Держать события: отдельный WSS

App-server открывает wss://api.openai.com/v1/live/<call_id>. Это sideband: JSON-события текста реплик, завершения и координации с агентом. Он не исчезает после открытия call: обрабатывает события и передаёт их CLI. Это не пересылка микрофонных Opus-пакетов.

Передавать звук: helper → WebRTC → сервис

Helper захватывает микрофон, кодирует звук в Opus и передаёт его защищёнными RTP/SRTP-пакетами на peer, выбранный SDP/ICE. Ответ проходит обратно к helper и динамикам. Нет HTTP-загрузки WAV/FLAC и нет аудио через app-server в этом V3-пути.

В нашем самостоятельном демо обязанности создания call и WSS перенесены в VoiceSession, а медиапоток обслуживает aiortc. App-server там не запускается; агентные функции намеренно не перенесены.

Код: CLI → start RPC, создание call, WSS sideband, VoiceSession нашего демо. Описание относится к исследованному CLI 0.156.1 / WebRTC V3.

← Вернуться к схеме app-server
Что делает app-server при открытии голосового соединения Helper создаёт SDP offer. CLI передаёт его app-server. App-server авторизует HTTPS POST создания call, получает SDP answer и call ID, возвращает answer через CLI helper и открывает WSS событий. Звук идёт напрямую через helper по WebRTC, не через app-server. Локально Удалённый голосовой сеанс Микрофони динамики codex-voice-hostСначала SDP offer, затем Opus / WebRTCЗахват микрофона и воспроизведение МедиасоединениеOpus / SRTPАудио туда и обратно TUI / CLIstart + SDP offerПолучает SDP answerВидит текст реплик app-server1. Получает thread + SDP offer2. Авторизация, голос, модель3. POST: создаёт call4. Возвращает SDP answer в CLI5. Держит WSS: события и текстНе пересылает аудиопакеты HTTPS: создать callPOST chatgpt.com/backend-api/codex/realtime/callsВход: JSON SDP + sessionОтвет: SDP answer + call ID WSS: события callapi.openai.com/v1/live/<call_id>JSON: реплики, done, координация Opus / SRTPаудиоответ IPC: SDP offer ↓ / SDP answer ↑startJSON: sdp + sessionSDP answer + call IDSDP answerОткрывает WSS Роль, текст, завершение → в историю CLI Реплики → app-server → CLI Звук не проходит через app-server Он открывает соединение и остаётся обслуживать сеанс. Endpoint и формат · нажмите для полных примеров ↓Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/doneФормат: Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демоПример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}Локальный IPC codex-voice-host · SDP / mute / stopФормат: Команды управления + SDP; аудио между устройствами и helper локальноеПример: SDP offer/answer; mute → отключить захват; stop → закрыть сеансPOST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avasФормат: application/json: sdp + session → plain SDP answer + Location/call IDПример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}WSS wss://api.openai.com/v1/live/<call_id>Формат: WebSocket UTF-8 JSON: тексты реплик, завершение, координацияПример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}WebRTC · выбранный SDP/ICE peer · не HTTP endpointФормат: Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файлПример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц
Схема по исходникам версии 0.156.1, не запись фактического сетевого трафика. Стрелки упрощены; signaling и управление имеют ответы.
Штатный CLI: media helper ↔ service отдельно от HTTPS/WSS app-server. Каждый блок ведёт к code-map.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

CLI ↔ локальный app-server

Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done

Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо

Пример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}

Полный запрос, ответ, код и ограничения

CLI ↔ voice helper

Локальный IPC codex-voice-host · SDP / mute / stop

Команды управления + SDP; аудио между устройствами и helper локальное

Пример: SDP offer/answer; mute → отключить захват; stop → закрыть сеанс

Полный запрос, ответ, код и ограничения

Создать WebRTC V3 call

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas

application/json: sdp + session → plain SDP answer + Location/call ID

Пример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}

Полный запрос, ответ, код и ограничения

События WebRTC V3

WSS wss://api.openai.com/v1/live/<call_id>

WebSocket UTF-8 JSON: тексты реплик, завершение, координация

Пример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}

Полный запрос, ответ, код и ограничения

Звук разговора

WebRTC · выбранный SDP/ICE peer · не HTTP endpoint

Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файл

Пример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Наш WebRTC V3: три канала в одном процессеНаш WebRTC V3: медиа отдельно от signaling и sideband InputTrackWAV или микрофон · mute aiortc peer / OpusICE · DTLS · SRTP · 20 мс АудиоответWAV или наушники VoiceSessionВместо app-server: call + WSS V3 voice serviceWebRTC mediaHTTPS create callWSS control + transcript PCM Opus / RTP Декодирование → PCM 48к mono SDP HTTPS / WSS Transcript Не запускаются Codex / app-server / Electron / helper. Делегация запрещена.Endpoint и формат · нажмите для полных примеров ↓POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avasФормат: application/json: sdp + session → plain SDP answer + Location/call IDПример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}WSS wss://api.openai.com/v1/live/<call_id>Формат: WebSocket UTF-8 JSON: тексты реплик, завершение, координацияПример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}WebRTC · выбранный SDP/ICE peer · не HTTP endpointФормат: Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файлПример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц
Зелёный - звук, синий пунктир - управление, фиолетовый - текст. WSS audio.delta не проигрывается второй раз.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Создать WebRTC V3 call

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas

application/json: sdp + session → plain SDP answer + Location/call ID

Пример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}

Полный запрос, ответ, код и ограничения

События WebRTC V3

WSS wss://api.openai.com/v1/live/<call_id>

WebSocket UTF-8 JSON: тексты реплик, завершение, координация

Пример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}

Полный запрос, ответ, код и ограничения

Звук разговора

WebRTC · выбранный SDP/ICE peer · не HTTP endpoint

Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файл

Пример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Что проходит по сети

Часть обменаDesktop / CLIНаш клиент
Создание callPOST backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas; JSON sdp/sessionТот же метод/path/основные поля. model=gpt-live-1-codex, voice=cove; instructions только для разговора, без startup context
Session/versionCLI V3; OpenAI-Alpha quicksilver=v2; voice V1 catalogТе же version/каталог. Значение alpha=v2 не превращает V3 в GA WS V2
ОтветPlain SDP answer + Location call IDТо же; проверка v=0, лимит размера и безопасного сегмента call ID
МедиаHelper mono Opus, 20мс; SDP opus/48000/2; RTP payload type11148к mono PCM16 → aiortc stereo libopus/20мс; negotiated PT из SDP. WAV header не отправляется. SRTP, без Base64
SidebandWSS api.openai.com/v1/live/call_id; oai-events создан, payload ignoredТот же WSS; без второго session.update. input/output_transcript.added.item.text; turn.done.turn.role/transcript. oai-events data ignored
ВходAuth manager + refresh; optional genuine attestation; originator CLI и session/thread metadataСвой auth.json read-only, no refresh/cookies/attestation; новый x-session-id, честный own originator/UA. Не заявляем сетевую неразличимость.
ДелегацияApp-server координирует handoff агентаСразу неподдерживаемая операция и stop. Никаких shell/tools/MCP.

Микрофон начинает работать после /voice и соединения. /voice mute прекращает capture и очищает backlog; clock и синтетическая тишина остаются. /voice settings показывает устройства/голос, /quit и Ctrl+C закрывают всё. Для файла нужен WAV48к monoPCM16, 0,2-60с. Файловая проба не открывает микрофон/динамики. AEC не реализован: при живом разговоре нужны наушники.

Как запустить

Каталог: открыть локальную папку. Инструкция и требования.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
voice_demo="$repo/scripts/webrtc-voice-isolated"
voice_venv="$voice_venv"
python3 -m venv "$voice_venv"
"$voice_venv/bin/pip" install -r "$voice_demo/requirements.txt"
"$voice_venv/bin/python" "$voice_demo/voice-console.py" --dry-run --auth-file "$auth_file"
"$voice_venv/bin/python" "$voice_demo/voice-console.py" --list-devices --auth-file "$auth_file"
"$voice_venv/bin/python" "$voice_demo/voice-console.py" --auth-file "$auth_file"
# Теперь введите /voice. Для файловой пробы - команда в README.

Как проверять и где возможна ошибка

Локальный peer-to-peer тест реально проходит ICE/DTLS/SRTP/Opus и декодирует тон. Это проверка медиакода, не удалённого доступа. Отдельная живая проба проверила call, sideband, оба RTP-направления и финальный ответ. В первом запросе отсутствовал alpha header: 400 invalid_quicksilver_alpha_header, challenge=False. Заголовок найден в realtime_request_headers и исправлен; также сверён default model ядра. Cookies/attestation не подделывались. Аппаратный разговор с каждым устройством не проверен.

Блоки схемы и code-map

InputTrack

Track тактирует 20-мс кадры. До соединения и во время mute это синтетическая тишина. После старта берётся файл или реальный микрофон. Старые callback после mute отбрасываются по epoch.

Вход и выход. PCM16 mono 48000 Гц → AudioFrame 960 samples → aiortc Opus encoder.

Code-map. scripts/webrtc-voice-isolated/voice-console.py. InputTrack.recv/next_pcm/enqueue/mute/unmute. Очередь ограничена 10 кадрами; capture.abort/close прекращает устройство.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

if muted: return bytes(1920)
frame.sample_rate=48000; frame.pts += 960
mute: stop capture + clear queue + epoch++

Проверка. test_mute_flushes_backlog_and_old_callback, test_local_peer_to_peer_opus_rtp_and_decode

← Вернуться к схеме

VoiceSession

Этот объект заменяет только сетевую координацию app-server и работу helper. Он не создаёт Codex thread, не использует tools и не читает проектный контекст.

Вход и выход. offer → POST call → plain answer/Location → apply answer → join sideband → RTP + captions.

Code-map. scripts/webrtc-voice-isolated/voice-console.py. VoiceSession._start/receive_events/receive_audio/close. call_body задаёт session до начала inference.

Форма HTTP-запроса или ответа. Учебные значения не заменяют вход и целый аудиофайл.

POST {sdp,session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}},...}}
apply SDP answer
WSS /v1/live/<call_id>  // без session.update

Проверка. test_outgoing_http_body_and_failure_cleanup, test_delegation_closes_without_running_tools

← Вернуться к схеме

V3 voice service

Сервис ведёт один разговор: WebRTC медиа и отдельный WSS контроль. Исходников сервера у нас нет. Здесь описаны контракт и полученные ответы, а не внутренняя реализация или название скрытой модели.

Вход и выход. Запрос по описанному ниже контракту → текст либо аудио. Авторизация и доступ проверяются сервисом.

Code-map. Клиентский контракт Codex; HTTP-контракты. Серверную функцию назвать по исходникам нельзя. Пример ниже показывает только внешний обмен.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

клиент → запрос → удалённый сервис
клиент ← ответ/события ← удалённый сервис

Проверка. 30.09.2026: call accepted, 682 sent/610 received RTP, user transcript99/assistant63 символа, WAV12,12с полностью декодирован.

← Вернуться к схеме

aiortc peer / Opus

Peer connection держит ICE, DTLS и SRTP. Он получает локальные AudioFrame и отдаёт декодированные кадры удалённого track. Это медиатракт; HTTP JSON с SDP не содержит этих аудиобайтов.

Вход и выход. PCM16 mono48к → aiortc stereo libopus → Opus/SRTP; обратный Opus → AudioFrame.

Code-map. voice-console.py: VoiceSession._start, addTransceiver, setCodecPreferences. Реализация Opus - aiortc1.15.0 codecs/opus.py: OpusEncoder/OpusDecoder.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

pc.addTransceiver(track, direction="sendrecv")
setCodecPreferences([audio/opus])
pc.createDataChannel("oai-events")

Проверка: test_local_peer_to_peer_opus_rtp_and_decode и живой V3 RTP в обе стороны.

← Вернуться к схеме

Аудиоответ

Полученный Opus декодируется aiortc. receive_audio приводит кадры к PCM16 mono48к. В консоли они идут в выбранное output device, в файловой пробе сохраняются в новый WAV с правами0600. После финального assistant turn демо ещё одну секунду принимает RTP-хвост; это минимальная файловая проба, не реализация полного playout-sync для всех сетевых условий.

Вход и выход. Remote AudioFrame → resampled PCM → sounddevice или атомарный WAV. Поток содержит также тишину; длительность WAV не равна длительности только произнесённых слов.

Code-map. voice-console.py: receive_audio, save_wav, file_probe.

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

AudioResampler(format="s16", layout="mono", rate=48000)
speaker.write(pcm) | save_wav(output, pcm)

Проверка: test_output_private_complete_no_overwrite_and_invalid_pcm; полученный WAV полностью декодирован ffmpeg, параметры сверены ffprobe.

← Вернуться к схеме

Переносимый комплект: подготовка и запуск. Ссылки и код рядом с этим файлом; команды выполняются из корня после подключения scripts/audio-textbook/env.sh. Вход и модели не входят в архив.

Результат

Голос работает через proxy.
Но не весь поток.

После прямого разговора можно отдельно исследовать маршрут. В датированной proxy-пробе HTTPS и WSS прошли через CONNECT, а WebRTC UDP шёл отдельно. Далее разберём, какой журнал подтверждает каждый из этих выводов и чего из него узнать нельзя.

201 · HTTPSСоздание call через CONNECT к chatgpt.com.
101 · WSSSideband через CONNECT к api.openai.com.
Opus · UDPМедиа в обход HTTP proxy; получен декодируемый WAV.

Это отдельный вариант прямого WebRTC-демо. Он не запускает Codex, app-server или Desktop, не меняет профиль входа и не выполняет агентную работу. В него перенесены только обязанности, нужные для разговора: свой вход, создание call, согласование WebRTC, captions и закрытие ресурсов.

Успешная живая проба дала 735 отправленных и 650 принятых RTP-пакетов. Аудиоответ - 12,48 секунды, PCM16 mono 48 кГц; файл полностью декодирован ffmpeg. Журнал зафиксировал оба CONNECT и выбранный UDP peer.

Это первый результат 30.09.2026, а не постоянные адреса или лимиты сервиса. Финальный повтор и точные границы подтверждений находятся в отчёте.

Прикладной HTTP proxy и системный сетевой маршрут - разные вещи. Прямой UDP из нашего процесса всё ещё может проходить через NAT или VPN машины. Их мы здесь не исследовали. Также клиентский журнал не показывает, какой OAuth-аккаунт proxy выбрал на своей стороне.

Контракт и наблюдение

Два endpoint и отдельный медиапоток

Endpoint обозначает операцию сервиса, выбранный ICE peer обозначает медиасоединение конкретного звонка. Поэтому HTTP proxy может обслуживать создание call и WSS, не перенося звук. Порт и IP в прошлой пробе - наблюдение, а не постоянный адрес загрузки WAV.

В этом разговоре нет запроса "загрузить WAV на /transcribe". Файл нужен только нашему источнику PCM. Сервис получает описание соединения по HTTPS, управляющие события по WSS и Opus-пакеты по WebRTC.

ВызовЧто отправляем и получаемФактический путь
POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avasJSON: sdp и session. В session: instructions, model=gpt-live-1-codex, audio.output.voice=cove, delegation.type=client. Ответ: plain SDP answer и call ID в Location. Аудиочанков в POST нет.Наш relay → gis-master.ru:18081 → целевой сервис. CONNECT 200, endpoint 201.
GET wss://api.openai.com/v1/live/<call_id>HTTP Upgrade → WSS. Принимаем input/output_transcript.added и turn.done; отправляем session.close. Повторного session.update нет. PCM/FLAC через этот WSS не отправляем.Тот же upstream proxy, другой CONNECT к api.openai.com:443. Upgrade 101.
WebRTC peer · не HTTP endpointDTLS-SRTP: Opus audio; ICE согласует адрес. Исходный PCM16 LE mono48k кадрами 960 сэмплов/20мс. WAV-заголовок не передаётся; Base64 отсутствует.UDP из aiortc напрямую к выбранному peer. В первой пробе 135.220.40.201:3478; этот адрес меняется и не прописан в коде.
DNS и сеть после proxyРазрешение адресов, NAT/VPN и дальнейший upstream proxy на сервере.Не снимались. Клиентские события не доказывают весь путь до модели после нашего gateway.

Как записать адрес аудиопотока

В последней проверенной пробе 30.09.2026 выбранный медиапоток шёл к 135.220.40.201:3478 по UDP. Для наглядности этот адрес можно записать как udp://135.220.40.201:3478. Это условная запись адреса и порта, а не HTTP/HTTPS endpoint и не команда запуска.

Адрес выбирается при согласовании WebRTC через SDP/ICE и может меняться между звонками; в коде демо он не закреплён. Отправить туда WAV или FLAC через curl нельзя: сначала нужен установленный WebRTC-сеанс, который согласует параметры и передаёт Opus в зашифрованных DTLS-SRTP медиапакетах. HTTPS-вызов создания call и WSS sideband имеют свои URL из таблицы выше; UDP-медиа идёт отдельно, в обход нашего HTTP CONNECT proxy.

Подтверждение: событие selected_ice в журнале финальной пробы - remote_host=135.220.40.201, remote_port=3478, transport=udp. Сопоставление со счётчиками RTP и полученным WAV описано в отчёте проверки. Роль медиасоединения и функции наблюдения разобраны в карте кода.

Совпадение формата не означает совпадение клиента

V3 JSON, alpha-заголовок и поля событий взяты из CLI 0.156.1. Но aiortc генерирует собственный SDP, ICE credentials и DTLS-параметры. Его Opus encoder преобразует наш mono PCM в stereo, тогда как helper CLI кодирует mono; у обоих SDP объявляет opus/48000/2. RTP clock 48 кГц не доказывает stereo входного файла. Идентификаторы и бинарные пакеты этих клиентов различаются.

Наши originator и User-Agent честно называют изолированный клиент. Cookies и integrity/attestation не копируются. Демонстрация не обещает неразличимость клиентов и не обходит запреты доступа.

Почему нельзя просто взять 0000

В сохранённом RubyMITM 1.1.113 marker bridge относится к responses и alpha/search. Voice требует обычного собственного ChatGPT Bearer. Proxy может заменить его активным gateway Bearer; этот механизм находится на сервере, не в демо. Подстановка аккаунта в конкретном production-сеансе требует серверного свидетельства - client 201/101 сам по себе не показывает email.

Если активный config переключится между POST и WSS, call и sideband могут получить разные учётные данные. В таком случае сначала сверяют серверные события. Не правят JWT/account_id вручную и не копируют токен другого аккаунта в локальный скрипт.

Исходная архитектура

Что делает app-server в штатном CLI

Штатная схема объясняет владение соединениями: app-server создаёт call и держит sideband, helper обслуживает media. Наличие HTTPS_PROXY не доказывает, что UDP тоже пошёл в CONNECT. Native CLI через proxy в этой работе не принят живой пробой; схема основана на исходниках.

В штатной системе TUI управляет разговором, app-server связывает его с ядром Codex, а helper владеет звуковым устройством и WebRTC. Авторизация, signaling, captions, thread/turn и возможная агентная делегация проходят через app-server. Пакеты WebRTC helper отправляет самостоятельно.

CLI с HTTP proxy: управление и звук отдельноTUI взаимодействует с app-server и helper. Helper отправляет WebRTC напрямую. App-server может отправлять HTTPS и WSS через proxy. Это схема исходного кода, не сетевой захват CLI в этом запуске. TUI · /voiceкоманды и captions codex-voice-hostмикрофон · Opus · WebRTC WebRTC media peerудалённые STT и разговор IPC Opus · UDPвне HTTP proxy app-server + ядроauth · signaling · sidebandагентные thread/turn Typed RPC SDP offer / answer Наш codex-proxyCONNECT · MITM · OAuthHTTP и WebSocket relay ChatGPT + OpenAIHTTPS /callsWSS /v1/live/call_id proxy env TLS / WSS Стрелки управления упрощены: SDP answer и captions возвращаются по тому же пути. Endpoint и формат · нажмите для полных примеров ↓Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/doneФормат: Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демоПример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}Локальный IPC codex-voice-host · SDP / mute / stopФормат: Команды управления + SDP; аудио между устройствами и helper локальноеПример: SDP offer/answer; mute → отключить захват; stop → закрыть сеансPOST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avasФормат: application/json: sdp + session → plain SDP answer + Location/call IDПример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}WSS wss://api.openai.com/v1/live/<call_id>Формат: WebSocket UTF-8 JSON: тексты реплик, завершение, координацияПример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}WebRTC · выбранный SDP/ICE peer · не HTTP endpointФормат: Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файлПример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГцHTTP proxy gis-master.ru:18081 · CONNECT chatgpt.com:443 / api.openai.com:443Формат: CONNECT handshake → TLS HTTP/WSS; RTP UDP в этот туннель не входитПример: CONNECT chatgpt.com:443 HTTP/1.1 → 200; целевой signaling → 201
Штатные роли по CLI 0.156.1. Запуск CLI /voice через proxy в этой задаче не выполнялся; живая проверка относится к Python-демо ниже. Нажмите блок для перехода к роли и исходнику.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

CLI ↔ локальный app-server

Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done

Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо

Пример: start {threadId:"demo",version:"v3",transport:{type:"webrtc",sdp:"v=0…"}}

Полный запрос, ответ, код и ограничения

CLI ↔ voice helper

Локальный IPC codex-voice-host · SDP / mute / stop

Команды управления + SDP; аудио между устройствами и helper локальное

Пример: SDP offer/answer; mute → отключить захват; stop → закрыть сеанс

Полный запрос, ответ, код и ограничения

Создать WebRTC V3 call

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas

application/json: sdp + session → plain SDP answer + Location/call ID

Пример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}

Полный запрос, ответ, код и ограничения

События WebRTC V3

WSS wss://api.openai.com/v1/live/<call_id>

WebSocket UTF-8 JSON: тексты реплик, завершение, координация

Пример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}

Полный запрос, ответ, код и ограничения

Звук разговора

WebRTC · выбранный SDP/ICE peer · не HTTP endpoint

Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файл

Пример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц

Полный запрос, ответ, код и ограничения

HTTP CONNECT через наш proxy

HTTP proxy gis-master.ru:18081 · CONNECT chatgpt.com:443 / api.openai.com:443

CONNECT handshake → TLS HTTP/WSS; RTP UDP в этот туннель не входит

Пример: CONNECT chatgpt.com:443 HTTP/1.1 → 200; целевой signaling → 201

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

Настройка HTTPS_PROXY для app-server не означает, что helper начнёт отправлять UDP в HTTP proxy. App-server нужен не для пересылки каждого аудиопакета, а для связи разговора с управлением агентом. В простом демо этой агентной части нет, поэтому создавать локальный app-server незачем.

Изолированный клиент

Что перенесено в наш процесс

Наш proxy-клиент держит эти обязанности в одном Python-процессе и использует loopback relay для управляющих TLS-соединений. Relay считает байты, но не расшифровывает TLS и не переносит UDP. Gateway MITM, если он настроен с доверенной CA, является отдельной границей доверия.

Aiohttp направляет управляющие соединения в relay. Одновременно aiortc согласует media-соединение с сервисом. Свой сохранённый вход читается только перед живым запросом.

Демо: HTTPS и WSS через proxy, Opus напрямуюPCM поступает в aiortc и уходит по UDP напрямую медиасервису. Aiohttp через loopback relay и codex-proxy создаёт HTTPS call и подключает WSS sideband. Журнал фиксирует TCP, CONNECT, endpoint статусы, выбранный ICE peer и RTP counters. WAV или микрофонPCM16 LE mono · 48 кГц aiortc · WebRTCOpus · ICE · DTLS-SRTP WebRTC media peerзвук туда и обратно PCM Opus по UDPв обход proxy Свой auth.jsonтолько чтение · без refresh V3 + aiohttpsession · SDP · captions Bearer SDP answer Loopback relayCONNECT + Basic до proxy явный proxy= Наш codex-proxygis-master.ru:18081 TCP → CONNECT ChatGPT + OpenAI/calls 201 · /live 101 MITM → upstream routes.json · 0600адреса · статусы · счётчики Журнал связывает trace HTTPS/WSS, реальные CONNECT, UDP audit, selected ICE и RTP stats.Тела запросов, SDP, токены и текст личных реплик не сохраняются. Endpoint и формат · нажмите для полных примеров ↓POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avasФормат: application/json: sdp + session → plain SDP answer + Location/call IDПример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}WSS wss://api.openai.com/v1/live/<call_id>Формат: WebSocket UTF-8 JSON: тексты реплик, завершение, координацияПример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}WebRTC · выбранный SDP/ICE peer · не HTTP endpointФормат: Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файлПример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГцHTTP proxy gis-master.ru:18081 · CONNECT chatgpt.com:443 / api.openai.com:443Формат: CONNECT handshake → TLS HTTP/WSS; RTP UDP в этот туннель не входитПример: CONNECT chatgpt.com:443 HTTP/1.1 → 200; целевой signaling → 201
Зелёный путь - медиа вне HTTP proxy. Синий - управление через proxy. Живая проба подтвердила оба CONNECT, 201/101, UDP peer и RTP. На блоках есть ссылки к карте кода.

Endpoint и данные этой схемы

Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.

Создать WebRTC V3 call

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas

application/json: sdp + session → plain SDP answer + Location/call ID

Пример: {sdp:"v=0…",session:{model:"gpt-live-1-codex",audio:{output:{voice:"cove"}}}}

Полный запрос, ответ, код и ограничения

События WebRTC V3

WSS wss://api.openai.com/v1/live/<call_id>

WebSocket UTF-8 JSON: тексты реплик, завершение, координация

Пример: {type:"turn.done",turn:{role:"assistant",transcript:"Привет"}}

Полный запрос, ответ, код и ограничения

Звук разговора

WebRTC · выбранный SDP/ICE peer · не HTTP endpoint

Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файл

Пример: Пакет: RTP header (sequence/timestamp/SSRC) + Opus payload; RTP-clock 48 кГц

Полный запрос, ответ, код и ограничения

HTTP CONNECT через наш proxy

HTTP proxy gis-master.ru:18081 · CONNECT chatgpt.com:443 / api.openai.com:443

CONNECT handshake → TLS HTTP/WSS; RTP UDP в этот туннель не входит

Пример: CONNECT chatgpt.com:443 HTTP/1.1 → 200; целевой signaling → 201

Полный запрос, ответ, код и ограничения

↑ Вернуться к этой схеме

WAV сначала проверяется: PCM16, mono, 48 кГц, 0,2-60 секунд, полный набор сэмплов. При ошибке сеть ещё не открыта. После готовности WebRTC InputTrack отправляет PCM кадрами по 20 мс; aiortc кодирует их в Opus. Ответ декодируется обратно в PCM и сохраняется как новый WAV.

WSS не является запасным транспортом аудио. Он получает captions существующего V3 call. При delegation.created клиент останавливает сеанс: у него нет ядра агента, прав выполнения команд и контекста проекта.

Воспроизведение

Запустить и прочитать маршрут

Проверяйте запуск по фазам: CONNECT 200, создание call 201, WSS 101, выбранная ICE-пара, оба RTP-направления и декодированный ответ. Первые три статуса не доказывают звук. Текущий wrapper не читает bootstrap при dry-run, list-devices и help.

Все файлы демо находятся в отдельном каталоге webrtc-voice-proxy. Его можно перенести целиком. Подробная установка зависимостей и варианты bootstrap находятся в README.

Проверка контракта без сети

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
"$voice_venv/bin/python"    "$repo/scripts/webrtc-voice-proxy/proxy-voice.py" --dry-run --auth-file "$auth_file"

Этот вызов не открывает auth.json, CA, proxy env или микрофон. Python и wrapper при dry-run не читают bootstrap. Он нужен только для живого proxy-запуска.

Реальная проба с файлом

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
proxy_demo="$repo/scripts/webrtc-voice-proxy"
proxy_run="$(mktemp -d "$audio_out/proxy-voice.XXXXXX")"
ffmpeg -nostdin -v error -n -i "$INPUT_AUDIO"    -ar 48000 -ac 1 -c:a pcm_s16le "$proxy_run/input.wav"
bash "$proxy_demo/run-proxy.sh"    --file "$proxy_run/input.wav" --output "$proxy_run/answer.wav"    --report "$proxy_run/routes.json" --voice cove    --auth-file "$auth_file" --timeout 30 --max-seconds 45
ffmpeg -nostdin -v error -i "$proxy_run/answer.wav" -f null -
ffprobe -v error -show_entries stream=codec_name,sample_rate,channels,bits_per_sample    -show_entries format=duration,size -of json "$proxy_run/answer.wav"

ffmpeg только меняет аудиоформат. Распознавание и разговор происходят удалённо. Успех требует RTP в обе стороны, ненулевого декодированного сигнала и финального текста обеих сторон. Одно стартовое приветствие сервиса не считается ответом на файл.

Разговор без Codex

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
proxy_run="$(mktemp -d "$audio_out/proxy-voice-console.XXXXXX")"
bash "$repo/scripts/webrtc-voice-proxy/run-proxy.sh"    --report "$proxy_run/routes.json" --voice cove --auth-file "$auth_file"

В консоли: /voice - начать/остановить; /voice mute - убрать/вернуть микрофон; /voice settings - устройства и голос; /quit - выход. Захват начинается только после создания разговора. Для реального микрофона нужны наушники, PortAudio и корректно выбранное устройство; аппаратная проба в этой итерации не выполнялась.

Журнал - не просто список настроек

client_response даёт статус endpoint. proxy_tcp_connected показывает настоящий TCP peer relay; proxy_connect_response - целевой host:443 и ответ proxy. selected_ice показывает выбранную медиапару, udp_sendto - адреса попыток UDP sendto, rtp_stats - отправленные и принятые пакеты. Эти независимые наблюдения вместе дают основание разделить маршруты.

НаблюдениеЧто можно заключитьЧего оно не доказывает
CONNECT 200 до proxyProxy принял туннель к указанному хосту.Успех voice endpoint или правильный upstream аккаунт.
Endpoint 201 и WSS 101 через relayСоздан V3 call и открыт sideband через выбранный proxy.Email активного config и отсутствие его переключения позже.
Selected ICE UDP + sendto + RTP + WAVМедиа действительно работало отдельно от HTTP CONNECT.Детальную трассу внешних NAT/VPN и роутеров.

По умолчанию оба управляющих соединения идут через proxy; NO_PROXY игнорируется. Опция --sideband-route direct существует только для явного сравнения. Автоматического fallback нет. Её маршрутизация проверена на локальном стенде, не на реальном call другого gateway-аккаунта.

Существующий proxy URL использует HTTP до порта 18081. Basic на этом участке сам по себе не шифруется. Это не повод отключать TLS целевого сервиса: доверенный CA и защищённая сеть нужны одновременно.

Каждый блок схемы

Роль, вход, выход и исходник

Здесь каждое наблюдение привязано к месту в коде. Попытка sendto ещё не означает доставку UDP. Выбранная ICE-пара, положительные RTP-счётчики, captions и ненулевой декодированный звук вместе дают более сильное подтверждение. Полный внешний маршрут машины этими данными не устанавливается.

TUI · /voice

Принимает команды пользователя, показывает captions и управляет голосовым lifecycle. Вход - клавиатура и app-server notifications; выход - RPC-команды и управление helper. Это не часть нашего демо.

Источник CLI: app_server_session/realtime.rs, вызовы thread/realtime/start и stop; chatwidget/realtime.rs. Коротко: TUI → ThreadRealtimeStartParams → app-server. Проверка здесь - source map, не живой CLI-сеанс.

← К схеме

codex-voice-host

Владеет устройствами, Opus и RTP. Вход - PCM микрофона и SDP answer; выход - SDP offer, медиапакеты и воспроизведение ответа.

Источники: audio_track.rs, transport.rs. Обработка 960 mono-сэмплов кодировщиком Opus. В демо эту роль выполняют InputTrack и aiortc; тест test_entire_v3_file_probe_over_proxy_and_direct_udp проверяет замену, не сам helper.

← К схеме

App-server + ядро Codex

Даёт клиенту RPC-интерфейс к auth, thread/turn, разрешениям и событиям ядра. Для V3 принимает offer, создаёт call и получает sideband. Модель и удалённая транскрипция находятся не внутри app-server.

Источник: core/realtime_conversation.rs: build_realtime_session_config и realtime_request_headers. Фрагмент контракта: model = gpt-live-1-codex; OpenAI-Alpha = quicksilver=v2. Проверка: поля call_body и локальный V3 end-to-end. Агентные функции app-server намеренно не воспроизведены.

← К схеме

WAV или микрофон → PCM

Файл принимает read_wav, устройство - InputTrack. Вход: WAV PCM16 mono48k либо PortAudio frames. Выход: 1920 байт raw PCM каждые 20мс. WAV/RIFF заголовок в track не попадает.

Наш voice_core.py: read_wav, InputTrack.next_pcm/recv/mute. Фрагмент: frame.sample_rate = 48000; frame.planes[0].update(next_pcm()). Проверки: неверный WAV до сети, direct/core mute-тесты и настоящий Opus в локальной пробе. Физические устройства live не проверялись.

← К схеме

aiortc · WebRTC

Согласует ICE/DTLS и кодек, кодирует PCM в Opus, шифрует SRTP и декодирует ответ. Вход: AudioStreamTrack и SDP answer. Выход: UDP медиа и decoded audio frames.

voice_core.py: VoiceSession._start/receive_audio; aiortc 1.15.0 codecs/opus.py. Фрагмент: setCodecPreferences(audio/opus); setRemoteDescription(answer). Проверка: test_entire_v3_file_probe_over_proxy_and_direct_udp, getStats, полный ffmpeg decode живого ответа. HTTP proxy к RTCPeerConnection не подключается.

← К схеме

Свой auth.json

Только чтение собственных tokens.access_token и account_id. Вход - выбранный файл; выход - Authorization и служебные V3 заголовки в памяти клиента. Refresh, cookies и запись профиля отсутствуют.

voice_core.py: read_auth. Фрагмент: headers["Authorization"] = "Bearer " + token. Marker 0000 и перенос строк отвергаются. Проверка: core auth tests и сравнение auth.json до/после живой пробы. Это не извлечение токена активного gateway config.

← К схеме

V3 + aiohttp

Создаёт session, отправляет SDP JSON и открывает WSS существующего call. Вход - voice, model, auth headers, offer. Выход - answer, captions и session.close.

voice_core.py: call_body, VoiceSession._start/receive_events; routes.py: RoutedClient. Фрагмент: session.post(url, proxy=relay.url, allow_redirects=False). Trace сохраняет метод/путь/статус, но не body/headers. Проверки: HTTPS/WSS при NO_PROXY=*, redirect refusal, два разных origin, 201/101 live.

← К схеме

Loopback CONNECT relay

Принимает локальный CONNECT только к двум разрешённым target, добавляет Basic до нашего proxy и пересылает непрозрачный TLS-поток. Вход - CONNECT и затем TLS bytes; выход - upstream TCP и CONNECT status.

routes.py: ConnectRelay.start/handle/close, Proxy.from_env. Фрагмент: open_connection(proxy.host, proxy.port); CONNECT target; Proxy-Authorization: Basic …. Заголовки исходного клиента в CONNECT не копируются. Проверки: 407, forbidden target, partial headers, Basic isolation, handlers=0. Не путать счётчики TLS с количеством raw PCM.

← К схеме

Существующий codex-proxy

Proxy Basic авторизует клиента отдельно от ChatGPT OAuth. Gateway MITM расшифровывает целевой HTTPS, пересылает HTTP/WSS и может заменить учётные заголовки активным config. Вход - CONNECT и TLS с JSON/WSS; выход - upstream-запросы и ответы.

Источник: proxy/proxy.rb сохранённого образа 1.1.113, commit 487b1b393672f96c55bab8ee020703b104326c4c. Функции handle_connect, forward_websocket_request, sanitize_upstream_headers, substituted_header_value. Фрагмент: bearer_authorization?(value) ? (active_access_token || value) : value. Это код старого образа, не доказательство нынешнего production. Живая проверка: peer TCP gis-master.ru:18081, CONNECT 200, endpoint 201/101. Текущий account/runtime source отдельно не инспектировались.

← К схеме

ChatGPT signaling и OpenAI sideband

HTTPS создаёт call по SDP/session; WSS привязывается к call_id и сообщает тексты и завершение реплик. Вход - авторизованный контракт; выход - SDP answer, 101 и события.

Клиентский контракт: realtime_call.rs и protocol_frameless_bidi.rs. Фрагмент: turn.done → turn.role + turn.transcript. Исходников закрытых серверов у нас нет; описаны контракт и наблюдение 201/101 с финальными репликами.

← К схеме

WebRTC media peer

Удалённая сторона ICE/DTLS-SRTP: принимает Opus, распознаёт речь и возвращает голос. Вход - зашифрованные медиапакеты; выход - Opus audio. Выбранный IP:port приходит из SDP/ICE, не из конфигурации HTTP proxy.

Исходники сервера закрыты. Наблюдаемый контракт - negotiated Opus/RTP. Наши Network.media_snapshot и Journal.install_udp_audit фиксируют selected ICE и socket.sendto, а aiortc даёт RTP stats. Тест: локальный Opus peer и живая проба с декодируемым WAV. Точное название серверной STT-модели этим не установлено.

← К схеме

routes.json

Связывает реальные наблюдения вместо предположения "HTTPS_PROXY задан - значит всё через него". Вход: aiohttp trace, CONNECT peer/status, UDP audit, выбранная ICE-пара, RTP stats. Выход - новый JSON 0600 даже при отказе.

routes.py: Journal.add/save и Network.media_snapshot; proxy-voice.py: ProxySession.stats/main. Фрагмент: os.link(temporary, report) публикует готовый файл без перезаписи. Проверки: no secrets/no overwrite, файл после ошибочного WAV, cleanup, корреляция выбранного peer со счётчиками. Audit sendto означает попытку syscall; успешное медиа подтверждают RTP и ответ, не одна эта запись.

← К схеме

Приёмка

Что доказано, а что осталось за границей

Успешный файл подтверждает конкретный клиент и конкретную пробу. Он не подтверждает физический микрофон, текущий аккаунт gateway, native CLI через proxy или все остальные STT/TTS endpoints. Для этих выводов нужны собственные наблюдения.

Файловый разговор через proxy воспроизведён именно как WebRTC V3, без замены на GA WebSocket. Локальный тест воспроизводит весь цикл на своих TLS/RTC peers. Негативные тесты проверяют отсутствие скрытого direct fallback, проверку CA, отказ 407, закрытие и безопасный журнал.

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
"$voice_venv/bin/python" -W error::ResourceWarning    -m unittest discover    -s "$repo/scripts/webrtc-voice-proxy"    -p 'test_*.py' -v
bash -n "$repo/scripts/webrtc-voice-proxy/run-proxy.sh" --auth-file "$auth_file"

Отдельно не проверялись: физический разговор с микрофоном/динамиками, нативный CLI /voice через proxy, текущий upstream email gateway, его полная серверная трасса и аппаратный AEC. Другие STT/TTS endpoint из основного учебника нельзя считать прошедшими proxy только потому, что прошёл этот V3-сеанс.

У HTML проверяются структура, локальные ссылки, каждая SVG-ссылка и логика hash/focus/history на DOM-модели. Схемы можно отдельно отрендерить через librsvg/ffmpeg. Это не браузерная проверка desktop/mobile/print: просмотр локального HTML заблокирован URL-политикой, обход не применялся.

Полные результаты и границы: отчёт проверки. Знания о старой настройке: Codex Proxy Setup. Документация транспорта: aiohttp proxy support, CPython audit events.

Совместимость

Где наши запросы совпадают со штатными, а где нет

Совместимость означает, что стороны понимают переданные данные. Она не означает одинаковый клиент. HTTP boundary, SDP, ICE, TLS-стек и идентификаторы закономерно отличаются. Мы исправляем упаковку и описание формата, но не копируем cookies, integrity или attestation и не обещаем неразличимость обращения.

У демо нет Electron networking, штатного refresh и состояния основного клиента. Поэтому сравнение ниже отдельно показывает совпадающие поля и различия среды исполнения.

ЧастьDesktop / CLIИзолированные демо и граница
Bearer / accountAuth manager / app-server получают актуальный входСвои tokens.access_token и account_id читаются непосредственно из auth.json. Только нужный собственный профиль; значения не в исходниках/argv
RefreshDesktop может refresh после eligible401; CLI использует auth managerНе выполняется. При просроченном входе сначала обычный login, затем повтор демо. Основной профиль read-only
Cookies / integrityElectron может иметь cookie jar, attach state → фактические служебные headers; CLI optional genuine attestationНет копирования/подделки. Наличие механизма в коде не означает, что конкретная диктовка несла эти поля
HTTP headersНаблюдавшийся Desktop UA/originator/Origin, context-specific metadataHTTP STT/TTS демо ранее используют static наблюдавшийся набор. Это не Chromium stack. Новый V3 честно сообщает own UA/originator; alpha quicksilver=v2 - protocol version, не доказательство идентичности CLI
IDs / metadataНастоящие dictation/session/thread/turn IDs, duration/language могут задаваться UIФайловый STT - только file; V3 x-session-id - новый UUID демо, не существующий чат. Не посылаем чужие IDs или agent startup context
TLS / HTTPElectron/Chromium HTTP2 в captured STT; Rust transport в CLIurllib HTTP1.1 или aiohttp; websocket-client для GA. TLS fingerprint, порядок headers, proxy env и соединения отличаются
Multipart / SDP / ICEUI выбирает boundary/offer/candidates и connection IDsНовые boundary/ICE/DTLS fingerprints/ports закономерно отличаются. Это нормально; их фиксация ради маскировки не нужна
OpusSDP opus/48000/2. Mono/20ms подтверждены для synthetic silence; параметры live encoder этим снимком не установленыaiortc1.15 encoder stereo с mono source. Не байтовая копия helper, но живой V3 принял media
Route/modelCodex V3 core default gpt-live-1-codex; ChatGPT HTTP hidden model неизвестнаНе выдаём gpt-realtime GA WSS за V3. Model в RPC может быть optional, но core добавляет default в wire session
Отказ403 challenge, quota, unsupported mode и validation - разные случаиНе меняем transport скрытно. Пишем конкретную фазу/code/status, не сохраняем HTTP error как аудио

Старый 403 challenge у файла и дальнейший 200 с другим набором HTTP headers показывают различие клиентского контекста. Они не доказывают, что причиной была одна конкретная строка User-Agent или что теперь любой запрос будет принят. В новой V3-пробе 400 был явной ошибкой версии: invalid_quicksilver_alpha_header. Исправление протокольного заголовка не является обходом защиты.

Часть параметров закрытого сервиса неизвестна: внутренние модели ChatGPT HTTP, полные ограничения, учёт расхода и логика допуска. Их не заменяем догадками. Публичная документация Realtime описывает GA API, но не доказывает приватный V3 контракт Codex. Здесь V3 привязан к тегу исходников и конкретной живой пробе.

01Что стоит перенять и что оставляем своим

Ниже сохранён source-разбор исходной редакции. Новые live-наблюдения добавлены в главу диагностики; полный voice E2E, качество ASR и runtime digest всё ещё не подтверждены. Опоры на код открываются из этого архива, без соседнего checkout.

Из Codex полезно взять разделение звука, управления разговором и работы агента. Для изолированной сети эту схему нужно собрать на собственных ASR, LLM и TTS. Перенос ChatGPT endpoint или токена автономности не даст.

У нас уже есть большая часть нужных границ: браузерный клиент, WebRTC в src_core, отдельное распознавание, src_agent, сценарии и Postgres. Я бы не начинал с замены всего стека. Сначала ограничил бы очереди и работу в полёте, замкнул зависимости внутри каждой площадки и подключил обратный аудиотрек. После этого - единые события сессии и перебивание ответа.

Этот учебник идёт от фактического пути речи к варианту автономного развёртывания, затем к изменениям и их проверке. Таблицы сравнивают конкретные обязанности. Числа в критериях приёмки - предложенные цели для испытаний, а не уже полученные показатели.

Исследование кода: 2 октября 2026 года. Сервисы, Desktop, proxy/direct и основной вход не запускались и не менялись. Исходники приложения не редактировались, commit/push/deploy не выполнялись.

Какие версии сравниваются

СистемаИсследованный срезЧто это доказывает
webrtc-komaroff, GitLab main1fc1702859381062eb7d3ffec2d3b0e310b1ebef, получен через fetch без checkoutОсновной источник сравнения. Не доказывает запущенный image или live-конфигурацию.
Локальный webrtc-komarofff0a6eac965c7d6be8a353265775b676248855905Старее main. Существующие untracked DOCS, presentations и videos сохранены.
ASR bridge py-faster-whisper21c85b0c7bc7a96497ce78ec2ead44b113f18db6, соседний локальный checkoutПодтверждает его реализацию. Совпадение с image в работающем стеке не проверено.
Codex CLIОфициальные Rust-исходники rust-v0.156.1, сохранённый срез из предыдущего исследованияПодтверждает устройство клиента, RPC и signaling. Серверные модели ChatGPT закрыты.
Community fork60cd00805b777bced0e6069d8bc52e529cc05cbb + незакоммиченные аудиодемо; 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, TURNICE выбирает путь; 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 / RTFp95 - граница для 95% замеров; RTF - время вычисления, делённое на длительность аудиоRTF < 1 нужен для устойчивого непрерывного распознавания на выбранной нагрузке; это не гарантия малой задержки первого текста.

Поэтому вопрос "у нас тоже WebRTC, почему ответ медленнее?" решается измерением всего пути. После быстрого транспорта могут идти ожидание конца фразы, файл, несколько вызовов модели и ожидание полного TTS. Сам WebRTC эти задержки не убирает.

К началу главы

03Как сейчас проходит речь в webrtc-komaroff

Браузер согласовывает соединение через Core, а затем отправляет микрофон по WebRTC. Core декодирует и нормализует аудио, передаёт кадры в NATS. Соседний ASR bridge находит законченные фразы, записывает временный WAV и отправляет файл в LinTO. Только после получения текста начинается работа агента.

Текущий путь webrtc-komaroff: речь приходит, ответ возвращается текстомOpus / SRTPNATS: meta + PCMmultipart WAVJSON запространзакцияtext/plain → bridgeNATS JSON текст → Corenats.agent.<user>: текстNATS events /ws → текст UIБраузерmic + WebRTCsrc_coreaiortc → PCM 16kASR bridgeVAD → фраза WAVLinTO ASRHTTP /transcribesrc_agentход + workflowPostgresruntime + историяLLM + toolsNATS request/reply
Текущий путь webrtc-komaroff: речь приходит, ответ возвращается текстом. Блоки открывают пояснения; сплошная стрелка - медиаданные, пунктир - сигнализация, события и вызовы сервисов.

Полученный ответ сценария сохраняется вместе с состоянием сессии и публикуется как текст или команды интерфейса. На схеме LinTO и LLM вынесены отдельно: текущая конфигурация использует соседний вычислительный контур, а не обязательно интернет. Для независимой площадки этот контур всё равно надо перенести внутрь её границы.

Обратный аудиотрек предусмотрен типом соединения sendrecv и обработчиком peer.ontrack в браузере. Но в проверенных handle_track и графе нет TTS producer и назначения его трека в sender Core. Наличие HTML audio element не доказывает, что агент говорит. Этот вывод относится к просмотренному voice-agent пути, а не ко всем экспериментам и TTS-демонстрациям репозитория.

К началу главы

04Что передаётся на каждом участке

У одного разговора несколько разных форматов. Между браузером и Core идут сжатые медиапакеты. Между Core и bridge идут сырые сэмплы. Между bridge и ASR снова файл с контейнером. Агент получает только текст. Ни один из этих форматов нельзя без преобразования подставить вместо другого.

Порядок: соединение, аудио, конец фразы, распознавание и ответBrowserCoreASR bridgeLinTOAgentPOST /core/offer: JSON SDPJSON SDP answer + session_idOpus / SRTP: непрерывное медиаNATS meta + PCM16LE 16kVAD конец фразы → multipart WAVASR HTTP: финальный text/plainNATS: text + phrase/session IDsNATS request: text → workflow + модели/toolsNATS /ws: финальный ответ UI после сохраненияПауза VAD и время HTTP ASR - разные задержки. Обратный TTS в этой последовательности пока не подключён.
Время идёт сверху вниз. После SDP медиа живёт своим циклом; шаги ниже описывают одну завершённую фразу.

Речь поступает кадрами, пока пользователь говорит. Распознавание одной фразы стартует после решения VAD, а агент - после final transcript. Pending показывается раньше текста, но не означает, что partial уже распознан. В этой последовательности результат Agent публикуется по NATS/WS: стрелка к браузеру не является новым прямым HTTP вызовом Agent.

Участок / endpointЗапросОтвет / следствие
Browser → POST /core/offerapplication/json: {sdp,type:"offer",session_id?}. SDP с ICE/DTLS параметрами, без аудио.application/json: {sdp,type:"answer",session_id}. Core добавляет session_id после establish_connection.
Browser ↔ выбранный ICE peer CoreOpus в защищённом 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_URLPOST, 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_idAgentInboundResponse; UI события публикуются отдельно. NATS request/reply не означает durable обработку.
Workflow → nats.llm.<user>JSON LlmRequest: mode, input, constraints и trace envelopeJSON LlmResponse. Ollama adapter вызывает POST /api/chat с stream:false, local adapter - llama.cpp.
UI события → browser /wsNATS по WebSocket; subject из конфигурации nats.events.user123ServerEvent: 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, в этом исследовании не проверялось.

К началу главы

Почему SDP answer ещё не означает подключённый звук

HTTP-сигнализация передаёт описание будущего соединения. Сам звук пойдёт по адресу и порту, которые выберет ICE. После этого должны установиться DTLS и защищённый RTP. Поэтому отдельный успешный POST /core/offer не доказывает ни доставку микрофона, ни распознавание.

Проверять сигнализацию, ICE, DTLS и RTP последовательно1. HTTP / SDPPOST /core/offer2. ICESTUN checks → pair3. DTLSключи SRTP4. RTPOpus → decoderВ пробе live: HTTP 200 → ICE без ответов → DTLS new → медиа не подтвержденоСоединение NATS /ws устанавливалось отдельно и не исправляло этот путь.
Сигнализация и события могут быть исправны одновременно с отказом медиатранспорта. Блоки ведут к контракту и признакам проверки.

Клиент отправляет offer, Core отвечает answer. Эти описания содержат кодеки, кандидаты и параметры защиты, но не записанное аудио. ICE проверяет пары адресов. Только найденная рабочая пара позволяет перейти к DTLS и передаче защищённых Opus-пакетов. Если ICE-запросы не получают ответа, Sonora и замена ASR-модели до этого этапа не доберутся.

Сигнализация: JSON с SDP, без аудиофайла

{"type":"offer","sdp":"v=0\r\n...","session_id":"00000000-0000-4000-8000-000000000001"}

POST /core/offer, Content-Type: application/json. Ответ содержит type: "answer", sdp и нормализованный session_id. Многоточие выше обозначает сокращённый учебный SDP: это не запускаемый offer. Исходники: negotiate.ts, handle_offer.py.

STUN, TURN и ICE - разные роли

STUN Binding сообщает клиенту адрес, который виден снаружи. Кандидат srflx содержит этот адрес. TURN выделяет ретранслятор и даёт кандидат relay, если прямой маршрут недоступен. ICE проверяет пары локальных и удалённых кандидатов и выбирает путь. STUN не ретранслирует Opus и не гарантирует, что другой peer доступен через его NAT.

stun:stun.gis-master.ru:3478 - UDP-сервис, не HTTP endpoint. В ICE между peer тоже используются STUN-проверки, но это не запрос к сервису 3478. Их нельзя смешивать в одной метрике. У Core в проверенном коде iceServers = []; в просмотренном answer были только внутренние host-кандидаты.

DTLS: защита медиа после найденного маршрута

DTLS согласует ключи SRTP по выбранному ICE-пути. Это не WebSocket и не TLS-соединение POST /core/offer. Проверяем состояние транспорта; new при нерабочем ICE не доказывает ошибку сертификата. В live-пробе до установленного DTLS дело не дошло.

Opus: подтверждать выбранный кодек, а не только наличие в SDP

# Учебный фрагмент SDP; 111 - пример динамического payload type.
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1
# RTP-clock 48 kHz не означает, что файл для ASR должен быть 48 kHz stereo.
# Выбранный кодек проверяется через codecId активного RTP-потока в getStats().

Opus переносится в RTP/SRTP-пакетах, не в MP3 и не в RIFF/WAV. Core декодирует его и отдельно ресемплирует в 16 kHz mono PCM для ASR bridge. Проверка должна связать outbound-rtp / inbound-rtp → codecId → codec и выбранную ICE-пару. Поддержка FEC/DTX в предложении не означает фактический эффект на потери. Подробный список проверок находится в главе Sonora / Opus.

Первичные контракты: STUN RFC 8489, Opus RTP RFC 7587, WebRTC Stats. Далее разбираем конкретную ошибку 701.

WebSocket не задаёт формат: что идёт внутри каждого WSS

WebSocket - двусторонний транспорт. В одном соединении можно передавать команды разговора, в другом - PCM в Base64, в третьем - протокол NATS. Само слово WSS не отвечает на вопрос, передаётся ли там звук. Это определяется контрактом конкретного клиента.

ПутьАдрес и сообщенияГде аудиоКод и разбор
Codex CLI V3HTTPS /backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas; WSS api.openai.com/v1/live/…, связываемый с call IDМедиа WebRTC отдельно. Sideband - события, текст и делегация.создание call, точная упаковка sideband, демо
Realtime STT демоWSS /v1/realtime?intent=transcription; input_audio_buffer.append, commitBase64 raw PCM16 mono 24 kHz внутри JSON, без WAV-заголовка.контракт STT, файловая проба
Realtime TTS демоWSS /v1/realtime с параметрами клиента; response.create, response.output_audio.deltaВозвращаемый Base64 PCM собирается в WAV локально.контракт TTS, демо
App-server WS-режимЛокальный JSON-RPC thread/realtime/appendAudio или appendSpeech → удалённый WebSocket ядраappendAudio несёт аудиочанк; appendSpeech - текст. Аудиоответ приходит уведомлениями.эксперимент, RPC и режимы
Наш frontendWSS https-origin → /ws, а в dev по умолчанию ws://localhost:9222. Внутри NATS, не произвольный JSON WS.В этом UI-канале события. Микрофон идёт WebRTC в Core; затем бинарный PCM через серверный NATS.NATS-контракт, appConfig

Одинаковый внешний JSON не делает протоколы совместимыми

{"type":"input_audio_buffer.append","audio":"AAAAAA=="}

Это форма события Realtime STT. Четыре декодированных байта в примере - два нулевых PCM16-сэмпла, а не запись фразы. В запросе должны быть реальные сэмплы нужной частоты, порядок событий и commit. Передать такой JSON прямо в NATS /ws нельзя: NATS требует собственное соединение, подписку и publish с subject.

# Иллюстрация NATS framing, не curl-запрос к WebSocket.
SUB nats.events.user123 1\r\n
MSG nats.events.user123 1 <число байт UTF-8 payload>\r\n
{"kind":"message","role":"assistant","text":"Пример"}\r\n
# Настоящий клиент рассчитывает длину и ведёт INFO/CONNECT/PING/PONG.
# Поля JSON события зависят от producer; этот текст не задаёт новую схему.

HTTP CONNECT-прокси способен провести HTTPS/WSS TCP-туннель, если endpoint и авторизация разрешены. WebRTC UDP-медиа от этого не становится проксированным. Поэтому в proxy-главах HTTP/WSS и ICE/RTP проверяются отдельно. У нашего контура есть ещё межсервисный NATS-канал; его статус также не подтверждает медиа.

NATS: где переносится PCM, а где только события и команды

В webrtc-komaroff NATS соединяет несколько стадий. Core публикует декодированные кадры для ASR bridge, bridge возвращает текст, Core передаёт запрос агенту, а frontend получает события через NATS over WebSocket. Это разные subjects и разные payload. Один зелёный индикатор NATS не проверяет весь путь.

Core → ASR bridge: бинарный пакет, не файл

Subject начинается с inference.whisper.stream., ответ - с inference.whisper.text.. Суффикс строится из stream/session routing; точную композицию смотрите в handle_track. Формат пакета: [meta_len:u32 big-endian][meta_json:UTF-8][PCM:int16 little-endian]. Для 20 мс при 16 kHz mono это 320 сэмплов, или 640 байт PCM, плюс заголовок и metadata.

{"type":"frame","seq":1,"sample_rate":16000,"sample_width":2,"channels":1,"session_id":"00000000-0000-4000-8000-000000000001"}
# Локальная сборка одного нулевого кадра. Не подключает NATS и не отправляет звук.
import json
meta = {"type": "frame", "seq": 1, "sample_rate": 16000,
        "sample_width": 2, "channels": 1}
header = json.dumps(meta, ensure_ascii=False).encode("utf-8")
pcm = bytes(320 * 2)  # 20 мс тишины, int16 LE mono.
packet = len(header).to_bytes(4, "big") + header + pcm
assert len(packet) == 4 + len(header) + 640  # Метаданные считаем в байтах, не символах.

Пакет type: "end" завершает поток и не содержит PCM. У такого payload нет RIFF-заголовка. Если поставить WAV вместо raw PCM, парсер может принять контейнерные байты за отсчёты. Если указать неверную частоту, звук получится ускоренным или замедленным, без обязательной ошибки сервиса. Исправлять надо producer, metadata и consumer вместе, а не только MIME.

Первичные исходники сохранены локально: producer, парсер, bridge. Состав и SHA256 среза находятся в source-manifest.json.

Bridge → ASR: уже WAV-фраза

Silero возвращает интервалы речи. Bridge вырезает соответствующие исходные сэмплы и записывает WAV PCM16 mono 16 kHz. Затем HTTP-клиент отправляет файл в настроенный LINTO_HTTP_URL с языком; адрес зависит от среды, это не неизменный публичный endpoint. Точный multipart contract и ответ text/plain разобраны в таблице форматов.

Silero не очищает запись. Browser AEC/NS или Sonora работают раньше, на клиенте. В bridge могут быть короткие буферы и ограничения фразы, которые влияют на контрольный короткий вопрос. Проверять "2+2" надо по четырём артефактам: доставленному PCM, интервалу VAD, фактически отправленному WAV и тексту ASR. По одной ошибочной строке невозможно выбрать, какой этап виноват.

Agent → frontend: события, отдельные от медиа

В проверенной frontend-конфигурации используется nats.events.user123; стек задаёт префиксы nats.events. и nats.agent.. Агентный request/reply и UI publish относятся к другому контракту, чем PCM. Для нескольких окон нужно фильтровать события по активной сессии и проверить разграничение доступа, а не просто добавить UUID в JSON.

Для непрерывного аудио нужен ограниченный бюджет очередей, число worker и контроль возраста кадра. Текущий semaphore ограничивает одновременно выполняемый publish, но не число накопленных task. Локальная проба с mock publish показала 100 созданных задач и только 3 вошедших вызова. Это подтверждение механизма, не измерение production-утечки. Реализация и критерии находятся в главе очередей.

Правило диагностики: NATS connected → подписка → кадры → end/flush → phrase → ASR result → agent result. На каждом переходе нужны свои session_id, seq, длительность и статус; секреты, полный SDP и личную речь в общий лог не сохраняем.

STUN 701: сервис отвечает, но отдельное семейство адресов даёт ошибку

2 октября 2026 года браузер писал "STUN host lookup received error" для stun.gis-master.ru:3478. Проверки не подтвердили исчезновение сервиса: DNS A разрешался, UDP Binding возвращал корректный ответ, и Chrome собирал IPv4 srflx-кандидаты. В этой пробе событие 701 относилось к локальному IPv6-интерфейсу.

Что действительно наблюдали

ПроверкаРезультат 02.10.2026Граница вывода
DNS A178.20.41.87; авторитетный ответ согласованАдрес из снимка, не постоянная настройка для копирования
DNS AAAANOERROR, записи AAAA нетНе NXDOMAIN. Не обещаем работоспособность IPv6 STUN
UDP Binding по имени / IPv420 байт запроса → 40 байт ответа; success 0x0101, transaction и XOR-MAPPED-ADDRESS проверены; 77 / 83.9 мсДва наблюдения, не p95 или SLA. Это не тест TURN и не доставка медиа
Chrome, hostname701 на IPv6; одновременно два успешных IPv4 srflxОшибка одного пути не описывает все сетевые интерфейсы
Chrome, IPv4 literal701 отсутствует; два IPv4 srflxДиагностическое сравнение, не изменение конфигурации сервиса

701 создаёт браузер, когда не смог получить ответ STUN/TURN по проверяемому пути. Код находится вне диапазона STUN error response. Текст про lookup уточняет причину конкретной попытки. Не всякий 701 означает отсутствие AAAA; такой вывод получен только из сочетания DNS и address-family в нашей пробе. Определение W3C и libwebrtc ResolveStunAddress / OnResolveResult объясняют эту границу.

Почему 701 не была основной причиной отказа в этой пробе

Временная browser-only проба использовала проверенный IPv4-адрес STUN и синтетическую тишину. Ошибка 701 исчезла, но ICE к Core всё равно не подключился. У наблюдаемых пар было 20-21 отправленное check и 0 ответов; номинированной пары не было, DTLS оставался new. Значит, убрать это событие недостаточно для восстановления звука.

{"probe":"IPv4 STUN literal, synthetic silence","ice_candidate_errors":[],"ipv4_srflx":2,"ice_checks_sent_range":[20,21],"ice_check_responses":0,"nominated_pair":false,"dtls_state":"new","transport_bytes_sent":0,"transport_bytes_received":0}

Core отдавал внутренние host-кандидаты 10.200.0.140, 10.0.14.22, 172.18.0.48 с динамическими UDP-портами. В исходном DRS опубликован 19303/UDP, но это не заставляет aiortc привязать именно этот порт. Точный NAT/firewall hop, фактически применённый port mapping и выбранный runtime image ещё не установлены. Частный кандидат сам по себе не доказывает невозможность всех NAT-сценариев; отсутствие ответов доказывает отказ проверенных пар.

Куда и когда "делся" сервис

Исчезновение не подтверждено. В Git видно только историю конфигурации: 14.10.2025 hostname был у frontend; 21.10.2025 legacy aiortc-путь вернули на GIS STUN; 10.12.2025 новый Core handler появился с пустым iceServers. Последняя дата - изменение исходников, не доказанная дата deployment или удаления STUN.

В этой проверке не удалось получить инфраструктурное описание STUN: Insight был недоступен в разрешённом браузерном пути, SSH не принял существующий ключ. Поэтому процесс, контейнер, software/vendor и владелец сервиса неизвестны. Называть его coturn без этих данных нельзя. Датированный журнал выводов: очищенный evidence.

Как повторить проверку без микрофона

# Из корня распакованного комплекта. Отправляется только STUN Binding, не аудио.
python3 scripts/network-lab/stun_probe.py stun.gis-master.ru --family 4
# Проверка IPv6 отдельно: DNS-ошибка здесь не отменяет успешный IPv4-путь.
python3 scripts/network-lab/stun_probe.py stun.gis-master.ru --family 6
# Сначала проверяем разрешение DNS в своей сети; старый IPv4 не фиксируем в production.
python3 -m unittest discover -s scripts/network-lab -p 'test_*.py' -v

Исходник STUN-пробы использует только Python standard library. Новый запуск обращается к названному серверу и печатает адрес, который увидел STUN; не публикуйте этот вывод без очистки. Все unit-тесты работают без сети. Для основной неисправности идём в диагностику Core.

Live: что теперь проверено, а что осталось неизвестным

Прежний документ описывал исходники и локальные пробы без полной live-приёмки. Теперь к нему добавлены транспортные наблюдения от 2 октября. Они подтверждают конкретный отказ подключения, но не проверенную точность модели и не завершённую поставку всего стека.

Разделяем HTTP, события, медиа и распознавание

СлойНаблюдениеЧто это означает
FrontendLive загрузил /assets/index-K_8EaX4G.js; панель отдельно показывала аудио и событияПроверено отображение диагностической сборки. Не доказан digest всех сервисов
СигнализацияPOST /core/offer → HTTP 200 и SDP answerCore сформировал описание. Звуковой путь ещё не установлен
NATS /wsСобытия подключены при ошибке аудиоTCP/WSS-путь работает независимо от UDP-медиа
STUNBinding success, IPv4 srflx; 701 убирался в отдельной пробеОбщий отказ STUN не подтверждён
ICE / DTLSChecks без ответов, нет nominated pair, DTLS newНе найден рабочий маршрут до Core в этой пробе
RTP / ASRПоложительная доставка аудио в live-пробе не подтвержденаПо ней нельзя измерять WER или выбирать другую ASR-модель
CI / rolloutПроверки и build прошли в pipeline 4834; deploy 9454 оставался manualПубликация image не равна подтверждённому rollout. Текущий Swarm task/digest не проверен

Source двух опубликованных UI-коммитов: 2e5aa37 и e29be7f; их snapshots лежат отдельно от старого архитектурного среза. Результат очистки и диагностики ресурсов в локальном loopback также не переносится на production. В этом задании live-сервис повторно не вызывался и приложение не перезапускалось: мы фиксируем уже собранные результаты, а не выдаём их за новый эксперимент.

Следующая проверка соединения

На стороне Core сначала сопоставляем UDP-порты из SDP с фактическими сокетами процесса, публикацией Swarm, firewall и маршрутом обратно к браузерному кандидату. Затем проверяем один допустимый прямой маршрут или локальный TURN внутри нужной сети. Не копируем чужой диапазон портов наугад и не снимаем защиту ради теста. Приёмка - nominated pair, DTLS connected, рост RTP-счётчиков в обе стороны и декодированный контрольный звук.

После доставки повторяем короткую контрольную фразу и речь с известным текстом. Для шума используем одинаковый clean reference, фиксированную смесь и сохранённую конфигурацию. Ошибки VAD, обрезанные слова, пустой WAV и неверную частоту учитываем отдельно от ошибки ASR. План таких испытаний связан с доработкой распознавания и метриками.

05Как устроен голос Codex и чем он отличается от диктовки

В CLI 0.156.1 команда /voice начинает разговор: helper работает с устройствами и WebRTC, app-server ведёт управляющую часть, а ядро согласует разговор с агентной работой. Локальный app-server - не удалённая нейросеть и не обязательный посредник для каждого аудиопакета.

CLI /voice: медиа и события идут разными соединениямиtyped request / JSON-RPChelper IPC: SDP, mute, stopHTTPS: SDP + sessionWSS sideband: событияOpus / SRTP: отдельный медиапутьаудиоответ тем же WebRTCTUI / CLI/voice, команды, текстapp-server + ядроauth, signaling, handoffvoice-hostмикрофон и OpusRealtime serviceзакрытая серверная часть
CLI /voice: медиа и события идут разными соединениями. Блоки открывают пояснения; сплошная стрелка - медиаданные, пунктир - сигнализация, события и вызовы сервисов.

App-server создаёт call по HTTPS и принимает текстовые события по WSS sideband. Helper отправляет и получает медиа непосредственно через WebRTC. Эти роли могут быть разделены логически, даже когда app-server встроен в процесс TUI: штатный embedded client использует InProcess/mpsc, а не строки JSON в stdin. STDIO относится к нашим отдельным RPC-демо.

ПутьEndpoint / форматЧто можно взять
CLI /voice, ChatGPT backend V3POST 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 ChatGPTGET /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 / appendSpeechthread/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 / forkwebrtc-komaroff mainРешение
Медиа отдельно от бизнес-логикиvoice-host и Realtime service; app-server управляетaiortc Core отдельно от Agent; после декодирования PCM идёт через NATSСохранить разделение. Замена PCM/NATS - только после нагрузочного доказательства узкого места.
Управление жизнью разговораrealtime start/stop, события, cancellation tokenconnect/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 queueFanOut queue без лимита; publish task на каждый кадрИсправить лимиты и политику перегрузки прежде роста нагрузки.
Речь агента в ответДвусторонний WebRTC audio; локальный Read Aloud отдельноТекст ответа есть, обратный TTS producer в voice path не найденДобавить локальный TTS и обратный track, сохраняя text-only режим.
Идентификаторы и долговечное состояниеthread/call/realtime session/turn/itemsession/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 автоматически.
ASRDRS: llm-models_linto_stt_whisper_http:80/transcribe через NATS proxyСвой ASR с весами и VAD. Direct LAN HTTP или локальный proxy с контрактом CONNECT; не зависеть от main-network receiver.
LLMLLM_MODE remote, nats-proxy-ollama.gis-master.ru; local llama.cpp путь существуетСвой Ollama в этой LAN либо локальный GGUF. Название remote в коде означает HTTP provider, а не обязательно интернет.
NATS topologyDRS: 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 в корпоративную систему.

Предложение: весь голосовой контур принадлежит одной изолированной площадкеPCM micOpus / SRTPNATS PCM16LE 16kSTT events: final + IDsLLM/tool JSON, deadlineanswer text + response IDcommit + eventsPCM → output track CoreWebRTC аудиоответPCM проигрывания + задержка → AECWSS control/eventsАудиоклиентмикрофон + динамикиОбработка звукаbrowser AEC / SonoraVoice Gatewayсуществующий Core + limitsASR / VADсобственные весаSession + AgentAgent / workflowLLM + toolsтолько локальный контурTTS workerсвои PCM/WAV → trackPostgres / eventsистория, result, outbox
Предложение: весь голосовой контур принадлежит одной изолированной площадке. Блоки открывают пояснения; сплошная стрелка - медиаданные, пунктир - сигнализация, события и вызовы сервисов.

Браузер: звук и понятное состояние

Браузер отправляет микрофон и воспроизводит обратный трек. В отдельном событийном канале он видит текущие 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 наружу запрещён. Можно синхронизировать выбранные обезличенные события, когда сеть снова появилась, но модельный контекст и голос не должны уходить туда без отдельной политики.

К началу главы

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 voiceAEC запрашивается по 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.
CLI исследован по открытым Rust-исходникам 0.156.1, Desktop - по локальному бандлу 26.928.20755. Выбор флагов найден в коде, но качество и реальная настройка трека требуют отдельного теста. Эти версии нельзя объявлять одним аудиотрактом.
Наш тракт: очистка в браузере, отсечение тихого звука и отдельная сегментация на серверезвукMediaStreamNATS meta + PCMmultipart WAVOpus / SRTP → декодированиеМикрофонMediaStreamTrackBrowser AEC / NSзапрошены при захватеEnergy gateопционально: RMS → gain 0/1WebRTC / Coredecoder → PCM16LE 16kSilero ONNX VADинтервалы start/endASR providerWAV-фраза → текст
Наш тракт: очистка в браузере, отсечение тихого звука и отдельная сегментация на сервере. Блоки открывают пояснения; сплошная стрелка - медиаданные, пунктир - сигнализация, события и вызовы сервисов.

У нас: 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 0.156.1: Sonora обрабатывает capture с учётом реально воспроизводимого ответаPCMOpus / SRTPаудиоответPCM проигрывания + задержка → AECМикрофонлокальные PCM-сэмплыSonora в voice-hostAEC + NS + AGC2Realtime serviceOpus / SRTP, речь и ответДинамикиреально сыгранный PCM
CLI 0.156.1: Sonora обрабатывает capture с учётом реально воспроизводимого ответа. Блоки открывают пояснения; сплошная стрелка - медиаданные, пунктир - сигнализация, события и вызовы сервисов.

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; здесь он не запускался и не заменяет акустический стенд.

Вернуться к схеме CLI

Desktop: три разных пути захвата, а не один общий процессор SonoraMediaStreamмедиаMediaStreamOpus / SRTPзаписьHTTP / WSSPCM ответа → воспроизведениеактивность пользователя → volumeCodex voice: micmono + NS=trueWeb Audio providerбуферизация / muteCodex RealtimeWebRTC Opus / SRTPChatGPT voice: micAEC по device policyBrowser WebRTCsettings / media trackChatGPT voiceзвук в обе стороныPlayout / duckingпри речи громкость нижеДиктовка: micbrowser / native captureSTT adapterфайл / аудиопотокУдалённый STTаудио → текст
Desktop: три разных пути захвата, а не один общий процессор Sonora. Блоки открывают пояснения; сплошная стрелка - медиаданные, пунктир - сигнализация, события и вызовы сервисов.

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 и пользовательские реплики не включены. Названия минифицированных функций относятся к этому бандлу и изменятся при пересборке.

Вернуться к схеме Desktop

Какие альтернативы сравнивать и что измерять

Для существующего браузерного клиента сначала проверяем встроенную обработку, 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 ProcessingCapture ProcessStream и render ProcessReverseStream. Контракт APM.Альтернатива нативной интеграции; не подтверждение конкретного алгоритма браузера/Desktop.
SpeexDSPPreprocessor и echo canceller для локального тракта; нужны reference и корректные параметры. Руководство.Кандидат стенда. Сравнение качества и CPU на наших записях не выполнялось.
RNNoiseНейросетевое шумоподавление, не самостоятельная замена AEC или VAD. Исходники и пример.Кандидат для шума. Веса/сборку поставить заранее: autogen.sh upstream может скачивать модель.

Одинаковый стенд должен включать обычную и тихую речь, фоновый шум, только ответ из динамиков и одновременную речь собеседника с ответом. Сравниваем WER, потерянные границы VAD, ложные перебивания, остаточное эхо, clipping, задержку и CPU. Остаточное эхо сначала измеряем при молчащем пользователе; double-talk оцениваем отдельно, чтобы не принять подавленную живую речь за хорошее AEC. Ни Sonora, ни другая библиотека ещё не показали у нас выигрыш; цели и порядок внедрения приведены в плане работ.

К началу главы

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 и сетевой приёмки.

К началу главы

Что внедрять сначала и чем подтвердить результат

Начинаем с наблюдаемого отказа сети. Потом фиксируем корректную доставку и очистку речи. Лишь после этого меняем сегментацию, ASR и сценарий агента. Sonora включена в план как нативный клиентский процессор, а не как замена Silero и не как серверное исправление ICE.

ПорядокПричина и способИзмеримая приёмкаПодробности
1. ICE / UDPСогласовать advertised candidates, bind ports, NAT/firewall. Для сетей, где прямой путь невозможен, спроектировать локальный TURNICE selected/nominated, DTLS connected, RTP обе стороны, корректный teardownдоказанный отказ, границы независимой сети
2. Opus и PCMПроверить активный codecId, частоту decoder/resampler, seq и возраст кадровНе теряются начало/конец фразы; loss/jitter и длительность измерены. Неверный формат ловится на своей границеOpus, PCM-пакет
3. ОчисткаBrowser AEC/NS - baseline. Sonora AEC/NS/AGC2 - отдельный native adapter с настоящим playout reference и задержкой; feature flagСравнить остаточное эхо, false barge-in, WER, clipping, p95 задержки и CPU на одинаковых записях. Выигрыш пока не измеренSonora, сравнение трактов
4. STT / SileroСохранить WAV каждой контрольной фразы, проверить короткую речь, min phrase и flush; затем сравнить streaming STTWER/CER и задержка известны на clean/noisy/double-talk наборе; пустые результаты имеют причинуASR, тесты
5. Очереди и отменаBounded workers, maxbytes/maxage, generation/epoch, отмена ASR/LLM/TTS и playbackПамять выходит на плато при stalled consumer; после cancel нет старого звука и чужих событийочереди, перебивание
6. Независимая поставкаЛокальные ASR/LLM/TTS, модели и лицензии в поставке; локальные DNS/STUN/TURN/NATS и картаCold start и полный голосовой сценарий при запрещённой внешней сетиизоляция, полный план

Пример Codex полезен границами ответственности: медиа отдельно, управление отдельно, маленькие очереди, явная отмена и локальная обработка звука. В независимый контур переносим эти принципы и доступные локальные библиотеки. ChatGPT endpoint, OAuth и внешние модели автономную поставку не обеспечивают.

11STT: сначала управляем фразой, потом решаем, нужен ли streaming

У текущего bridge уже есть Silero VAD, phrase_id, pending и финальный текст. Это сохраняем. Первое изменение - локальный ASR provider с тем же HTTP-контрактом, заранее поставленными весами и явной readiness. Затем измеряем, сколько времени занимает ожидание границы фразы отдельно от распознавания.

ИзменениеПочемуПриёмка и измеримый эффект
Зафиксировать VAD/start/end policyEnergy 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 resultMCP 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 continuityCancel очищает непроигранный хвост; underrun/dropped audio измеряются, не скрываются.
Graph shutdownCancel tasks и wait с deadline, повторное close безопасноShutdown завершается даже при блокированном NATS/ASR. End marker не ждёт бесконечно занятого транспорта.

Повторяемая проба очередей. Ожидаемый результат относится к main 1fc1702; после исправления её проверки должны измениться.

# Запустить из корня webrtc-komaroff в окружении с numpy, av и nats-py.
# --source-root указывает на исследуемый checkout; сеть не используется.
python3 tooling/probe_audio_bounds.py --source-root "$WEBRTC_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_idSession владеет историей; call - одно медиасоединение. Reconnect создаёт новый call, но не должен обнулить историю.
turn_id / response_idUser turn и ответ связаны, но не равны. У отменённого ответа остаётся собственный status.
generation / seqGeneration отсеивает старую работу; seq задаёт порядок событий внутри договорённого потока. Нельзя сравнивать seq независимых producers без агрегатора.
event_id / schema_versionEvent_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 / TTFTFinal 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 clientsRTF <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 workqsize, pending tasks, age_ms, bytes, RSSQueue ≤10 frames как стартовый бюджет; task count фиксирован; stalled consumer 10 мин → память выходит на плато, overload виден.
СемантикаWER, task success, JSON validity, duplicate actions, session leak0 чужих сессий и дублированных 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Испытания: от контракта до холодного старта без сети

Ниже сохранён source-разбор исходной редакции. Новые live-наблюдения добавлены в главу диагностики; полный voice E2E, качество ASR и runtime digest всё ещё не подтверждены. Опоры на код открываются из этого архива, без соседнего checkout.

Для сравнения достаточно локальных тестов; сегодня они не доказывают работу всего стенда. Будущую приёмку проводим в отдельном 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 micDeadline и cleanup уже покрыты частью тестов main; добавить повторный connect и частично созданные ресурсы.
Входные кадрыКадры 44.1/48 kHz с неполным remainder; 20 мс normalizer; stall publisherТочная длительность после resample/flush; bounded queues и task count; gap policy проверяется на WER.
STT/agentPartial меняется, final повторяется, pending→error, неверный phrase_idPartial не запускает 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 и jobsCore 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 lifecycleSchema-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 могут быть следующими решениями, но только если измеренный сценарий этого требует.

К началу главы

Проверки

Как отличить правильную упаковку от рабочего результата

Проверка идёт по слоям: сначала локальный формат, затем доступ к сервису, соответствие ответа запросу, завершение результата и полное декодирование аудио. Неверная частота raw PCM может дать принятую, но искажённую речь вместо HTTP-ошибки. Нельзя считать MIME, HTTP 200 или started:true доказательством всего результата.

Первым делом проверяем то, что отправляет клиент. HTTP-перехватчик извлекает file из multipart и сравнивает с исходником; Realtime fake socket проверяет session и собирает обратно PCM из Base64. Затем смотрим на результат: правильный ID, completed/done и полный decode. Только после этого делаем вывод о живом способе.

ПробаЧто проверено / ожидаетсяЧто не доказывает
Валидные WAV/FLACЛокальная валидация; реальные STT/TTS успехи датированы в главахУспех одного аккаунта не означает доступ для всех
MIME не соответствует файлуHTTP interceptor сверяет неизменные file bytes; известный неверный container magic отклоняется до сетиMagic-check не декодирует весь контейнер. Сервисный код отказа для mismatch не проверен
WAV вместо raw PCMВ Realtime тесте outbound PCM не содержит RIFF header; файл отдельно проверяется wave/ffmpegСервер может трактовать header как шум, а не обязан вернуть HTTP error
Неверная rate/channels/widthКлиенты WAV24000/48000 отказывают до чтения credentials/сети при неподходящем headerЕсли header солгал о реальном rate сэмплов, это невозможно определить из raw bytes без внешнего знания
Неверная заявленная частотаПроверка математической длительности len/(rate*channels*width) и тонового ресэмплингаВозможны скорость/высота/плохая STT; сетевой отказ не гарантирован
Обрезанный файл / нечётный PCMWAV validators и TTS decoder отказывают; старый результат не перезаписываетсяMIME или 200 не означают целый аудиопоток
Неверный Base64validate=True; ошибка не объявляется успехом и socket закрываетсяBase64 не сообщает исходный codec
Пустой / failed / незавершённый ответSTT требует commit item + непустой final; GA TTS требует свой response ID + completed + PCMappendSpeech имеет эвристику паузы, поэтому его полнота слабее explicit done
Чужой item/responseSTT item_id и GA TTS response_id корреляция протестированыV3 приватные turn captions не содержат тот же GA response_id: привязка - отдельный call/socket
Локальные RTC peersРеальный Opus, ICE/DTLS/SRTP, положительные RTP stats, ненулевые decoded samplesЭто не proof remote authorization
Живой V3 fileSDP/call, WSS, RTP в обе стороны, final user/assistant captions, WAV decoded полностьюНе proof AEC, всех устройств/голосов, долгого разговора и полной точности речи

Прогон тестов

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
python3 -W error::ResourceWarning -m unittest discover -s "$repo/scripts/tts-demos" -p "test_*.py" -v
python3 -W error::ResourceWarning -m unittest discover -s "$repo/scripts/voice-probes" -p "test_*.py" -v
python3 -W error::ResourceWarning -m unittest discover -s "$repo/scripts/realtime-stt-isolated" -p "test_*.py" -v
python3 -W error::ResourceWarning -m unittest discover -s "$repo/scripts/remote-tts-isolated" -p "test_*.py" -v
"$voice_venv/bin/python" -W error::ResourceWarning -m unittest discover -s "$repo/scripts/webrtc-voice-isolated" -p "test_*.py" -v
python3 -m unittest discover -s "$repo/scripts/audio-textbook" -p "test_*.py" -v

Ни один из этих тестов не стартует Desktop. Fake RPC может запустить только свой тестовый процесс. Живые файлы сохраняются в отдельный каталог; в учебнике только параметры и счётчики, без личных ответов и credentials.

Ограничения и невоспроизведённые способы

СпособРезультат на 30.09.2026Что можно утверждать
OpenAI /v1/audio/speech429 credit_balance_exhausted / insufficient_quota; без аудиоМаршрут удалённый, но рабочее получение Speech этой пробой не подтверждено
ChatGPT audio summaryСначала 422 из-за ошибочного format=mp3; после исправления podcast/executive_briefing - 404Причина404 неизвестна. Не называем маршрут отсутствующим для всех
Desktop streaming dictation/codex/dictation-stream-connect-info → WSS /dictation/stream найдено в кодеНет независимой живой пробы этой ветки; не выдаём за доказанный transport UI-сеанса
Desktop voice / все голосаКонтракты есть, но точный Desktop live WebRTC trace и каждый voice не провереныЖивая новая проба доказывает наш V3 клиент, не каждый режим Desktop
Локальная read-aloud кнопка в установленном payloadBackend протестирован, patch/gate отсутствуют в исследованном снимкеИсходники функции и рабочий backend не означают включённую кнопку UI

Проверка самого учебника

Отчёт приёмки: 87 Python-тестов, Node DOM-mock, 20 SVG и датированные живые пробы.

Статические проверки контролируют уникальность ID, все SVG-ссылки, локальные пути, shell-команды, JS syntax и логику hash/deep-link/history/focus. SVG проверяются отдельно рендерером. Полный просмотр HTML в разрешённом браузерном окружении ранее заблокирован URL-политикой для локальных HTML-файлов; обход через другой браузер, headless или HTTP server не делаем. Поэтому desktop/mobile/print screenshot и настоящая клавиатура браузера остаются непроверенными. Логический DOM-тест не заменяет визуальную приёмку.

Границы знания

Что проверено, а что ещё нет

В конце отделим источник знания от результата. Исходник показывает возможный путь, mock проверяет нашу обработку, локальный peer проверяет WebRTC без сервиса. Историческая живая проба имеет дату и условия. При редактировании 01.10.2026 доступ аккаунта, микрофон и Desktop заново не проверялись.

ПровереноЛокальный CLI 0.156.1, ChatGPT login, helper и GStreamer, включённые voice-флаги.

Проверено по кодуWebRTC V3, роль app-server, call signaling, Opus/RTP, sideband и transcript-события - по тегу rust-v0.156.1.

12 тестовWAV, токен, чанки, события, item_id, timeout, ошибки и dry-run отдельной копии. Сервер в тестах имитируется.

Живой WSS STT30.09.2026: raven.flac → WAV, 7,21 с, gpt-transcribe + текущий ChatGPT OAuth. Получен transcript, 96 символов; содержимое скрыто.

Не проверено живьёмМикрофон, динамики, полный голосовой сеанс CLI, фактические URL/ICE его соединения. Приложение не запускалось и не перезапускалось.

Не установленоТочная модель STT внутри CLI; будущая доступность OAuth-STT; качество на разных языках; gpt-live-transcribe и Platform key для этого нового демо.

Получение 96 символов подтверждает непустой ответ сервиса, но не независимую проверку дословной точности. Аудио, транскрипт и credentials не добавлены в репозиторий. Совпадение версии установленного бинарника и тега - не побайтовое доказательство воспроизводимой сборки.

Если не заработало

СимптомЧто проверитьЧто нельзя заключить
HTTP 401Правильный auth.json, auth_mode chatgpt, наличие/срок токена; вход обновлять штатно.Не печатать token и не копировать его в код.
HTTP 403Хост, режим, разрешение аккаунта, сетевой маршрут; сравнить с тем же endpoint и типом session.403 не доказывает одну причину. Работа Desktop-диктовки не гарантирует любой публичный audio API.
Ошибка session / моделиДоступ к выбранной модели и актуальную схему session в официальной документации.Успех handshake ещё не означает, что transcription разрешён.
429 / timeoutЛимиты аккаунта, доступ к WSS, длину записи, общий timeout. Не делать бесконечные ретраи.Не считать сбой сети успешной транскрипцией.
Ошибка WAV / пустой текстMono PCM16 24 кГц; длительность; слышимую речь; код завершения. FLAC напрямую демо не принимает.ffmpeg-конвертация - не локальная STT.
CLI voice недоступенОсновной чат, voice-feature, packaged helper/runtime, разрешение устройства и сетевой доступ.Static musl CLI сам по себе не запрещает voice: в текущем пакете есть отдельный GNU helper.

Code-map: от действия к исходнику

МеханизмФайл и ориентир
Чтение WAV / credentialrealtime-transcribe.py: read_audio, read_credential. WAV → PCM; секрет только из файла.
Сеанс и отправкаТот же файл: session_update, transcribe. session → append → commit → item_id → completed; finally закрывает WSS.
CLI / dry-runТот же файл: main. Аргументы, проверка timeout, dry-run без сети и авторизации.
Несетевые проверкиtest_realtime_transcribe.py: 12 тестов, включая отдельную копию и скрытие секретов.
TUI → start / stopchatwidget/realtime.rs - toggle/start и transcript; app_server_session/realtime.rs - параметры V3 WebRTC.
Helper / transportrealtime-webrtc/session.rs - packaged runtime; voice-host/transport.rs - peer connection, oai-events; audio_track.rs - Opus/RTP.
App-server / corecore/realtime_conversation.rs - transport, validation, sideband; core/client.rs - auth и создание call.
Сеть / события V3realtime_call.rs - routes, SDP; methods.rs - sideband URL; protocol_frameless_bidi.rs - input/output transcript и turn.done.

Проверить демо самостоятельно

Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.

# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
# Только локальные тесты, без сервиса, credentials и запуска приложения.
python3 -W error::ResourceWarning -m unittest discover \
  -s "$repo/scripts/realtime-stt-isolated" \
  -p 'test_*.py' -v
# Проверить, что за CLI установлен: эти команды не включают микрофон.
command -v codex
codex --version
codex login status
codex features list

Небольшой словарь

PCM, WAV и Opus - чем отличаются?

PCM - отсчёты звука. WAV - файл-контейнер, который может хранить PCM и заголовок. Opus - сжатый аудиокодек. Демо вынимает PCM из WAV; CLI кодирует микрофонный звук в Opus для передачи по WebRTC.

SDP, WebRTC и sideband

SDP описывает предлагаемое соединение: кодеки, transport и параметры. WebRTC переносит медиа. Sideband - дополнительный служебный WebSocket, связанный с тем же call ID. Наличие SDP в JSON не значит, что JSON содержит звук.

Delta, Done и commit

Delta - частичный текст, Done - завершение в CLI. В учебном STT commit завершает входной аудиобуфер; completed содержит итоговую транскрипцию. item_id связывает результат с конкретным входом.

App-server - это удалённый сервер OpenAI?

Нет. В данном контексте это локальный компонент Codex, обслуживающий клиент через JSON-RPC и координирующий работу с удалённым API. Его имя не означает, что распознавание речи выполняется на вашем компьютере.

Первичные источники и соседние документы

Контракты: endpoint, данные и ответы

Схемы показывают направление, здесь приведены конкретные сообщения. ID и PCM-отсчёты учебные. Это не дампы личного сеанса. На каждый контракт распространяются условия собственного входа и датированного evidence соответствующей главы.

CLI ↔ локальный app-server

Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done

Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо

Передаём:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "id": 10,
  "method": "thread/realtime/start",
  "params": {
    "threadId": "demo-thread",
    "version": "v3",
    "outputModality": "audio",
    "transport": {
      "type": "webrtc",
      "sdp": "<SDP offer>"
    }
  }
}

Получаем:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

[
  {
    "id": 10,
    "result": {}
  },
  {
    "method": "thread/realtime/sdp",
    "params": {
      "threadId": "demo-thread",
      "sdp": "<SDP answer>"
    }
  },
  {
    "method": "thread/realtime/transcript/done",
    "params": {
      "threadId": "demo-thread",
      "role": "user",
      "text": "Привет"
    }
  }
]

В CLI 0.156.1 WebRTC без version выбирает V1, поэтому V3 указан явно. Результат с id=10 подтверждает вызов, а SDP и реплики приходят уведомлениями отдельно; порядок списка здесь учебный, не гарантия планировщика. Встроенный TUI использует InProcess, daemon - Unix-сокет; оболочка JSON на строку относится к нашему отдельному app-server процессу. Это не HTTP endpoint и не аудиопакеты. Перед start нужны initialize/initialized и thread.

Исходник вызова · Проверка результата · Термины

CLI ↔ voice helper

Локальный IPC codex-voice-host · SDP / mute / stop

Команды управления + SDP; аудио между устройствами и helper локальное

Передаём:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

Согласование SDP и управление helper; это не HTTP-запрос.

Получаем:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

Звук с устройства / в устройство и состояние helper.

Пример описывает смысл команд, а не выдуманную JSON-схему IPC. Поля IPC нужно брать из конкретной версии helper. Частота устройства не тождественна RTP-clock Opus.

Исходник вызова · Проверка результата · Термины

Создать WebRTC V3 call

POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas

application/json: sdp + session → plain SDP answer + Location/call ID

Передаём:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "sdp": "<SDP offer>",
  "session": {
    "instructions": "Отвечай кратко",
    "audio": {
      "output": {
        "voice": "cove"
      }
    },
    "delegation": {
      "type": "client"
    },
    "model": "gpt-live-1-codex"
  }
}

Получаем:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

HTTP 200/201; тело начинается с v=0 (SDP answer), заголовок Location содержит call ID.

Пример полей взят из независимого V3-демо. Версионный заголовок OpenAI-Alpha: quicksilver=v2; Authorization только в заголовке. Настоящие SDP/ICE credentials и call ID не публикуем. Это не отправка аудиофайла и не GA Realtime WS.

Исходник вызова · Проверка результата · Термины

События WebRTC V3

WSS wss://api.openai.com/v1/live/<call_id>

WebSocket UTF-8 JSON: тексты реплик, завершение, координация

Передаём:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "type": "session.close"
}

Получаем:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "type": "turn.done",
  "turn": {
    "role": "assistant",
    "transcript": "Привет"
  }
}

input_transcript.added / output_transcript.added содержат item.text. Подключение присоединяется к уже созданному call; наше V3-демо не посылает повторный session.update. Это отдельный канал от WebRTC-медиа.

Исходник вызова · Проверка результата · Термины

Звук разговора

WebRTC · выбранный SDP/ICE peer · не HTTP endpoint

Opus в защищённом RTP/SRTP-потоке; бинарные пакеты, не JSON и не файл

Передаём:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

Микрофон → локальные PCM-сэмплы → Opus → WebRTC-пакеты.

Получаем:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

WebRTC-пакеты → Opus decoder → PCM → динамики или WAV нашего демо.

Точный IP/порт выбирается SDP/ICE для сеанса. RTP-clock 48000 не определяет исходную частоту микрофона. Наш файловый вход: WAV PCM16LE mono 48 кГц; RIFF-заголовок по WebRTC не отправляется. Нельзя послать файл curl на UDP peer.

Исходник вызова · Проверка результата · Термины

Файловая диктовка /transcribe

POST https://chatgpt.com/backend-api/transcribe

multipart/form-data: file бинарно, MIME по контейнеру → JSON text

Передаём:

Форма HTTP-запроса или ответа. Учебные значения не заменяют вход и целый аудиофайл.

Content-Disposition: form-data; name="file"; filename="demo.flac"
Content-Type: audio/flac

<байты FLAC целиком>

Получаем:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "text": "Привет"
}

Boundary разделяет поля multipart. Файл не превращается в Base64. В Bash-демо только file; Desktop может передать language и metadata попытки/сообщения. FLAC нашего примера не означает, что MediaRecorder Desktop пишет FLAC. Серверный source закрыт.

Исходник вызова · Проверка результата · Термины

Потоковая диктовка Desktop

POST https://chatgpt.com/backend-api/codex/dictation-stream-connect-info → выданный WSS URL

JSON connect-info → session.start + audio.append: Base64 raw PCM16 mono

Передаём:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "type": "session.start",
  "dictation_session_id": "demo-session",
  "attempt_id": "demo-attempt",
  "config": {
    "language": null,
    "input_audio_format": "pcm16",
    "sample_rate_hz": 48000,
    "num_channels": 1,
    "provider_mode": "streaming_sse",
    "transcript_delivery_mode": "final_only"
  }
}

Получаем:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

Connect-info: websocketUrl + protocols; поток затем возвращает транскрипт. Полный контракт текстовых событий в этой итерации не воспроизведён.

source-only: connect-info и audio.append найдены в бандле. WSS адрес берётся из ответа; нативная bridge-ветка использует /dictation/stream, но фиксированный публичный URL здесь не установлен. 48000 - иллюстрация значения; клиент передаёт фактическую частоту AudioContext. При сбое возможен файловый fallback.

Исходник вызова · Проверка результата · Термины

Наше Realtime STT

WSS wss://api.openai.com/v1/realtime?intent=transcription

UTF-8 JSON; Base64 raw PCM16LE mono 24000; WAV header не отправляется

Передаём:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

[
  {
    "type": "session.update",
    "session": {
      "type": "transcription",
      "audio": {
        "input": {
          "format": {
            "type": "audio/pcm",
            "rate": 24000
          },
          "transcription": {
            "model": "gpt-transcribe"
          },
          "turn_detection": null
        }
      }
    }
  },
  {
    "type": "input_audio_buffer.append",
    "audio": "AQD//w=="
  },
  {
    "type": "input_audio_buffer.commit"
  }
]

Получаем:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "type": "conversation.item.input_audio_transcription.completed",
  "item_id": "demo-item",
  "transcript": "Привет"
}

Чанк демо - до 48000 байт (секунда PCM). В примере Base64 содержит лишь два синтетических отсчёта +1/-1: этого недостаточно для реальной STT. Completed принимается только для item ID своего committed входа. Это не транспорт CLI /voice.

Исходник вызова · Проверка результата · Термины

Произвольный текст → MP3

POST https://chatgpt.com/backend-api/pronunciation/synthesize?format=mp3

application/json: text / pronunciation_language / source → MP3 bytes

Передаём:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "text": "Привет",
  "pronunciation_language": "ru",
  "source": "translation"
}

Получаем:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

HTTP 200; аудиобайты MP3 (audio/*); проверить полное декодирование ffmpeg.

Вход - текст, не файл аудио и не ID ответа. Формат query mp3; MIME и контейнер результата проверяются отдельно. Доступ приватного ChatGPT API зависит от аккаунта; успешная историческая проба не обещает будущую доступность.

Исходник вызова · Проверка результата · Термины

Read Aloud существующего ответа

GET https://chatgpt.com/backend-api/synthesize?conversation_id=<id>&message_id=<id>&voice=<voice>&format=mp3

Query-параметры, тела нет → MP3 / AAC; не отправка текста ответа

Передаём:

Форма HTTP-запроса или ответа. Учебные значения не заменяют вход и целый аудиофайл.

GET /backend-api/synthesize?conversation_id=demo&message_id=answer&voice=juniper&format=mp3 (учебные ID, не рабочий разговор)

Получаем:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

Бинарный MP3 или AAC. Демо поддерживает mp3/aac; оригинальный клиент проверяет возможности MediaSource.

Нужны ID собственного ChatGPT-разговора и ответа, не локальный Codex thread ID. Authorization в заголовке; cookies и токен в URL не копируем. Для ответа с готовым аудиоasset штатный клиент может воспроизвести asset без synthesize.

Исходник вызова · Проверка результата · Термины

Наше GA Realtime TTS

WSS wss://api.openai.com/v1/realtime?model=gpt-realtime

UTF-8 JSON input_text → response.output_audio.delta: Base64 PCM16 mono 24 кГц

Передаём:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

[
  {
    "type": "conversation.item.create",
    "item": {
      "type": "message",
      "role": "user",
      "content": [
        {
          "type": "input_text",
          "text": "Привет"
        }
      ]
    }
  },
  {
    "type": "response.create"
  }
]

Получаем:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "type": "response.output_audio.delta",
  "response_id": "demo-response",
  "delta": "AQD//w=="
}

Сначала session.update задаёт realtime, output_modalities=[audio], voice, формат audio/pcm rate=24000 и отключённый VAD. Затем ждём session.updated. Чанки своего response ID собираются до response.done completed; WAV формируется только локально. В учебной delta два PCM-отсчёта, не настоящий ответ сервиса.

Исходник вызова · Проверка результата · Термины

app-server appendSpeech

stdio:// · thread/realtime/start → thread/realtime/appendSpeech → outputAudio/delta

JSON-RPC text → audio.data (Base64 PCM) + sampleRate / numChannels

Передаём:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "id": 11,
  "method": "thread/realtime/appendSpeech",
  "params": {
    "threadId": "demo-thread",
    "text": "Привет"
  }
}

Получаем:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "method": "thread/realtime/outputAudio/delta",
  "params": {
    "threadId": "demo-thread",
    "audio": {
      "data": "AQD//w==",
      "sampleRate": 24000,
      "numChannels": 1
    }
  }
}

Это текст для произнесения, не appendAudio. Для опыта выбран start transport=websocket, version=v2. PCM16 mono24000 подтверждён декодированием опыта; ширина сэмпла не отдельное поле RPC. Учебный data - два синтетических отсчёта. Демо завершает сбор по паузе 3 с, не гарантированному end-of-audio событию.

Исходник вызова · Проверка результата · Термины

Ядро Codex → Realtime WS v2

WSS provider URL + /realtime?model=<model> (не /live/<call_id>)

JSON session.update / input_text → JSON-события аудио и реплик

Передаём:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

session.update, затем текст; формирование событий зависит от RealtimeEventParser версии.

Получаем:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

Аудиодельты сервиса → нормализованные thread/realtime/outputAudio/delta клиенту.

CLI0.156.1 строит URL из provider и config; для v2 это /realtime, для V3 frameless - /live. Конкретный URL app-server опыта не был захвачен NetLog: здесь source-контракт, а не заявление о наблюдавшемся адресе. apiKey-путь проверяется отдельно от ChatGPT auth.

Исходник вызова · Проверка результата · Термины

Кнопка Linux Read Aloud

Локальный IPC linux-read-aloud · action=speak

Объект с text → backend result; сетевой endpoint по умолчанию отсутствует

Передаём:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "action": "speak",
  "source": "button",
  "text": "Привет"
}

Получаем:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "spoken": true,
  "engine": "kokoro"
}

JS backend выбирает свою команду либо настроенный движок/системный fallback. Не путать с удалённым ChatGPT GET /synthesize. Если установлена custom command, сеть и формат определяет именно она.

Исходник вызова · Проверка результата · Термины

Локальный синтез / проигрывание

stdin / локальный процесс / ONNX · нет HTTP endpoint

UTF-8 text → локальные PCM-сэмплы / WAV / проигрывание

Передаём:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

UTF-8 текст: Привет. В local-file.py вход - путь к UTF-8 файлу, не сетевой запрос.

Получаем:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

WAV нашего демо: Piper Denis 22050 Гц mono, Kokoro 24000 Гц mono, eSpeak 22050 Гц mono (проверенные модели).

Параметры зависят от движка/модели, не универсальный формат всех TTS. Непосредственная кнопка и Speech Dispatcher могут проигрывать звук, не сохраняя WAV. В native-схеме работает JS/Rust helper, а local-file.py - только наше отдельное демо.

Исходник вызова · Проверка результата · Термины

Read Aloud MCP STDIO

stdio:// · tools/call(name=read_aloud) → Rust helper

JSON-RPC 2.0 на строку; arguments.text UTF-8 → результат запуска

Передаём:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "read_aloud",
    "arguments": {
      "text": "Привет",
      "interrupt": true
    }
  }
}

Получаем:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

MCP CallToolResult с text/structuredContent; read_aloud сообщает результат запуска, а не возвращает MP3 по HTTP.

Сначала initialize и notifications/initialized. doctor проверяет backend, stop завершает речь. MCP - оболочка вызова; локальность/сеть определяется backend, а не самим названием MCP.

Исходник вызова · Проверка результата · Термины

HTTP CONNECT через наш proxy

HTTP proxy gis-master.ru:18081 · CONNECT chatgpt.com:443 / api.openai.com:443

CONNECT handshake → TLS HTTP/WSS; RTP UDP в этот туннель не входит

Передаём:

Форма HTTP-запроса или ответа. Учебные значения не заменяют вход и целый аудиофайл.

CONNECT chatgpt.com:443 HTTP/1.1
Host: chatgpt.com:443

 (Proxy-Authorization не опубликован)

Получаем:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

200 Connection established; затем отдельный HTTPS status и WSS 101. Loopback relay порт выбирается локально.

HTTP до proxy не шифрует Basic сам по себе; целевой TLS и доверенная CA проверяются отдельно. Proxy знает авторизацию upstream по своей конфигурации. Эти адреса - наше окружение, не обязательные endpoints сервиса OpenAI.

Исходник вызова · Проверка результата · Термины

Клиент ↔ ядро / модель

Локальные thread/start, turn/start; удалённый URL зависит от model provider

JSON-RPC input[] → события; модельные запросы не Opus-медиаканал

Передаём:

Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.

{
  "id": 3,
  "method": "turn/start",
  "params": {
    "threadId": "demo-thread",
    "input": [
      {
        "type": "text",
        "text": "Привет"
      }
    ]
  }
}

Получаем:

Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.

События thread/turn, текст ответа, запросы разрешений и результаты инструментов.

Архитектурная схема не фиксирует профиль/model provider. Поэтому конкретный домен модели не выдуман. Внутренний вызов app-server → ядро - Rust API, не отдельный публичный HTTP endpoint. Медиа /voice идёт другим путём.

Исходник вызова · Проверка результата · Термины

Словарь: термин и его роль в этой системе

Этот словарь поясняет новые понятия, а не заменяет контракты. Каждое определение связано с главой, где оно используется.

STT, TTS, transcript и captions
STT превращает речь в текст, TTS - текст в звук. Transcript - распознанная или произнесённая реплика; captions - отображаемые текстовые события. Файл STT, TTS.
TUI, Electron renderer/main, core и helper
TUI - терминальный интерфейс; renderer рисует Desktop, main выполняет его привилегированные операции. Core ведёт агента, helper обслуживает конкретную дополнительную задачу. Роли.
RPC, ACK, notification, STDIO и InProcess
RPC вызывает операцию. Ответ с request ID подтверждает её; уведомления сообщают дальнейшие события отдельно. STDIO - ввод/вывод процесса, InProcess - обмен внутри процесса; одинаковая операция не означает одинаковый транспорт. Lifecycle.
PCM, sample rate, endian, контейнер и кодек
PCM - отсчёты; rate - их число в секунду; endian - порядок байтов числа. Контейнер хранит дорожки, кодек представляет звук. Байты с примером.
WSS, append, commit, VAD, delta и completed
WSS - WebSocket под TLS. Append добавляет данные, commit завершает вход одной реплики, VAD выделяет речь по активности. Delta - часть результата, completed - завершение соответствующего item/response, не всего сокета. STT.
SDP, ICE, STUN/TURN, RTP/RTCP и DTLS/SRTP
SDP описывает media-соединение, ICE выбирает адресную пару. STUN помогает определить доступный адрес, TURN может ретранслировать media. RTP переносит media с временными метками, RTCP даёт контрольные данные. DTLS согласует защиту, SRTP защищает media-пакеты. Номер порта сам по себе не устанавливает роль peer. V3.
Signaling, sideband и data channel
Signaling создаёт соединение, sideband приносит события того же call. Data channel - отдельный канал данных внутри WebRTC. Наличие одного не доказывает, что именно он несёт transcript или PCM в выбранной реализации. Три канала.
OAuth, Bearer, Platform key, account ID и refresh
OAuth-вход ChatGPT и Platform key - разные credentials. Bearer - форма HTTP-заголовка. Account ID выбирает аккаунт; refresh обновляет вход. Демо читает собственный файл без refresh и без копирования cookies. Подготовка.
CONNECT, MITM, Basic и CA
CONNECT создаёт proxy-туннель. Basic авторизует proxy, Bearer - сервис. MITM может расшифровать TLS при доверенной CA. Клиенту нужен публичный сертификат доверия, а не private key CA. Границы доверия.
Backend, inference, MCP, ONNX и player
Backend - исполнитель синтеза, inference - запуск модели. ONNX - формат модели/runtime, player проигрывает звук. MCP вызывает инструмент; локальность определяется backend. Оболочки.
AEC, mute, playout и media drain
AEC подавляет попадание динамиков в микрофон. Mute демо отключает захват и очищает очередь, media несёт тишину. Playout - воспроизведение, drain - завершение буферов. Полного AEC и гарантии полного drain у файловой пробы нет; используйте наушники. Ограничения.
Source-only, mock, peer test и live evidence
Source-only - путь найден в коде; mock проверяет клиент; peer test проверяет локальное соединение; live evidence - датированный ответ удалённого сервиса. Ни один не подменяет остальные. Доказательства.

Аудит четырьмя ролями и карта покрытия

Вопросы ученика по каждой главе

Ученик просил объяснить переходы и незнакомые термины. Преподаватель свёл ответы после сверки ролей архитектором и контрактов аналитиком. В последней колонке - раздел с кодом и проверкой, а не ссылка на неподтверждённый серверный исходник.

Раздел и вопросОтвет после сверкиОбъяснение, код и проверка
Почему похожие голосовые кнопки ведут разными путями?Диктовка нужна для текста в поле ввода, озвучка для готового текста, разговор для двустороннего сеанса. Путь выбирает обработчик, а не расширение входного файла.Перейти к разбору
Как понять, что кнопка действительно вызывает этот endpoint?Снимок показывает место кнопки. Bundle показывает доступную ветку обработчика. Только журнал конкретного запуска доказывает, какая ветка была выбрана.Перейти к разбору
App-server - это удалённый сервер OpenAI?Нет. Это интерфейс ядра Codex. Встроенный TUI использует InProcess, daemon может использовать Unix-сокет. Наш appendSpeech-скрипт запускает отдельный процесс со STDIO.Перейти к разбору
Почему нельзя отправить WAV как Base64 PCM?Base64 сохраняет все байты, включая RIFF-заголовок. Raw PCM контракт требует только отсчёты с согласованными rate, каналами и разрядностью.Перейти к разбору
Можно ли перенести токен и venv вместе с книгой?Книга и исходники переносимы. Credentials в архив не входят; venv создаётся заново. Команды выбирают внешний auth-файл явно и не обновляют основной вход.Перейти к разбору
Где распознают файл и что именно передаётся?Клиент упаковывает целый контейнер в multipart file. Удалённая сторона возвращает JSON text. Локальная проверка сигнатуры не заменяет полного декодирования файла.Перейти к разбору
MCP делает синтез удалённым?Нет. MCP вызывает helper. Piper, Kokoro, eSpeak и Speech Dispatcher работают локально после подготовки; custom backend может быть сетевым, если так настроен.Перейти к разбору
Этот Realtime STT повторяет CLI /voice?Нет. Здесь WSS получает Base64 PCM и возвращает transcript committed item. CLI V3 отдельно согласует WebRTC и получает звук с текстовыми событиями sideband.Перейти к разбору
Почему недостаточно первой completed-реплики?Она может относиться к другому item. Клиент сопоставляет committed item_id и финальную транскрипцию своего буфера.Перейти к разбору
Можно ли выбрать voice у pronunciation?Наш контракт передаёт text, pronunciation_language и source. Параметра voice у этого демо нет; каталог соседнего endpoint сюда не переносится.Перейти к разбору
Можно ли передать свою строку или Codex thread ID?Этот GET читает сохранённый ответ по ChatGPT conversation_id/message_id. Сам текст не передаётся. Для своей строки нужен другой TTS путь.Перейти к разбору
Почему ответ собирается не из любых audio delta?response_id отделяет наш ответ от других событий. Клиент проверяет формат PCM, ждёт завершения соответствующего response и только затем публикует WAV.Перейти к разбору
OAuth в поле apiKey стал Platform key?Нет. Это экспериментальная локальная конфигурация временного профиля. appendSpeech отправляет текст, а outputAudio/delta возвращает PCM; дословность не гарантирована.Перейти к разбору
Голос CLI идёт через app-server или напрямую?У выбранного V3 helper отправляет WebRTC media сервису. App-server создаёт call, держит sideband и координирует агента. Другое RPC appendAudio действительно переносит чанки через app-server.Перейти к разбору
SDP, ACK и transcript - один ответ?Нет. RPC result подтверждает вызов. thread/realtime/sdp и transcript приходят уведомлениями. HTTPS signaling и WSS sideband не являются аудиопакетами RTP.Перейти к разбору
Что заменено и что осталось без Codex?Наш процесс выполняет signaling, WSS и media. Команды агента не выполняются. Mute прекращает захват и очищает очередь. Файловая проба ограничена одной репликой; хвост не доказывает полный drain.Перейти к разбору
Рабочий proxy означает, что весь звук проходит через него?Нет. Историческая проба показала HTTPS/WSS через CONNECT и отдельный UDP media. Этот вывод относится к наблюдавшемуся запуску.Перейти к разбору
IP:3478 - endpoint для отправки WAV?Нет. Это адрес выбранного ICE peer конкретного звонка. Endpoint создания call - отдельная HTTPS операция с JSON SDP.Перейти к разбору
Схема доказывает работу штатного CLI через proxy?Нет. Схема следует исходникам CLI. Живая proxy-проба выполнялась нашим Python-клиентом, а не native CLI.Перейти к разбору
Relay расшифровывает TLS и переносит UDP?Нет. Loopback relay передаёт CONNECT байты и считает их. Возможный upstream MITM с доверенной CA - другая граница доверия; media проходит отдельно.Перейти к разбору
После CONNECT 200 всё работает?Нет. Нужны call, sideband, ICE, положительные RTP-счётчики, реплики и декодированный ответ. Dry-run не использует bootstrap и auth.Перейти к разбору
Лог sendto доказывает доставку пакета?Он доказывает попытку отправки. RTP stats, выбранная пара, transcript и полученное аудио подтверждают следующие слои. Полный внешний маршрут машины неизвестен.Перейти к разбору
Какой аккаунт фактически выбрал gateway?Клиентским журналом это не установлено. Снимок proxy описывает возможную подстановку credentials, но текущий production config здесь не проверялся.Перейти к разбору
Совпадение JSON скрывает внешний клиент?Нет. Формат данных может совпасть, но boundary, TLS, SDP/ICE и ID различаются. Cookies, integrity и attestation не подделываются, неразличимость не обещается.Перейти к разбору
HTTP 200 и audio MIME достаточно для аудио?Нет. Проверяем корреляцию, завершение, сигнатуру и полное декодирование. Неверная частота PCM может быть принята, но дать ускоренную или испорченную речь.Перейти к разбору
Исторический успех означает доступ сегодня?Нет. В книге сохранены даты и условия. Эта редакция проверяет код и комплект локально, не выполняя новый вход или live-вызов.Перейти к разбору
Учебные ID и четыре PCM байта можно отправить как готовое демо?Они поясняют структуру, не заменяют реальный call/item/message и достаточную запись. Запускаемые примеры отдельно связаны с каталогами, README и тестами.Перейти к разбору
Номер V3, v2 и GA обозначает одну версию?Нет. Это номера разных уровней: voice protocol, WS выбор, alpha negotiation и семейство API. Каталог голосов выбирается для конкретного endpoint.Перейти к разбору
Аудит и DOM-тесты заменяют браузерную приёмку?Нет. Они проверяют факты, ссылки и алгоритм навигации. Настоящие keyboard events, mobile layout и печать остаются непроверенными из-за запрета просмотра.Перейти к разбору

Проверка двенадцати разделов карты интерфейса

Место вызова и вопросОтвет и граница вывода
Как запустить примеры?Подключить единое env.sh; не выполнять установочные stage.sh из code-map.
Что делает микрофон?Выбор streaming/file зависит от состояния обработчика; оба пути относятся к STT.
Разговор - это запрос транскрипции файла?Нет. Он создаёт call и двусторонний media-сеанс; локальный start координирует ядро.
Что включает /voice?Медиапомощник и V3-сеанс, а не файловый HTTP /transcribe.
Что озвучивает перевод?pronunciation передаёт текст выбранного языкового виджета; voice в нашем демо не выбирается.
Что получает Read Aloud?Существующий audio asset либо синтез по IDs; конкретная ветка зависит от сообщения.
Что делает кнопка форка?Локальный helper получает текст ответа. Состояние установленной кнопки в этой редакции не проверялось.
Настройка движка запускает сервер?Она выбирает backend/пути/команду; не доказывает сеть или наличие модели.
started:true означает услышанный звук?Нет. Это подтверждение приёма задачи helper; устройство, backend и результат проверяются отдельно.
Отдельный скрипт вызывает кнопку?Нет. Он воспроизводит контракт выбранной операции без UI и не импортирует Desktop.
Что меняет proxy?Маршрут HTTPS/WSS и возможную upstream авторизацию, но не автоматически UDP media.
Чьи скриншоты?Подписанный источник и дата сохранены. Они не выдаются за свежие кадры установленного Community.

Аудит выполнен четырьмя отдельными агентами: ученик задал вопросы по всем главам, преподаватель подготовил ответы, аналитик сверил данные и архитектор проверил роли. Главный агент свёл результаты и исправления. Проверены 17 прежних основных глав, 7 proxy-глав и 12 UI-разделов; ниже сохранена карта всех 23 схем.

Это редакторская и локальная проверка от 01.10.2026. Новые live-пробы и браузерная приёмка не выполнялись. Путь найден в исходнике не равен доказательству работающей кнопки.

Замечания и проверка исправлений

Вопрос ученикаПроверенное объяснениеИсточник и тест
V3 startБез version выбиралась V1. В примере теперь явно version=v3.Код; test_native_v3_selector
Транспорт TUIInProcess и Unix-сокет отделены от STDIO subprocess-демо.Код; test_native_transport_source
Выбор входаВсе основные команды передают auth_file, Bash поддерживает --auth-file.Код; test_bash_auth_argument
Summary formatmp3 давал 422; format здесь тип резюме. Успех не подтверждён.Код; test_summary_contract
Полнота voiceРанний assistant-final и пауза не доказывают ответ на весь файл. Ограничение: одна реплика.Код; test_early_final_not_full_completion

Каждая прежняя схема сохранена

Прежняя страница и IDМесто в едином учебникеКонтракты
codex-cli-voice-textbook.html#overview-flowoverview-flowrpc, ipc, call, sideband, media
codex-cli-voice-textbook.html#cli-flowcli-flowrpc, ipc, call, sideband, media
codex-cli-voice-textbook.html#cli-sequencecli-sequencerpc, ipc, call, sideband, media
codex-cli-voice-textbook.html#stt-native-flowstt-native-flowrpc, ipc, call, sideband, media
codex-cli-voice-textbook.html#stt-flowstt-flowstt-ws
codex-cli-voice-textbook.html#architecture-flowarchitecture-flowrpc, agent, call, sideband, media
codex-cli-voice-textbook.html#stt-http-nativestt-http-nativestt-file, stt-stream
codex-cli-voice-textbook.html#stt-http-demostt-http-demostt-file
codex-cli-voice-textbook.html#tts-http-nativetts-http-nativepronunciation
codex-cli-voice-textbook.html#tts-http-demotts-http-demopronunciation
codex-cli-voice-textbook.html#tts-read-aloud-nativetts-read-aloud-nativemessage
codex-cli-voice-textbook.html#tts-read-aloud-demotts-read-aloud-demomessage
codex-cli-voice-textbook.html#tts-realtime-nativetts-realtime-nativerpc, ipc, call, sideband, media
codex-cli-voice-textbook.html#tts-realtime-demotts-realtime-demotts-ws
codex-cli-voice-textbook.html#tts-appserver-nativetts-appserver-nativespeech-rpc, codex-ws
codex-cli-voice-textbook.html#tts-appserver-demotts-appserver-demospeech-rpc, codex-ws
codex-cli-voice-textbook.html#tts-local-nativetts-local-nativelocal-button, mcp, local-process
codex-cli-voice-textbook.html#tts-local-demotts-local-demolocal-process
codex-cli-voice-textbook.html#voice-console-nativevoice-console-nativerpc, ipc, call, sideband, media
codex-cli-voice-textbook.html#voice-console-demovoice-console-democall, sideband, media
codex-voice-proxy-textbook.html#native-flowproxy-native-flowrpc, ipc, call, sideband, media, proxy
codex-voice-proxy-textbook.html#demo-flowproxy-demo-flowcall, sideband, media, proxy
audio-ui-entrypoints.html#routesui-routesstt-file, stt-stream, call, sideband, media, pronunciation, message, local-button, mcp, stt-ws, tts-ws, speech-rpc

Вопросы для самопроверки

Почему WAV нельзя целиком поместить в raw PCM append? Почему CONNECT 200 не доказывает звук? Почему Codex thread ID не подходит для Read Aloud ChatGPT? Ответы связаны с форматами, фазами проверки и ID сохранённого ответа.

В каждой главе исходники и тесты объясняют границу вывода. Если параметр серверной реализации неизвестен, он так и обозначен. Копирование cookies и обход проверок доступа в комплект не входят.

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.

Реестр исходников

ПроектФайл / строкаРоль
webrtcsrc_core/handlers/handle_offer.py:63Создание aiortc peer и SDP answer
webrtcsrc_front/src/webrtc/negotiate.ts:3POST /core/offer, SDP offer/answer
webrtcsrc_front/src/webrtc/session.ts:19Микрофон, WebRTC, cleanup при ошибке
webrtcsrc_front/src/config/appConfig.ts:54STUN и событийный subject
webrtcsrc_front/src/audio/mic.ts:3Запрос браузерного эхоподавления и шумоподавления
webrtcsrc_front/src/audio/vad.ts:5Energy gate: порог RMS/dB и gain 0/1, не нейросетевой denoiser
webrtcsrc_core/handlers/handle_track.py:18Сборка графа входного аудио
webrtcsrc_core/processors/track_source_node.py:122Resample, 20 мс и padding хвоста
webrtcsrc_core/processors/base.py:16Очередь без maxsize
webrtcsrc_core/processors/whisper_stream_node.py:76Бинарный NATS packet и задачи publish
asrsrc/stt_whisper_to_nats.py:341Фраза в WAV, HTTP ASR, текст
asrsrc/stt_whisper_to_nats.py:83Silero VAD, интервалы и настройка буфера
asrsrc/stt_whisper_to_nats.py:303Выделение фраз из исходных сэмплов по интервалам Silero
asrsrc/stt_whisper_to_nats.py:424Загрузка ONNX Silero; при отсутствии файла возможна загрузка модели
asrsrc/utils/wire_packet.py:9Разбор метаданных и PCM16LE
asrsrc/webui/file_mode.py:1202Multipart file, query language, ответ text/plain
asrsrc/model_transport.py:6CONNECT или прямое соединение, без fallback
webrtcsrc_core/handlers/handle_transcription.py:34Текст в agent request; voice blocked
webrtcsrc_agent/agent.py:138Workflow и атомарное сохранение runtime + истории
webrtcsrc_agent/service.py:151Валидация запроса, ответ и UI publisher
webrtcsrc_agent/ui_events.py:15Ответ и команды UI с trace
webrtcsrc_front/src/assistant/assistantApp.ts:249Обработка событий и управление микрофоном
webrtcsrc_shared/contracts/common.py:22trace/request/correlation/session IDs
webrtcsrc_langgraph/runtime_io.py:48Отдельные NATS-вызовы LLM и tools
webrtcsrc_llm/clients/local_chat.py:8Локальный llama.cpp chat completion
webrtcsrc_llm/utils/model_downloader.py:12Путь к GGUF или Hugging Face download
webrtcsrc_llm/service.py:109Создание локальной модели без явного GPU placement
webrtcsrc_llm/clients/ollama_chat.py:12POST /api/chat с stream:false
webrtcstack/webrtc.drs:51Желаемые ASR, proxy, Ollama и leafnode связи
webrtcsrc_front/src/map/mapController.ts:40Внешний style.json для карты
webrtcsrc_core/utils/ice.py:6Ограниченный deadline ICE gathering
webrtcsrc_core/utils/pc_lifecycle.py:22Остановка графа при disconnected/failed/closed
webrtctests/test_core_ice.py:12Три теста deadline ICE
webrtctests/test_agent_persistence.py:38Транзакция и повторы workflow
webrtcsrc_front/src/webrtc/session.test.ts:62Очистка ресурсов при ошибке signaling
codexcodex-rs/codex-api/src/endpoint/realtime_call.rs:129Создание WebRTC call
codexcodex-rs/core/src/realtime_conversation.rs:623Bounded channels и cancellation
codexcodex-rs/app-server-protocol/src/protocol/v2/realtime.rs:143RPC audio chunk и realtime transport
codexcodex-rs/app-server-client/src/lib.rs:1InProcess, daemon и typed app-server client
codexcodex-rs/tui/src/app_server_session/realtime.rs:20TUI запускает V3 через app-server
codexcodex-rs/codex-api/src/endpoint/realtime_websocket/protocol_frameless_bidi.rs:20Transcript, turn.done и delegation
codexcodex-rs/voice-host/src/processing.rs:107Sonora: AEC/NS/AGC, PCM 48k mono и Opus
codexcodex-rs/voice-host/src/devices.rs:390Копия воспроизводимого звука и время вывода для AEC
codexcodex-rs/voice-host/src/capture_worker.rs:61Reference перед capture, сброс обработки на mute и разрыве
codexcodex-rs/voice-host/src/processing_tests.rs:3Тест удаления синтетического эха; здесь не запускался
aiortcsrc/aiortc/codecs/__init__.py:27Поддерживаемые кодеки aiortc 1.14.0; Opus первый в списке
desktopdesktop-audio-processing-evidence.json:1Desktop 26.928.20755: фрагменты захвата, AEC и ducking с SHA256
forkscripts/webrtc-voice-isolated/voice-console.py:117Лимит кадров, mute, epoch и signaling
forkread-aloud-linux/src/main.rs:204Локальные backend и custom command
forkscripts/voice-probes/chatgpt-transcribe.sh:1HTTP диктовка вне Desktop
forkscripts/webrtc-voice-proxy/README.md:1Раздельное наблюдение HTTP/WSS и ICE/RTP
forkdocs/codex-cli-voice-textbook.html:1Предыдущий учебник и срез Rust-исходников

К началу главы

23Что подтверждено, что предложено и чего пока не знаем

Ниже сохранён source-разбор исходной редакции. Новые live-наблюдения добавлены в главу диагностики; полный voice E2E, качество ASR и runtime digest всё ещё не подтверждены. Опоры на код открываются из этого архива, без соседнего checkout.

VERIFIED / SOURCE означает, что связь найдена в коде или конфигурации. LOCAL TEST - что конкретная проверка выполнена без живых сервисов. INFERRED - механизм риска или ожидаемый эффект без нужного замера. PROPOSED - проект изменения. UNKNOWN - сведений недостаточно. Эти статусы не превращаются в production evidence от наличия хорошей схемы.

IDУтверждениеСтатусОпораГраница
C01Core создаёт peer, media после SDP идёт по WebRTC, не HTTP uploadVERIFIED / SOURCEoffer, negotiate, trackICE codec/peer конкретного runtime не измерен
C02Core передаёт PCM16LE mono 16k, 20 мс по NATS с metadata prefixVERIFIED / SOURCEtrack, pcm, publishПодтверждено в main; deployed image не проверен
C03Исследованный ASR bridge пишет фразу WAV и вызывает HTTPVERIFIED / SOURCEasr, asr-httpСоседний checkout ASR, не runtime trace
C04Voice path возвращает текст; TTS producer не найден в graphVERIFIED / SOURCEtrack, ui-events, front-eventsНе утверждает отсутствие TTS экспериментов во всём repository
C05Очередь без maxsize; pending publish tasks не ограничены semaphoreVERIFIED / SOURCE + LOCAL TESTqueue, publishMock stall: 100 frames → 100 tasks, network=0; не production RAM benchmark
C06Main исправляет ICE deadline, error cleanup и SQL transactionVERIFIED / SOURCE + LOCAL TESTice, tests-core, front-session, tests-front, agent, tests-agent13 Python + 1 frontend unit tests; не e2e
C07Voice blocked выключает sender track во время agent requestVERIFIED / SOURCEtranscription, front-eventsНе полный cancel и не остановка device capture
C08Browser не имеет единого session filter в dispatch событийVERIFIED / SOURCEfront-events, front-configРиск межсессионного смешения INFERRED; live leak не проверен
C09DRS зависит от центрального ASR/LLM/leafnodeVERIFIED / SOURCE CONFIGstackDesired config, не фактическое подключение production
C10Local GGUF путь есть, cache miss вызывает downloadVERIFIED / SOURCEllm-local, llm-modelНаличие файла/качество модели на offline host UNKNOWN
C11CLI V3 разделяет signaling/sideband и медиапутьVERIFIED / SOURCEcodex-call, codex-tui, codex-eventsCLI rust-v0.156.1; серверная реализация UNKNOWN
C12Код WebRTC демо использует bounded capture queue и epochVERIFIED / SOURCEvoice-demoНаше демо, не автоматически поведение Desktop/CLI
C13Возможен padding на хвосте каждого resampled frameVERIFIED / SOURCE; влияние INFERREDpcmDrift и влияние на ASR не измерены
C14Ожидаемое снижение latency и рост ёмкостиPROPOSED / NOT MEASUREDqueue, publish, asrВсе процентные и временные цели - критерии будущего benchmark
C15CLI voice-host включает локальные Sonora AEC/NS/AGC и playout referenceVERIFIED / SOURCEcodex-processing, codex-render, codex-capture, codex-aec-testUpstream тест найден, не запускался; акустическое качество не измерено; перенос в наш клиент PROPOSED
C16Browser запрашивает AEC/NS; aiortc поддерживает OpusVERIFIED / SOURCEfront-mic, front-config, aiortc-codecsФактические getSettings, selected codec, FEC/DTX и качество конкретного runtime UNKNOWN
C17Energy gate закрывает тихий сигнал; Silero вырезает фразы из исходного PCM, не очищает егоVERIFIED / SOURCEfront-gate, front-session, asr-segments, asr-vad-loadКачество gate/VAD и внутренний preprocessing ASR provider UNKNOWN; ONNX надо заранее поставить для offline
C18Desktop имеет разные web capture настройки для Codex voice, ChatGPT voice и диктовки; ducking меняет playout volumeVERIFIED / STATIC BUNDLEdesktop-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Проверяем понимание и переходим к реализации

Ниже сохранён source-разбор исходной редакции. Новые live-наблюдения добавлены в главу диагностики; полный voice E2E, качество ASR и runtime digest всё ещё не подтверждены. Опоры на код открываются из этого архива, без соседнего checkout.
Вопрос нового участникаОтвет
Если 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 учебника.

К началу главы

Перенос на другой компьютер: одна папка и относительные ссылки

Архив содержит одну корневую папку voice-systems-textbook. Откройте её index.html: весь учебник, оформление и навигация работают локально. Из неё же запускаются демо; исходники объясняющих блоков сохранены в sources. Соседний checkout больше не нужен.

# Распаковать в выбранном каталоге; полный путь к машине автора не используется.
tar -xzf voice-systems-textbook-2026-10-02-search.tar.gz
cd voice-systems-textbook
# Проверить ссылки, файлы и SHA256 без входа пользователя и сети.
python3 tooling/verify.py
# Запустить безопасные unit-тесты новой сетевой пробы.
python3 -m unittest discover -s scripts/network-lab -p 'test_*.py' -v
# Подключить пути демо относительно положения самого env.sh.
source ./scripts/audio-textbook/env.sh
# Посмотреть контракт HTTP TTS без токена и сетевого запроса.
python3 "$tts_demo/http-tts.py" chatgpt-text "$audio_out/check.mp3" \
  --text-file "$tts_demo/sample-ru.txt" --auth-file /does-not-exist --dry-run

Для чтения не нужен сервер. При желании можно открыть через localhost: python3 -m http.server 8765 --bind 127.0.0.1 из корня, затем адрес http://127.0.0.1:8765/. Сервер отдаёт только каталог учебника и не должен запускаться из домашней папки. Для чтения он необязателен.

Строка вверху страницы ищет по всему учебному комплекту. Можно ввести STUN 701, Sonora, thread/realtime/appendAudio или имя скрипта. Все слова обязательны, регистр не важен, ё и е считаются одинаковыми. Фраза в кавычках, например "STUN host lookup", ищется целиком. Поиск учитывает текст всех глав, подписи схем, таблицы, комментарии, исходники, демо, тесты, README и очищенные evidence.

Каждый результат показывает совпадение и ведёт к подразделу или к конкретной строке файла. Если результатов больше 30, кнопка "Показать ещё" открывает следующую порцию; остальные не отбрасываются. Ctrl+K или Cmd+K возвращает фокус в поиск, Enter открывает список, стрелки перемещают фокус, Escape его закрывает. Поле остаётся доступным при прокрутке главы, но не выводится на печать.

Словарь встроен в HTML и строится из включённых файлов: сборка индекса и поиск и интерфейс. При открытии книги запросы к серверу не выполняются. Дублирующие HTML-копии и служебные seeds не дают повторных результатов. Бинарные изображения и аудио не распознаются поиском: это не OCR и не STT. Личные файлы за пределами комплекта не читаются. После изменения текста пересоберите книгу, чтобы обновить словарь; валидатор проверяет и SHA256, и точное соответствие индекса тексту.

Что переносится, а что устанавливается отдельно

Переносятся текст, схемы, иллюстрации, выбранные source snapshots, синтетические образцы, демо и тесты. Python, Node, FFmpeg, PortAudio, Rust и модели устанавливаются по README. Venv после перемещения пересоздаётся. Для полностью offline запуска моделей надо заранее подготовить wheels, системные пакеты и веса под целевую платформу. Этот архив - учебник с демо, не готовый offline image всего voice-chat стека.

Удалённые ChatGPT-демо требуют собственного auth-файла вне архива и разрешённого доступа к сервису. Прокси требует собственного CA и настройки окружения. Никакие секреты, login refresh или запуск Desktop не выполняются при открытии index.html или verify.py. STUN-проба идёт в сеть только после явного запуска, а не при чтении учебника.

Как собрать тот же комплект заново

# Из корня распакованного комплекта. Читает только свои seeds и source snapshots.
python3 tooling/assemble.py --output ../voice-systems-textbook-next
# Повторно проверить и упаковать только файлы manifest, без .runtime и auth.json.
python3 tooling/verify.py --root ../voice-systems-textbook-next
python3 tooling/pack.py --root ../voice-systems-textbook-next \
  --output ../voice-systems-textbook-next.tar.gz

Существующий чужой каталог сборщик не затирает; обновлять можно только комплект с его маркером. Упаковщик не заменяет готовый архив. Для объяснений блоков кода используются относительные HTML-страницы с реальными номерами строк, а рядом остаются исходные файлы. Поэтому переходы к коду работают и без GitHub/GitLab.