const $ = id => document.getElementById(id); const SR = 16000; let rec = null, chunks = [], ctx = null; /* Decode + resample in the browser. decodeAudioData handles wav/mp3/m4a/webm, so the server never has to guess a container format or ship ffmpeg. */ async function toSamples(blob) { ctx = ctx || new (window.AudioContext || window.webkitAudioContext)(); const buf = await ctx.decodeAudioData(await blob.arrayBuffer()); const off = new OfflineAudioContext(1, Math.ceil(buf.duration * SR), SR); const src = off.createBufferSource(); // mix to mono first; a stereo file otherwise loses one channel const mono = off.createBuffer(1, buf.length, buf.sampleRate); const m = mono.getChannelData(0); for (let c = 0; c < buf.numberOfChannels; c++) { const d = buf.getChannelData(c); for (let i = 0; i < d.length; i++) m[i] += d[i] / buf.numberOfChannels; } src.buffer = mono; src.connect(off.destination); src.start(); return (await off.startRendering()).getChannelData(0); } function drawWave(s) { const N = 72, step = Math.floor(s.length / N) || 1; let html = ""; for (let i = 0; i < N; i++) { let peak = 0; for (let j = i * step; j < (i + 1) * step && j < s.length; j++) peak = Math.max(peak, Math.abs(s[j])); html += ``; } $("wave").innerHTML = html; } async function send(samples) { drawWave(samples); $("hint").textContent = "بيفرّغ…"; const t0 = performance.now(); try { const r = await fetch("/api/asr", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ samples: Array.from(samples) }) }); const d = await r.json(); if (d.error) { $("hint").textContent = d.error; return; } const wall = Math.round(performance.now() - t0); $("out").insertAdjacentHTML("afterbegin", `

${d.text || "(مفيش كلام اتعرف عليه)"}

الصوت ${d.duration}s audio tokens ${d.audio_tokens} اتولد ${d.new_tokens} token الموديل ${d.ms}ms (${d.rtf}× realtime) كلي ${wall}ms
`); $("hint").textContent = "جاهز"; } catch (e) { $("hint").textContent = "تعذّر الاتصال"; } } $("file").addEventListener("change", async e => { const f = e.target.files[0]; if (!f) return; $("hint").textContent = "بيقرا الملف…"; send(await toSamples(f)); e.target.value = ""; }); $("rec").addEventListener("click", async () => { if (rec && rec.state === "recording") { rec.stop(); return; } let stream; try { stream = await navigator.mediaDevices.getUserMedia({ audio: true }); } catch (e) { $("hint").textContent = "مفيش إذن للمايك"; return; } chunks = []; rec = new MediaRecorder(stream); rec.ondataavailable = e => chunks.push(e.data); rec.onstop = async () => { stream.getTracks().forEach(t => t.stop()); $("rec").classList.remove("on"); $("rec").textContent = "🎙️ سجّل"; send(await toSamples(new Blob(chunks, { type: rec.mimeType }))); }; rec.start(); $("rec").classList.add("on"); $("rec").textContent = "⏹️ وقّف"; $("hint").textContent = "بيسجّل… اتكلم بالعربي"; }); fetch("/api/ready").then(r => r.json()).then(d => { $("meta").textContent = `${d.model} · ${(d.params / 1e6).toFixed(1)}M params · CPU · لحد ${d.max_sec}s`; $("hint").textContent = "اضغط سجّل أو ارفع ملف"; }).catch(() => { $("hint").textContent = "الموديل لسه بيحمّل…"; });