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

195 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Передача аудио через 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+, одно сообщение):**
```json
{"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)
```nginx
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+):
```javascript
// 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с
```javascript
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 и деплой
```bash
# Сервер (Германия)
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'
```