Files
lang/doc/ws-dpi-solution.md
T

8.8 KiB
Raw Blame History

Передача аудио через DPI: WebSocket-решение

Дата: 2026-05-22
Задача: передача аудиофайлов из РФ на германский сервер для Groq Whisper, в обход DPI.


Проблема

  • Напрямую из РФ → api.groq.com нельзя (блокировка Groq)
  • HTTP POST из РФ → proxy.kube5s.ru (Германия): DPI режет тело запроса
    • ≤16KB: проходит
    • 17-24KB: нестабильно
    • 24KB: 100% таймаут

  • WebM/Opus с MediaRecorder: 2-3с записи ≈ 15-22KB (сжатый)
  • WAV (несжатый) ≈ 64KB — гарантированный таймаут → убрали в v34

Исследование обхода

Тест 1: WebSocket бинарные фреймы

Сервер получает JSON-заголовок (68 байт), ждёт бинарный блоб — таймаут 30с.
Бинарные WebSocket-фреймы DPI режет так же как HTTP POST.

Тест 2: WebSocket текстовые фреймы

Аудио кодируется в base64, отпрaвляется как JSON-строка.

Размер Результат
26KB base64 0.15с
250KB base64 0.54с
3.8MB base64 message too big (лимит сервера 1MB, не DPI)

Вывод: DPI режет ТОЛЬКО бинарные фреймы. Текстовые — любые размеры — проходят.

Тест 3: HTTP POST большие данные (контроль)

250KB POST → таймаут 22с. DPI режет HTTP POST.


Архитектура решения (v48+)

Браузер (РФ)                  proxy.kube5s.ru (Германия)          api.groq.com
    │                                │                                │
    │  WebSocket wss://              │                                │
    │  JSON {audio_b64, token} ─────▶│                                │
    │  (текстовый фрейм, ~30KB)     │  base64 decode                 │
    │                                │  multipart POST ──────────────▶│
    │                                │                            Whisper
    │                                │  ◀────── JSON {text} ──────────│
    │  ◀──── JSON {text} ───────────│                                │
    │                                │                                │

Ключевые точки:

  1. Аудио НЕ отправляется бинарными фреймами
  2. Всё в JSON — один текстовый WebSocket-фрейм
  3. base64 кодирование на клиенте (FileReader.readAsDataURL)
  4. Декодирование на сервере (base64.b64decode)
  5. Сервер формирует multipart и отправляет в Groq

Файлы

/opt/groq-proxy/ws_server.py (Vultr 95.179.252.111)

WebSocket-сервер на 127.0.0.1:8766, systemd unit groq-ws.

Протокол (v48+, одно сообщение):

 {"token": "gsk_...", "audio_b64": "<base64>", "mime": "audio/webm"}
 {"text": "распознанный текст", "status": 200}

Важно: KillSignal=SIGKILL в systemd — asyncio не умирает от SIGTERM.

/opt/groq-proxy/proxy.py (Vultr)

Универсальный HTTP-прокси для всех путей Groq (/openai/v1/*api.groq.com).
gunicorn 4 workers, порт 8765.

/etc/nginx/conf.d/groq-proxy.conf (Vultr)

location /ws {
    proxy_pass http://127.0.0.1:8766;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 120s;
}
location / {
    proxy_pass http://127.0.0.1:8765;
    proxy_read_timeout 60s;
}

~/lang/index.html (K8s capire.kube5s.ru)

Функция transcribe(blob) (v50+):

// 1. Кодируем blob → base64 (FileReader)
// 2. WebSocket wss://proxy.kube5s.ru/ws
// 3. trySend() полинг readyState (не onopen — race condition)
// 4. Отправляем JSON {token, audio_b64, mime}
// 5. Ждём onmessage → ответ
// 6. onclose с задержкой 500ms (close frame != text frame порядок)

Найденные race condition и фиксы

1. onopen не срабатывает (v50)

Симптом: WS открыт (nginx 101), данные не отправлены, keepalive timeout на сервере.
Причина: new WebSocket() может завершить handshake до присвоения ws.onopen.
Фикс: trySend() — полинг ws.readyState, вызывается и в onopen, и сразу после конструктора.

2. onclose обгоняет onmessage (v51)

Симптом: сервер отвечает, но браузер показывает «Анализ: ДОЛГО».
Причина: сервер отправляет text frame → close frame. Браузер может обработать close раньше text → onclose вызывает done('network') → результат потерян.
Фикс: onclose ждёт 500мс перед вызовом done(). Если onmessage уже сработал — settled=true, onclose ничего не делает.

3. Promise.all блокирует UI (v52)

Симптом: транскрипция (0.5с) готова, но результат не показывается — таймер «Анализ: ДОЛГО».
Причина: Promise.all([transcribe, phoneticAnalysis])phoneticAnalysis вызывает decodeAudioData, который может зависнуть на WebM/Opus в Chrome. Пока фонетика не отвиснет — транскрипция не показывается.
Фикс: последовательное выполнение:

  1. transcribe() — показать сразу
  2. phoneticAnalysis() — с таймаутом 10с (Promise.race)

4. isAnalyzing не сбрасывается

Симптом: повторные нажатия игнорируются.
Причина: в старом коде catch делал return '' без сброса isAnalyzing.
Фикс: isAnalyzing = false вызывается в одном месте после clearInterval(timerId), на всех путях.


Фонетический анализ: таймаут 10с

phon = await Promise.race([
    phoneticAnalysis(blob),                                          // норма: 0.1-0.5с
    new Promise(r => setTimeout(() => r({...нули...}), 10000))       // предел: 10с
]);
  • В 99% случаев phoneticAnalysis завершается за 0.1-0.5 секунд
  • 10 секунд — предел, защита от зависания decodeAudioData
  • При таймауте фонетика показывает нули, но транскрипция работает
  • НЕ влияет на скорость в нормальном режиме — Promise.race берёт первый результат

Версии

Версия Дата Изменения
v34 2026-05-22 Убран blobToWav (WAV 64KB — DPI гарантированно режет)
v42 2026-05-22 HTTP chunked upload — НЕ РАБОТАЕТ, DPI режет каждый POST
v45 2026-05-22 WebSocket: бинарный блоб одним сообщением
v46 2026-05-22 ws.send(blob) напрямую вместо arrayBuffer (ошибка — не помогло)
v47 2026-05-22 base64 чанками по 4KB (избыточно — DPI текст не режет)
v48 2026-05-22 Один JSON с полным base64, без чанков
v49 2026-05-22 WS robust: onclose, readyState, setTimeout defer, settled guard
v50 2026-05-22 trySend полинг readyState вместо onopen
v51 2026-05-22 onclose ждёт 500мс перед done()
v52 2026-05-22 doCompare: транскрипция → фонетика последовательно, фонетика с таймаутом 10с

SSH и деплой

# Сервер (Германия)
ssh -i ~/.ssh/vultr_openssh root@95.179.252.111

# Деплой index.html
cd ~/lang && bash deploy_lang.sh

# WS сервер
scp ws_server.py root@95.179.252.111:/opt/groq-proxy/ws_server.py
ssh root@95.179.252.111 'systemctl restart groq-ws'

# Логи
ssh root@95.179.252.111 'journalctl -u groq-ws -f'