File size: 3,784 Bytes
6ff26b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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 = "الموديل لسه بيحمّل…"; });