Spaces:
Configuration error
Configuration error
Add mic provenance metadata to exports
Browse files
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,
|
| 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 (
|