Mike0021 commited on
Commit
c95c085
·
verified ·
1 Parent(s): 4ebe6ff

Add mic provenance metadata to exports

Browse files
Files changed (2) hide show
  1. README.md +1 -1
  2. app.js +33 -0
README.md CHANGED
@@ -75,7 +75,7 @@ Use **Run benchmark suite** after loading models to execute the current stack's
75
 
76
  Microphone startup is capped at 15 seconds. If browser permissions, fake media devices, or audio worklet startup hang, the active mic benchmark is cancelled and the event log records the failure instead of leaving a silent pending run. Loopback audio is also followed by an explicit ASR flush after the synthesized prompt and trailing silence have been fed, so sticky VAD/STT candidates fail or finish as rows instead of waiting for the global benchmark timeout.
77
 
78
- Exported result rows include runtime metadata under `stack.environment`: page URL, secure-context status, browser user agent, CPU concurrency hint, device memory hint when exposed, WebGPU availability, WebGPU adapter info/features/software-adapter classification when exposed by the browser, and microphone or loopback sample rate. The benchmark stack also records voice, TTS steps, VAD silence, partial-ASR state, full-stack model load time as `modelLoadMs`, first-response latency, and scheduled full-audio completion as `audioEndMs` / `speechEndToAudioEndMs`. Keep that metadata with WebGPU results so latency numbers can be compared across machines.
79
 
80
  The latency panel counts **speech end** from the detected acoustic end of speech, not from the later worker event that closes the turn after trailing silence. The separate **VAD close delay** metric shows how much silence was observed before the turn was finalized.
81
 
 
75
 
76
  Microphone startup is capped at 15 seconds. If browser permissions, fake media devices, or audio worklet startup hang, the active mic benchmark is cancelled and the event log records the failure instead of leaving a silent pending run. Loopback audio is also followed by an explicit ASR flush after the synthesized prompt and trailing silence have been fed, so sticky VAD/STT candidates fail or finish as rows instead of waiting for the global benchmark timeout.
77
 
78
+ Exported result rows include runtime metadata under `stack.environment`: page URL, secure-context status, browser user agent, CPU concurrency hint, device memory hint when exposed, WebGPU availability, WebGPU adapter info/features/software-adapter classification when exposed by the browser, microphone or loopback sample rate, and sanitized microphone track settings such as channel count, sample rate, latency, and audio-processing flags when the browser reports them. The benchmark stack also records voice, TTS steps, VAD silence, partial-ASR state, full-stack model load time as `modelLoadMs`, first-response latency, and scheduled full-audio completion as `audioEndMs` / `speechEndToAudioEndMs`. Keep that metadata with WebGPU results so latency numbers can be compared across machines.
79
 
80
  The latency panel counts **speech end** from the detected acoustic end of speech, not from the later worker event that closes the turn after trailing silence. The separate **VAD close delay** metric shows how much silence was observed before the turn was finalized.
81
 
app.js CHANGED
@@ -204,6 +204,7 @@ const state = {
204
  modelLoadStartedAt: null,
205
  loadedStack: null,
206
  inputSampleRate: null,
 
207
  micInputStats: {
208
  chunks: 0,
209
  samples: 0,
@@ -940,12 +941,15 @@ async function startMic() {
940
  autoGainControl: true,
941
  },
942
  });
 
943
  state.audioContext = new AudioContext();
944
  state.inputSampleRate = state.audioContext.sampleRate;
945
  if (state.activeBenchmark) {
946
  state.activeBenchmark.inputSampleRate = state.audioContext.sampleRate;
 
947
  if (state.activeBenchmark.stack?.environment) {
948
  state.activeBenchmark.stack.environment.inputSampleRate = state.audioContext.sampleRate;
 
949
  }
950
  }
