# WebRTC `/voice` через наш codex-proxy

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

Этот каталог можно скопировать отдельно от форка. Python-клиент не запускает
Codex CLI, app-server, Electron или `codex-voice-host`: создание звонка,
WebRTC, captions и завершение находятся в самом демо. Оно ведёт разговор,
но не выполняет команды, не вызывает MCP и не продолжает задачи Codex.

За основу взято [прямое демо](</experiments/voice-ai/archive/c/DOCS/voice-systems-textbook-2026-10-02/voice-systems-textbook/scripts/webrtc-voice-isolated/README.md>) по контракту
CLI 0.156.1. `voice_core.py` — его снимок на 30.09.2026; заменена только
фабрика HTTP-сессии. Исходное direct-демо и установленное приложение не менялись.

## Что проверено 30 сентября 2026 года

Файловая живая проба с `raven.flac`, приведённым к WAV PCM16 mono 48 кГц,
получила транскрипцию и голосовой ответ. Создание звонка вернуло 201 через
CONNECT на `chatgpt.com:443`; sideband вернул 101 через CONNECT на
`api.openai.com:443`. Оба TCP-соединения relay действительно открывал к
`gis-master.ru:18081`, а не непосредственно к этим сервисам.

Аудио прошло отдельным WebRTC UDP-потоком: выбранный ICE peer был
`135.220.40.201:3478`. Этот адрес — наблюдение одного запуска, не настройка,
которую нужно прописывать. Наличие порта 3478 само по себе не означает STUN:
здесь это выбранный peer медиасеанса, подтверждённый RTP и аудиоответом.

Счётчики: 735 отправленных и 650 принятых RTP-пакетов; 599040 декодированных
сэмплов, из них 143935 ненулевых. WAV ответа: PCM16 mono 48 кГц, 12,48 с,
1198124 байта. Полное декодирование `ffmpeg` прошло. Удалённая сторона
прислала финальные реплики пользователя и ассистента; личный текст в журнал
не сохранён. `auth.json` остался неизменным. Финальный повтор после проверок
завершения описан в [отчёте](</experiments/voice-ai/archive/c/DOCS/voice-systems-textbook-2026-10-02/voice-systems-textbook/docs/codex-voice-proxy-verification.md>).

**«Работает через proxy» относится здесь к HTTPS и WSS, а не ко всему звуку.**
HTTP CONNECT proxy не перенаправляет UDP aiortc. Системные VPN, NAT и дальнейшие
маршрутизаторы не исследовались: «в обход proxy» означает обход именно нашего
прикладного HTTP proxy, а не отсутствие любых сетевых туннелей машины.

## Быстрый запуск на этой машине

Сначала проверьте контракт без сети, файлов входа и микрофона:

```bash
# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
proxy_demo="$repo/scripts/webrtc-voice-proxy"
proxy_python="$voice_venv/bin/python"
"$proxy_python" "$proxy_demo/proxy-voice.py" --dry-run --auth-file "$auth_file"
```

Разговор в консоли:

```bash
# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
proxy_run="$(mktemp -d "$audio_out/proxy-voice-console.XXXXXX")"
bash "$repo/scripts/webrtc-voice-proxy/run-proxy.sh"    --report "$proxy_run/routes.json" --voice cove --auth-file "$auth_file"
```

`/voice` начинает или останавливает разговор. До этой команды микрофон не
открыт; после соединения он работает постоянно. `/voice mute` останавливает
захват, очищает очередь и отправляет только синтетическую тишину; повторная
команда включает микрофон. `/voice settings` показывает устройства и голос.
`/quit`, EOF и Ctrl+C закрывают собственные ресурсы. Наденьте наушники:
полноценного AEC здесь нет. Физический разговор с устройствами в этой итерации
не проверялся — живая проба работала с файлом без микрофона и динамиков.

## Файловая проверка без участия пользователя

```bash
# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
proxy_demo="$repo/scripts/webrtc-voice-proxy"
proxy_run="$(mktemp -d "$audio_out/proxy-voice-file.XXXXXX")"

# Это перекодирование, не локальная транскрипция. Распознаёт удалённый сервис.
ffmpeg -nostdin -v error -n -i "$INPUT_AUDIO"    -ar 48000 -ac 1 -c:a pcm_s16le "$proxy_run/input.wav"

bash "$proxy_demo/run-proxy.sh"    --file "$proxy_run/input.wav" --output "$proxy_run/answer.wav"    --report "$proxy_run/routes.json" --voice cove    --auth-file "$auth_file" --timeout 30 --max-seconds 45

ffmpeg -nostdin -v error -i "$proxy_run/answer.wav" -f null -
ffprobe -v error -show_entries stream=codec_name,sample_rate,channels,bits_per_sample    -show_entries format=duration,size -of json "$proxy_run/answer.wav"
```

