Mike0021 commited on
Commit
82e9b36
·
verified ·
1 Parent(s): a7bb098

Add browser evidence capture control

Browse files
Files changed (4) hide show
  1. README.md +3 -1
  2. app.js +123 -4
  3. index.html +1 -0
  4. styles.css +2 -0
README.md CHANGED
@@ -22,6 +22,8 @@ Direct app URL: https://mike0021-browser-speak.static.hf.space
22
 
23
  After benchmark or real-mic runs, use **Download JSON** to save the evidence file locally. The export includes the hosted Space commit when the static host exposes it, a concise evidence-readiness summary, and the raw benchmark rows. The Runtime panel also shows the served build.
24
 
 
 
25
  From the source workspace, the one-command hosted evidence path is:
26
 
27
  ```bash
@@ -35,7 +37,7 @@ Known testing notes:
35
  - Use Chrome or another browser with WebGPU/WASM support and microphone permission enabled.
36
  - The app automatically falls back to WASM if hardware WebGPU is unavailable.
37
  - Initial load can be large because STT, LLM, TTS, and voice embeddings are all browser-side assets.
38
- - For validation evidence, run **Run benchmark suite**, **Run 3 real-mic series**, then **Download JSON**.
39
  - 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.
40
  - The exported `hostMetadata.hfSpaceCommit` identifies the exact static Space revision served to the browser.
41
  - 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.
 
22
 
23
  After benchmark or real-mic runs, use **Download JSON** to save the evidence file locally. The export includes the hosted Space commit when the static host exposes it, a concise evidence-readiness summary, and the raw benchmark rows. The Runtime panel also shows the served build.
24
 
25
+ In the browser UI, **Run evidence capture** is the guided validation path. It captures a hardware WebGPU row when the browser exposes a real GPU adapter, runs the 3-row real-mic series, then leaves **Download JSON** enabled for saving the evidence file.
26
+
27
  From the source workspace, the one-command hosted evidence path is:
28
 
29
  ```bash
 
37
  - Use Chrome or another browser with WebGPU/WASM support and microphone permission enabled.
38
  - The app automatically falls back to WASM if hardware WebGPU is unavailable.
39
  - Initial load can be large because STT, LLM, TTS, and voice embeddings are all browser-side assets.
40
+ - For validation evidence, run **Run evidence capture**, then **Download JSON**.
41
  - 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.
42
  - The exported `hostMetadata.hfSpaceCommit` identifies the exact static Space revision served to the browser.
43
  - 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.
