# Аудио в ChatGPT Community: app-server, диктовка и прямой API

Переносимый запуск: команды ниже выполняются из корня комплекта, не из каталога этой страницы. Подготовка описана в [AUDIO-README.md](</experiments/voice-ai/archive/c/DOCS/voice-systems-textbook-2026-10-02/voice-systems-textbook/AUDIO-README.md>).

Схемы и примеры сообщений: [аудиоучебник](</experiments/voice-ai/archive/c/DOCS/voice-systems-textbook-2026-10-02/voice-systems-textbook/docs/codex-cli-voice-textbook.html#appserver>),
[интерфейс → обработчик → endpoint](</experiments/voice-ai/archive/c/DOCS/voice-systems-textbook-2026-10-02/voice-systems-textbook/docs/audio-ui-entrypoints.html>),
[WebRTC через proxy](</experiments/voice-ai/archive/c/DOCS/voice-systems-textbook-2026-10-02/voice-systems-textbook/docs/codex-voice-proxy-textbook.html>). Версии и результаты
этого исторического отчёта ниже сохранены отдельно от дополнений учебника.

Проверено 29 сентября 2026 г. для собранного здесь ChatGPT Community на основе
официального Linux-пакета `chatgpt 26.924.50649` и bundled `codex app-server`
`0.158.0-alpha.2.1`. Это исследование контракта установленной версии, а не
обещание стабильного публичного API. В репозитории не менялся код приложения.

## Короткий ответ

- **Голосовой разговор Codex Desktop:** интерфейс получает микрофон через
  `getUserMedia`, создаёт `RTCPeerConnection`, передаёт аудио WebRTC-дорожкой и
  получает ответ той же технологией. App-server управляет сеансом через
  `thread/realtime/*`; он не является обычным HTTP upload endpoint для файлов.
- **Альтернативный экспериментальный транспорт app-server:**
  `thread/realtime/start` допускает `transport: {"type":"websocket"}`.
  `thread/realtime/appendAudio` принимает аудиочанк в JSON-RPC, а
  `thread/realtime/outputAudio/delta` выдаёт чанки ответа. Это путь для
  собственного клиента, не стандартное голосовое окно Desktop.
- **Диктовка Desktop:** приложение отправляет запись на приватный `/transcribe`
  или поток PCM16 на `/dictation/stream`. Распознавание выполняется удалённым
  сервисом, а не локальной моделью в Electron. Это другой API, не
  `thread/realtime/appendAudio`.
- **Прямой публичный API:** готовый файл можно отправить на
  `POST https://api.openai.com/v1/audio/transcriptions`; голосовой ответ на
  запись можно получить через
  `wss://api.openai.com/v1/realtime?model=gpt-realtime-2.1`. Нужен настоящий
  OpenAI Platform API key; вход в ChatGPT Desktop и gateway-маркер `0000` его
  не заменяют.

### Проверка прямого приватного маршрута 29 сентября 2026 г.

Первый отдельный процесс отправил `исторический-каталог/Documents/raven.flac`
(FLAC, 44,1 кГц, стерео, 133316 байт) на
`https://chatgpt.com/backend-api/transcribe`. Он использовал ChatGPT access
token из `~/.codex/auth.json`, но получил **HTTP 403,
`cf-mitigated: challenge`**. Даже обычный `HEAD https://chatgpt.com/` из `curl`
здесь получал тот же challenge. Это не доказывало ошибку токена, формата
файла или STT: запрос мог быть остановлен Cloudflare до backend.

После отдельной диктовки в авторизованном Community закрытый Electron NetLog
зафиксировал **реальный `POST /backend-api/transcribe` по HTTP/2**: 76 621 байт
multipart, `Authorization: Bearer`, `ChatGPT-Account-Id`, cookies,
`originator: Codex Desktop`, User-Agent с префиксом `CodexBrowser`.
Ответ — **200, `application/json`**. Значения токена, cookies и account ID
остались только в локальном закрытом журнале и не включены в репозиторий.
Запись Desktop была сформирована из микрофона; это не доказательство, что
Desktop отправил исходный FLAC без преобразования.

Контрольные запросы с теми же локальным токеном, `raven.flac` и Python-клиентом
показали: стандартный User-Agent `Python-urllib` снова даёт 403 challenge;
**замена только User-Agent** на наблюдавшийся у Desktop даёт 200 и JSON с
полем `text`. Только cookies либо только `originator` 403 не устраняют.
Обычный браузерный User-Agent без префикса `CodexBrowser` тоже дал 200.
Следовательно, в этой проверке решающим отличием оказался User-Agent, а не
необходимость копировать cookies или менять HTTP-клиент. Конкретное правило
Cloudflare нам неизвестно, и поведение приватного API может измениться.

Минимальная повторная проба теперь работает так:

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

Скрипт использует наблюдавшийся Desktop User-Agent и сохранённый локальный
ChatGPT-токен; выводит транскрипт, но не токен и не cookies. Истёкший токен
потребует обновления входа штатным клиентом. Скрипт не является
официальным клиентом и не использует публичный OpenAI Platform API.

Проверка реализации также показывает, откуда берутся авторизация и файл. Renderer
создаёт multipart `file` + опциональный `language` и вызывает свой `fetch`
с внутренними маркерами `X-OpenAI-Attach-Auth` и
`X-OpenAI-Attach-Integrity-State`. Electron main удаляет эти маркеры, берёт
**актуальный** токен у app-server, добавляет `Authorization`,
`ChatGPT-Account-Id` и `originator`, при наличии — состояние `X-OAI-IS`,
затем отправляет запрос через `applicationNetwork.fetch` и допускает одно
обновление токена после 401. В наблюдённом запросе `X-OAI-IS` отсутствовал.
Это не публичный HTTP proxy и не локальный слушающий сокет: для независимого
процесса потребовалось отдельно воспроизвести HTTP-запрос.

Практическая проба через уже авторизованное приложение без копирования
credential — подать файл на временный виртуальный микрофон:

```bash
# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
bash scripts/voice-probes/dictate-through-desktop.sh --dry-run \
  "$INPUT_AUDIO"
bash scripts/voice-probes/dictate-through-desktop.sh --manual-start \
  "$INPUT_AUDIO"
```

После сообщения `Waiting for Enter` нужно включить **диктовку текста** в
Community и нажать Enter в терминале; после воспроизведения остановить её и
прочитать результат в поле ввода, не
отправляя сообщение. Скрипт только декодирует FLAC для виртуального
микрофона, не выполняет STT и восстанавливает прежний источник звука. Это
проверка **штатного UI-маршрута**, а не произвольный файловый HTTP API.
В живой пробе 29 сентября скрипт завершился с кодом 0, источник звука
вернулся к исходному и пользователь подтвердил, что полученный в поле ввода
текст соответствует `raven.flac` по смыслу. Это `VERIFIED / RUNTIME` для
цепочки «файл → виртуальный микрофон → авторизованная диктовка Desktop →
текст». Отдельный NetLog теперь подтверждает HTTP-маршрут `/transcribe` для
следующей диктовки. Desktop получил PCM-звук и сам сформировал свою запись;
формат FLAC на сервере подтверждён только отдельным успешным Python-запросом.

Отдельный `codex app-server` с текущим ChatGPT-входом успешно выполнил
`initialize`, `thread/start` с `ephemeral: true` и принял
`thread/realtime/start` с текстовым WebSocket-транспортом, но затем прислал
`thread/realtime/error` с сообщением `realtime conversation requires API key
auth`. Этот транспорт в установленной версии тоже **не даёт** транскрипцию
через текущий ChatGPT-вход. Он не равен маршруту диктовки Desktop.

Официальные контракты: [app-server](<https://learn.chatgpt.com/docs/app-server>),
[file transcription](<https://developers.openai.com/api/docs/guides/speech-to-text>),
[Realtime WebSocket](<https://developers.openai.com/api/docs/guides/voice-websockets>),
[Realtime WebRTC](<https://developers.openai.com/api/docs/guides/voice-webrtc>).

## Как получен контракт app-server

Источник — **генерируемая установленным бинарником экспериментальная схема**, а
не предполагаемые URL. Повторить проверку после обновления можно так:

```bash
# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
cd "$repo"
schema_dir="$(mktemp -d "$audio_out/codex-voice-schema.XXXXXX")"
./codex-app/resources/codex app-server generate-json-schema \
  --experimental --out "$schema_dir"
rg 'thread/realtime/' "$schema_dir/ClientRequest.json" \
  "$schema_dir/ServerNotification.json"
```

App-server здесь — JSON-RPC. Его локальный `stdio://` переносит один JSON-объект
на строку. `--listen ws://127.0.0.1:PORT` меняет **транспорт JSON-RPC клиента к
app-server**, а не автоматически превращает WebRTC-аудио Desktop в WebSocket.
Методы ниже требуют `initialize` с `capabilities.experimentalApi: true`.

### Клиентские запросы (все шесть `thread/realtime/*` в этой схеме)

| Метод | Вход | Непосредственный ответ | Аудио/файл |
| --- | --- | --- | --- |
| `thread/realtime/listVoices` | `{}` | `voices`: массивы `v1`, `v2`, значения `defaultV1`, `defaultV2` | Нет. Только каталог голосов. |
| `thread/realtime/start` | Обязательны `threadId`, `outputModality: "audio" \| "text"`. `transport`: `{"type":"websocket"}`, `{"type":"webrtc","sdp":"…"}` или `{"type":"existingCall","callId":"…"}`; опционально `voice`, `model`, `version`, `prompt`, `initialItems` и настройки handoff. | Схема описывает объект ответа без определённых полей; начало и SDP приходят **отдельными уведомлениями**. | Для WebRTC передаётся **SDP, не аудио**. Для existingCall — ID вызова. |
| `thread/realtime/stop` | `threadId` | Объект ответа без определённых полей; затем `closed`. | Нет. |
| `thread/realtime/appendAudio` | `threadId`, `audio: {data, sampleRate, numChannels, itemId?, samplesPerChannel?}` | Объект ответа без определённых полей. Транскрипт или ответ приходят асинхронно. | **Да, чанк**, строка `data` внутри JSON-RPC. Это не имя файла и не multipart. Схема не указывает кодировку `data`; без проверки реализации нельзя обещать, что произвольный base64 WAV здесь сработает. |
| `thread/realtime/appendText` | `threadId`, `text`, опционально `role: "user" \| "developer" \| "assistant"` | Объект ответа без определённых полей. | Нет. |
| `thread/realtime/appendSpeech` | `threadId`, `text` | Объект ответа без определённых полей. | Нет: передаётся **текст для произнесения**, не звуковой файл. |

`ThreadRealtimeAudioChunk.data` имеет тип `string`; `sampleRate` и
`numChannels` обязательны, `itemId` и `samplesPerChannel` опциональны.
Схема не задаёт файловый контейнер, имя файла, MIME type или путь к файлу.
`appendAudio` не следует путать с `/v1/audio/transcriptions`.

Минимальная форма запросов собственного JSON-RPC-клиента (это **не** готовая
команда для загрузки WAV):

```json
{"id":10,"method":"thread/realtime/start","params":{"threadId":"<existing-thread-id>","outputModality":"audio","transport":{"type":"websocket"}}}
{"id":11,"method":"thread/realtime/appendAudio","params":{"threadId":"<existing-thread-id>","audio":{"data":"<encoded-audio-chunk>","sampleRate":24000,"numChannels":1,"samplesPerChannel":2400}}}
{"id":12,"method":"thread/realtime/stop","params":{"threadId":"<existing-thread-id>"}}
```

Перед ними требуются `initialize`/`initialized` и существующий `threadId`.
Клиент должен обрабатывать уведомления, выбирать формат и темп чанков, а не
просто записать эти три строки в stdin. Кодировка `data` не установлена
схемой именно этой поставки; поэтому здесь оставлен placeholder, а не
непроверенный совет «положить WAV в base64».

### Уведомления сервера (все 11 `thread/realtime/*` в этой схеме)

Все перечисленные уведомления содержат `threadId`.

| Уведомление | Дополнительные поля | Значение |
| --- | --- | --- |
| `thread/realtime/started` | `version`, опционально `realtimeSessionId` | Сеанс принят и запущен. |
| `thread/realtime/sdp` | `sdp` | SDP-ответ для WebRTC; **не аудио**. |
| `thread/realtime/transcript/delta` | `role`, `delta` | Фрагмент транскрипта. |
| `thread/realtime/transcript/done` | `role`, `text` | Финальный текст транскрипта. |
| `thread/realtime/outputAudio/delta` | `audio` с полями чанка выше | **Аудиобайты в строковом поле** для соответствующего транспорта; не файл. |
| `thread/realtime/itemAdded` | `item` | Сырой не-аудио item от backend. |
| `thread/realtime/item/started` | `item` | Начат item канонической временной линии. |
| `thread/realtime/item/completed` | `item` | Завершён item временной линии. |
| `thread/realtime/item/transcript/delta` | `itemId`, `delta` | Транскрипт конкретного item. |
| `thread/realtime/error` | `message` | Ошибка сеанса. |
| `thread/realtime/closed` | опционально `reason` | Транспорт закрыт. |

Перечень выше полный для данной сборки. Вложенный `item`
может обозначать начало/конец сеанса, сегмент транскрипта или продвижение
item Codex; это не аудиофайл.

## Где происходит распознавание

### Готовый файл в диктовке Desktop

В `resources/app.asar` renderer-бандл
`webview/assets/app-initial-a75e8b4c0841.js` формирует multipart-запрос
`POST /transcribe` с записью (обычно WebM) и языком. Ответ разбирается как JSON.
В этом пути **нет локального STT**: клиент готовит файл и показывает текст,
удалённый backend выполняет распознавание. В данной сборке URL по умолчанию —
`https://chatgpt.com/backend-api/transcribe`; приватная модель распознавания
не раскрыта контрактом Desktop.

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

Renderer получает данные подключения через
`POST /codex/dictation-stream-connect-info`. Electron main-bundle
`.vite/build/main-CwVD5NxA.js` открывает `wss://…/dictation/stream` с
авторизацией текущего ChatGPT-пользователя. В поток идут PCM16-чанки; команда
`session.start` задаёт частоту дискретизации, язык, VAD и режим выдачи
сегментов. Транскрипты возвращает удалённый сервис. Ни этот WebSocket, ни
`/transcribe` не являются HTTP-методами `codex app-server`.

### Разговор Codex

Микрофон и речь ответа идут по WebRTC при штатном запуске Desktop; app-server
получает события транскрипта из realtime backend и публикует
`thread/realtime/transcript/*`. При экспериментальном WebSocket-транспорте
клиент может передавать чанки через `appendAudio`; app-server оркестрирует
сеанс, но в установленном бинарнике нет доказательств локальной STT-модели.
Конкретная внутренняя модель распознавания и маршрут до неё остаются
`UNKNOWN` без сетевого trace или исходника backend.

## Прямые вызовы без Desktop и без клиента Codex

### 1. Только расшифровать файл

```bash
# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
bash scripts/voice-probes/transcribe-file.sh --dry-run "$INPUT_AUDIO"
export OPENAI_API_KEY='[скрыто]'
bash scripts/voice-probes/transcribe-file.sh "$INPUT_AUDIO"
```

Скрипт делает один `POST /v1/audio/transcriptions` с `multipart/form-data`,
`file` и `model=gpt-transcribe`; ответ — JSON с `text` и метаданными языка.
Лимит примера — 25 MB. Ключ не кладётся в URL и не передаётся в аргументах
`curl`. API key может тарифицироваться отдельно от подписки ChatGPT.

### 2. Отправить WAV и получить голосовой ответ без Codex

```bash
# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
# Один раз в отдельном виртуальном окружении:
voice_venv="$(mktemp -d "$audio_out/realtime-demo-venv.XXXXXX")"
python3 -m venv "$voice_venv"
"$voice_venv/bin/python" -m pip install websocket-client

# Приведение записи к формату демонстрации:
ffmpeg -i "$INPUT_AUDIO" -ac 1 -ar 24000 -c:a pcm_s16le "$audio_out/question.wav"

# Сначала проверить файл и последовательность событий без сети:
"$voice_venv/bin/python" scripts/voice-probes/realtime-file.py \
  --dry-run "$audio_out/question.wav" "$audio_out/reply.wav"

# Реальный запрос (нужен OPENAI_API_KEY):
"$voice_venv/bin/python" scripts/voice-probes/realtime-file.py \
  "$audio_out/question.wav" "$audio_out/reply.wav"
```

Скрипт использует **публичный** `wss://api.openai.com/v1/realtime` и модель
`gpt-realtime-2.1`. Он отправляет PCM16 из WAV как один `input_audio` item,
запрашивает ответ, собирает `response.output_audio.delta` и сохраняет новый
WAV. Это короткий **файловый smoke-test голосового ответа**, не полноценный
разговор с непрерывным микрофоном, прерываниями и управлением инструментами.
Вход ограничен 30 секундами, выход — 120 секундами; существующий файл ответа
не перезаписывается.

Публичный WebRTC-вызов тоже можно создать через `POST /v1/realtime/calls`,
передав SDP offer; HTTP-ответ содержит SDP answer. **Одного `curl` для разговора
недостаточно**: после обмена SDP нужен WebRTC peer connection с аудиодорожками
и data channel. Пример выше использует WebSocket, чтобы показать весь
голосовой цикл без браузера.

### 3. Проверить аудио-контракт установленного app-server без сети модели

```bash
# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
python3 scripts/voice-probes/appserver-list-voices.py
```

Проба посылает `initialize` и `thread/realtime/listVoices` локальному bundled
app-server. Она **не** создаёт thread, не отправляет аудио и не запускает
realtime-сеанс. Успех этой пробы не доказывает доступность голосового вызова
для аккаунта.

## Граница proxy и готовности

У текущего запущенного Community-процесса наблюдался флаг
`--no-proxy-server`. Включение HTTP-прокси может провести через него HTTPS
сигнализацию или upload, но **не доказывает** прохождение WebRTC-медиа и
нативного WebSocket диктовки. Для `codex-proxy` отдельно нужны приёмочные
проверки маршрутов `/wham/realtime/calls`, `/transcribe`,
`/dictation/stream`, авторизации, сертификата и фактической WebRTC candidate
pair. Приватные ChatGPT-маршруты не поддерживаются публичным Platform API key,
и в этой работе через `codex-proxy` не тестировались.

Статус доказательств: контракты app-server — `VERIFIED / SOURCE` по схеме
бинарника; ответ `listVoices` — `VERIFIED / RUNTIME` локально; методы публичного
API — официально документированный контракт; внешний вызов с микрофоном,
счёт API, приватные маршруты через proxy и конкретная STT-модель — `UNKNOWN`
без дополнительных живых тестов. Никакие секреты или записи в этот отчёт не
включены.