WAV и JSON создаются с правами 0600, старые файлы не перезаписываются.
Журнал сохраняется также при отказе после разбора аргументов; он позволяет
отличить недоступный proxy от отказа сервиса или незавершённого разговора.
Чтобы проверить другой аудиофайл, замените только входной путь `ffmpeg`.
Неверные частота, каналы, разрядность и обрезанный WAV отвергаются до чтения
токена и до сети. В WebRTC уходит не WAV-контейнер, а сэмплы, закодированные Opus.

## Откуда берутся настройки

`run-proxy.sh` выполняет доверенный локальный `~/.local/bin/acodex` в режиме
`ACODEX_ENV_ONLY=1`. Это именно вычисление настроек shell-кодом, не попытка
вытащить literal URL регулярным выражением. CLI не запускается. Basic URL
остаётся в прежнем bootstrap и памяти дочернего процесса; в argv, исходники
демо и журнал он не копируется. Wrapper наследует уже заданный HTTPS_PROXY,
как исходный acodex; фактический upstream виден в безопасном JSON.

Python читает `HTTPS_PROXY` явно. `HTTP_PROXY`, `ALL_PROXY`, `NO_PROXY` и
неявные HTTP-настройки окружения не выбирают маршрут. TLS проверяется;
системный trust store дополняется `--ca-file`. На этой машине это
`~/.codex/ca-bundle.pem`, а не закрытый CA-ключ. Default берётся из
CODEX_CA_CERTIFICATE, затем SSL_CERT_FILE, затем этого стандартного пути.

Собственный Bearer читается непосредственно из `--auth-file`, по умолчанию
`~/.codex/auth.json`, **не** из marker-профиля `~/.codex-acodex-api`, который
назначает bootstrap. `0000` здесь не используется. Никакого refresh,
копирования gateway OAuth, cookies или attestation в демо нет.

Снимок RubyMITM 1.1.113 из старого чата содержит подстановку активного Bearer
и account ID для обычных запросов на перехватываемые ChatGPT/OpenAI хосты.
Маркер `0000` имеет отдельный bridge только для responses/alpha search,
поэтому переносить его на voice нельзя. Мы не снимали логи production-server:
конкретный upstream-аккаунт и текущая версия этой подстановки не подтверждены
клиентским журналом. Успех 201/101 доказывает создание call и открытие WSS через proxy,
но не email выбранного им аккаунта. Если active config меняется между созданием
call и подключением sideband, возможна несовместимость авторизации.

Первый участок до `http://gis-master.ru:18081` в существующей настройке не
шифрует Basic сам по себе. Используйте доверенную сеть/VPN/защищённый туннель;
демо не меняет периметр proxy. Relay слушает только loopback и допускает только
CONNECT к двум нужным хостам. Это не публичный proxy-сервис.

Файловая проба рассчитана на одну реплику. Она ждёт конец входа, финальную
реплику ассистента и короткий хвост, но финал мог прийти раньше конца файла.
Это не доказывает ответ на весь файл или полный media drain. RTP-счётчики и
декодируемый WAV подтверждают обмен звуком, не смысловую полноту разговора.

## Что видно в JSON

| Событие | Что подтверждает |
| --- | --- |
| `client_request` / `client_response` | Метод, endpoint, выбранный маршрут и фактический HTTP-статус |
| `proxy_tcp_connected` | Реальный TCP peer relay: IP и порт upstream proxy |
| `proxy_connect_response` | CONNECT к целевому host:443 и статус proxy; сам 200 ещё не доказывает успешную модельную операцию |
| `proxy_tunnel_closed` | Счётчики зашифрованных TLS-байтов по направлениям, не размер исходного аудиофайла |
| `selected_ice` | Выбранная runtime ICE-пара: локальный/удалённый адрес, транспорт, candidate types |
| `udp_sendto` | CPython audit наблюдает адреса попыток UDP sendto; payload не читает |
| `rtp_stats` | Счётчики aiortc, декодированные сэмплы и длины финальных captions |
| `voice_failed`, `relay_error` | Фаза отказа и безопасный класс ошибки, без тела сервера |
| `relay_closed` | Число оставшихся handlers после закрытия |

Полный SDP, call ID, заголовки, cookies, Bearer, Basic, PCM и личные реплики
в JSON не попадают. Но он содержит локальные адреса и временные метки:
не выкладывайте его целиком в публичный репозиторий без просмотра.
Реальные gateway traces не включались и чужие сессии не снимались.

## Отдельное сравнение WSS-маршрута

`--sideband-route direct` явно отправляет WSS мимо proxy, оставляя POST через
proxy. Это диагностическая опция, **не автоматический обход отказа**. В этой
итерации она проверена на локальном TLS/WebSocket стенде, не на чужом аккаунте.
Если gateway создал call под другим аккаунтом, собственный локальный Bearer
для прямого sideband может не иметь доступа к этому call. Не копируйте для
такого сравнения токен чужого active config. Основной рабочий режим — proxy.

