8.8 KiB
Передача аудио через 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} ───────────│ │
│ │ │
Ключевые точки:
- Аудио НЕ отправляется бинарными фреймами
- Всё в JSON — один текстовый WebSocket-фрейм
- base64 кодирование на клиенте (FileReader.readAsDataURL)
- Декодирование на сервере (base64.b64decode)
- Сервер формирует 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. Пока фонетика не отвиснет — транскрипция не показывается.
Фикс: последовательное выполнение:
transcribe()— показать сразу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'