diff --git a/index.html b/index.html index e2fe774..957a27b 100644 --- a/index.html +++ b/index.html @@ -53,7 +53,7 @@ let mediaRecorder, audioChunks = [], userAudioBlob = null, ttsText = ''; let isRecording = false, isAnalyzing = false, micAnalyser = null, micAnimId = null; let recordStartTime = 0, recordDuration = 0; // ---------- Версия ---------- -const VERSION = 'v48'; +const VERSION = 'v60'; document.getElementById('ver').textContent = VERSION; // ---------- Перевод на русский (Groq) ---------- @@ -627,14 +627,19 @@ async function transcribe(blob) { return new Promise((resolve) => { try { + const CHUNK = 30000; // макс 30KB на фрейм (DPI режет >48KB) + const total = Math.ceil(base64.length / CHUNK); const ws = new WebSocket('wss://proxy.kube5s.ru/ws'); const timeout = setTimeout(() => { console.log('[WS] timeout'); ws.close(); resolve({ text: '', error: 'timeout' }); - }, 60000); + }, 90000); ws.onopen = () => { - ws.send(JSON.stringify({ token: GROQ_API_KEY, audio_b64: base64, mime: blob.type || 'audio/webm' })); - console.log('[WS] sent'); + for (let i = 0; i < total; i++) { + ws.send(JSON.stringify({ c: i, n: total, t: GROQ_API_KEY, b: base64.slice(i * CHUNK, (i + 1) * CHUNK), m: blob.type || 'audio/webm' })); + } + ws.send(JSON.stringify({ c: -1 })); + console.log('[WS] sent ' + total + ' chunks'); }; ws.onmessage = (e) => { diff --git a/ws_server.py b/ws_server.py new file mode 100644 index 0000000..8c730b6 --- /dev/null +++ b/ws_server.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""WebSocket Groq Whisper proxy — chunked text frames (DPI-proof).""" +import asyncio, json, websockets, requests, uuid, sys, time, base64 + +GROQ_BASE = "https://api.groq.com" + +def log(msg): + print(f"[{time.strftime('%H:%M:%S')}] {msg}", file=sys.stderr, flush=True) + +async def handler(ws): + chunks = {} # index -> base64_part + total = None + token = "" + mime = "audio/webm" + try: + log(f"conn from {ws.remote_address}") + async for raw in ws: + msg = json.loads(raw) + ci = msg.get("c", 0) + + if ci == -1: + # Final chunk — reassemble + if not chunks: + await ws.send(json.dumps({"error": "no chunks"})) + return + audio_b64 = "".join(chunks[i] for i in sorted(chunks)) + log(f"assembled {len(chunks)} chunks → {len(audio_b64)} chars, decoding...") + audio_data = base64.b64decode(audio_b64) + log(f"decoded {len(audio_data)}B") + + ext = "webm" + if "ogg" in mime: ext = "ogg" + elif "mp4" in mime or "aac" in mime: ext = "mp4" + + boundary = "----WKFBoundary" + uuid.uuid4().hex[:16] + payload = b"".join([ + f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="audio.{ext}"\r\nContent-Type: {mime}\r\n\r\n'.encode(), + audio_data, + f'\r\n--{boundary}\r\nContent-Disposition: form-data; name="model"\r\n\r\nwhisper-large-v3'.encode(), + f'\r\n--{boundary}\r\nContent-Disposition: form-data; name="language"\r\n\r\nit'.encode(), + f'\r\n--{boundary}--\r\n'.encode(), + ]) + + t0 = time.time() + loop = asyncio.get_event_loop() + r = await loop.run_in_executor(None, lambda: requests.post( + f"{GROQ_BASE}/openai/v1/audio/transcriptions", + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": f"multipart/form-data; boundary={boundary}", + }, + data=payload, timeout=60 + )) + log(f"Groq {r.status_code} in {time.time()-t0:.2f}s: {r.text[:120]}") + await ws.send(json.dumps({"text": r.json().get("text", ""), "status": r.status_code})) + return + else: + # Data chunk + chunks[ci] = msg.get("b", "") + token = msg.get("t", token) + mime = msg.get("m", mime) + total = msg.get("n", total) + # log(f"chunk {ci+1}/{total}") + + except Exception as e: + log(f"ERR: {e}") + try: await ws.send(json.dumps({"error": str(e)})) + except: pass + +async def main(): + async with websockets.serve(handler, "127.0.0.1", 8766): + log("groq-ws :8766 (chunked)") + await asyncio.Future() + +asyncio.run(main())