Spaces:
Running
Running
Add WebGPU evidence row action
Browse files- README.md +1 -1
- app.js +86 -2
- index.html +1 -0
- styles.css +2 -0
README.md
CHANGED
|
@@ -28,7 +28,7 @@ Known testing notes:
|
|
| 28 |
- The app automatically falls back to WASM if hardware WebGPU is unavailable.
|
| 29 |
- Initial load can be large because STT, LLM, TTS, and voice embeddings are all browser-side assets.
|
| 30 |
- For validation evidence, run **Run benchmark suite**, **Run 3 real-mic series**, then **Download JSON**.
|
| 31 |
-
- The Latency panel shows separate real-mic and hardware-WebGPU validation cards.
|
| 32 |
- The exported `hostMetadata.hfSpaceCommit` identifies the exact static Space revision served to the browser.
|
| 33 |
- Hosted smoke validation passed on May 28, 2026: TTS, identity, and loopback rows completed with zero benchmark-phase network requests, zero benchmark errors, and zero server-inference suspects.
|
| 34 |
- The latest local validation also passed UI smoke, client-side/no-server smoke, synthetic loopback stability, and first-TTS-chunk safety. The remaining external gates are real human microphone rows and a hardware WebGPU row from a browser that exposes a real GPU adapter.
|
|
|
|
| 28 |
- The app automatically falls back to WASM if hardware WebGPU is unavailable.
|
| 29 |
- Initial load can be large because STT, LLM, TTS, and voice embeddings are all browser-side assets.
|
| 30 |
- For validation evidence, run **Run benchmark suite**, **Run 3 real-mic series**, then **Download JSON**.
|
| 31 |
+
- The Latency panel shows separate real-mic and hardware-WebGPU validation cards. On hardware browsers, **Run WebGPU evidence row** captures a fast row for the downloaded JSON.
|
| 32 |
- The exported `hostMetadata.hfSpaceCommit` identifies the exact static Space revision served to the browser.
|
| 33 |
- Hosted smoke validation passed on May 28, 2026: TTS, identity, and loopback rows completed with zero benchmark-phase network requests, zero benchmark errors, and zero server-inference suspects.
|
| 34 |
- The latest local validation also passed UI smoke, client-side/no-server smoke, synthetic loopback stability, and first-TTS-chunk safety. The remaining external gates are real human microphone rows and a hardware WebGPU row from a browser that exposes a real GPU adapter.
|
app.js
CHANGED
|
@@ -10,6 +10,7 @@ const elements = {
|
|
| 10 |
ttsBenchmarkButton: $("ttsBenchmarkButton"),
|
| 11 |
loopbackButton: $("loopbackButton"),
|
| 12 |
bargeInButton: $("bargeInButton"),
|
|
|
|
| 13 |
micBenchmarkButton: $("micBenchmarkButton"),
|
| 14 |
micSeriesButton: $("micSeriesButton"),
|
| 15 |
copyResultsButton: $("copyResultsButton"),
|
|
@@ -88,6 +89,14 @@ const IDENTITY_PRIMER_MESSAGES = [
|
|
| 88 |
role: "assistant",
|
| 89 |
content: APP_IDENTITY_ANSWER,
|
| 90 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
];
|
| 92 |
const APP_IDENTITY_QUALITY_RULES = [
|
| 93 |
{ label: "client/browser/local", pattern: /\b(client(?:-side)?|browser|local)\b/i },
|
|
@@ -120,9 +129,18 @@ const MIN_VAD_SILENCE_MS = 200;
|
|
| 120 |
const MAX_VAD_SILENCE_MS = 800;
|
| 121 |
const DEFAULT_LOOPBACK_SPEED = 1.0;
|
| 122 |
const DEFAULT_LOOPBACK_PREROLL_MS = 180;
|
| 123 |
-
const DEFAULT_LOOPBACK_PROMPT = "
|
| 124 |
const REAL_MIC_TARGET_RUNS = 3;
|
| 125 |
const REAL_MIC_MAX_WER = 0.25;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
const MIC_START_TIMEOUT_MS = 15000;
|
| 127 |
const WEBGPU_ADAPTER_TIMEOUT_MS = 5000;
|
| 128 |
const DEFAULT_TTS_CHUNKING = Object.freeze({
|
|
@@ -469,6 +487,7 @@ function setBenchmarkControlsDisabled(disabled) {
|
|
| 469 |
elements.bargeInButton.disabled = locked;
|
| 470 |
elements.micBenchmarkButton.disabled = locked;
|
| 471 |
elements.micSeriesButton.disabled = locked;
|
|
|
|
| 472 |
}
|
| 473 |
|
| 474 |
function resetMetrics() {
|
|
@@ -579,6 +598,21 @@ function updateGpuValidationStatus() {
|
|
| 579 |
elements.gpuValidationStatus.textContent = status;
|
| 580 |
elements.gpuValidationDetail.textContent = detail;
|
| 581 |
elements.gpuValidationProgressBar.style.width = `${Math.round(Math.max(0, Math.min(1, progress)) * 100)}%`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 582 |
}
|
| 583 |
|
| 584 |
function resetTurnState() {
|
|
@@ -1356,7 +1390,11 @@ function updateOutputQuality(benchmark, output) {
|
|
| 1356 |
|
| 1357 |
function isAppIdentityQuestion(text) {
|
| 1358 |
const normalized = normalizeForQuality(text);
|
| 1359 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1360 |
}
|
| 1361 |
|
| 1362 |
function words(text) {
|
|
@@ -2302,6 +2340,48 @@ function runBargeInBenchmark() {
|
|
| 2302 |
return true;
|
| 2303 |
}
|
| 2304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2305 |
async function runMicBenchmark(options = {}) {
|
| 2306 |
const series = options?.series === true;
|
| 2307 |
if (!state.modelsLoaded) return false;
|
|
@@ -2690,6 +2770,7 @@ function installAutomationApi() {
|
|
| 2690 |
runTts: (options) => runSingleAutomationBenchmark(runTtsBenchmark, options),
|
| 2691 |
runLoopback: (options) => runSingleAutomationBenchmark(runLoopbackBenchmark, options),
|
| 2692 |
runBargeIn: (options) => runSingleAutomationBenchmark(runBargeInBenchmark, options),
|
|
|
|
| 2693 |
runMic: (options) => runSingleAutomationBenchmark(runMicBenchmark, options),
|
| 2694 |
runMicSeries: runAutomationMicSeries,
|
| 2695 |
synthesizeAudio: synthesizeAutomationAudio,
|
|
@@ -2734,6 +2815,9 @@ elements.chatBenchmarkButton.addEventListener("click", runChatBenchmark);
|
|
| 2734 |
elements.ttsBenchmarkButton.addEventListener("click", runTtsBenchmark);
|
| 2735 |
elements.loopbackButton.addEventListener("click", runLoopbackBenchmark);
|
| 2736 |
elements.bargeInButton.addEventListener("click", runBargeInBenchmark);
|
|
|
|
|
|
|
|
|
|
| 2737 |
elements.micBenchmarkButton.addEventListener("click", runMicBenchmark);
|
| 2738 |
elements.micSeriesButton.addEventListener("click", runMicSeriesBenchmark);
|
| 2739 |
elements.copyResultsButton.addEventListener("click", () => {
|
|
|
|
| 10 |
ttsBenchmarkButton: $("ttsBenchmarkButton"),
|
| 11 |
loopbackButton: $("loopbackButton"),
|
| 12 |
bargeInButton: $("bargeInButton"),
|
| 13 |
+
gpuBenchmarkButton: $("gpuBenchmarkButton"),
|
| 14 |
micBenchmarkButton: $("micBenchmarkButton"),
|
| 15 |
micSeriesButton: $("micSeriesButton"),
|
| 16 |
copyResultsButton: $("copyResultsButton"),
|
|
|
|
| 89 |
role: "assistant",
|
| 90 |
content: APP_IDENTITY_ANSWER,
|
| 91 |
},
|
| 92 |
+
{
|
| 93 |
+
role: "user",
|
| 94 |
+
content: "Please identify this browser demo.",
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
role: "assistant",
|
| 98 |
+
content: APP_IDENTITY_ANSWER,
|
| 99 |
+
},
|
| 100 |
];
|
| 101 |
const APP_IDENTITY_QUALITY_RULES = [
|
| 102 |
{ label: "client/browser/local", pattern: /\b(client(?:-side)?|browser|local)\b/i },
|
|
|
|
| 129 |
const MAX_VAD_SILENCE_MS = 800;
|
| 130 |
const DEFAULT_LOOPBACK_SPEED = 1.0;
|
| 131 |
const DEFAULT_LOOPBACK_PREROLL_MS = 180;
|
| 132 |
+
const DEFAULT_LOOPBACK_PROMPT = "Please identify this browser demo.";
|
| 133 |
const REAL_MIC_TARGET_RUNS = 3;
|
| 134 |
const REAL_MIC_MAX_WER = 0.25;
|
| 135 |
+
const HARDWARE_WEBGPU_EVIDENCE_STACK = Object.freeze({
|
| 136 |
+
device: "webgpu",
|
| 137 |
+
llm: "HuggingFaceTB/SmolLM2-135M-Instruct",
|
| 138 |
+
asr: "onnx-community/moonshine-base-ONNX",
|
| 139 |
+
voice: "F2",
|
| 140 |
+
ttsSteps: 2,
|
| 141 |
+
vadSilenceMs: 480,
|
| 142 |
+
partialAsr: true,
|
| 143 |
+
});
|
| 144 |
const MIC_START_TIMEOUT_MS = 15000;
|
| 145 |
const WEBGPU_ADAPTER_TIMEOUT_MS = 5000;
|
| 146 |
const DEFAULT_TTS_CHUNKING = Object.freeze({
|
|
|
|
| 487 |
elements.bargeInButton.disabled = locked;
|
| 488 |
elements.micBenchmarkButton.disabled = locked;
|
| 489 |
elements.micSeriesButton.disabled = locked;
|
| 490 |
+
updateGpuEvidenceButton();
|
| 491 |
}
|
| 492 |
|
| 493 |
function resetMetrics() {
|
|
|
|
| 598 |
elements.gpuValidationStatus.textContent = status;
|
| 599 |
elements.gpuValidationDetail.textContent = detail;
|
| 600 |
elements.gpuValidationProgressBar.style.width = `${Math.round(Math.max(0, Math.min(1, progress)) * 100)}%`;
|
| 601 |
+
updateGpuEvidenceButton();
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
function updateGpuEvidenceButton() {
|
| 605 |
+
if (!elements.gpuBenchmarkButton) return;
|
| 606 |
+
const hasHardware = state.webgpuAvailable === true && state.webgpuSoftwareAdapter !== true;
|
| 607 |
+
const busy =
|
| 608 |
+
state.modelsLoading ||
|
| 609 |
+
state.suiteRunning ||
|
| 610 |
+
state.micSeries.active ||
|
| 611 |
+
Boolean(state.activeBenchmark);
|
| 612 |
+
const hardwareRows = evidenceSummaryForRows(state.benchmarkResults).hardwareWebgpu.hardwareRows ?? 0;
|
| 613 |
+
elements.gpuBenchmarkButton.disabled = busy || !hasHardware;
|
| 614 |
+
elements.gpuBenchmarkButton.textContent =
|
| 615 |
+
hardwareRows > 0 ? "Run another WebGPU evidence row" : "Run WebGPU evidence row";
|
| 616 |
}
|
| 617 |
|
| 618 |
function resetTurnState() {
|
|
|
|
| 1390 |
|
| 1391 |
function isAppIdentityQuestion(text) {
|
| 1392 |
const normalized = normalizeForQuality(text);
|
| 1393 |
+
return (
|
| 1394 |
+
/^(what's|what is|what app is|what demo is|what application is) this$/.test(normalized) ||
|
| 1395 |
+
/^(what's|what is) this (app|application|demo)$/.test(normalized) ||
|
| 1396 |
+
/^(please )?identify this (browser )?(demo|app|application)$/.test(normalized)
|
| 1397 |
+
);
|
| 1398 |
}
|
| 1399 |
|
| 1400 |
function words(text) {
|
|
|
|
| 2340 |
return true;
|
| 2341 |
}
|
| 2342 |
|
| 2343 |
+
async function runHardwareWebGpuEvidenceBenchmark() {
|
| 2344 |
+
if (state.activeBenchmark || state.suiteRunning || state.micSeries.active || state.modelsLoading) {
|
| 2345 |
+
logEvent("Finish the current benchmark before collecting WebGPU evidence.");
|
| 2346 |
+
return false;
|
| 2347 |
+
}
|
| 2348 |
+
await supportsWebGPU();
|
| 2349 |
+
if (!state.webgpuAvailable || state.webgpuSoftwareAdapter) {
|
| 2350 |
+
logEvent("Hardware WebGPU evidence requires a browser exposing a real GPU adapter.");
|
| 2351 |
+
updateGpuValidationStatus();
|
| 2352 |
+
return false;
|
| 2353 |
+
}
|
| 2354 |
+
if (!state.modelsLoaded) {
|
| 2355 |
+
applyHardwareWebGpuEvidenceStack();
|
| 2356 |
+
updateRuntimeStatus();
|
| 2357 |
+
logEvent("Loading the default hardware WebGPU evidence stack.");
|
| 2358 |
+
await loadModels({ ttsWarmup: false });
|
| 2359 |
+
if (!state.modelsLoaded) {
|
| 2360 |
+
logEvent("WebGPU evidence stack did not finish loading.");
|
| 2361 |
+
return false;
|
| 2362 |
+
}
|
| 2363 |
+
}
|
| 2364 |
+
const stack = currentBenchmarkStack();
|
| 2365 |
+
if (stack.device !== "webgpu") {
|
| 2366 |
+
logEvent("Unload the current stack, select WebGPU or Auto on a hardware adapter, then rerun WebGPU evidence.");
|
| 2367 |
+
return false;
|
| 2368 |
+
}
|
| 2369 |
+
logEvent("Running hardware WebGPU evidence row.");
|
| 2370 |
+
return runBenchmark();
|
| 2371 |
+
}
|
| 2372 |
+
|
| 2373 |
+
function applyHardwareWebGpuEvidenceStack() {
|
| 2374 |
+
elements.deviceSelect.value = HARDWARE_WEBGPU_EVIDENCE_STACK.device;
|
| 2375 |
+
elements.llmModelSelect.value = HARDWARE_WEBGPU_EVIDENCE_STACK.llm;
|
| 2376 |
+
elements.asrModelSelect.value = HARDWARE_WEBGPU_EVIDENCE_STACK.asr;
|
| 2377 |
+
elements.voiceSelect.value = HARDWARE_WEBGPU_EVIDENCE_STACK.voice;
|
| 2378 |
+
elements.ttsSteps.value = String(HARDWARE_WEBGPU_EVIDENCE_STACK.ttsSteps);
|
| 2379 |
+
elements.vadSilence.value = String(HARDWARE_WEBGPU_EVIDENCE_STACK.vadSilenceMs);
|
| 2380 |
+
elements.partialToggle.checked = HARDWARE_WEBGPU_EVIDENCE_STACK.partialAsr;
|
| 2381 |
+
updateTtsStepsLabel();
|
| 2382 |
+
updateVadSilenceLabel();
|
| 2383 |
+
}
|
| 2384 |
+
|
| 2385 |
async function runMicBenchmark(options = {}) {
|
| 2386 |
const series = options?.series === true;
|
| 2387 |
if (!state.modelsLoaded) return false;
|
|
|
|
| 2770 |
runTts: (options) => runSingleAutomationBenchmark(runTtsBenchmark, options),
|
| 2771 |
runLoopback: (options) => runSingleAutomationBenchmark(runLoopbackBenchmark, options),
|
| 2772 |
runBargeIn: (options) => runSingleAutomationBenchmark(runBargeInBenchmark, options),
|
| 2773 |
+
runWebGpuEvidence: (options) => runSingleAutomationBenchmark(runHardwareWebGpuEvidenceBenchmark, options),
|
| 2774 |
runMic: (options) => runSingleAutomationBenchmark(runMicBenchmark, options),
|
| 2775 |
runMicSeries: runAutomationMicSeries,
|
| 2776 |
synthesizeAudio: synthesizeAutomationAudio,
|
|
|
|
| 2815 |
elements.ttsBenchmarkButton.addEventListener("click", runTtsBenchmark);
|
| 2816 |
elements.loopbackButton.addEventListener("click", runLoopbackBenchmark);
|
| 2817 |
elements.bargeInButton.addEventListener("click", runBargeInBenchmark);
|
| 2818 |
+
elements.gpuBenchmarkButton.addEventListener("click", () => {
|
| 2819 |
+
runHardwareWebGpuEvidenceBenchmark().catch((error) => logEvent(`WebGPU evidence failed: ${error.message}`));
|
| 2820 |
+
});
|
| 2821 |
elements.micBenchmarkButton.addEventListener("click", runMicBenchmark);
|
| 2822 |
elements.micSeriesButton.addEventListener("click", runMicSeriesBenchmark);
|
| 2823 |
elements.copyResultsButton.addEventListener("click", () => {
|
index.html
CHANGED
|
@@ -190,6 +190,7 @@
|
|
| 190 |
<button id="ttsBenchmarkButton" type="button" disabled>Run TTS benchmark</button>
|
| 191 |
<button id="loopbackButton" type="button" disabled>Run voice loopback</button>
|
| 192 |
<button id="bargeInButton" type="button" disabled>Run barge-in check</button>
|
|
|
|
| 193 |
<button id="micBenchmarkButton" type="button" disabled>Benchmark real mic</button>
|
| 194 |
<button id="micSeriesButton" type="button" disabled>Run 3 real-mic series</button>
|
| 195 |
</section>
|
|
|
|
| 190 |
<button id="ttsBenchmarkButton" type="button" disabled>Run TTS benchmark</button>
|
| 191 |
<button id="loopbackButton" type="button" disabled>Run voice loopback</button>
|
| 192 |
<button id="bargeInButton" type="button" disabled>Run barge-in check</button>
|
| 193 |
+
<button id="gpuBenchmarkButton" type="button" disabled>Run WebGPU evidence row</button>
|
| 194 |
<button id="micBenchmarkButton" type="button" disabled>Benchmark real mic</button>
|
| 195 |
<button id="micSeriesButton" type="button" disabled>Run 3 real-mic series</button>
|
| 196 |
</section>
|
styles.css
CHANGED
|
@@ -426,6 +426,7 @@ h2 {
|
|
| 426 |
#ttsBenchmarkButton,
|
| 427 |
#loopbackButton,
|
| 428 |
#bargeInButton,
|
|
|
|
| 429 |
#micBenchmarkButton,
|
| 430 |
#micSeriesButton {
|
| 431 |
width: 100%;
|
|
@@ -436,6 +437,7 @@ h2 {
|
|
| 436 |
#ttsBenchmarkButton,
|
| 437 |
#loopbackButton,
|
| 438 |
#bargeInButton,
|
|
|
|
| 439 |
#micBenchmarkButton,
|
| 440 |
#micSeriesButton {
|
| 441 |
margin-top: 8px;
|
|
|
|
| 426 |
#ttsBenchmarkButton,
|
| 427 |
#loopbackButton,
|
| 428 |
#bargeInButton,
|
| 429 |
+
#gpuBenchmarkButton,
|
| 430 |
#micBenchmarkButton,
|
| 431 |
#micSeriesButton {
|
| 432 |
width: 100%;
|
|
|
|
| 437 |
#ttsBenchmarkButton,
|
| 438 |
#loopbackButton,
|
| 439 |
#bargeInButton,
|
| 440 |
+
#gpuBenchmarkButton,
|
| 441 |
#micBenchmarkButton,
|
| 442 |
#micSeriesButton {
|
| 443 |
margin-top: 8px;
|