// Voiceprint: three on-device apps on top of AntSpeaker speaker embeddings. const $ = (id) => document.getElementById(id); const SR = 16000; const MAX_CLIP = 20; // seconds fed to the embedder for a single clip const SPK_VARS = ["--s1", "--s2", "--s3", "--s4", "--s5", "--s6"]; const css = (v) => getComputedStyle(document.documentElement).getPropertyValue(v).trim(); const spkColor = (i) => css(SPK_VARS[i % SPK_VARS.length]); const fmt = (t) => `${Math.floor(t / 60)}:${String(Math.floor(t % 60)).padStart(2, "0")}`; const fmt1 = (t) => `${Math.floor(t / 60)}:${(t % 60).toFixed(1).padStart(4, "0")}`; // ---------------------------------------------------------------- storage const store = { get(k, d) { try { const v = localStorage.getItem("vp:" + k); return v ? JSON.parse(v) : d; } catch (_) { return d; } }, set(k, v) { try { localStorage.setItem("vp:" + k, JSON.stringify(v)); } catch (_) {} }, del(k) { try { localStorage.removeItem("vp:" + k); } catch (_) {} }, }; // ---------------------------------------------------------------- worker const worker = new Worker("worker.js", { type: "module" }); let reqId = 0; const pending = new Map(); let readyResolve; const ready = new Promise((r) => (readyResolve = r)); window.__vp = { ready: null, timings: [] }; worker.onmessage = (ev) => { const m = ev.data; if (m.type === "progress") { $("loadFill").style.width = `${(100 * m.got) / m.total}%`; $("statusText").textContent = `Model ${(m.got / 1e6).toFixed(1)} / ${(m.total / 1e6).toFixed(1)} MB`; } else if (m.type === "status") { $("statusText").textContent = m.text; } else if (m.type === "warn") { console.warn(m.text); } else if (m.type === "ready") { $("status").classList.add("ready"); $("statusText").textContent = `${m.backend === "webgpu" ? "WebGPU" : "WASM"} · ${m.precision} · ready in ${(m.totalMs / 1000).toFixed(1)}s`; window.__vp.ready = m; readyResolve(m); } else if (m.type === "vadProgress") { vadProgressCb && vadProgressCb(m.done / m.total); } else if (m.type === "result" || m.type === "error") { const p = pending.get(m.id); if (!p) { if (m.type === "error") fail(m.text); return; } pending.delete(m.id); m.type === "error" ? p.reject(new Error(m.text)) : p.resolve(m); } }; worker.onerror = (e) => fail(e.message || "worker error"); let vadProgressCb = null; function call(type, data, transfer = []) { return ready.then(() => new Promise((resolve, reject) => { const id = ++reqId; pending.set(id, { resolve, reject }); worker.postMessage({ type, id, ...data }, transfer); })); } async function embed(clips) { const r = await call("embed", { clips: clips.map((c) => c.slice()) }); window.__vp.timings.push({ n: clips.length, ms: r.ms }); return r.embs; } const embedOne = async (pcm) => (await embed([pcm.length > MAX_CLIP * SR ? pcm.subarray(0, MAX_CLIP * SR) : pcm]))[0]; function fail(text) { $("status").classList.add("err"); $("statusText").textContent = "Error: " + text; toast(text); } let toastT; function toast(text) { let el = document.querySelector(".toast"); if (!el) { el = document.createElement("div"); el.className = "toast"; document.body.appendChild(el); } el.textContent = text; el.hidden = false; clearTimeout(toastT); toastT = setTimeout(() => (el.hidden = true), 3500); } worker.postMessage({ type: "load", webgpu: new URLSearchParams(location.search).get("wasm") === null }); // ---------------------------------------------------------------- vector math const dot = (a, b) => { let s = 0; for (let i = 0; i < a.length; i++) s += a[i] * b[i]; return s; }; function meanNorm(vs) { const m = new Float32Array(vs[0].length); for (const v of vs) for (let i = 0; i < m.length; i++) m[i] += v[i]; const n = Math.sqrt(dot(m, m)) || 1; for (let i = 0; i < m.length; i++) m[i] /= n; return m; } // ---------------------------------------------------------------- audio io let actx = null; const audioCtx = () => (actx ||= new (window.AudioContext || window.webkitAudioContext)()); async function decodeTo16k(arrayBuf) { // decoding through a 16 kHz offline context resamples straight to the model rate const buf = await new OfflineAudioContext(1, 1, SR).decodeAudioData(arrayBuf); const out = new Float32Array(buf.length); for (let c = 0; c < buf.numberOfChannels; c++) { const d = buf.getChannelData(c); for (let i = 0; i < out.length; i++) out[i] += d[i] / buf.numberOfChannels; } return out; } const fetchSample = async (name) => decodeTo16k(await (await fetch(`samples/${name}.wav`)).arrayBuffer()); function pickFiles(multiple = false) { return new Promise((resolve) => { const inp = $("filePick"); inp.multiple = multiple; inp.value = ""; inp.onchange = () => resolve([...inp.files]); inp.click(); }); } function wavBlob(pcm) { const b = new ArrayBuffer(44 + pcm.length * 2), v = new DataView(b); const w = (o, s) => [...s].forEach((c, i) => v.setUint8(o + i, c.charCodeAt(0))); w(0, "RIFF"); v.setUint32(4, 36 + pcm.length * 2, true); w(8, "WAVE"); w(12, "fmt "); v.setUint32(16, 16, true); v.setUint16(20, 1, true); v.setUint16(22, 1, true); v.setUint32(24, SR, true); v.setUint32(28, SR * 2, true); v.setUint16(32, 2, true); v.setUint16(34, 16, true); w(36, "data"); v.setUint32(40, pcm.length * 2, true); for (let i = 0; i < pcm.length; i++) v.setInt16(44 + i * 2, Math.max(-1, Math.min(1, pcm[i])) * 32767, true); return new Blob([b], { type: "audio/wav" }); } // Recorder: one at a time. Calls onTick(seconds, level). Resolves 16 kHz PCM on stop. let activeRec = null; async function record({ maxSec = 8, onTick = () => {} } = {}) { if (activeRec) { activeRec.stop(); return null; } const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: false, autoGainControl: true } }); const ctx = audioCtx(); await ctx.resume(); const srcNode = ctx.createMediaStreamSource(stream); const an = ctx.createAnalyser(); an.fftSize = 1024; srcNode.connect(an); const tmp = new Float32Array(1024); const mr = new MediaRecorder(stream); const chunks = []; mr.ondataavailable = (e) => e.data.size && chunks.push(e.data); const t0 = performance.now(); let raf; const done = new Promise((resolve) => { mr.onstop = async () => { cancelAnimationFrame(raf); stream.getTracks().forEach((t) => t.stop()); srcNode.disconnect(); activeRec = null; try { resolve(await decodeTo16k(await new Blob(chunks).arrayBuffer())); } catch (e) { toast("Could not decode recording"); resolve(null); } }; }); const tick = () => { const s = (performance.now() - t0) / 1000; an.getFloatTimeDomainData(tmp); let p = 0; for (const x of tmp) p = Math.max(p, Math.abs(x)); onTick(s, Math.min(1, p * 1.4)); if (s >= maxSec) { mr.state === "recording" && mr.stop(); return; } raf = requestAnimationFrame(tick); }; activeRec = { stop: () => mr.state === "recording" && mr.stop() }; mr.start(); tick(); return done; } // Wire a button as a record toggle. function recButton(btn, maxSec, onPcm, label = btn.textContent) { btn.addEventListener("click", async () => { if (activeRec) { activeRec.stop(); return; } btn.classList.add("rec"); let pcm = null; try { pcm = await record({ maxSec, onTick: (s) => (btn.textContent = `Stop · ${s.toFixed(1)}s`) }); } catch (e) { toast("Microphone unavailable: " + e.message); } btn.classList.remove("rec"); btn.textContent = label; if (pcm && pcm.length > SR * 0.5) onPcm(pcm); else if (pcm) toast("That was too short. Speak for a couple of seconds."); }); } // ---------------------------------------------------------------- barcode (a voiceprint drawn as 192 bars) function drawBarcode(canvas, v) { const dpr = window.devicePixelRatio || 1; const w = canvas.clientWidth || 200, h = canvas.clientHeight || 30; canvas.width = w * dpr; canvas.height = h * dpr; const g = canvas.getContext("2d"); g.scale(dpr, dpr); g.clearRect(0, 0, w, h); const ink = css("--ink"), bw = w / v.length; let mx = 0; for (const x of v) mx = Math.max(mx, Math.abs(x)); for (let i = 0; i < v.length; i++) { const a = Math.abs(v[i]) / mx; g.globalAlpha = 0.12 + 0.88 * a; g.fillStyle = ink; const bh = h * (0.35 + 0.65 * a); g.fillRect(i * bw, (h - bh) / 2, Math.max(1, bw - (bw > 2 ? 0.6 : 0)), bh); } g.globalAlpha = 1; } // ---------------------------------------------------------------- tabs document.querySelectorAll(".tabs button").forEach((b) => b.addEventListener("click", () => { document.querySelectorAll(".tabs button").forEach((x) => x.classList.toggle("on", x === b)); document.querySelectorAll(".pane").forEach((p) => p.classList.toggle("on", p.id === "pane-" + b.dataset.tab)); if (b.dataset.tab === "diar" && diar) drawTimeline(); if (b.dataset.tab === "lock" && lockPrint) drawBarcode($("enrollCode"), lockPrint); if (b.dataset.tab === "id") renderRoster(); })); // ================================================================= VOICE LOCK let enroll = [null, null, null]; // {emb, dur, pcm?} let lockPrint = null; const savedLock = store.get("lock", null); if (savedLock) { lockPrint = Float32Array.from(savedLock.print); enroll = savedLock.slots.map((s) => s && { dur: s.dur, emb: null, saved: true }); $("rememberLock").checked = true; } function renderSlots() { const box = $("enrollSlots"); box.innerHTML = ""; enroll.forEach((s, i) => { const el = document.createElement("button"); el.className = "slot" + (s ? " filled" : ""); if (s) { el.innerHTML = `${s.dur.toFixed(1)}sclip ${i + 1}${s.pcm ? ' · play' : ""}`; const a = el.querySelector("[data-play]"); if (a) a.onclick = (e) => { e.stopPropagation(); playPcm(s.pcm); }; } else { el.innerHTML = `Recordclip ${i + 1}`; el.onclick = () => recordSlot(i, el); } box.appendChild(el); }); } async function recordSlot(i, el) { if (activeRec) { activeRec.stop(); return; } el.classList.add("recording"); const t = el.querySelector(".t"), lvl = el.querySelector(".lvl i"); let pcm = null; try { pcm = await record({ maxSec: 5, onTick: (s, l) => { t.textContent = `${s.toFixed(1)}s`; lvl.style.width = `${l * 100}%`; } }); } catch (e) { toast("Microphone unavailable: " + e.message); } if (pcm && pcm.length > SR) await addEnroll(pcm, i); else { if (pcm) toast("Too short. Speak for 2 to 5 seconds."); renderSlots(); } } async function addEnroll(pcm, slot = enroll.findIndex((s) => !s || s.saved)) { if (slot < 0) slot = 2; if (enroll.some((s) => s && s.saved)) enroll = enroll.map((s) => (s && s.saved ? null : s)); // replacing a remembered print enroll[slot] = { pcm, dur: pcm.length / SR, emb: null }; renderSlots(); enroll[slot].emb = await embedOne(pcm); updatePrint(); } function updatePrint() { const embs = enroll.filter((s) => s && s.emb).map((s) => s.emb); if (!embs.length) { if (!enroll.some((s) => s && s.saved)) { lockPrint = null; $("enrollVp").hidden = true; } updateLockIdle(); return; } lockPrint = meanNorm(embs); let cons = ""; if (embs.length > 1) { let s = 0, n = 0; for (let i = 0; i < embs.length; i++) for (let j = i + 1; j < embs.length; j++) { s += dot(embs[i], embs[j]); n++; } cons = ` · clip agreement ${(s / n).toFixed(2)}`; } $("enrollVp").hidden = false; $("enrollMeta").textContent = `Voiceprint from ${embs.length} clip${embs.length > 1 ? "s" : ""}${cons}`; drawBarcode($("enrollCode"), lockPrint); if ($("rememberLock").checked) saveLock(); updateLockIdle(); } function saveLock() { store.set("lock", { print: [...lockPrint], slots: enroll.map((s) => s && { dur: s.dur }) }); } $("rememberLock").onchange = () => ($("rememberLock").checked && lockPrint ? saveLock() : store.del("lock")); function updateLockIdle() { if (lastLock) return; $("lockVerdict").textContent = lockPrint ? "Locked" : "Enroll a voice first"; $("lockSub").textContent = lockPrint ? "Speak to unlock, or try one of the samples." : "Then speak, upload a clip or try a sample."; } $("enrollUpload").onclick = async () => { const files = await pickFiles(true); for (const f of files.slice(0, 3)) { try { await addEnroll(await decodeTo16k(await f.arrayBuffer())); } catch (e) { toast(`Could not read ${f.name}`); } } }; $("enrollSample").onclick = async () => { enroll = [null, null, null]; renderSlots(); const pcms = await Promise.all(["enroll_1", "enroll_2", "enroll_3"].map(fetchSample)); pcms.forEach((p, i) => (enroll[i] = { pcm: p, dur: p.length / SR, emb: null })); renderSlots(); const embs = await Promise.all(pcms.map(embedOne)); embs.forEach((e, i) => (enroll[i].emb = e)); updatePrint(); }; $("enrollClear").onclick = () => { enroll = [null, null, null]; lockPrint = null; lastLock = null; store.del("lock"); $("rememberLock").checked = false; renderSlots(); updatePrint(); setLockState(null); $("lockHist").innerHTML = ""; }; let lastLock = null; // {score, label} const lockTh = () => Number($("lockTh").value); function setLockState(r) { const box = $("lockBox"), th = lockTh(); $("lockThMark").style.left = `${th * 100}%`; box.classList.remove("open", "shut", "idle"); $("lockMeter").classList.remove("ok", "no"); if (!r) { box.classList.add("idle"); $("lockFill").style.width = "0"; updateLockIdle(); return; } const ok = r.score >= th; box.classList.add(ok ? "open" : "shut"); $("lockMeter").classList.add(ok ? "ok" : "no"); $("lockFill").style.width = `${Math.max(0, Math.min(1, r.score)) * 100}%`; $("lockVerdict").textContent = ok ? "Unlocked" : "Access denied"; $("lockSub").textContent = `Similarity ${r.score.toFixed(3)} ${ok ? "≥" : "<"} ${th.toFixed(2)} · ${r.label}`; } async function tryUnlock(pcm, label) { if (!lockPrint) { toast("Enroll a voice first"); return; } $("lockVerdict").textContent = "Listening…"; $("lockSub").textContent = label; const t0 = performance.now(); const e = await embedOne(pcm); const score = dot(lockPrint, e); lastLock = { score, label, ms: performance.now() - t0 }; window.__vp.lastLock = lastLock; setLockState(lastLock); const li = document.createElement("li"); const ok = score >= lockTh(); li.innerHTML = `${ok ? "open" : "denied"}${score.toFixed(3)}`; li.querySelector(".src").textContent = `${label} · ${(pcm.length / SR).toFixed(1)}s`; $("lockHist").prepend(li); while ($("lockHist").children.length > 5) $("lockHist").lastChild.remove(); } $("lockTh").oninput = () => { $("lockThOut").textContent = lockTh().toFixed(2); setLockState(lastLock); }; recButton($("lockRec"), 6, (pcm) => tryUnlock(pcm, "microphone")); $("lockUpload").onclick = async () => { const [f] = await pickFiles(); if (f) tryUnlock(await decodeTo16k(await f.arrayBuffer()), f.name); }; $("lockSame").onclick = async () => tryUnlock(await fetchSample("unlock_same"), "sample: same person, new sentence"); $("lockOther").onclick = async () => tryUnlock(await fetchSample("unlock_impostor"), "sample: a different speaker"); renderSlots(); if (lockPrint) { $("enrollVp").hidden = false; $("enrollMeta").textContent = "Remembered voiceprint"; requestAnimationFrame(() => drawBarcode($("enrollCode"), lockPrint)); } setLockState(null); // ================================================================= VOICE ID let roster = (store.get("roster", []) || []).map((p) => ({ ...p, print: Float32Array.from(p.print), embs: p.embs.map((e) => Float32Array.from(e)) })); const saveRoster = () => store.set("roster", roster.map((p) => ({ name: p.name, sample: !!p.sample, print: [...p.print], embs: p.embs.map((e) => [...e]) }))); function addToRoster(name, emb, sample = false) { name = name.trim() || `Person ${roster.length + 1}`; let p = roster.find((x) => x.name.toLowerCase() === name.toLowerCase()); if (!p) { p = { name, embs: [], sample }; roster.push(p); } p.embs.push(emb); p.print = meanNorm(p.embs); saveRoster(); renderRoster(); return p; } function renderRoster() { const ul = $("roster"); ul.innerHTML = ""; if (!roster.length) { ul.innerHTML = `