951
  const workletUrl = URL.createObjectURL(
@@ -1003,6 +1007,25 @@ async function getUserMediaWithTimeout(constraints, timeoutMs = MIC_START_TIMEOU
1003
  return stream;
1004
  }
1005
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
  function withTimeout(promise, timeoutMs, label) {
1007
  let timeoutId = 0;
1008
  return Promise.race([
@@ -1027,6 +1050,7 @@ function stopMic() {
1027
  state.audioContext?.close();
1028
  state.audioContext = null;
1029
  state.inputSampleRate = null;
 
1030
  state.mediaStream = null;
1031
  state.workletNode = null;
1032
  state.micSource = null;
@@ -1485,6 +1509,7 @@ function runtimeEnvironment() {
1485
  webgpuFeatures: state.webgpuAdapterFeatures,
1486
  webgpuSoftwareAdapter: state.webgpuSoftwareAdapter,
1487
  inputSampleRate: state.inputSampleRate,
 
1488
  };
1489
  }
1490
 
@@ -1520,6 +1545,7 @@ function startBenchmark(kind, prompt, { referenceText = "", timeoutMs = 120000 }
1520
  speechEndToAudioEndMs: null,
1521
  loopbackTtsSynthesisMs: null,
1522
  inputSampleRate: state.inputSampleRate,
 
1523
  micInputChunks: null,
1524
  micInputPeak: null,
1525
  micInputRms: null,
@@ -1933,6 +1959,7 @@ function evidenceSummaryForRows(results = []) {
1933
  const completed = results.filter((result) => !result.error);
1934
  const micRows = completed.filter((result) => result.kind === "mic");
1935
  const micRowsWithInput = micRows.filter(hasRealMicInputStats);
 
1936
  const micIdentityPasses = micRows.filter((result) => result.llmQualityPass === true);
1937
  const micMedianWer = median(micRows.map((result) => result.sttWer));
1938
  const micMedianSpeechEndToFirstAudioMs = median(
@@ -1949,6 +1976,7 @@ function evidenceSummaryForRows(results = []) {
1949
  maxMedianWer: REAL_MIC_MAX_WER,
1950
  rows: micRows.length,
1951
  rowsWithInput: micRowsWithInput.length,
 
1952
  identityPasses: micIdentityPasses.length,
1953
  medianWer: micMedianWer,
1954
  medianSpeechEndToFirstAudioMs: micMedianSpeechEndToFirstAudioMs,
@@ -1987,6 +2015,11 @@ function hasRealMicInputStats(row) {
1987
  );
1988
  }
1989
 
 
 
 
 
 
1990
  function rowUsesHardwareWebGpu(row) {
1991
  const environment = row.stack?.environment ?? {};
1992
  return (
 
204
  modelLoadStartedAt: null,
205
  loadedStack: null,
206
  inputSampleRate: null,
207
+ inputTrackSettings: null,
208
  micInputStats: {
209
  chunks: 0,
210
  samples: 0,
 
941
  autoGainControl: true,
942
  },
943
  });
944
+ state.inputTrackSettings = readAudioTrackSettings(state.mediaStream);
945
  state.audioContext = new AudioContext();
946
  state.inputSampleRate = state.audioContext.sampleRate;
947
  if (state.activeBenchmark) {
948
  state.activeBenchmark.inputSampleRate = state.audioContext.sampleRate;
949
+ state.activeBenchmark.inputTrackSettings = state.inputTrackSettings;
950
  if (state.activeBenchmark.stack?.environment) {
951
  state.activeBenchmark.stack.environment.inputSampleRate = state.audioContext.sampleRate;
952
+ state.activeBenchmark.stack.environment.inputTrackSettings = state.inputTrackSettings;
953
  }
954
  }
955
  const workletUrl = URL.createObjectURL(
 
1007
  return stream;
1008
  }
1009
 
1010
+ function readAudioTrackSettings(stream) {
1011
+ const settings = stream?.getAudioTracks?.()[0]?.getSettings?.();
1012
+ if (!settings) return null;
1013
+ const allowed = [
1014
+ "autoGainControl",
1015
+ "channelCount",
1016
+ "echoCancellation",
1017
+ "latency",
1018
+ "noiseSuppression",
1019
+ "sampleRate",
1020
+ "sampleSize",
1021
+ ];
1022
+ return Object.fromEntries(
1023
+ allowed
1024
+ .filter((key) => settings[key] != null)
1025
+ .map((key) => [key, settings[key]]),
1026
+ );
1027
+ }
1028
+
1029
  function withTimeout(promise, timeoutMs, label) {
1030
  let timeoutId = 0;
1031
  return Promise.race([
 
1050
  state.audioContext?.close();
1051
  state.audioContext = null;
1052
  state.inputSampleRate = null;
1053
+ state.inputTrackSettings = null;
1054
  state.mediaStream = null;
1055
  state.workletNode = null;
1056
  state.micSource = null;
 
1509
  webgpuFeatures: state.webgpuAdapterFeatures,
1510
  webgpuSoftwareAdapter: state.webgpuSoftwareAdapter,
1511
  inputSampleRate: state.inputSampleRate,
1512
+ inputTrackSettings: state.inputTrackSettings,
1513
  };
1514
  }
1515
 
 
1545
  speechEndToAudioEndMs: null,
1546
  loopbackTtsSynthesisMs: null,
1547
  inputSampleRate: state.inputSampleRate,
1548
+ inputTrackSettings: state.inputTrackSettings,
1549
  micInputChunks: null,
1550
  micInputPeak: null,
1551
  micInputRms: null,
 
1959
  const completed = results.filter((result) => !result.error);
1960
  const micRows = completed.filter((result) => result.kind === "mic");
1961
  const micRowsWithInput = micRows.filter(hasRealMicInputStats);
1962
+ const micRowsWithTrackSettings = micRows.filter(hasInputTrackSettings);
1963
  const micIdentityPasses = micRows.filter((result) => result.llmQualityPass === true);
1964
  const micMedianWer = median(micRows.map((result) => result.sttWer));
1965
  const micMedianSpeechEndToFirstAudioMs = median(
 
1976
  maxMedianWer: REAL_MIC_MAX_WER,
1977
  rows: micRows.length,
1978
  rowsWithInput: micRowsWithInput.length,
1979
+ rowsWithTrackSettings: micRowsWithTrackSettings.length,
1980
  identityPasses: micIdentityPasses.length,
1981
  medianWer: micMedianWer,
1982
  medianSpeechEndToFirstAudioMs: micMedianSpeechEndToFirstAudioMs,
 
2015
  );
2016
  }
2017
 
2018
+ function hasInputTrackSettings(row) {
2019
+ const settings = row.inputTrackSettings ?? row.stack?.environment?.inputTrackSettings;
2020
+ return Boolean(settings && Object.keys(settings).length > 0);
2021
+ }
2022
+
2023
  function rowUsesHardwareWebGpu(row) {
2024
  const environment = row.stack?.environment ?? {};
2025
  return (