const recordBtn = document.getElementById("recordBtn"); const stopBtn = document.getElementById("stopBtn"); const uploadBtn = document.getElementById("uploadBtn"); const fileInput = document.getElementById("fileInput"); const preview = document.getElementById("preview"); const transcript = document.getElementById("transcript"); const raw = document.getElementById("raw"); const health = document.getElementById("health"); const recordStatus = document.getElementById("recordStatus"); const maxTokens = document.getElementById("maxTokens"); const hotwords = document.getElementById("hotwords"); const hotwordStrengthSegments = document.getElementById("hotwordStrengthSegments"); const latency = document.getElementById("latency"); const precisionSegments = document.getElementById("precisionSegments"); const precisionNote = document.getElementById("precisionNote"); const audioPrecisionSegments = document.getElementById("audioPrecisionSegments"); const audioPrecisionNote = document.getElementById("audioPrecisionNote"); let stream = null; let audioContext = null; let sourceNode = null; let processorNode = null; let monitorGain = null; let recordedBuffers = []; let recordingSampleRate = 16000; let isStoppingRecording = false; const stopTailCaptureMs = 80; const wavTailSilenceMs = 300; let selectedPrecision = "int8"; let availablePrecisions = ["fp32", "int8", "int4"]; let selectedAudioPrecision = "int8"; let availableAudioPrecisions = ["fp32", "int8"]; let selectedHotwordStrength = "normal"; const hotwordStrengths = { normal: { topk: 50, startBoost: 6.0, continuationBoost: 8.0, }, strong: { topk: 50, startBoost: 7.0, continuationBoost: 9.0, }, }; function apiUrl(path) { if (window.location.protocol === "file:") { return `http://127.0.0.1:7860${path}`; } return path; } function formatBytes(bytes) { if (!Number.isFinite(bytes)) return "--"; const units = ["B", "KB", "MB", "GB", "TB"]; let value = bytes; let unit = 0; while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; } return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`; } function formatSeconds(value) { const number = Number(value); return Number.isFinite(number) ? `${number.toFixed(2)}s` : "--"; } function setBusy(isBusy, label = "Ready") { recordStatus.textContent = label; recordBtn.disabled = isBusy; uploadBtn.disabled = isBusy; precisionSegments.querySelectorAll("button").forEach((button) => { button.disabled = isBusy || !availablePrecisions.includes(button.dataset.precision); }); audioPrecisionSegments.querySelectorAll("button").forEach((button) => { button.disabled = isBusy || !availableAudioPrecisions.includes(button.dataset.audioPrecision); }); hotwordStrengthSegments.querySelectorAll("button").forEach((button) => { button.disabled = isBusy; }); } function setPrecisionButtons() { precisionSegments.querySelectorAll("button").forEach((button) => { const precision = button.dataset.precision; button.classList.toggle("active", precision === selectedPrecision); button.disabled = !availablePrecisions.includes(precision); }); precisionNote.textContent = availablePrecisions.length ? `${availablePrecisions.join(" / ")} available` : "cache unavailable"; } function setAudioPrecisionButtons() { audioPrecisionSegments.querySelectorAll("button").forEach((button) => { const precision = button.dataset.audioPrecision; button.classList.toggle("active", precision === selectedAudioPrecision); button.disabled = !availableAudioPrecisions.includes(precision); }); audioPrecisionNote.textContent = availableAudioPrecisions.length ? `${availableAudioPrecisions.join(" / ")} available` : "audio quant unavailable"; } function setHotwordStrengthButtons() { hotwordStrengthSegments.querySelectorAll("button").forEach((button) => { const strength = button.dataset.hotwordStrength; button.classList.toggle("active", strength === selectedHotwordStrength); }); } function applyRuntime(runtime) { if (!runtime) return; availablePrecisions = Array.isArray(runtime.available_cache_precisions) ? runtime.available_cache_precisions : ["fp32"]; availableAudioPrecisions = Array.isArray(runtime.available_audio_precisions) ? runtime.available_audio_precisions : ["fp32"]; selectedPrecision = runtime.cache_precision || selectedPrecision || "fp32"; selectedAudioPrecision = runtime.audio_precision || selectedAudioPrecision || "fp32"; setPrecisionButtons(); setAudioPrecisionButtons(); health.textContent = `${runtime.backend || "--"} · ${runtime.cache_precision || "n/a"} · audio ${runtime.audio_precision || "n/a"}`; document.getElementById("backend").textContent = runtime.backend || "--"; document.getElementById("precision").textContent = `${runtime.cache_precision || "n/a"} / ${runtime.audio_precision || "n/a"}`; document.getElementById("peakRss").textContent = formatBytes(runtime.service_peak_rss_bytes); document.getElementById("requestPeak").textContent = formatBytes(runtime.last_request_peak_rss_bytes); } async function checkRuntime() { try { const response = await fetch(apiUrl("/api/runtime")); const data = await response.json(); applyRuntime(data.runtime); } catch (error) { health.textContent = `runtime check failed: ${error}`; } } async function switchPrecision(precision) { if (precision === selectedPrecision || !availablePrecisions.includes(precision)) return; setBusy(true, "Loading"); const form = new FormData(); form.append("backend", "onnx_cache"); form.append("cache_precision", precision); form.append("audio_precision", selectedAudioPrecision); try { const response = await fetch(apiUrl("/api/reload"), { method: "POST", body: form }); const data = await response.json(); if (!response.ok) { throw new Error(data.detail || response.statusText); } applyRuntime(data.runtime); recordStatus.textContent = "Ready"; } catch (error) { transcript.textContent = `Runtime switch error: ${error}`; recordStatus.textContent = "Error"; } finally { setBusy(false, recordStatus.textContent); } } async function switchAudioPrecision(precision) { if (precision === selectedAudioPrecision || !availableAudioPrecisions.includes(precision)) return; setBusy(true, "Loading"); const form = new FormData(); form.append("backend", "onnx_cache"); form.append("cache_precision", selectedPrecision); form.append("audio_precision", precision); try { const response = await fetch(apiUrl("/api/reload"), { method: "POST", body: form }); const data = await response.json(); if (!response.ok) { throw new Error(data.detail || response.statusText); } applyRuntime(data.runtime); recordStatus.textContent = "Ready"; } catch (error) { transcript.textContent = `Runtime switch error: ${error}`; recordStatus.textContent = "Error"; } finally { setBusy(false, recordStatus.textContent); } } async function uploadBlob(blob, name = "recording.wav") { try { setBusy(true, "Transcribing"); transcript.textContent = "Working..."; raw.textContent = ""; latency.textContent = "--"; const form = new FormData(); form.append("audio", blob, name); form.append("max_new_tokens", maxTokens.value || "128"); form.append("cache_precision", selectedPrecision); form.append("audio_precision", selectedAudioPrecision); if (hotwords && hotwords.value.trim()) { form.append("hotwords", hotwords.value.trim()); const strength = hotwordStrengths[selectedHotwordStrength] || hotwordStrengths.normal; form.append("hotword_topk", String(strength.topk)); form.append("hotword_start_boost", String(strength.startBoost)); form.append("hotword_continuation_boost", String(strength.continuationBoost)); } const response = await fetch(apiUrl("/asr"), { method: "POST", body: form }); const text = await response.text(); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch (error) { throw new Error(`Invalid ASR response: ${text.slice(0, 240)}`); } if (!response.ok) { throw new Error(data.detail || response.statusText); } transcript.textContent = data.text || "(empty)"; latency.textContent = `${formatSeconds(data.elapsed_seconds)} · ${data.cache_precision || selectedPrecision}`; raw.textContent = JSON.stringify(data, null, 2); applyRuntime(data.runtime); setBusy(false, "Ready"); } catch (error) { transcript.textContent = `Transcription error: ${error}`; raw.textContent = ""; setBusy(false, "Error"); throw error; } } function mergeAudioBuffers(buffers) { const totalLength = buffers.reduce((sum, buffer) => sum + buffer.length, 0); const merged = new Float32Array(totalLength); let offset = 0; for (const buffer of buffers) { merged.set(buffer, offset); offset += buffer.length; } return merged; } function appendSilence(samples, sampleRate, durationMs) { const silenceLength = Math.max(0, Math.round((sampleRate * durationMs) / 1000)); if (silenceLength === 0) return samples; const padded = new Float32Array(samples.length + silenceLength); padded.set(samples); return padded; } function delay(ms) { return new Promise((resolve) => { window.setTimeout(resolve, ms); }); } function writeString(view, offset, value) { for (let index = 0; index < value.length; index += 1) { view.setUint8(offset + index, value.charCodeAt(index)); } } function encodeWav(samples, sampleRate) { const bytesPerSample = 2; const channelCount = 1; const buffer = new ArrayBuffer(44 + samples.length * bytesPerSample); const view = new DataView(buffer); writeString(view, 0, "RIFF"); view.setUint32(4, 36 + samples.length * bytesPerSample, true); writeString(view, 8, "WAVE"); writeString(view, 12, "fmt "); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, channelCount, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * channelCount * bytesPerSample, true); view.setUint16(32, channelCount * bytesPerSample, true); view.setUint16(34, 8 * bytesPerSample, true); writeString(view, 36, "data"); view.setUint32(40, samples.length * bytesPerSample, true); let offset = 44; for (const sample of samples) { const clamped = Math.max(-1, Math.min(1, sample)); view.setInt16(offset, clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff, true); offset += bytesPerSample; } return new Blob([view], { type: "audio/wav" }); } async function stopRecording() { if (!audioContext || isStoppingRecording) return; isStoppingRecording = true; recordStatus.textContent = "Finishing"; stopBtn.disabled = true; await delay(stopTailCaptureMs); recordStatus.textContent = "Preparing"; if (processorNode) { processorNode.disconnect(); processorNode.onaudioprocess = null; } if (sourceNode) sourceNode.disconnect(); if (monitorGain) monitorGain.disconnect(); if (stream) stream.getTracks().forEach((track) => track.stop()); const samples = appendSilence(mergeAudioBuffers(recordedBuffers), recordingSampleRate, wavTailSilenceMs); const wav = encodeWav(samples, recordingSampleRate); preview.src = URL.createObjectURL(wav); await audioContext.close(); audioContext = null; sourceNode = null; processorNode = null; monitorGain = null; stream = null; recordedBuffers = []; isStoppingRecording = false; await uploadBlob(wav, "recording.wav"); } recordBtn.addEventListener("click", async () => { try { stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true, }, }); const AudioContextClass = window.AudioContext || window.webkitAudioContext; audioContext = new AudioContextClass(); recordingSampleRate = audioContext.sampleRate; recordedBuffers = []; isStoppingRecording = false; sourceNode = audioContext.createMediaStreamSource(stream); processorNode = audioContext.createScriptProcessor(4096, 1, 1); monitorGain = audioContext.createGain(); monitorGain.gain.value = 0; processorNode.onaudioprocess = (event) => { recordedBuffers.push(new Float32Array(event.inputBuffer.getChannelData(0))); }; sourceNode.connect(processorNode); processorNode.connect(monitorGain); monitorGain.connect(audioContext.destination); recordStatus.textContent = "Recording"; recordBtn.disabled = true; stopBtn.disabled = false; uploadBtn.disabled = true; } catch (error) { transcript.textContent = `Microphone error: ${error}`; recordStatus.textContent = "Error"; } }); stopBtn.addEventListener("click", () => { stopRecording().catch((error) => { transcript.textContent = String(error); setBusy(false, "Error"); }); }); uploadBtn.addEventListener("click", async () => { const file = fileInput.files && fileInput.files[0]; if (!file) { transcript.textContent = "Choose an audio file first."; return; } preview.src = URL.createObjectURL(file); try { await uploadBlob(file, file.name); } catch (error) { transcript.textContent = String(error); setBusy(false, "Error"); } }); precisionSegments.addEventListener("click", (event) => { const button = event.target.closest("button[data-precision]"); if (button) { switchPrecision(button.dataset.precision); } }); audioPrecisionSegments.addEventListener("click", (event) => { const button = event.target.closest("button[data-audio-precision]"); if (button) { switchAudioPrecision(button.dataset.audioPrecision); } }); hotwordStrengthSegments.addEventListener("click", (event) => { const button = event.target.closest("button[data-hotword-strength]"); if (!button || !hotwordStrengths[button.dataset.hotwordStrength]) return; selectedHotwordStrength = button.dataset.hotwordStrength; setHotwordStrengthButtons(); }); function renderGpuList(gpus) { const gpuList = document.getElementById("gpuList"); const gpuSummary = document.getElementById("gpuSummary"); gpuList.innerHTML = ""; if (!Array.isArray(gpus) || gpus.length === 0) { gpuSummary.textContent = "unified / n/a"; gpuList.innerHTML = "
No discrete GPU telemetry.
"; return; } const totalUsed = gpus.reduce((sum, gpu) => sum + Number(gpu.memory_used_mb || 0), 0); const total = gpus.reduce((sum, gpu) => sum + Number(gpu.memory_total_mb || 0), 0); gpuSummary.textContent = `${totalUsed.toFixed(0)} / ${total.toFixed(0)} MB`; for (const gpu of gpus) { const used = Number(gpu.memory_used_mb || 0); const capacity = Number(gpu.memory_total_mb || 0); const percent = capacity > 0 ? Math.min(100, Math.max(0, (used / capacity) * 100)) : 0; const item = document.createElement("div"); item.className = "gpu-item"; item.innerHTML = `
GPU ${gpu.index} ${used.toFixed(0)} / ${capacity.toFixed(0)} MB · ${Number(gpu.utilization_percent || 0).toFixed(0)}%
${gpu.name || ""} `; gpuList.appendChild(item); } } async function refreshMetrics() { try { const response = await fetch(apiUrl("/metrics")); const data = await response.json(); document.getElementById("metricStamp").textContent = new Date().toLocaleTimeString(); document.getElementById("rss").textContent = formatBytes(data.process.rss_bytes); document.getElementById("sysmem").textContent = `${formatBytes(data.system.used_bytes)} / ${formatBytes( data.system.total_bytes, )}`; renderGpuList(data.gpu && data.gpu.nvidia); applyRuntime(data.runtime); document.getElementById("metricsRaw").textContent = JSON.stringify( { providers: data.runtime && data.runtime.providers, cache_graph: data.runtime && data.runtime.cache_graph, audio_graph: data.runtime && data.runtime.audio_graph, cpu_percent: data.process.cpu_percent, }, null, 2, ); } catch (error) { document.getElementById("metricsRaw").textContent = String(error); } } checkRuntime(); setHotwordStrengthButtons(); refreshMetrics(); setInterval(refreshMetrics, 2000);