## Перенос на другую машину

Скопируйте весь каталог, включая `voice_core.py`, `routes.py` и requirements.
После копирования Python-код не зависит от путей форка. Установите зависимости
только в отдельный venv и укажите ему путь через `PROXY_VOICE_PYTHON`:

```bash
# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
python3 -m venv "$voice_venv"
"$voice_venv/bin/pip" install -r "$proxy_demo/requirements.txt"
"$voice_venv/bin/pip" check
```

Нужен PortAudio только для устройств. Если подготовленный bootstrap называется
иначе, `ACODEX_BOOTSTRAP` указывает его файл; он должен поддерживать env-only.
Можно запускать Python без wrapper из уже настроенного доверенного окружения:

```bash
# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
"$voice_venv/bin/python" "$proxy_demo/proxy-voice.py"    --auth-file "$auth_file" --ca-file "${AUDIO_CA_FILE:?Укажите свой CA-файл}"    --report "$audio_out/new-private-routes.json"
```

Переменная HTTPS_PROXY должна быть задана локальным секретным bootstrap;
не вставляйте URL с паролем в историю shell или аргументы. Основной вход,
ярлыки и production-конфигурация в этой задаче не менялись.

## Тесты и карта кода

```bash
# Выполняйте из корня распакованного комплекта.
source ./scripts/audio-textbook/env.sh
"$voice_venv/bin/python" -W error::ResourceWarning    -m unittest discover    -s "$repo/scripts/webrtc-voice-proxy"    -p 'test_*.py' -v
bash -n "$repo/scripts/webrtc-voice-proxy/run-proxy.sh" --auth-file "$auth_file"
```

Есть полностью локальный end-to-end: TLS origin + два уровня CONNECT + настоящий
WebRTC Opus/DTLS-SRTP + WSS captions + WAV. Остальные проверки покрывают 407,
мертвый proxy, запрет другого target, отсутствие direct fallback, NO_PROXY,
TLS trust, WSS redirect, Basic только в CONNECT, скрытие секретов, приватный
отчёт и cleanup. Тесты core/direct дополняют mute, устройства и аудиоконтракт.

Каталог V3 CLI 0.156.1: juniper, maple, spruce, ember, vale, breeze, arbor,
sol, cove. Живой тест этой версии — только cove. `--list-voices` показывает
тот же фиксированный каталог без запроса к сервису; `--voice` выбирает голос
перед стартом. `--list-devices`, `--input-device N` и `--output-device N`
относятся к локальным устройствам, а не к параметрам gateway.

| Файл / функция | Ответственность |
| --- | --- |
| `run-proxy.sh` | Проверенный bootstrap сети без запуска acodex CLI |
| `proxy-voice.py`: `main`, `run` | CLI, входные файлы, обязательный журнал, lifecycle relay |
| `proxy-voice.py`: `ProxySession` | WebRTC core + runtime ICE/RTP-наблюдения |
| `routes.py`: `Proxy.from_env`, `tls_context` | Отдельные Basic credentials и доверие CA |
| `routes.py`: `ConnectRelay.handle` | CONNECT до proxy, фактический TCP peer, opaque relay, cleanup |
| `routes.py`: `RoutedClient` | Явный proxy для HTTPS/WSS, trace endpoint/status, запрет redirect |
| `routes.py`: `Journal`, `Network.media_snapshot` | Безопасный JSON, UDP audit, выбранная ICE-пара |
| `voice_core.py`: `read_auth`, `call_body`, `VoiceSession._start` | Собственный ChatGPT Bearer, V3 session и signaling |
| `voice_core.py`: `InputTrack`, `receive_audio`, `receive_events` | PCM→Opus, RTP→WAV/динамики, captions, отказ делегации |
| `test_proxy.py` | Локальные сетевые стенды и негативные проверки |

Источник знаний: [сохранённый контекст Codex Proxy Setup](</experiments/voice-ai/archive/c/DOCS/voice-systems-textbook-2026-10-02/voice-systems-textbook/docs/codex-voice-proxy-verification.md#proxy-context>),
старый переносимый комплект и read-only извлечение `proxy.rb` из сохранённого
образа 1.1.113. Это не runtime-инспекция текущего production-сервиса.

Кликабельные схемы и пояснения: [отдельный учебник](</experiments/voice-ai/archive/c/DOCS/voice-systems-textbook-2026-10-02/voice-systems-textbook/docs/codex-voice-proxy-textbook.html>).
Подтверждения: [отчёт проверки](</experiments/voice-ai/archive/c/DOCS/voice-systems-textbook-2026-10-02/voice-systems-textbook/docs/codex-voice-proxy-verification.md>).
