Codex CLI / ChatGPT / Community и webrtc-komaroff: аудио, сеть, состояние и проверяемые улучшения.
Один голосовой тракт, несколько независимых каналов
Голосовой интерфейс не выбирает один универсальный API. Диктовка возвращает текст, озвучка возвращает звук, а разговор держит двусторонний сеанс. В Codex и в webrtc-komaroff эти задачи соединены разными способами. Здесь обе реализации разобраны в одном учебнике, вместе с демо, исходниками и результатами диагностики.
Сначала отделяем звук от управления. Затем разбираем форматы и воспроизводимые примеры Codex, переходим к нашему Core и NATS, проверяем подключение и только после этого обсуждаем точность распознавания. Такой порядок помогает не лечить модель, когда до неё ещё не дошёл ни один аудиокадр.
Это карта ролей, не одна общая реализация. У Codex удалённый Realtime заменяет наш Core/ASR bridge; app-server координирует разговор. У нас NATS связывает отдельные локальные сервисы.
Микрофон отдаёт отсчёты звука. Клиент очищает и кодирует их, а WebRTC доставляет медиа собеседнику. Управляющие каналы создают сеанс и передают события. NATS в нашей реализации дополнительно переносит декодированное аудио между Core и ASR bridge. У каждого перехода свой контракт, и успех одного канала ничего не говорит о готовности соседнего.
SOURCE - связь найдена в закреплённом коде; RUNTIME - поведение наблюдали в названной пробе; INFERRED - объяснение следует из наблюдений, но точный узел отказа ещё не установлен; PROPOSED - предлагаемое изменение. Исторический успех API не обещает доступ тому же аккаунту сегодня. Улучшения ниже не внедрялись при сборке этой книги.
Читать можно без сети. Ссылки на наши файлы и исходники относительные и остаются внутри комплекта. Ссылки на RFC, GitHub и GitLab - дополнительные первоисточники; переход к ним требует сети. В архиве нет входа пользователя, cookies, личных аудиозаписей, моделей или установленного Desktop.
Приёмка этой объединённой редакции: ссылки, повторная сборка, тесты и ограничение браузерного просмотра. Исторические отчёты отдельных демо остаются в своих главах.
Переносимый комплект: подготовка и запуск. Ссылки и код рядом с этим файлом; команды выполняются из корня после подключения scripts/audio-textbook/env.sh. Вход и модели не входят в архив.
Как голос становится текстом
Здесь собраны разные задачи, которые легко спутать из-за похожих кнопок. Диктовка даёт текст для поля ввода, озвучка даёт звук, а разговор держит двусторонний сеанс. Сначала выберите результат, который нужен вам. Затем проследите путь от действия пользователя до конкретного запроса и проверки ответа.
Helper - локальный помощник, который обслуживает звук. App-server управляет разговором и событиями Codex. По WebRTC передаётся звук, по HTTPS/WSS - управление и текстовые события. SDP описывает соединение, а не аудиозапись; подробности дальше в форматах и словаре.
ЗвукУправлениеТекст
Endpoint и данные этой схемы
Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.
CLI ↔ локальный app-server
Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done
Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо
Выбор аудиопути начинается с обработчика кнопки или команды, а не с расширения файла. Микрофон Desktop может выбрать файловую либо потоковую диктовку; команда /voice начинает разговор. Снимки помогают найти элемент, а код показывает, какой путь он способен вызвать. Снимок сам по себе не доказывает сетевой маршрут.
Снято 30.09.2026 из уже открытого Codex. Это app-gpt-fork, не доказательство наличия Linux Read Aloud в Community. Квадрат справа - остановка ответа, не старт разговора. Переписка и список чатов не включены.
Маленький микрофон превращает речь в текст. Разговор создаёт отдельный realtime-сеанс. Штатный ChatGPT Read Aloud запрашивает аудио по ID ответа, а Linux-кнопка расширения передаёт сам текст выбранному backend. Для GA Realtime и самостоятельных демо кнопки в Desktop нет: контракт задаёт команда запуска.
Снимки микрофона получены. Read Aloud, настройки и экран разговора описаны по коду, но не сняты из установленного Community. Сетевые успешные пробы не подменяют GUI-проверку.
Пример: тот же pronunciation API вне интерфейса
Здесь используется демонстрационный текст. --dry-run не обращается к серверу и не читает токен. Подготовка остальных команд находится на отдельной странице.
Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.
Настоящий снимок уже открытого Codex, 30.09.2026. Взята только область ввода, без переписки и списка чатов. Это окно из app-gpt-fork/chatgpt-app, а не подтверждение установки Community из codex-desktop-linux. В момент снимка шёл ответ: круглая кнопка с квадратом останавливает ход, а не начинает голосовой разговор.
Удалось снять поле ввода и его кнопки. Снимки разговора, меню удалённого Read Aloud, кнопки Linux Read Aloud и её настроек в этом проходе не получены. Ниже их положение описано по коду; нарисованных "скриншотов" вместо них нет. Рабочий сетевой контракт и видимость кнопки в конкретном экземпляре - разные проверки.
Здесь нет универсального переключателя STT/TTS: путь задают выбранная функция, доступность сервиса и её конфигурация.
Endpoint и данные этой схемы
Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.
Файловая диктовка /transcribe
POST https://chatgpt.com/backend-api/transcribe
multipart/form-data: file бинарно, MIME по контейнеру → JSON text
Все команды используют единое окружение комплекта. Подключите env.sh и явно передавайте выбранный auth_file.
1. Микрофон: записать речь в поле ввода
Нажимаете маленький микрофон справа от названия модели, произносите текст и завершаете запись. Результат появляется в поле ввода; это диктовка, а не разговор с аудиоответом. В проверенном бандле 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{returnawait 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}=awaitimport(`./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"
Когда интерфейс предлагает 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
В штатном 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.
В 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. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.
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 демо.
При включённой функции 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, но не доказывает появление кнопки.
Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.
Команды 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"
Дополнение добавляет в настройки раздел Read aloud. В нём доступны Choose folder, Download voice и Speech pace. Выбор папки указывает существующие Kokoro-модели; загрузка подготавливает runtime и модель; скорость сохраняется в настройках. Эти действия сами по себе не читают ответ и не делают запрос /synthesize.
UI-класс LinuxReadAloudSettings читает global state и сериализует запись параметров; codexLinuxReadAloudSetup отправляет action: setup с режимом choose/download. Где лежат модели и как не менять основной профиль, описано в README. Новых загрузок ради снимка мы не запускали.
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"
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. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.
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 ниже.
Что проверено и что пока нельзя считать проверенным
Снимки получены системным захватом уже открытого окна; пиксели нужных областей считаны через XGetImage. Переписка, названия чатов и исходный полный кадр в документ не включены. Скриншот показывает положение микрофона и состояние остановки хода, но не маршрут сети и не установленную Linux-функцию.
Code-map проверен по codex-app/resources/app.asar и исходникам дополнения 30.09.2026. Имена минифицированных файлов, символы и смещения относятся только к этому снимку бандла. При обновлении их нужно проверять заново. Серверные алгоритмы STT/TTS закрыты: описаны наблюдаемые контракты, а не выдуманный серверный исходник.
Автотесты проверяют изображения, источники снимков, ссылки, якоря, SVG и команды. Они не заменяют открытие документа в браузере или щелчок по каждой кнопке Community. Просмотр учебника в разрешённом браузерном окружении ранее заблокирован; через другой сервер или браузер это ограничение не обходилось.
Чтобы читать следующие схемы, сначала разделим роли. Интерфейс принимает действие пользователя. 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>>,
},
}
Так разные интерфейсы используют одно ядро, не дублируя агентную логику. На схеме показаны роли, а не обязательное число процессов: интерфейс и app-server могут связываться внутри одного процесса.
Это управляющая цепочка. В 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-демо
Для аудио роль зависит от пути. В файловой диктовке Desktop получает token через app-server, но Electron сам отправляет multipart-файл. В CLI /voice app-server создаёт call, возвращает SDP и слушает sideband, а звук отправляет helper напрямую по WebRTC. В WebSocket appendAudio сэмплы, наоборот, действительно проходят через RPC. Нельзя объединить эти три случая словом "прокси".
Сценарий
Где идёт звук
Что делает app-server
Desktop mic dictation
Recorder Blob → Electron HTTPS /transcribe; иной streaming путь есть в коде
Даёт актуальный вход. Не принимает файл через thread/realtime RPC
Desktop voice conversation
WebRTC media к голосовому сервису; детали конкретного live Desktop call здесь не проверены
Управляет клиентской сессией/входом и агентной координацией там, где выбран Codex voice путь; не переносим CLI контракт на каждый Desktop экран
CLI /voice, 0.156.1 V3
codex-voice-host → Opus/SRTP → service
HTTPS signaling, WSS sideband, transcript notifications и agent handoff
Подмена model base URL или HTTP-прокси не делает его RTP-реле. Для голоса отдельно нужны маршруты HTTPS call, WSS sideband и ICE/media. RTP может идти по выбранным ICE-кандидатам напрямую или через разрешённый TURN, если такой сервер настроен. Наше демо не настраивает TURN; среда с закрытым UDP может не пройти media даже после успешного HTTPS.
thread/realtime: что именно принимает RPC
Операция
Вход
Ответ / событие
start
threadId, prompt?, model?, version?, voice?, outputModality; transport Webrtc{sdp}, Websocket или ExistingCall{callId}
RPC {}. SDP answer, started/error и captions приходят уведомлениями отдельно
RPC acknowledgement; аудио/текст приходит последующими событиями. Это chunk, не filesystem path
appendText
threadId, text, role
Добавление текста; не upload файла
appendSpeech
threadId, text
Текст для голосового вывода; outputAudio/delta, не готовый MP3 в RPC result
listVoices
Без начала разговора
voice catalogs/defaults версий; доступ к каждому voice не гарантирован
stop
threadId
Завершение принадлежащего thread realtime-сеанса
Контракт: app-server-protocol/src/protocol/v2/realtime.rs. Здесь описаны клиентские типы. Какие сочетания transport/version поддерживает сервер, проверяется отдельно; наличие поля в схеме ещё не доказывает живой вызов.
Роли схемы
Ваш компьютер
На вашей машине находятся интерфейс, файлы записи и сетевые клиенты. Локальное размещение app-server не переносит сюда распознавание: STT и удалённый TTS выполняет сервис.
Вход и выход. Файл/микрофон/текст → клиентские процессы → сеть.
Устройства получают или воспроизводят звук. Они не распознают речь. В Desktop браузерный MediaStream попадает в recorder; CLI использует отдельный voice helper.
Вход и выход. Звуковое давление → локальные сэмплы; полученные сэмплы → звук в наушниках.
Code-map.voice-host/src/audio_track.rs; MediaRecorder в Desktop. CLI audio track отправляет EncodedAudio; захват и live-кодирование не установлены этим файлом. Точное MIME записи MediaRecorder конкретного живого UI-запуска не установлено.
Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.
Это медиапроцесс 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.
Это интерфейс к ядру Codex для Desktop/TUI. Он ведёт клиентские запросы, вход, thread/turn, события, разрешения и работу агента. В WebRTC voice он согласует call и держит sideband; не выполняет локальную STT.
Вход и выход. Типизированная операция start с SDP → удалённый call; SDP answer и transcript → уведомления клиенту.
Интерфейс начинает или останавливает сеанс и показывает transcript. Он не является удалённой моделью. /voice в CLI запускает разговор, а не загрузку готового аудиофайла.
Вход и выход. Действие пользователя → IPC/RPC; Delta/Done → субтитры и состояние.
Сервис принимает media и проводит голосовой разговор. Расшифровки V3 приходят по WSS sideband, звук - по WebRTC. Исходников сервера у нас нет. Здесь описаны контракт и полученные ответы, а не внутренняя реализация или название скрытой модели.
Вход и выход. Запрос по описанному ниже контракту → текст либо аудио. Авторизация и доступ проверяются сервисом.
Ядро содержит логику агента. Оно хранит состояние turn, готовит контекст модели, координирует tools и policy. App-server выставляет эту логику клиентам, а не является отдельной альтернативной моделью.
Вход и выход. Запросы клиента → Session/turn/model client → удалённые модели и инструменты → события.
Модель генерирует ответ удалённо. Инструменты Codex могут работать в локальной среде или подключённых сервисах; слово tools не означает, что всё выполняет голосовой сервер.
Вход и выход. Контекст/медиа → ответ, tool request; разрешённый tool → результат для модели.
Сначала определяем байты, затем выбираем способ отправки. Имя файла, 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 не являются личными.
Это корректная форма учебного сообщения, но два отсчёта недостаточны для транскрипции. 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 и последующий успешный запрос относятся к разному сетевому контексту; конкретную решающую причину заголовков мы не установили.
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.
Endpoint и данные этой схемы
Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.
Файловая диктовка /transcribe
POST https://chatgpt.com/backend-api/transcribe
multipart/form-data: file бинарно, MIME по контейнеру → JSON text
Тот же 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.
Вход и metadata
Token из app-server; Electron может refresh
Token/account ID из файла, static наблюдавшийся UA; нет refresh/cookies/integrity. Подробно - в сравнении клиентов.
Boundary разделяет части multipart, а не аудиочанки. Для FLAC частота/каналы описаны внутри файла; клиент не объявляет raw PCM rate. Одна запись может содержать много кодированных FLAC-фреймов, но это по-прежнему один file в запросе.
Сравниваем извлечённую multipart file с исходным файлом побайтово. MIME выбирается по расширению. Для известных контейнеров перед сетью сверяется сигнатура: MP3 или WAV с именем .flac получают локальный отказ. Это не полное декодирование: повреждённые фреймы внутри правильной сигнатуры ещё может обнаружить сервис. Конкретный серверный HTTP-код для mismatch не проверяли.
Блоки схемы и code-map
Desktop recorder
После нажатия на микрофон Desktop собирает запись из MediaRecorder chunks в Blob. Это файловая диктовка; альтернативный streaming dictation в коде не надо смешивать с этим вызовом.
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 в том сеансе не доказан.
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.
Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.
Скрипт читает собственный 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-байты>
Когда вместо готового файла сервису нужны отсчёты звука, меняется упаковка. Наш клиент читает WAV локально и отправляет его PCM-содержимое в Base64 JSON по WSS. RIFF-заголовок остаётся на машине. Эта файловая STT-проба не является уменьшенной копией CLI /voice: у них разные контракты и результаты.
Ссылки рассчитаны на открытие этого HTML как локального файла. Если браузер запрещает переход в каталог, скопируйте путь и откройте его в файловом менеджере. На другой машине задайте свой вход и пересоздайте venv.
App-server ведёт signaling и sideband; полный управляющий тракт показан в схеме CLI.
Endpoint и данные этой схемы
Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.
CLI ↔ локальный app-server
Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done
Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо
Это сопоставление двух способов распознавания, не обещание одинакового wire-формата. В /voice идёт Opus по WebRTC и возвращается также аудиоответ. Следующее WSS-демо использует raw PCM и возвращает только финальный текст.
Endpoint и данные этой схемы
Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.
Наш клиент сам выполняет минимум его сетевых обязанностей: читает credential, открывает защищённое соединение и задаёт session. Для выделенного STT не нужны thread, SDP, голосовой helper, handoff и ответы голосом. Это другой режим API с той же идеей удалённого распознавания, а не воспроизведение внутренних запросов TUI.
Реальный CLI 0.156.1
Наш изолированный пример
Вход
Микрофон через helper
Готовый WAV; извлекается PCM
Аудиотранспорт
WebRTC, Opus/RTP
WSS, JSON + Base64 PCM16
Режим
Разговорный WebRTC V3
Отдельная session type transcription
Модель STT
Не установлена из исследованных исходников
gpt-transcribe явно в запросе
Авторизация
Штатный auth manager app-server
Чтение файла; без refresh и записи
Результат
Субтитры, голосовой диалог, handoff
Финальный 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 байты.
PCM - последовательность signed 16-bit little-endian сэмплов. У неё нет собственного заголовка с частотой; 24000 Гц задаётся отдельно в session. Неправильное заявление частоты может изменить скорость, не вызвав сетевого отказа.
Вход и выход. Raw mono PCM16 → куски по 48000 байт, последний короче.
Сервис распознаёт PCM после commit. Исходников сервера у нас нет. Здесь описаны контракт и полученные ответы, а не внутренняя реализация или название скрытой модели.
Вход и выход. Запрос по описанному ниже контракту → текст либо аудио. Авторизация и доступ проверяются сервисом.
Контракт уже разобран; теперь воспроизведём его. Сначала подготовим 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 077if"$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 не обнаружены; этот учебник их не включает.
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.
Endpoint и данные этой схемы
Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.
Кнопка Linux Read Aloud
Локальный IPC linux-read-aloud · action=speak
Объект с text → backend result; сетевой endpoint по умолчанию отсутствует
Не нужна; для 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. Ни одна оболочка сама по себе не означает удалённый синтез.
Вход и выход. Текст/параметры → локальный движок или явно заданная команда.
Проверка. 29-30.09.2026 все три создали декодируемый WAV; параметры образцов проверяются ffprobe. Speech Dispatcher отдельно проигрывает, не сохраняет WAV.
Piper и Kokoro выполняют inference модели на машине; eSpeak синтезирует речь локально. Speech Dispatcher маршрутизирует текст системному синтезатору и управляет воспроизведением. Это не удалённый сервер ChatGPT.
Вход и выход. Текст → локальный звук. Для Piper нужен ONNX; для Kokoro ONNX и voices.bin.
Для удалённой озвучки собственной строки в исследованном Desktop найден pronunciation endpoint. В него передаётся текст, язык и source, а обратно приходит MP3. У нашего примера нет параметра выбора голоса; возможности соседних TTS API на этот контракт переносить нельзя.
Результат и дата. 30.09.2026 - 200 и декодируемый MP3. Ранний образец 29.09.2026: 24 кГц mono, 7,488 с. Новый размер ответа - 155520 байт.
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.
Endpoint и данные этой схемы
Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.
Произвольный текст → MP3
POST https://chatgpt.com/backend-api/pronunciation/synthesize?format=mp3
application/json: text / pronunciation_language / source → MP3 bytes
POST https://chatgpt.com/backend-api/pronunciation/synthesize?format=mp3
JSON
text; необязательные hint/language/speed/source/version зависят от виджета
text, pronunciation_language=ru, source=translation; hint/speed/version не добавляем
Аудио на вход
Нет
Нет. Только JSON UTF-8, Content-Type application/json
Аудио на выход
Blob MP3
audio/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 само по себе ещё не доказывает декодируемость.
Тест перехватывает 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())
Этот клиент делает ту же основную операцию произвольного текста, но не воспроизводит весь 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.
Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.
Озвучка сохранённого ответа устроена иначе: клиент передаёт 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. Это разные получения, их размеры не надо объединять.
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.
Endpoint и данные этой схемы
Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.
Read Aloud существующего ответа
GET https://chatgpt.com/backend-api/synthesize?conversation_id=<id>&message_id=<id>&voice=<voice>&format=mp3
Query-параметры, тела нет → MP3 / AAC; не отправка текста ответа
Тот же 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/AAC
format=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.
Штатное чтение сохранённого ChatGPT-ответа вызывает /synthesize с ID сообщения и голоса. Это другой механизм, чем добавленная Linux-функция Read Aloud.
Вход и выход. IDs ответа и голос → GET /synthesize → Blob/плеер.
Клиент получает каталог голосов, при необходимости читает свой закреплённый чат и выбирает подходящее сообщение. Затем запрашивает аудио. Личный текст/IDs в итог проверки не печатаются.
Вход и выход. Auth/state или явные IDs → GET → MP3/AAC.
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. Все голоса синтезом не проверялись.
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.
Endpoint и данные этой схемы
Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.
CLI ↔ локальный app-server
Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done
Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо
Свой OAuth из файла; Bearer в WSS handshake, без refresh/cookies/env proxy.
Чанки delta - транспортные порции raw PCM, их длина не равна обязательно одной секунде или фразе. Клиент склеивает байты и проверяет выравнивание PCM16. WAV header записывается только локально после completed. У CLI RTP-clock 48 кГц, у этого raw PCM contract 24 кГц - это разные величины.
Тесты проверяют порядок 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.
Теперь вызовем озвучку через локальный интерфейс 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 отказ остаётся отдельным результатом.
Нажмите на блок, чтобы открыть его пояснение. Обратная ссылка в пояснении возвращает к этой схеме.
Endpoint и данные этой схемы
Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.
Base64 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-файлом.
Тесты запускают только 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.
В отличие от нового 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.
Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.
Теперь вернёмся к разговору. В 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. Интерфейс TUIВы вводите /voice или нажимаете F8. CLI запускает helper и получает предложение соединения - SDP.
2. Локальный app-serverПолучает thread/realtime/start, использует вход Codex и создаёт удалённый разговорный сеанс WebRTC v3.
3. Звук и текстHelper передаёт аудио по WebRTC. App-server получает текстовые события через отдельный sideband и возвращает их TUI.
Схема по исходникам версии 0.156.1, не запись фактического сетевого трафика. Стрелки упрощены; signaling и управление имеют ответы.
Endpoint и данные этой схемы
Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.
CLI ↔ локальный app-server
Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done
Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо
Связывает голосовой сеанс с thread и его контекстом; обрабатывает start/stop и состояние.
Берёт авторизацию через штатную систему Codex. Создаёт удалённый call, возвращает SDP answer и call ID.
Открывает sideband - отдельный WebSocket для текстовых и служебных событий. Переводит их в уведомления для клиента.
Поддерживает передачу задач обычному агенту (handoff). Голосовой режим - не только STT, он может отвечать и делегировать работу.
App-server не является локальным Whisper. Его можно использовать без Electron-клиента, но он не заменяет WebRTC/helper и не превращает thread/realtime/start в endpoint загрузки FLAC.
Как включить в самом CLI
В терминале запустите codex. Внутри существующего основного чата: /voice - начать/остановить, /voice mute - выключить/включить микрофон, /voice settings - параметры. Стандартные клавиши: F8 и Ctrl + X; пользовательская раскладка может их менять. Voice не запускается в side conversation.
Разговор создаётся в несколько шагов. Локальный 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 не надо считать отдельным новым запросом модели.
Endpoint и данные этой схемы
Схема показывает путь данных. Полные учебные запросы, ответы и ограничения приведены в связанных контрактах; это не новый дамп личного сеанса.
CLI ↔ локальный app-server
Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done
Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо
Ответ на 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, держит 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 там не запускается; агентные функции намеренно не перенесены.
← Вернуться к схеме app-serverСхема по исходникам версии 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-демо
Свой 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.
Этот объект заменяет только сетевую координацию app-server и работу helper. Он не создаёт Codex thread, не использует tools и не читает проектный контекст.
Вход и выход. offer → POST call → plain answer/Location → apply answer → join sideband → RTP + captions.
Сервис ведёт один разговор: WebRTC медиа и отдельный WSS контроль. Исходников сервера у нас нет. Здесь описаны контракт и полученные ответы, а не внутренняя реализация или название скрытой модели.
Вход и выход. Запрос по описанному ниже контракту → текст либо аудио. Авторизация и доступ проверяются сервисом.
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.
Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.
Полученный Opus декодируется aiortc. receive_audio приводит кадры к PCM16 mono48к. В консоли они идут в выбранное output device, в файловой пробе сохраняются в новый WAV с правами0600. После финального assistant turn демо ещё одну секунду принимает RTP-хвост; это минимальная файловая проба, не реализация полного playout-sync для всех сетевых условий.
Вход и выход. Remote AudioFrame → resampled PCM → sounddevice или атомарный WAV. Поток содержит также тишину; длительность WAV не равна длительности только произнесённых слов.
Переносимый комплект: подготовка и запуск. Ссылки и код рядом с этим файлом; команды выполняются из корня после подключения 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=avas
JSON: sdp и session. В session: instructions, model=gpt-live-1-codex, audio.output.voice=cove, delegation.type=client. Ответ: plain SDP answer и call ID в Location. Аудиочанков в POST нет.
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 endpoint
DTLS-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 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-демо
Настройка 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-соединение с сервисом. Свой сохранённый вход читается только перед живым запросом.
Зелёный путь - медиа вне 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
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.
Этот вызов не открывает auth.json, CA, proxy env или микрофон. Python и wrapper при dry-run не читают bootstrap. Он нужен только для живого proxy-запуска.
Реальная проба с файлом
Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.
ffmpeg только меняет аудиоформат. Распознавание и разговор происходят удалённо. Успех требует RTP в обе стороны, ненулевого декодированного сигнала и финального текста обеих сторон. Одно стартовое приветствие сервиса не считается ответом на файл.
Разговор без Codex
Команды Bash. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.
В консоли: /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 до proxy
Proxy принял туннель к указанному хосту.
Успех 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-сеанс.
Владеет устройствами, 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.
Даёт клиенту 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 намеренно не воспроизведены.
Файл принимает 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 не проверялись.
Согласует 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 не подключается.
Только чтение собственных 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.
Принимает локальный 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.
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 отдельно не инспектировались.
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 с финальными репликами.
Удалённая сторона 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-модели этим не установлено.
Связывает реальные наблюдения вместо предположения "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. Запускайте из корня комплекта; комментарии объясняют подготовку и действие.
Отдельно не проверялись: физический разговор с микрофоном/динамиками, нативный 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-политикой, обход не применялся.
Совместимость означает, что стороны понимают переданные данные. Она не означает одинаковый клиент. HTTP boundary, SDP, ICE, TLS-стек и идентификаторы закономерно отличаются. Мы исправляем упаковку и описание формата, но не копируем cookies, integrity или attestation и не обещаем неразличимость обращения.
У демо нет Electron networking, штатного refresh и состояния основного клиента. Поэтому сравнение ниже отдельно показывает совпадающие поля и различия среды исполнения.
Часть
Desktop / CLI
Изолированные демо и граница
Bearer / account
Auth manager / app-server получают актуальный вход
Свои tokens.access_token и account_id читаются непосредственно из auth.json. Только нужный собственный профиль; значения не в исходниках/argv
Refresh
Desktop может refresh после eligible401; CLI использует auth manager
Не выполняется. При просроченном входе сначала обычный login, затем повтор демо. Основной профиль read-only
Cookies / integrity
Electron может иметь cookie jar, attach state → фактические служебные headers; CLI optional genuine attestation
Нет копирования/подделки. Наличие механизма в коде не означает, что конкретная диктовка несла эти поля
HTTP 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 / HTTP
Electron/Chromium HTTP2 в captured STT; Rust transport в CLI
urllib HTTP1.1 или aiohttp; websocket-client для GA. TLS fingerprint, порядок headers, proxy env и соединения отличаются
Multipart / SDP / ICE
UI выбирает boundary/offer/candidates и connection IDs
Новые boundary/ICE/DTLS fingerprints/ports закономерно отличаются. Это нормально; их фиксация ради маскировки не нужна
Opus
SDP opus/48000/2. Mono/20ms подтверждены для synthetic silence; параметры live encoder этим снимком не установлены
aiortc1.15 encoder stereo с mono source. Не байтовая копия helper, но живой V3 принял media
Route/model
Codex 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 main
1fc1702859381062eb7d3ffec2d3b0e310b1ebef, получен через fetch без checkout
Основной источник сравнения. Не доказывает запущенный image или live-конфигурацию.
Локальный webrtc-komaroff
f0a6eac965c7d6be8a353265775b676248855905
Старее main. Существующие untracked DOCS, presentations и videos сохранены.
Подтверждает его реализацию. Совпадение с image в работающем стеке не проверено.
Codex CLI
Официальные Rust-исходники rust-v0.156.1, сохранённый срез из предыдущего исследования
Подтверждает устройство клиента, RPC и signaling. Серверные модели ChatGPT закрыты.
Community fork
60cd00805b777bced0e6069d8bc52e529cc05cbb + незакоммиченные аудиодемо; Desktop-бандл 26.928.20755 повторно прочитан 2 октября
Фрагменты захвата проверены статически. Это не проверка настроек запущенного окна, качества звука или всего installed payload.
Успешные обращения к ChatGPT из прежнего учебника - исторические результаты, здесь они не повторялись. Для этого сравнения достаточно клиентского контракта. Мы не знаем внутреннее устройство серверного ASR/TTS, аппаратные ресурсы ChatGPT и его задержки в одинаковых с нашим проектом условиях.
WebRTC доставляет звук, но не понимает слова. ASR, или STT, превращает звук в текст. LLM выбирает или формирует текстовый ответ. TTS превращает этот текст в звук. Агент отличается от обычной модели тем, что может выбрать сценарий, вызвать инструмент, сохранить состояние и вернуть результат действия.
Термин
Простое объяснение
Зачем он здесь
SDP / signaling
Текстовое описание возможностей соединения и обмен им до разговора
POST /core/offer передаёт не запись голоса, а предложение WebRTC.
ICE, STUN, TURN
ICE выбирает путь; STUN помогает найти адрес; TURN ретранслирует медиа, когда прямой путь не получается
В автономной площадке эти службы либо местные, либо не нужны при доступных host candidates.
Сырые знаковые 16-битные сэмплы с little-endian порядком байтов
После декодирования WebRTC у нас ASR получает mono 16 кГц.
VAD
Определение участков речи и тишины
Граница фразы влияет на момент запуска распознавания и задержку ответа.
Sideband
Отдельный канал событий и команд, рядом с медиапотоком
Распознанный текст, состояние, отмена и вызовы tools не обязаны ехать вместе со звуком.
Сессия / ход / item
Сессия - разговор; ход - одна работа; item - реплика или результат внутри хода
Идентификаторы не дают старому ответу попасть в новый разговор.
Backpressure
Что делаем, когда потребитель медленнее источника
Нужно ограничение памяти и явная политика: ждать, отбрасывать или завершать поток с ошибкой.
p95 / RTF
p95 - граница для 95% замеров; RTF - время вычисления, делённое на длительность аудио
RTF < 1 нужен для устойчивого непрерывного распознавания на выбранной нагрузке; это не гарантия малой задержки первого текста.
Поэтому вопрос "у нас тоже WebRTC, почему ответ медленнее?" решается измерением всего пути. После быстрого транспорта могут идти ожидание конца фразы, файл, несколько вызовов модели и ожидание полного TTS. Сам WebRTC эти задержки не убирает.
Браузер согласовывает соединение через Core, а затем отправляет микрофон по WebRTC. Core декодирует и нормализует аудио, передаёт кадры в NATS. Соседний ASR bridge находит законченные фразы, записывает временный WAV и отправляет файл в LinTO. Только после получения текста начинается работа агента.
Текущий путь webrtc-komaroff: речь приходит, ответ возвращается текстом. Блоки открывают пояснения; сплошная стрелка - медиаданные, пунктир - сигнализация, события и вызовы сервисов.
Полученный ответ сценария сохраняется вместе с состоянием сессии и публикуется как текст или команды интерфейса. На схеме LinTO и LLM вынесены отдельно: текущая конфигурация использует соседний вычислительный контур, а не обязательно интернет. Для независимой площадки этот контур всё равно надо перенести внутрь её границы.
Обратный аудиотрек предусмотрен типом соединения sendrecv и обработчиком peer.ontrack в браузере. Но в проверенных handle_track и графе нет TTS producer и назначения его трека в sender Core. Наличие HTML audio element не доказывает, что агент говорит. Этот вывод относится к просмотренному voice-agent пути, а не ко всем экспериментам и TTS-демонстрациям репозитория.
У одного разговора несколько разных форматов. Между браузером и Core идут сжатые медиапакеты. Между Core и bridge идут сырые сэмплы. Между bridge и ASR снова файл с контейнером. Агент получает только текст. Ни один из этих форматов нельзя без преобразования подставить вместо другого.
Время идёт сверху вниз. После SDP медиа живёт своим циклом; шаги ниже описывают одну завершённую фразу.
Речь поступает кадрами, пока пользователь говорит. Распознавание одной фразы стартует после решения VAD, а агент - после final transcript. Pending показывается раньше текста, но не означает, что partial уже распознан. В этой последовательности результат Agent публикуется по NATS/WS: стрелка к браузеру не является новым прямым HTTP вызовом Agent.
Участок / endpoint
Запрос
Ответ / следствие
Browser → POST /core/offer
application/json: {sdp,type:"offer",session_id?}. SDP с ICE/DTLS параметрами, без аудио.
application/json: {sdp,type:"answer",session_id}. Core добавляет session_id после establish_connection.
Browser ↔ выбранный ICE peer Core
Opus в защищённом WebRTC RTP-потоке; кодек уточняется из SDP/getStats
Декодированные AudioFrame. Это не HTTP endpoint и не файл.
Core → inference.whisper.stream.<session>
4 байта BE длины JSON + UTF-8 JSON metadata + raw PCM16LE; mono, 16000 Гц, шаг 20 мс
seq считает пакеты, но producer не ждёт ASR acknowledgement каждого кадра.
ASR bridge → LINTO_HTTP_URL
POST, multipart поле file, WAV PCM16 mono 16k; query language=ru, Accept: text/plain. В DRS URL заканчивается /transcribe.
Ответ читается целиком, извлекается текст. Через настроенный proxy используется CONNECT, без скрытого direct fallback.
Bridge → inference.whisper.text.<session>
{pending:true,phrase_id,session_id}, затем {text,phrase_id,duration,session_id} либо error
Это pending и финальный текст, не доказанный поток partial transcript.
AgentInboundResponse; UI события публикуются отдельно. NATS request/reply не означает durable обработку.
Workflow → nats.llm.<user>
JSON LlmRequest: mode, input, constraints и trace envelope
JSON LlmResponse. Ollama adapter вызывает POST /api/chat с stream:false, local adapter - llama.cpp.
UI события → browser /ws
NATS по WebSocket; subject из конфигурации nats.events.user123
ServerEvent: type, kind, data, uid, service, time. Payload некоторых событий содержит trace/session_id; не единый session-specific поток.
Один кадр: 640 байт аудио, не считая метаданных
Иллюстрация текущей упаковки без отправки пакета. Нулевой звук взят вместо личной записи.
# Модуль нужен для UTF-8 представления метаданных перед сырыми сэмплами.importjson# Производитель объявляет формат сырых сэмплов, а не 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 не доказывает ни доставку микрофона, ни распознавание.
Сигнализация и события могут быть исправны одновременно с отказом медиатранспорта. Блоки ведут к контракту и признакам проверки.
Клиент отправляет offer, Core отвечает answer. Эти описания содержат кодеки, кандидаты и параметры защиты, но не записанное аудио. ICE проверяет пары адресов. Только найденная рабочая пара позволяет перейти к DTLS и передаче защищённых Opus-пакетов. Если ICE-запросы не получают ответа, Sonora и замена ASR-модели до этого этапа не доберутся.
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.
WebSocket не задаёт формат: что идёт внутри каждого WSS
WebSocket - двусторонний транспорт. В одном соединении можно передавать команды разговора, в другом - PCM в Base64, в третьем - протокол NATS. Само слово WSS не отвечает на вопрос, передаётся ли там звук. Это определяется контрактом конкретного клиента.
Путь
Адрес и сообщения
Где аудио
Код и разбор
Codex CLI V3
HTTPS /backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas; WSS api.openai.com/v1/live/…, связываемый с call ID
Медиа WebRTC отдельно. Sideband - события, текст и делегация.
Это форма события 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.
# Локальная сборка одного нулевого кадра. Не подключает 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.
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 A
178.20.41.87; авторитетный ответ согласован
Адрес из снимка, не постоянная настройка для копирования
DNS AAAA
NOERROR, записи AAAA нет
Не NXDOMAIN. Не обещаем работоспособность IPv6 STUN
Два наблюдения, не p95 или SLA. Это не тест TURN и не доставка медиа
Chrome, hostname
701 на IPv6; одновременно два успешных IPv4 srflx
Ошибка одного пути не описывает все сетевые интерфейсы
Chrome, IPv4 literal
701 отсутствует; два 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. Значит, убрать это событие недостаточно для восстановления звука.
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, события, медиа и распознавание
Слой
Наблюдение
Что это означает
Frontend
Live загрузил /assets/index-K_8EaX4G.js; панель отдельно показывала аудио и события
Проверено отображение диагностической сборки. Не доказан digest всех сервисов
Сигнализация
POST /core/offer → HTTP 200 и SDP answer
Core сформировал описание. Звуковой путь ещё не установлен
NATS /ws
События подключены при ошибке аудио
TCP/WSS-путь работает независимо от UDP-медиа
STUN
Binding success, IPv4 srflx; 701 убирался в отдельной пробе
Общий отказ STUN не подтверждён
ICE / DTLS
Checks без ответов, нет 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: медиа и события идут разными соединениями. Блоки открывают пояснения; сплошная стрелка - медиаданные, пунктир - сигнализация, события и вызовы сервисов.
App-server создаёт call по HTTPS и принимает текстовые события по WSS sideband. Helper отправляет и получает медиа непосредственно через WebRTC. Эти роли могут быть разделены логически, даже когда app-server встроен в процесс TUI: штатный embedded client использует InProcess/mpsc, а не строки JSON в stdin. STDIO относится к нашим отдельным RPC-демо.
Путь
Endpoint / формат
Что можно взять
CLI /voice, ChatGPT backend V3
POST https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas; JSON {sdp,session} → plain SDP answer + Location/call ID. Sideband wss://api.openai.com/v1/live/<call_id>; медиа Opus/SRTP по ICE.
Разделение сигнализации, событий и звука; call/thread/turn identities. Не сам закрытый endpoint.
Desktop диктовка
POST /backend-api/transcribe, multipart аудиофайл → JSON text; в бандле есть и отдельная потоковая ветка диктовки.
Отдельный файловый STT режим и строгая проверка MIME/контейнера. Не путать с voice conversation.
Desktop / ChatGPT voice conversation
В исследованном Desktop встречается иной маршрут /tbo/{tbo_id}/voice/calls; фактическая доступность режима для аккаунта не проверена.
Не утверждать, что весь Desktop использует CLI V3.
Read Aloud ChatGPT
GET /backend-api/synthesize?conversation_id=…&message_id=…&voice=…&format=mp3; ID существующего ответа → аудио.
Связь озвучки с конкретной сохранённой репликой; локальная реализация хранит собственный text/response_id.
Linux Read Aloud форка
Локальный Rust helper / CLI / MCP → Kokoro или custom command, Speech Dispatcher и другие backend; Piper подключается командой.
Provider adapter и локальные модели. MCP - оболочка инструмента, не медиа транспорт.
Типизированный контракт и события. Это альтернативный путь, не доказательство, что 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.
Сравнивать надо не имена библиотек, а границы работы. У нас есть аналог управляющей части app-server, но он распределён между Core, Agent и workflow runtime. Дополнительный сервис с названием app-server не нужен сам по себе: сначала достаточно согласованного интерфейса этих компонентов.
Обязанность
Codex / fork
webrtc-komaroff main
Решение
Медиа отдельно от бизнес-логики
voice-host и Realtime service; app-server управляет
aiortc Core отдельно от Agent; после декодирования PCM идёт через NATS
Сохранить разделение. Замена PCM/NATS - только после нагрузочного доказательства узкого места.
Управление жизнью разговора
realtime start/stop, события, cancellation token
connect/disconnect и pc_lifecycle; voice blocked через UI
Добавить явные состояния и единый cancel. Cleanup при ошибке уже есть в main.
Текстовые события и прогресс
item/transcript delta и завершение хода
pending, transcription, message, thought, model statuses
Bounded channels в ядре; демо ограничивает input queue
FanOut queue без лимита; publish task на каждый кадр
Исправить лимиты и политику перегрузки прежде роста нагрузки.
Речь агента в ответ
Двусторонний WebRTC audio; локальный Read Aloud отдельно
Текст ответа есть, обратный TTS producer в voice path не найден
Добавить локальный TTS и обратный track, сохраняя text-only режим.
Идентификаторы и долговечное состояние
thread/call/realtime session/turn/item
session/phrase/turn/request/trace; Postgres
Не заменить IDs одной UUID; добавить generation/epoch и response_id. Транзакция уже исправлена.
Инструменты и делегация
Handoff соединяет голос с ядром агента
Workflow вызывает собственные tools по NATS
Перенять явную связь request/result/cancel/permission, а не shell-функции Codex.
Полная автономность площадки
ChatGPT voice зависит от удалённого сервиса; локальная озвучка - иной путь
Локальный LLM путь есть; DRS указывает центральные ASR/LLM и leafnode
Замкнуть media, control, модели, assets и управление внутри каждой площадки.
Три уже выполненных улучшения исключены из нового плана: deadline ICE gathering, очистка front ресурсов при отказе signaling и транзакция runtime+history. Остаются вопросы порядка ходов, отмены и идемпотентности tools: SQL-транзакция не отменяет уже выполненное внешнее действие.
Изоляция означает, что новая площадка может загрузиться и вести разговор без основной сети, интернет-DNS, центрального брокера и внешних моделей. "Всё в Docker" для этого недостаточно: контейнер может обратиться к основному H100, скачать модель или загрузить style.json снаружи.
Зависимость
Что найдено
Что требуется внутри площадки
Медиа адреса
Браузер: stun.gis-master.ru; Core: iceServers=[]
Доступные host candidates либо свой STUN/TURN. При разных подсетях/запрете UDP нужен проверенный TURN; HTTPS ingress не ретранслирует RTP автоматически.
ASR
DRS: llm-models_linto_stt_whisper_http:80/transcribe через NATS proxy
Свой ASR с весами и VAD. Direct LAN HTTP или локальный proxy с контрактом CONNECT; не зависеть от main-network receiver.
LLM
LLM_MODE remote, nats-proxy-ollama.gis-master.ru; local llama.cpp путь существует
Свой Ollama в этой LAN либо локальный GGUF. Название remote в коде означает HTTP provider, а не обязательно интернет.
NATS topology
DRS: leafnode на 192.168.28.134:37422; локальный account + inference mappings
Самостоятельный broker и local responders. Удалённый leafnode в offline profile не нужен. Не смешивать изолированные площадки по user123.
Модели
LLM downloader обращается к Hugging Face при cache miss; VAD к GitHub при отсутствии ONNX
Предустановленные точные веса, лицензии, SHA256, проверка отсутствия файла до старта. Offline mode должен явно запрещать download, а не ждать DNS timeout.
Браузер и карта
MapLibre style: demotiles.maplibre.org/style.json
Локальные style, glyphs, sprites и tiles; отключаемый map fallback. Карта не должна блокировать голос. Это контекст UI, не замена исследования WebRTC.
HTTPS и устройства
getUserMedia требует безопасного контекста
Локальный доверенный CA, сертификаты, DNS и время. Для другого компьютера LAN HTTP обычно не подходит; localhost - специальное исключение.
Образы и пакеты
Образы и сборка используют registries и package sources
Заранее собранные images с digest, wheel/npm/model bundle; offline startup не запускает build/install/pull.
Состояние и наблюдаемость
Postgres, NATS events и OTel в проекте
Собственные DB, backup, collector, логи и secret store на каждой площадке; никакого обязательного центрального telemetry sink.
Не предлагаю свести всё к одной машине. Независимая сеть может содержать несколько серверов, включая собственный GPU. Для одной CPU-машины нужны отдельные тесты качества и задержек. Если одновременно модели и их локальные аналоги отсутствуют, останется только заранее описанный сценарный помощник с записанными репликами; свободный разговор обещать нельзя.
08Как собрать автономный контур, сохранив существующий проект
Предлагаю оставить браузер, aiortc, Agent, workflow и Postgres. Вокруг них задаём local-only профиль и добавляем обратное аудио. Голосовой Gateway здесь - ответственность Core, а не требование написать новый продукт или поставить Codex app-server в корпоративную систему.
Предложение: весь голосовой контур принадлежит одной изолированной площадке. Блоки открывают пояснения; сплошная стрелка - медиаданные, пунктир - сигнализация, события и вызовы сервисов.
Браузер: звук и понятное состояние
Браузер отправляет микрофон и воспроизводит обратный трек. В отдельном событийном канале он видит текущие session/turn IDs, распознанный текст, источник ответа, отмену и деградацию. Локальные NATS WS можно сохранить на первом шаге, но credentials должны быть с узкими правами. Позже application WSS gateway может скрыть broker от браузера и проверять доступ к сессии.
На схеме первые два блока находятся на одном клиентском устройстве. Для браузера сохраняем его встроенную обработку. Sonora предлагаем как альтернативу для нативного клиента, не как второй обязательный фильтр после browser AEC. Возвратная стрелка в AEC означает локальную копию реально проигрываемого PCM и его задержку, а не повторную отправку TTS на ASR.
Core: медиа, согласование формата и границы жизни
Core владеет peer connection, аудиочасами, bounded queues и ресемплингом. Он переводит WebRTC в собственный ASR контракт и ответ TTS обратно в WebRTC. Бизнес-сценарий остаётся в Agent: не надо переносить туда aiortc, а в Core - prompts и права tools. Локальный брокер соединяет workers, но не делает их автоматически независимыми от центральных моделей.
Опциональное подключение к основной сети строится позже как явно разрешённый adapter. Его отказ не должен менять local-only сценарий. В строгом offline profile fallback наружу запрещён. Можно синхронизировать выбранные обезличенные события, когда сеть снова появилась, но модельный контекст и голос не должны уходить туда без отдельной политики.
В выдержке из бокового чата верно разделены Silero и Sonora. Но наш путь содержит ещё один этап: до отправки звука браузер может пропустить его через energy gate. Поэтому сравниваем не названия библиотек, а последовательность обработки и то, что остаётся на выходе каждого этапа.
AEC удаляет акустическое эхо ответа из микрофона, NS уменьшает шум, AGC регулирует уровень. Gate закрывает сигнал ниже порога, а VAD находит участки речи. Opus решает транспортную задачу: сжимает звук. Эти механизмы не взаимозаменяемы; Silero можно сохранить после Sonora или browser AEC/NS.
Путь
Очистка / регулировка
Отбор речи
Что подтверждено и что нет
Наш браузер
getUserMedia запрашивает echoCancellation и noiseSuppression; defaults=true. AGC явно не задан.
Опциональный energy gate: RMS/dB → gain 0 или 1.
SOURCE. Реальные getSettings, акустическое качество и настройки конкретного устройства не измерены.
Наш Core → ASR bridge
Декодирование и приведение к PCM16LE mono 16k. В просмотренной цепочке отдельный denoiser/AEC не найден.
Silero ONNX VAD возвращает интервалы; bridge вырезает исходные сэмплы и пишет WAV.
SOURCE. Это сегментация, не очищенная нейросетью запись; внутренний preprocessing ASR provider не установлен.
Codex CLI 0.156.1
Локальный voice-host включает Sonora AEC/NS/AGC2; получает playout reference и задержку.
Серверная сегментация голосового разговора не установлена этим разбором.
SOURCE. Качество на наших устройствах не проверено; app-server сам звук не очищает.
Desktop: Codex voice
В просмотренном web provider явно mono + noiseSuppression:true. AEC/AGC не заданы явно.
Не приравнивается к серверному VAD или Silero нашего bridge.
STATIC BUNDLE. Отсутствие параметра не означает false; фактическая обработка браузера неизвестна.
Desktop: ChatGPT voice
AEC запрашивается по device policy; код читает getSettings().echoCancellation. Есть ducking воспроизведения.
Монитор активности помогает управлять громкостью ответа, но не доказывает серверную сегментацию.
STATIC BUNDLE. NS/AGC в этом фрагменте явно не заданы. Ducking не очищает микрофон.
Desktop: диктовка
Отдельные browser и native capture ветки. В browser-вызове явно задан mono, не универсальный набор AEC/NS.
Запись или streaming adapter → удалённый STT.
STATIC BUNDLE. Обработка native capture и закрытого STT не установлена; это не вызов voice-host Sonora.
CLI исследован по открытым Rust-исходникам 0.156.1, Desktop - по локальному бандлу 26.928.20755. Выбор флагов найден в коде, но качество и реальная настройка трека требуют отдельного теста. Эти версии нельзя объявлять одним аудиотрактом.
Наш тракт: очистка в браузере, отсечение тихого звука и отдельная сегментация на сервере. Блоки открывают пояснения; сплошная стрелка - медиаданные, пунктир - сигнализация, события и вызовы сервисов.
У нас: AEC и NS запрашиваются на стороне браузера
startMic передаёт два флага из audio state в getUserMedia. Они включены в defaults, но браузер и устройство могут обработать запрос по-разному. В просмотренном коде нет отдельного вызова Sonora, RNNoise или SpeexDSP в этой точке. На выходе - MediaStream, уже после запрошенной браузерной обработки. Снять настройки нужно у исходного микрофонного трека, до замены его треком Web Audio.
Параметры из нашего mic.ts; diagnostics getSettings предложена выше, но в приложение здесь не добавлена.
// Фрагмент параметров startMic. Это запрос обработки, а не замер качества.conststream=awaitnavigator.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 опущено.constactive=db>gate;// Порог сравнивается с общей энергией, не смыслом речи.consttarget=active?1.0:0.0;// Пропускаем либо закрываем весь сигнал.gainNode.gain.setTargetAtTime(target,ctx.currentTime,T);// Сглаживаем переход.// Это gain 0/1, а не восстановление речи из смеси с шумом.
WebRTC доставляет закодированное медиа; aiortc декодирует его. TrackSourceNode приводит аудио к mono PCM16LE 16 кГц и кадрам по 20 мс, WhisperStreamNode добавляет метаданные и публикует в NATS. Название normalization здесь относится к формату и длительности кадров. В этом коде не найден процессор очистки или автоматического выравнивания громкости. Детальный бинарный пример находится в главе о кадре NATS.
На стороне 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 с учётом реально воспроизводимого ответа. Блоки открывают пояснения; сплошная стрелка - медиаданные, пунктир - сигнализация, события и вызовы сервисов.
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-процессора не нужен.
render_output сохраняет реально проигранные сэмплы и временные данные. Capture worker сначала подаёт reference в процессор, затем обрабатывает микрофон; это позволяет сопоставить ответ из динамиков с его эхом. После mute или разрыва обработка сбрасывается, чтобы не использовать старую опору. Поставить Sonora на сервер только после получения PCM не равно этому пути: без синхронизированного render reference там нет сведений о реальном воспроизведении у пользователя.
App-server управляет сеансом, signaling, событиями и агентом. В этом V3-пути он не выполняет capture DSP и не несёт Opus между устройством и сервисом. Подробности - в архитектурном сравнении. В upstream есть синтетический тест влияния reference; здесь он не запускался и не заменяет акустический стенд.
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 из минифицированного бандла; оформление раскрыто.constWbo={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.constconstraints=policy==="nowhere"?{}:{echoCancellation:true};// Пустой объект оставляет настройки реализации, а не принудительно выключает AEC.const{echoCancellation}=microphoneTrack.getSettings();// Наблюдаемое значение.// Отсутствующий параметр нужно обозначать неизвестным, не заменять false.
Ducking уменьшает громкость ответа, но не очищает запись
В updateDucking при активности пользователя уменьшается playback volume; после паузы громкость восстанавливается. Монитор активности может использовать energy/Silero-ветку. Пользователю проще говорить поверх ответа, а эхо может стать тише. Но ducking не вычитает шум и эхо из микрофонных сэмплов. Это управление воспроизведением рядом с AEC, не его замена. Наличие Silero в таком мониторе не означает одинаковую с нашим ASR bridge сегментацию.
Диктовка Desktop не запускает тот же voice processor
У диктовки отдельный recorder/streaming adapter. В бандле есть выбор native microphoneCapture и browser capture; в просмотренном browser-вызове явно задаётся mono. Дальше адаптер отправляет запись или аудиопоток удалённому STT и получает текст. Этот код не доказывает Sonora, полный набор AEC/NS/AGC, обработку native capture или очистку внутри закрытого сервиса. Кнопку микрофона нельзя описывать как тот же путь, что разговор или CLI /voice.
Сохранённые фрагменты Desktop с SHA256 и byte offsets позволяют проверить эти выводы без абсолютных путей к компьютеру. Там только код обработки звука; credentials и пользовательские реплики не включены. Названия минифицированных функций относятся к этому бандлу и изменятся при пересборке.
Для существующего браузерного клиента сначала проверяем встроенную обработку, gate и Opus. Для собственного native клиента начинаем с Sonora, поскольку в Codex виден пример всей цепочки capture/render. Остальные варианты нужны для сравнительного стенда, а не для автоматического включения всех фильтров подряд.
Вариант
Назначение и условия
Статус для нашего проекта
Browser AEC/NS + Silero
Клиентская очистка и серверные границы фраз. Проверить settings исходного track, тихую речь, шум, динамики и double-talk.
Уже есть в коде; качество не измерено. Gate on/off - отдельный фактор.
Sonora
Локальные AEC/NS/AGC; native capture + render reference. API и лицензия.
Предложенный прототип, не внедрён. Silero сохраняется после очистки.
C++ WebRTC Audio Processing
Capture ProcessStream и render ProcessReverseStream. Контракт APM.
Альтернатива нативной интеграции; не подтверждение конкретного алгоритма браузера/Desktop.
SpeexDSP
Preprocessor и echo canceller для локального тракта; нужны reference и корректные параметры. Руководство.
Кандидат стенда. Сравнение качества и CPU на наших записях не выполнялось.
RNNoise
Нейросетевое шумоподавление, не самостоятельная замена AEC или VAD. Исходники и пример.
Кандидат для шума. Веса/сборку поставить заранее: autogen.sh upstream может скачивать модель.
Одинаковый стенд должен включать обычную и тихую речь, фоновый шум, только ответ из динамиков и одновременную речь собеседника с ответом. Сравниваем WER, потерянные границы VAD, ложные перебивания, остаточное эхо, clipping, задержку и CPU. Остаточное эхо сначала измеряем при молчащем пользователе; double-talk оцениваем отдельно, чтобы не принять подавленную живую речь за хорошее AEC. Ни Sonora, ни другая библиотека ещё не показали у нас выигрыш; цели и порядок внедрения приведены в плане работ.
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 и отключить прототип.
Мы используем 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());constreport=awaitpeer.getStats();// Читаем статистику уже работающего соединения.for(conststreamofreport.values()){// Смотрим только локальные отчёты об отправленном или полученном аудио.if(!["outbound-rtp","inbound-rtp"].includes(stream.type))continue;if((stream.kind??stream.mediaType)!=="audio"||stream.isRemote)continue;constcodec=report.get(stream.codecId);// Связываем поток с его кодеком.constpackets=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. Для сетей, где прямой путь невозможен, спроектировать локальный TURN
ICE selected/nominated, DTLS connected, RTP обе стороны, корректный teardown
Пример Codex полезен границами ответственности: медиа отдельно, управление отдельно, маленькие очереди, явная отмена и локальная обработка звука. В независимый контур переносим эти принципы и доступные локальные библиотеки. ChatGPT endpoint, OAuth и внешние модели автономную поставку не обеспечивают.
11STT: сначала управляем фразой, потом решаем, нужен ли streaming
У текущего bridge уже есть Silero VAD, phrase_id, pending и финальный текст. Это сохраняем. Первое изменение - локальный ASR provider с тем же HTTP-контрактом, заранее поставленными весами и явной readiness. Затем измеряем, сколько времени занимает ожидание границы фразы отдельно от распознавания.
Изменение
Почему
Приёмка и измеримый эффект
Зафиксировать VAD/start/end policy
Energy gate в browser и Silero на сервере решают разные задачи. Жёсткий browser gate может срезать тихие слова до ASR.
Корпус обычной/тихой речи и шума; WER, false end, missed start, p95 конца фразы. Цель оптимизации: уменьшить p95 end→final на ≥20% относительно baseline без ухудшения WER >1 процентного пункта.
Накопитель PCM с правильным хвостом
TrackSourceNode дополняет хвост каждого resampled frame до 20 мс. Это не равнозначно накоплению remainder между кадрами.
Для входа 44100/48000 Гц сравнить входную и выходную длительность на 60 с. Цель: drift ≤20 мс после flush; padding только одного конечного неполного кадра. Искажение сейчас не измерено.
Дополнительно streaming ASR adapter
Первые partial можно показать до окончания длинной фразы; текущий HTTP final путь этого не даёт.
Поток partial/final привязан к utterance_id; только final запускает необратимые tools. Цель для локального RTF и t_first_partial задаётся после теста железа; пример: p95 partial ≤500 мс после начала речи.
Правила ошибки ASR
Ошибка модели не должна висеть как вечный pending или превращаться в вопрос агенту.
При error/timeout UI получает финальную ошибку по тому же phrase_id; ни одного agent call на пустой/ошибочный transcript. Offline fallback - ручной текстовый ввод.
Цель в 500 мс - кандидат для будущего испытания, не утверждение о доступной нам модели. Если локальный ASR распознаёт только после конца фразы и этого хватает пользователю, streaming можно не добавлять. Файловый режим полезен для больших записей, но его очередь и таймауты отделяем от разговорных.
Первый рабочий шаг - озвучить подтверждённый ответ сценария своим TTS и отправить его по обратному WebRTC-треку. Сначала можно синтезировать целую короткую реплику. Потоковая генерация и синтез предложениями появляются позже, когда есть cancellation, порядок chunks и контроль того, что пользователь действительно услышал.
Вариант
Что переиспользовать
Ограничение
TTS worker на площадке
Идею backend adapter из Linux Read Aloud: text + voice/config → проверяемый audio result
MCP helper запускает локальное воспроизведение на своём компьютере. Его нельзя без адаптации назвать серверным WebRTC TTS.
Piper или другой локальный движок
Изолированное демо загрузки model+config и создания WAV, проверки ffprobe/полного decode
Выбор русского голоса, лицензии, CPU/GPU, качество терминов и RTF проверяются отдельно. Не обещаем равенство ChatGPT.
Предсинтезированные служебные реплики
Связь аудио с response_id + checksum и явной версией текста
Для "ASR недоступен" можно не ждать нейросеть. Нельзя выдать запись за свежий ответ инструмента.
Synthesis по завершённым фразам
Нумерация chunks и output lifecycle как в Realtime clients
Наш LLM adapter stream:false; mode возвращает структурированный JSON. Нельзя озвучивать токены JSON или ещё не проверенные результаты tools.
Browser speechSynthesis
Только дополнительный доступный режим
Голос браузера/ОС не всегда локальный или одинаковый на устройствах. Для строгой автономности не основной путь.
Проектируемая граница TTS. Значение ru-local - placeholder, а не гарантированно установленный голос.
# Предложение собственного интерфейса, не существующий endpoint проекта.job={"session_id":"s-1","turn_id":"t-1","response_id":"r-1","generation":4,"text":"Найден один аэропорт.","voice":"ru-local"}# TTS выдаёт сэмплы в своём формате, например PCM16LE mono 22050 Гц.# Voice Gateway ресемплирует их, задаёт монотонный PTS и кодирует в Opus.# RTP-clock Opus = 48000; он не объявляет исходные сэмплы TTS как 48000 Гц.# Каждый chunk несёт response_id, generation и seq; отменённые chunks удаляются.
Цель первой реализации: для 100 коротких ответов text→decoded audio завершается 100/100 раз, у каждого ответ и waveform имеют один response_id. После отмены удаляем ещё не отправленный хвост и не принимаем новые chunks старого ответа. Уже отправленные RTP пакеты и буфер браузера отозвать нельзя; слышимый хвост измеряем отдельно. Затем проверяем p95 first audio, RTF TTS и задержку перебивания. Наличие WAV и успешный decode - необходимые проверки, но качество произношения и слышимость проверяются отдельно.
Сейчас 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+=1old_turn.cancel()# Отменяем вычисления, которые действительно поддерживают отмену.playback_queue.clear()# Убираем ещё не услышанное аудио.# Сетевой callback со старой generation завершается без показа и воспроизведения.ifchunk_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, ни одного сетевого запроса.
Проба доказывает механизм роста, а не утечку или падение production. После восстановления потребителя очередь может уйти; время восстановления и рост памяти на реальной нагрузке ещё не измерены. Остановившийся потребитель способен оставить память пропорционально длине ожидания, поэтому одного semaphore недостаточно.
Граница
Предлагаемая политика
Критерий
PCM frame queue
Начальный лимит 10 × 20 мс = 200 мс; одновременно maxbytes и age. При overflow - gap/overload и явное решение ASR reset или отказ
qsize ≤10 при stalled consumer; старый звук не догоняет разговор через секунды. Silent drop может испортить слова и не подходит автоматически для записи файла.
NATS publishing
Один или небольшой фиксированный набор worker, bounded вход; preserve seq order или contract gap detection
Число task не растёт с длительностью stall; память выходит на плато в 10-минутном тесте.
ASR phrase jobs
Лимит количества/байтов/возраста фраз до постановки task; конечный deadline
При недоступном ASR новая фраза получает overload, UI выходит из pending. Большие файлы - отдельная durable очередь.
TTS playback
Небольшой prebuffer, лимит будущего аудио и sequence continuity
Cancel очищает непроигранный хвост; underrun/dropped audio измеряются, не скрываются.
Graph shutdown
Cancel tasks и wait с deadline, повторное close безопасно
Shutdown завершается даже при блокированном NATS/ASR. End marker не ждёт бесконечно занятого транспорта.
Повторяемая проба очередей. Ожидаемый результат относится к main 1fc1702; после исправления её проверки должны измениться.
# Запустить из корня webrtc-komaroff в окружении с numpy, av и nats-py.# --source-root указывает на исследуемый checkout; сеть не используется.
python3tooling/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 не поддерживает комментарии; поля объяснены ниже.
Session владеет историей; call - одно медиасоединение. Reconnect создаёт новый call, но не должен обнулить историю.
turn_id / response_id
User turn и ответ связаны, но не равны. У отменённого ответа остаётся собственный status.
generation / seq
Generation отсеивает старую работу; seq задаёт порядок событий внутри договорённого потока. Нельзя сравнивать seq независимых producers без агрегатора.
event_id / schema_version
Event_id обеспечивает дедупликацию/возобновление; версия даёт совместимость старых consumers.
ts_ms
Время для журналов. Для latency измерять monotonic на одном процессе или учитывать синхронизацию часов.
payload
Только данные разрешённого типа. Для TTS format и voice; для transcript текст и final; credentials сюда не попадают.
Состояния удобно держать раздельно: соединение connecting/connected/closed, вход listening/muted, работа transcribing/thinking/tool_running/speaking/cancelling. Mute и speaking могут быть одновременно, поэтому одна линейная строка статуса на всё будет давать ложные переходы. UI показывает наблюдаемое состояние, а не считает HTTP 200 доказательством слышимого ответа.
При reconnect клиент просит snapshot + события после cursor. Для media потерянное время не воспроизводится; для завершённых результатов допустим replay. Цель: в двух одновременных сессиях ни одного чужого сообщения/voice block; после reconnect - snapshot без дублирования финального ответа. Core NATS не хранит события для отсутствующего подписчика, это надо обеспечить на уровне приложения или отдельного durable stream.
16Сохранение хода, повтор запроса и права инструментов
В свежем main runtime_state и история сохраняются в одной транзакции. Тесты также подтверждают, что обычная ошибка сохранения не повторяет workflow. Но конфликт версии состояния ещё вызывает повтор workflow, а tool мог успеть выполнить действие до SQL commit. Для голоса с reconnect и повтором фразы это особенно чувствительно.
Что добавить
Зачем
Как измерить
Idempotency на границе входного хода
Повтор того же final ASR/HTTP request не должен создавать второй результат
Десять повторов одного ключа → один принятый ход и один final result. Ключ scoped по site/user/session/turn, а не просто строка text.
Idempotency key для tool
Повтор workflow при конфликте не должен второй раз менять внешний объект
Crash after tool success before commit: при восстановлении одно действие. Инструмент либо поддерживает ключ, либо требует reconcile; exactly-once не обещать.
Очередь/lease по сессии
Два хода не должны одновременно менять последовательную память разговора
Две параллельные фразы → детерминированная политика: очередь, reject либо interrupt, без потери версии.
Outbox результата
После commit сервис может упасть до publish UI события
Crash after commit before publish: после restart/reconnect ответ появляется один раз, без повторного tool.
Разрешения и аудит
Голосовая модель сообщает намерение, но не является источником прав
Любой недопустимый tool отклоняется server-side; speech injection не меняет права. Отменённое действие имеет явный terminal status.
История слышимого ответа
Сохранённый полный текст не равен прослушанному
Для interrupted response сохранить playback position/статус. Не транслировать время в слова без aligner; контексту явно сообщать об усечении.
В этом месте полезна идея Codex handoff: голосовой канал отдаёт агенту конкретную задачу и получает конкретный результат, сохраняя связь идентификаторов. Копировать терминал и unrestricted shell не надо. Нужны только доменные инструменты площадки с собственной проверкой прав и сроком жизни запроса.
17Локальные модели: совместимость интерфейса не гарантирует качество
В проекте уже два пути LLM: llama.cpp внутри процесса и Ollama через HTTP. Ollama можно поставить в той же изолированной сети. Для CPU-площадки GGUF путь полезен, но текущий init не содержит явной настройки GPU layers; установка GPU сама по себе не доказывает ускорение этого adapter.
Решение
Почему именно так
Результат и цена
Сначала local-only profile на существующем adapter
Меньше изменений в workflow/NATS контрактах
Ноль обращений к основной сети при cold start и разговоре. Качество structured JSON и tools проверяется на локальной модели.
Фиксировать модель и формат ответа
Downloader без model_file может выбрать минимальный GGUF. Замена файла меняет качество и память.
SHA256/version/license и конкретный GGUF path в manifest. Нет автоматического выбора другой quantization в offline run.
Readiness после реального model warmup
Существующий ready после ensure_model_path ещё не доказывает загрузку в llama.cpp или успешный infer
Один локальный canary: JSON по схеме и простой ответ; отдельно время cold/warm. Дешёвый health не должен постоянно запускать большие inference.
Детерминированные доменные пути
Для разрешённых команд можно не вызывать модель на каждом шаге
В main уже есть guard явного поиска аэропорта до _infer_json. Продолжение требует extraction/tools и не означает полную работу без LLM. Считать число infer/turn и точность маршрутизации.
Streaming LLM - позже
Сейчас stream:false и структурированные modes. Голос не должен читать невалидированный JSON
Отдельный разговорный text stream с финальной валидацией. Текст tools и критичные факты озвучиваются только после результата.
Деградация при недоступной модели
Полная автономность не равна наличию любого inference
Предложить text-only/recorded prompts/детерминированные команды согласно профилю; UI честно сообщает ограничение. Не пробовать тайно внешний provider.
Выбор конкретной ASR/LLM/TTS модели стоит сделать по одному русскому набору: тихая речь, имена аэропортов, номера рейсов, паузы, шум и попытки вызвать недопустимый инструмент. Без такого сравнения нельзя обосновать, что меньшая модель даст тот же результат быстрее. Ускорение измеряем на одном железе и одной нагрузке, качество - отдельно от синтаксической валидности JSON.
18Что можно переносить как код, а что только как приём
Совпадение архитектурной идеи не требует копирования большого куска Codex. Для нашего Python/TypeScript проекта дешевле собственный небольшой interface и тесты, чем зависимость от всего Rust-ядра с облачным разговорным контрактом. Перед переносом файлов отдельно проверить лицензии исходников, пакетов, весов и конкретного голоса.
Источник
Перенос
Адаптация / проверка
WebRTC isolated demo: queue/epoch/mute/cleanup
Небольшие алгоритмы и сценарии тестов; Python, aiortc и PyAV совпадают по классу задачи
Не копировать ChatGPT auth/signaling. Демо не имеет полноценного AEC и production reconnect.
Codex bounded channels + cancellation
Приём управления временем жизни; реализовать asyncio equivalents
Проверить shutdown и late callbacks. Не переносить числа capacity без измерения нашего latency.
Codex protocol types / item lifecycle
Принцип start/delta/done + ID и completion
Наш envelope с schema_version; не объявлять совместимость OpenAI RPC, если семантика другая.
Локальные TTS demos / helper
Проверку файлов/формата и adapter подготовки модели
Backend должен вернуть PCM/audio в Gateway, а не проиграть звук на сервере. Установленные voices и лицензии не переносим по умолчанию.
Proxy observation demo
Раздельный учёт HTTPS/WSS и ICE/RTP
Для isolated LAN - allowlist фактических peers и отсутствие egress. HTTP proxy не закрывает UDP маршрут автоматически.
Штатные HTTP диктовка / synthesis
Контрактная дисциплина: точный MIME, correlation, полное декодирование результата
Не зависимость от ChatGPT endpoints. Собственный ASR/TTS контракт и локальная авторизация.
ChatGPT model/session internals
Не переносим
Закрытые модели/веса/сервис недоступны. Нельзя обещать on-prem ChatGPT voice из fork исходников.
MCP / CLI read_aloud
Возможный административный или тестовый интерфейс
MCP не заменяет WebRTC, VAD, сессию и потоковый playback.
LiveKit/SFU не включаю в обязательный первый этап. Сейчас разговор один-на-один с сервером; aiortc уже используется. Рассматривать смену transport runtime стоит после проблем распределения peers, TURN эксплуатации или измеренной нагрузки. Новый framework добавит серверы, зависимости и offline packaging; само его имя не уменьшает latency модели.
Базовая единица замера - один законченный пользовательский ход в конкретной сессии. Отдельно пишем длительность аудио, размер моделей/quantization, версии, CPU/GPU/RAM, число клиентов, сеть и cold/warm режим. Сравниваем одинаковые входы. Тест на наушниках не заменяет тест AEC с динамиками.
Метрика
Точки времени / счётчик
Предлагаемая цель, не достигнутый результат
t_first_partial / t_final_STT
Начало речи→первый partial; конец акустической речи→final
Сначала baseline 100+ реплик; p50/p95. Для streaming-кандидата partial p95 ≤500 мс; конец→final снизить ≥20% без потери WER >1 п.п.
t_agent / TTFT
Final ASR→agent result; для stream - первый разрешённый текст
Считать infer/turn и tools duration. Снижение infer calls должно сопровождаться сохранением task success, не только speed.
t_first_audio
Конец пользовательской речи→первые декодированные сэмплы ответа у browser
Бюджет-предложение для короткого локального сценария p95 ≤2 с; подтвердить на целевом железе. HTTP 200/TTS accepted - не first audio.
t_interrupt
Речь пользователя во время ответа→прекращение слышимого старого аудио
p95 ≤300 мс как цель; старые chunks не принимаются после cancel generation. Уже переданные/буферизованные RTP пакеты нельзя отозвать: фактический слышимый хвост измеряется отдельно.
RTF ASR/TTS и вместимость
Compute time/audio duration, GPU memory и очередь при N clients
RTF <1 на N_accept, без бесконечного queue growth. N_accept устанавливается из испытания, не из MAX_CONCURRENCY.
0 чужих сессий и дублированных tool side effects в fault-тестах; WER/task success сравниваются на неизменном корпусе.
Автономность
Процессы и трафик при отключённой основной сети
10/10 cold starts + 100 диалогов без внешних успешных соединений и без попыток egress приложения; missing model даёт local error, не download.
Ресурсы и восстановление
100 close/reconnect циклов, worker failure, CPU/RAM и task count
Ресурсы возвращаются к baseline; cancel/shutdown deadline соблюдается; результат сохранённого tool не повторяется.
Предложение instrumentation. В общие журналы не выводить SDP, ICE credentials, tokens и личный transcript.
// Пример чтения медиаметрик в месте, где приложение владеет RTCPeerConnection.// Это не готовая команда для браузерной консоли: pc берётся из кода приложения.constreport=awaitpc.getStats();for(constitemofreport.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 mic
Deadline и cleanup уже покрыты частью тестов main; добавить повторный connect и частично созданные ресурсы.
Точная длительность после resample/flush; bounded queues и task count; gap policy проверяется на WER.
STT/agent
Partial меняется, final повторяется, pending→error, неверный phrase_id
Partial не запускает tool; один final принят один раз; ошибочный transcript не становится запросом.
Двустороннее media
Файл-фикстура → local ASR → agent echo/controlled reply → local TTS → browser capture
Соответствие каждому request ID; полный decode и параметры; отдельно ручная оценка текста, произношения и слышимости.
Сессии
Два browser клиента одного пользователя; reconnect с cursor; позднее событие old call
Ни одного чужого voice block/ответа; generation отсекает старый поток, snapshot не дублирует результат.
Инструменты
Повтор запроса; crash до/после tool/commit/publish; конфликт runtime
Нет повторного side effect; outbox/reconcile даёт финальный результат; права проверяются сервером.
Прерывание
Mute/unmute с queued capture; речь на фоне TTS; Stop во время ASR/LLM/tool
Отдельная политика каждого действия. Late callback не оживляет ответ; незавершённые задачи освобождены.
Сеть и ресурсы
Host candidates; разные VLAN; UDP forbidden; только локальный TURN; отказ DNS/ASR/NATS
Проверяем selected ICE candidate pair и все application peers. Отказ одного этапа виден как конкретная ошибка, не бесконечный spinner.
Offline cold start
Пакет предзагружен, internet/main-network отключены; отдельно отсутствует модель
В первом случае диалог работает; во втором отказ локальный и понятный. Блокировка egress без попыток сети - более строгий критерий, чем просто отсутствие успешного HTTP.
Негативные тесты должны показать точную границу отказа: parser, ASR provider, Agent, TTS или media. Для load-теста увеличиваем число сессий, пока хотя бы один критерий не нарушится; последняя успешная нагрузка становится подтверждённым N_accept. Команду "docker compose up" нельзя объявить проверкой полной автономности.
21Порядок работ: сначала автономность и ограничения, затем разговор
Менять транспорт, модель, схему сессии и workflow сразу неудобно: при регрессии непонятно, где искать причину. Ниже этапы, после каждого остаётся рабочий режим. Это проект изменений, не внесённые этим документом изменения приложения.
Этап
Область и конкретная работа
Как принимаем
Цена / откат
0. Baseline и версия
Подтвердить deployed SHA/image отдельно от main; собрать 100 реплик и запись этапов. Использовать свежие ICE/cleanup/transaction исправления.
Воспроизводимый baseline с hardware/model config и p50/p95.
Только read-only сбор. Никакой обещанной экономии до baseline.
0a. Клиентский звук и Opus
Фактические getSettings, SDP/getStats и активный codec в обе стороны. Sonora: отдельно нативный прототип с playout reference и задержкой, без двойной обработки по умолчанию.
Сравнить browser AEC и Sonora на динамиках: эхо, false barge-in, WER, p95 задержки/CPU; loss/jitter тест Opus. Выбор кодека подтверждён счётчиками.
Feature flag; browser path сохраняется. Sonora не заменяет ASR/TTS и не переносится вслепую на сервер.
1. Изолированный профиль
Local DNS/CA/STUN/TURN по необходимости; свои NATS/DB/ASR/LLM; модели/assets/images заранее; startup без downloads.
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; сетевой отказ не гарантирован
Обрезанный файл / нечётный PCM
WAV validators и TTS decoder отказывают; старый результат не перезаписывается
MIME или 200 не означают целый аудиопоток
Неверный Base64
validate=True; ошибка не объявляется успехом и socket закрывается
Base64 не сообщает исходный codec
Пустой / failed / незавершённый ответ
STT требует commit item + непустой final; GA TTS требует свой response ID + completed + PCM
appendSpeech имеет эвристику паузы, поэтому его полнота слабее explicit done
Чужой item/response
STT item_id и GA TTS response_id корреляция протестированы
V3 приватные turn captions не содержат тот же GA response_id: привязка - отдельный call/socket
Ни один из этих тестов не стартует Desktop. Fake RPC может запустить только свой тестовый процесс. Живые файлы сохраняются в отдельный каталог; в учебнике только параметры и счётчики, без личных ответов и credentials.
Ограничения и невоспроизведённые способы
Способ
Результат на 30.09.2026
Что можно утверждать
OpenAI /v1/audio/speech
429 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 кнопка в установленном payload
Backend протестирован, patch/gate отсутствуют в исследованном снимке
Исходники функции и рабочий backend не означают включённую кнопку UI
Статические проверки контролируют уникальность 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 отдельной копии. Сервер в тестах имитируется.
Не проверено живьёмМикрофон, динамики, полный голосовой сеанс CLI, фактические URL/ICE его соединения. Приложение не запускалось и не перезапускалось.
Не установленоТочная модель STT внутри CLI; будущая доступность OAuth-STT; качество на разных языках; gpt-live-transcribe и Platform key для этого нового демо.
Получение 96 символов подтверждает непустой ответ сервиса, но не независимую проверку дословной точности. Аудио, транскрипт и credentials не добавлены в репозиторий. Совпадение версии установленного бинарника и тега - не побайтовое доказательство воспроизводимой сборки.
Команды 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. Его имя не означает, что распознавание речи выполняется на вашем компьютере.
Схемы показывают направление, здесь приведены конкретные сообщения. ID и PCM-отсчёты учебные. Это не дампы личного сеанса. На каждый контракт распространяются условия собственного входа и датированного evidence соответствующей главы.
CLI ↔ локальный app-server
Локальный API · thread/realtime/start; stop; sdp; transcript/delta; transcript/done
Типизированный RPC: встроенные каналы / Unix-сокет / remote; STDIO только у subprocess-демо
Передаём:
Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.
В CLI 0.156.1 WebRTC без version выбирает V1, поэтому V3 указан явно. Результат с id=10 подтверждает вызов, а SDP и реплики приходят уведомлениями отдельно; порядок списка здесь учебный, не гарантия планировщика. Встроенный TUI использует InProcess, daemon - Unix-сокет; оболочка JSON на строку относится к нашему отдельному app-server процессу. Это не HTTP endpoint и не аудиопакеты. Перед start нужны initialize/initialized и thread.
Команды управления + SDP; аудио между устройствами и helper локальное
Передаём:
Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.
Согласование SDP и управление helper; это не HTTP-запрос.
Получаем:
Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.
Звук с устройства / в устройство и состояние helper.
Пример описывает смысл команд, а не выдуманную JSON-схему IPC. Поля IPC нужно брать из конкретной версии helper. Частота устройства не тождественна RTP-clock Opus.
Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.
HTTP 200/201; тело начинается с v=0 (SDP answer), заголовок Location содержит call ID.
Пример полей взят из независимого V3-демо. Версионный заголовок OpenAI-Alpha: quicksilver=v2; Authorization только в заголовке. Настоящие SDP/ICE credentials и call ID не публикуем. Это не отправка аудиофайла и не GA Realtime WS.
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.
Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.
{
"text": "Привет"
}
Boundary разделяет поля multipart. Файл не превращается в Base64. В Bash-демо только file; Desktop может передать language и metadata попытки/сообщения. FLAC нашего примера не означает, что MediaRecorder Desktop пишет FLAC. Серверный source закрыт.
Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.
Connect-info: websocketUrl + protocols; поток затем возвращает транскрипт. Полный контракт текстовых событий в этой итерации не воспроизведён.
source-only: connect-info и audio.append найдены в бандле. WSS адрес берётся из ответа; нативная bridge-ветка использует /dictation/stream, но фиксированный публичный URL здесь не установлен. 48000 - иллюстрация значения; клиент передаёт фактическую частоту AudioContext. При сбое возможен файловый fallback.
Чанк демо - до 48000 байт (секунда PCM). В примере Base64 содержит лишь два синтетических отсчёта +1/-1: этого недостаточно для реальной STT. Completed принимается только для item ID своего committed входа. Это не транспорт CLI /voice.
Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.
HTTP 200; аудиобайты MP3 (audio/*); проверить полное декодирование ffmpeg.
Вход - текст, не файл аудио и не ID ответа. Формат query mp3; MIME и контейнер результата проверяются отдельно. Доступ приватного ChatGPT API зависит от аккаунта; успешная историческая проба не обещает будущую доступность.
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.
Сначала session.update задаёт realtime, output_modalities=[audio], voice, формат audio/pcm rate=24000 и отключённый VAD. Затем ждём session.updated. Чанки своего response ID собираются до response.done completed; WAV формируется только локально. В учебной delta два PCM-отсчёта, не настоящий ответ сервиса.
Это текст для произнесения, не appendAudio. Для опыта выбран start transport=websocket, version=v2. PCM16 mono24000 подтверждён декодированием опыта; ширина сэмпла не отдельное поле RPC. Учебный data - два синтетических отсчёта. Демо завершает сбор по паузе 3 с, не гарантированному end-of-audio событию.
CLI0.156.1 строит URL из provider и config; для v2 это /realtime, для V3 frameless - /live. Конкретный URL app-server опыта не был захвачен NetLog: здесь source-контракт, а не заявление о наблюдавшемся адресе. apiKey-путь проверяется отдельно от ChatGPT auth.
Учебный JSON. Поля и ограничения объяснены рядом; значения ID не являются личными.
{
"spoken": true,
"engine": "kokoro"
}
JS backend выбирает свою команду либо настроенный движок/системный fallback. Не путать с удалённым ChatGPT GET /synthesize. Если установлена custom command, сеть и формат определяет именно она.
Параметры зависят от движка/модели, не универсальный формат всех TTS. Непосредственная кнопка и Speech Dispatcher могут проигрывать звук, не сохраняя WAV. В native-схеме работает JS/Rust helper, а local-file.py - только наше отдельное демо.
Поясняющий фрагмент или результат, не готовая команда. Его смысл указан в этом разделе.
MCP CallToolResult с text/structuredContent; read_aloud сообщает результат запуска, а не возвращает MP3 по HTTP.
Сначала initialize и notifications/initialized. doctor проверяет backend, stop завершает речь. MCP - оболочка вызова; локальность/сеть определяется backend, а не самим названием MCP.
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/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 - датированный ответ удалённого сервиса. Ни один не подменяет остальные. Доказательства.
Аудит четырьмя ролями и карта покрытия
Вопросы ученика по каждой главе
Ученик просил объяснить переходы и незнакомые термины. Преподаватель свёл ответы после сверки ролей архитектором и контрактов аналитиком. В последней колонке - раздел с кодом и проверкой, а не ссылка на неподтверждённый серверный исходник.
Диктовка нужна для текста в поле ввода, озвучка для готового текста, разговор для двустороннего сеанса. Путь выбирает обработчик, а не расширение входного файла.
Нет. Это интерфейс ядра Codex. Встроенный TUI использует InProcess, daemon может использовать Unix-сокет. Наш appendSpeech-скрипт запускает отдельный процесс со STDIO.
Книга и исходники переносимы. Credentials в архив не входят; venv создаётся заново. Команды выбирают внешний auth-файл явно и не обновляют основной вход.
Клиент упаковывает целый контейнер в multipart file. Удалённая сторона возвращает JSON text. Локальная проверка сигнатуры не заменяет полного декодирования файла.
Нет. MCP вызывает helper. Piper, Kokoro, eSpeak и Speech Dispatcher работают локально после подготовки; custom backend может быть сетевым, если так настроен.
Нет. Здесь WSS получает Base64 PCM и возвращает transcript committed item. CLI V3 отдельно согласует WebRTC и получает звук с текстовыми событиями sideband.
Нет. Это экспериментальная локальная конфигурация временного профиля. appendSpeech отправляет текст, а outputAudio/delta возвращает PCM; дословность не гарантирована.
У выбранного V3 helper отправляет WebRTC media сервису. App-server создаёт call, держит sideband и координирует агента. Другое RPC appendAudio действительно переносит чанки через app-server.
Нет. RPC result подтверждает вызов. thread/realtime/sdp и transcript приходят уведомлениями. HTTPS signaling и WSS sideband не являются аудиопакетами RTP.
Наш процесс выполняет signaling, WSS и media. Команды агента не выполняются. Mute прекращает захват и очищает очередь. Файловая проба ограничена одной репликой; хвост не доказывает полный drain.
Он доказывает попытку отправки. RTP stats, выбранная пара, transcript и полученное аудио подтверждают следующие слои. Полный внешний маршрут машины неизвестен.
Нет. Формат данных может совпасть, но boundary, TLS, SDP/ICE и ID различаются. Cookies, integrity и attestation не подделываются, неразличимость не обещается.
Нет. Проверяем корреляцию, завершение, сигнатуру и полное декодирование. Неверная частота PCM может быть принята, но дать ускоренную или испорченную речь.
Они поясняют структуру, не заменяют реальный call/item/message и достаточную запись. Запускаемые примеры отдельно связаны с каталогами, README и тестами.
Нет. Они проверяют факты, ссылки и алгоритм навигации. Настоящие keyboard events, mobile layout и печать остаются непроверенными из-за запрета просмотра.
Подписанный источник и дата сохранены. Они не выдаются за свежие кадры установленного Community.
Аудит выполнен четырьмя отдельными агентами: ученик задал вопросы по всем главам, преподаватель подготовил ответы, аналитик сверил данные и архитектор проверил роли. Главный агент свёл результаты и исправления. Проверены 17 прежних основных глав, 7 proxy-глав и 12 UI-разделов; ниже сохранена карта всех 23 схем.
Это редакторская и локальная проверка от 01.10.2026. Новые live-пробы и браузерная приёмка не выполнялись. Путь найден в исходнике не равен доказательству работающей кнопки.
Замечания и проверка исправлений
Вопрос ученика
Проверенное объяснение
Источник и тест
V3 start
Без version выбиралась V1. В примере теперь явно version=v3.
Почему 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 закрывает открытые ресурсы.
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.
_process_message разбирает wire packet; _handle_frame накапливает сэмплы; VAD выбирает сегменты; _transcribe_and_publish пишет временный WAV, ждёт слот и вызывает transcribe_http_text. После обработки WAV удаляется в finally. Это не доказательство удаления всех данных сервера LinTO или журналов.
AgentServer.handle_request валидирует AgentInboundRequest, запускает runner и публикует UI события. Runner передаёт сценарную работу выбранному workflow через NATS, сохраняет состояние и историю, возвращает typed response. Голосовые identifiers полезны для корреляции, но нынешняя обработка остаётся текстовой.
Свежий 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 профиль обязан выбирать конкретный файл и проверять его наличие.
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.
23Что подтверждено, что предложено и чего пока не знаем
Ниже сохранён source-разбор исходной редакции. Новые live-наблюдения добавлены в главу диагностики; полный voice E2E, качество ASR и runtime digest всё ещё не подтверждены. Опоры на код открываются из этого архива, без соседнего checkout.
VERIFIED / SOURCE означает, что связь найдена в коде или конфигурации. LOCAL TEST - что конкретная проверка выполнена без живых сервисов. INFERRED - механизм риска или ожидаемый эффект без нужного замера. PROPOSED - проект изменения. UNKNOWN - сведений недостаточно. Эти статусы не превращаются в production evidence от наличия хорошей схемы.
ID
Утверждение
Статус
Опора
Граница
C01
Core создаёт peer, media после SDP идёт по WebRTC, не HTTP upload
Бандл 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.
Ниже сохранён 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.
Роли 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.