oddadmix's picture
Upload folder using huggingface_hub
6ff26b5 verified
Raw
History Blame
3.78 kB
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 = "الموديل لسه بيحمّل…"; });