v60: чанки 30KB — обход DPI-порога ~48KB на WebSocket text-фреймы
This commit is contained in:
+9
-4
@@ -53,7 +53,7 @@ let mediaRecorder, audioChunks = [], userAudioBlob = null, ttsText = '';
|
|||||||
let isRecording = false, isAnalyzing = false, micAnalyser = null, micAnimId = null;
|
let isRecording = false, isAnalyzing = false, micAnalyser = null, micAnimId = null;
|
||||||
let recordStartTime = 0, recordDuration = 0;
|
let recordStartTime = 0, recordDuration = 0;
|
||||||
// ---------- Версия ----------
|
// ---------- Версия ----------
|
||||||
const VERSION = 'v48';
|
const VERSION = 'v60';
|
||||||
document.getElementById('ver').textContent = VERSION;
|
document.getElementById('ver').textContent = VERSION;
|
||||||
|
|
||||||
// ---------- Перевод на русский (Groq) ----------
|
// ---------- Перевод на русский (Groq) ----------
|
||||||
@@ -627,14 +627,19 @@ async function transcribe(blob) {
|
|||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
|
const CHUNK = 30000; // макс 30KB на фрейм (DPI режет >48KB)
|
||||||
|
const total = Math.ceil(base64.length / CHUNK);
|
||||||
const ws = new WebSocket('wss://proxy.kube5s.ru/ws');
|
const ws = new WebSocket('wss://proxy.kube5s.ru/ws');
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
console.log('[WS] timeout'); ws.close(); resolve({ text: '', error: 'timeout' });
|
console.log('[WS] timeout'); ws.close(); resolve({ text: '', error: 'timeout' });
|
||||||
}, 60000);
|
}, 90000);
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
ws.send(JSON.stringify({ token: GROQ_API_KEY, audio_b64: base64, mime: blob.type || 'audio/webm' }));
|
for (let i = 0; i < total; i++) {
|
||||||
console.log('[WS] sent');
|
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) => {
|
ws.onmessage = (e) => {
|
||||||
|
|||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user