Spaces:
Running
Running
| 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 += `<i style="height:${Math.max(2, peak * 100)}%"></i>`; | |
| } | |
| $("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", ` | |
| <div class="res"> | |
| <p class="txt ${d.text ? "" : "empty"}">${d.text || "(مفيش كلام اتعرف عليه)"}</p> | |
| <div class="stats"> | |
| <span>الصوت <b>${d.duration}s</b></span> | |
| <span>audio tokens <b>${d.audio_tokens}</b></span> | |
| <span>اتولد <b>${d.new_tokens}</b> token</span> | |
| <span>الموديل <b>${d.ms}ms</b> (${d.rtf}× realtime)</span> | |
| <span>كلي <b>${wall}ms</b></span> | |
| </div> | |
| </div>`); | |
| $("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 = "الموديل لسه بيحمّل…"; }); | |