app.js CHANGED
@@ -11,6 +11,7 @@ const elements = {
11
  loopbackButton: $("loopbackButton"),
12
  bargeInButton: $("bargeInButton"),
13
  gpuBenchmarkButton: $("gpuBenchmarkButton"),
 
14
  micBenchmarkButton: $("micBenchmarkButton"),
15
  micSeriesButton: $("micSeriesButton"),
16
  copyResultsButton: $("copyResultsButton"),
@@ -210,6 +211,7 @@ const state = {
210
  benchmarkTimeout: null,
211
  pendingPlaybackSchedules: 0,
212
  suiteRunning: false,
 
213
  micSeries: {
214
  active: false,
215
  target: 3,
@@ -478,7 +480,7 @@ function setSessionControls(mode) {
478
  }
479
 
480
  function setBenchmarkControlsDisabled(disabled) {
481
- const locked = disabled || state.micSeries.active;
482
  elements.suiteButton.disabled = locked;
483
  elements.benchmarkButton.disabled = locked;
484
  elements.chatBenchmarkButton.disabled = locked;
@@ -488,6 +490,7 @@ function setBenchmarkControlsDisabled(disabled) {
488
  elements.micBenchmarkButton.disabled = locked;
489
  elements.micSeriesButton.disabled = locked;
490
  updateGpuEvidenceButton();
 
491
  }
492
 
493
  function resetMetrics() {
@@ -599,6 +602,7 @@ function updateGpuValidationStatus() {
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() {
@@ -607,6 +611,7 @@ function updateGpuEvidenceButton() {
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;
@@ -615,6 +620,30 @@ function updateGpuEvidenceButton() {
615
  hardwareRows > 0 ? "Run another WebGPU evidence row" : "Run WebGPU evidence row";
616
  }
617
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
  function resetTurnState() {
619
  state.messages = initialMessages();
620
  state.currentAssistant = "";
@@ -860,6 +889,7 @@ async function unloadModels({ quiet = false } = {}) {
860
  state.modelLoadStartedAt = null;
861
  state.loadedStack = null;
862
  state.suiteRunning = false;
 
863
  resetTurnState();
864
  resetMetrics();
865
  resetConversationUi();
@@ -2023,6 +2053,7 @@ function interruptForBargeIn({ preserveLoopbackFeed = false, cancelTts = true, l
2023
 
2024
  function stopAll() {
2025
  state.suiteRunning = false;
 
2026
  cancelMicSeries();
2027
  interruptForBargeIn();
2028
  cancelActiveBenchmark();
@@ -2370,6 +2401,81 @@ async function runHardwareWebGpuEvidenceBenchmark() {
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;
@@ -2467,7 +2573,11 @@ async function runSuiteStep(label, runner) {
2467
  await waitForBenchmarkRow(previousCount, label);
2468
  }
2469
 
2470
- function waitForBenchmarkRow(previousCount, label) {
 
 
 
 
2471
  return new Promise((resolve, reject) => {
2472
  const startedAt = performance.now();
2473
  const timer = window.setInterval(() => {
@@ -2476,12 +2586,12 @@ function waitForBenchmarkRow(previousCount, label) {
2476
  resolve();
2477
  return;
2478
  }
2479
- if (!state.suiteRunning) {
2480
  window.clearInterval(timer);
2481
  reject(new Error(`${label} benchmark was cancelled.`));
2482
  return;
2483
  }
2484
- if (performance.now() - startedAt > 135000) {
2485
  window.clearInterval(timer);
2486
  reject(new Error(`${label} benchmark did not finish.`));
2487
  }
@@ -2547,6 +2657,7 @@ function automationSnapshot() {
2547
  },
2548
  activeBenchmark: state.activeBenchmark ? { ...state.activeBenchmark } : null,
2549
  suiteRunning: state.suiteRunning,
 
2550
  micSeries: { ...state.micSeries, timer: Boolean(state.micSeries.timer) },
2551
  ttsChunking: { ...state.ttsChunking },
2552
  stack: state.loadedStack ? currentBenchmarkStack() : null,
@@ -2771,6 +2882,11 @@ function installAutomationApi() {
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,
@@ -2818,6 +2934,9 @@ 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", () => {
 
11
  loopbackButton: $("loopbackButton"),
12
  bargeInButton: $("bargeInButton"),
13
  gpuBenchmarkButton: $("gpuBenchmarkButton"),
14
+ evidenceCaptureButton: $("evidenceCaptureButton"),
15
  micBenchmarkButton: $("micBenchmarkButton"),
16
  micSeriesButton: $("micSeriesButton"),
17
  copyResultsButton: $("copyResultsButton"),
 
211
  benchmarkTimeout: null,
212
  pendingPlaybackSchedules: 0,
213
  suiteRunning: false,
214
+ evidenceCaptureRunning: false,
215
  micSeries: {
216
  active: false,
217
  target: 3,
 
480
  }
481
 
482
  function setBenchmarkControlsDisabled(disabled) {
483
+ const locked = disabled || state.micSeries.active || state.evidenceCaptureRunning;
484
  elements.suiteButton.disabled = locked;
485
  elements.benchmarkButton.disabled = locked;
486
  elements.chatBenchmarkButton.disabled = locked;
 
490
  elements.micBenchmarkButton.disabled = locked;
491
  elements.micSeriesButton.disabled = locked;
492
  updateGpuEvidenceButton();
493
+ updateEvidenceCaptureButton();
494
  }
495
 
496
  function resetMetrics() {
 
602
  elements.gpuValidationDetail.textContent = detail;
603
  elements.gpuValidationProgressBar.style.width = `${Math.round(Math.max(0, Math.min(1, progress)) * 100)}%`;
604
  updateGpuEvidenceButton();
605
+ updateEvidenceCaptureButton();
606
  }
607
 
608
  function updateGpuEvidenceButton() {
 
611
  const busy =
612
  state.modelsLoading ||
613
  state.suiteRunning ||
614
+ state.evidenceCaptureRunning ||
615
  state.micSeries.active ||
616
  Boolean(state.activeBenchmark);
617
  const hardwareRows = evidenceSummaryForRows(state.benchmarkResults).hardwareWebgpu.hardwareRows ?? 0;
 
620
  hardwareRows > 0 ? "Run another WebGPU evidence row" : "Run WebGPU evidence row";
621
  }
622
 
623
+ function updateEvidenceCaptureButton() {
624
+ if (!elements.evidenceCaptureButton) return;
625
+ const busy =
626
+ state.modelsLoading ||
627
+ state.suiteRunning ||
628
+ state.evidenceCaptureRunning ||
629
+ state.micSeries.active ||
630
+ Boolean(state.activeBenchmark);
631
+ elements.evidenceCaptureButton.disabled = busy;
632
+ elements.evidenceCaptureButton.textContent = state.evidenceCaptureRunning
633
+ ? "Capturing evidence..."
634
+ : "Run evidence capture";
635
+ }
636
+
637
+ function evidenceCaptureBusy() {
638
+ return (
639
+ state.activeBenchmark ||
640
+ state.suiteRunning ||
641
+ state.micSeries.active ||
642
+ state.modelsLoading ||
643
+ state.evidenceCaptureRunning
644
+ );
645
+ }
646
+
647
  function resetTurnState() {
648
  state.messages = initialMessages();
649
  state.currentAssistant = "";
 
889
  state.modelLoadStartedAt = null;
890
  state.loadedStack = null;
891
  state.suiteRunning = false;
892
+ state.evidenceCaptureRunning = false;
893
  resetTurnState();
894
  resetMetrics();
895
  resetConversationUi();
 
2053
 
2054
  function stopAll() {
2055
  state.suiteRunning = false;
2056
+ state.evidenceCaptureRunning = false;
2057
  cancelMicSeries();
2058
  interruptForBargeIn();
2059
  cancelActiveBenchmark();
 
2401
  return runBenchmark();
2402
  }
2403
 
2404
+ async function runEvidenceCaptureSequence() {
2405
+ if (evidenceCaptureBusy()) {
2406
+ logEvent("Finish the current run before starting evidence capture.");
2407
+ return false;
2408
+ }
2409
+ state.evidenceCaptureRunning = true;
2410
+ setBenchmarkControlsDisabled(true);
2411
+ logEvent("Starting evidence capture: hardware WebGPU row when available, then 3 real-mic rows.");
2412
+ try {
2413
+ await supportsWebGPU();
2414
+ const hasHardwareWebGpu = state.webgpuAvailable === true && state.webgpuSoftwareAdapter !== true;
2415
+ if (!state.modelsLoaded) {
2416
+ if (hasHardwareWebGpu) {
2417
+ applyHardwareWebGpuEvidenceStack();
2418
+ logEvent("Hardware WebGPU available; loading the default WebGPU evidence stack.");
2419
+ } else {
2420
+ logEvent("Loading the selected stack for real-mic evidence capture.");
2421
+ }
2422
+ updateRuntimeStatus();
2423
+ await loadModels({ ttsWarmup: false });
2424
+ if (!state.modelsLoaded) throw new Error("Model load did not complete.");
2425
+ }
2426
+
2427
+ const beforeGpuEvidence = evidenceSummaryForRows(state.benchmarkResults).hardwareWebgpu;
2428
+ if (hasHardwareWebGpu && beforeGpuEvidence.hardwareRows === 0) {
2429
+ if (currentBenchmarkStack().device === "webgpu") {
2430
+ const previousCount = state.benchmarkResults.length;
2431
+ const started = await runHardwareWebGpuEvidenceBenchmark();
2432
+ if (!started) throw new Error("Hardware WebGPU evidence row did not start.");
2433
+ await waitForBenchmarkRow(previousCount, "Hardware WebGPU evidence", {
2434
+ timeoutMs: 150000,
2435
+ isActive: () => state.evidenceCaptureRunning,
2436
+ });
2437
+ } else {
2438
+ logEvent("Hardware WebGPU is available, but the loaded stack is not WebGPU; hardware evidence still needs a WebGPU row.");
2439
+ }
2440
+ } else if (!hasHardwareWebGpu) {
2441
+ logEvent("Hardware WebGPU is not exposed; evidence capture will collect real-mic rows only.");
2442
+ }
2443
+
2444
+ const realMicEvidence = evidenceSummaryForRows(state.benchmarkResults).realMic;
2445
+ if (!realMicEvidence.passed) {
2446
+ const started = await runMicSeriesBenchmark();
2447
+ if (!started) throw new Error("Real-mic series did not start.");
2448
+ await waitForAutomationCondition(
2449
+ () => !state.micSeries.active && !state.activeBenchmark,
2450
+ 420000,
2451
+ "Evidence real-mic series",
2452
+ );
2453
+ } else {
2454
+ logEvent("Real-mic evidence is already complete.");
2455
+ }
2456
+
2457
+ const evidence = evidenceSummaryForRows(state.benchmarkResults);
2458
+ const missing = [];
2459
+ if (!evidence.realMic.passed) missing.push("real mic");
2460
+ if (!evidence.hardwareWebgpu.passed) missing.push("hardware WebGPU");
2461
+ logEvent(
2462
+ missing.length === 0
2463
+ ? "Evidence capture complete; use Download JSON to save the proof."
2464
+ : `Evidence capture finished; still missing ${missing.join(" and ")} evidence. Use Download JSON to save current rows.`,
2465
+ );
2466
+ return true;
2467
+ } catch (error) {
2468
+ if (state.micSeries.active) cancelMicSeries();
2469
+ logEvent(`Evidence capture stopped: ${error.message}`);
2470
+ return false;
2471
+ } finally {
2472
+ state.evidenceCaptureRunning = false;
2473
+ if (state.modelsLoaded && !state.suiteRunning) setBenchmarkControlsDisabled(false);
2474
+ updateEvidenceCaptureButton();
2475
+ updateMicValidationStatus();
2476
+ }
2477
+ }
2478
+
2479
  function applyHardwareWebGpuEvidenceStack() {
2480
  elements.deviceSelect.value = HARDWARE_WEBGPU_EVIDENCE_STACK.device;
2481
  elements.llmModelSelect.value = HARDWARE_WEBGPU_EVIDENCE_STACK.llm;
 
2573
  await waitForBenchmarkRow(previousCount, label);
2574
  }
2575
 
2576
+ function waitForBenchmarkRow(
2577
+ previousCount,
2578
+ label,
2579
+ { timeoutMs = 135000, isActive = () => state.suiteRunning } = {},
2580
+ ) {
2581
  return new Promise((resolve, reject) => {
2582
  const startedAt = performance.now();
2583
  const timer = window.setInterval(() => {
 
2586
  resolve();
2587
  return;
2588
  }
2589
+ if (!isActive()) {
2590
  window.clearInterval(timer);
2591
  reject(new Error(`${label} benchmark was cancelled.`));
2592
  return;
2593
  }
2594
+ if (performance.now() - startedAt > timeoutMs) {
2595
  window.clearInterval(timer);
2596
  reject(new Error(`${label} benchmark did not finish.`));
2597
  }
 
2657
  },
2658
  activeBenchmark: state.activeBenchmark ? { ...state.activeBenchmark } : null,
2659
  suiteRunning: state.suiteRunning,
2660
+ evidenceCaptureRunning: state.evidenceCaptureRunning,
2661
  micSeries: { ...state.micSeries, timer: Boolean(state.micSeries.timer) },
2662
  ttsChunking: { ...state.ttsChunking },
2663
  stack: state.loadedStack ? currentBenchmarkStack() : null,
 
2882
  runLoopback: (options) => runSingleAutomationBenchmark(runLoopbackBenchmark, options),
2883
  runBargeIn: (options) => runSingleAutomationBenchmark(runBargeInBenchmark, options),
2884
  runWebGpuEvidence: (options) => runSingleAutomationBenchmark(runHardwareWebGpuEvidenceBenchmark, options),
2885
+ async runEvidenceCapture() {
2886
+ const started = await runEvidenceCaptureSequence();
2887
+ if (!started) throw new Error("Evidence capture did not start.");
2888
+ return automationSnapshot();
2889
+ },
2890
  runMic: (options) => runSingleAutomationBenchmark(runMicBenchmark, options),
2891
  runMicSeries: runAutomationMicSeries,
2892
  synthesizeAudio: synthesizeAutomationAudio,
 
2934
  elements.gpuBenchmarkButton.addEventListener("click", () => {
2935
  runHardwareWebGpuEvidenceBenchmark().catch((error) => logEvent(`WebGPU evidence failed: ${error.message}`));
2936
  });
2937
+ elements.evidenceCaptureButton.addEventListener("click", () => {
2938
+ runEvidenceCaptureSequence().catch((error) => logEvent(`Evidence capture failed: ${error.message}`));
2939
+ });
2940
  elements.micBenchmarkButton.addEventListener("click", runMicBenchmark);
2941
  elements.micSeriesButton.addEventListener("click", runMicSeriesBenchmark);
2942
  elements.copyResultsButton.addEventListener("click", () => {
index.html CHANGED
@@ -191,6 +191,7 @@
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>
 
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="evidenceCaptureButton" type="button" disabled>Run evidence capture</button>
195
  <button id="micBenchmarkButton" type="button" disabled>Benchmark real mic</button>
196
  <button id="micSeriesButton" type="button" disabled>Run 3 real-mic series</button>
197
  </section>
styles.css CHANGED
@@ -427,6 +427,7 @@ h2 {
427
  #loopbackButton,
428
  #bargeInButton,
429
  #gpuBenchmarkButton,
 
430
  #micBenchmarkButton,
431
  #micSeriesButton {
432
  width: 100%;
@@ -438,6 +439,7 @@ h2 {
438
  #loopbackButton,
439
  #bargeInButton,
440
  #gpuBenchmarkButton,
 
441
  #micBenchmarkButton,
442
  #micSeriesButton {
443
  margin-top: 8px;
 
427
  #loopbackButton,
428
  #bargeInButton,
429
  #gpuBenchmarkButton,
430
+ #evidenceCaptureButton,
431
  #micBenchmarkButton,
432
  #micSeriesButton {
433
  width: 100%;
 
439
  #loopbackButton,
440
  #bargeInButton,
441
  #gpuBenchmarkButton,
442
+ #evidenceCaptureButton,
443
  #micBenchmarkButton,
444
  #micSeriesButton {
445
  margin-top: 8px;