browser-speak / tools /run-loopback-series.mjs
Mike0021's picture
Add worker network telemetry to browser evidence
d2ae80e verified
Raw
History Blame Contribute Delete
5.95 kB
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { sourceFingerprint } from "./source-fingerprint.mjs";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const resultPath = resolve(process.env.BROWSER_SPEAK_LOOPBACK_JSON ?? `${tmpdir()}/browser-speak-loopback-series.json`);
const rawPath = resolve(
process.env.BROWSER_SPEAK_LOOPBACK_RAW_JSON ?? `${tmpdir()}/browser-speak-loopback-series-raw.json`,
);
const count = Math.max(1, Number(process.env.BROWSER_SPEAK_LOOPBACK_COUNT ?? 3));
const speed = Number(process.env.BROWSER_SPEAK_LOOPBACK_SPEED ?? 1.0);
const text = process.env.BROWSER_SPEAK_LOOPBACK_TEXT ?? "Identify this browser demo.";
const stack = {
device: process.env.BROWSER_SPEAK_DEVICE ?? "wasm",
llm: process.env.BROWSER_SPEAK_LLM ?? "HuggingFaceTB/SmolLM2-135M-Instruct",
asr: process.env.BROWSER_SPEAK_ASR ?? "onnx-community/moonshine-base-ONNX",
voice: process.env.BROWSER_SPEAK_VOICE ?? "F2",
ttsSteps: Number(process.env.BROWSER_SPEAK_TTS_STEPS ?? 2),
vadSilenceMs: Number(process.env.BROWSER_SPEAK_VAD_SILENCE_MS ?? 480),
partialAsr: process.env.BROWSER_SPEAK_PARTIAL_ASR !== "false",
};
async function main() {
await mkdir(dirname(resultPath), { recursive: true });
await mkdir(dirname(rawPath), { recursive: true });
const env = {
...process.env,
BROWSER_SPEAK_LOCAL_JSON: rawPath,
BROWSER_SPEAK_LOCAL_STACKS: JSON.stringify([stack]),
BROWSER_SPEAK_LOCAL_MAX_STACKS: "1",
BROWSER_SPEAK_LOCAL_PROFILE_DIR:
process.env.BROWSER_SPEAK_LOCAL_PROFILE_DIR ?? `${tmpdir()}/browser-speak-loopback-series-profile`,
BROWSER_SPEAK_LOCAL_TASKS: Array.from({ length: count }, () => "loopback").join(","),
BROWSER_SPEAK_LOAD_TIMEOUT_MS: process.env.BROWSER_SPEAK_LOAD_TIMEOUT_MS ?? "900000",
BROWSER_SPEAK_TASK_TIMEOUT_MS: process.env.BROWSER_SPEAK_TASK_TIMEOUT_MS ?? "240000",
BROWSER_SPEAK_LOOPBACK_SPEED: String(speed),
BROWSER_SPEAK_LOOPBACK_TEXT: text,
};
const exitCode = await runChild(process.execPath, [resolve(scriptDir, "run-local-candidate-benchmark.mjs")], env);
const raw = await readJson(rawPath).catch(() => null);
const payload = await summarize(raw, exitCode);
await writeFile(resultPath, `${JSON.stringify(payload, null, 2)}\n`);
console.log(`Wrote loopback series JSON: ${resultPath}`);
printSummary(payload);
if (!payload.passed) process.exitCode = exitCode || 1;
}
function runChild(command, args, env) {
return new Promise((resolvePromise) => {
const child = spawn(command, args, { env, stdio: "inherit" });
child.on("exit", (code) => resolvePromise(code ?? 1));
});
}
async function readJson(path) {
return JSON.parse(await readFile(path, "utf8"));
}
async function summarize(raw, exitCode) {
const rows = raw?.results?.filter((row) => row.kind === "loopback") ?? [];
const completed = rows.filter((row) => !row.error);
const errors = rows.filter((row) => row.error);
const transcripts = completed.map((row) => row.transcript ?? "");
return {
generatedAt: new Date().toISOString(),
sourceFingerprint: await sourceFingerprint(),
passed: exitCode === 0 && rows.length >= count && errors.length === 0,
rawPath,
config: {
count,
text,
speed,
stack,
taskTimeoutMs: Number(process.env.BROWSER_SPEAK_TASK_TIMEOUT_MS ?? 240000),
url: process.env.BROWSER_SPEAK_URL ?? "http://127.0.0.1:5174/",
},
summary: {
requestedRuns: count,
producedRuns: rows.length,
completedRuns: completed.length,
errorRuns: errors.length,
exactTranscriptRuns: completed.filter((row) => row.sttWer === 0).length,
medianWer: median(completed.map((row) => row.sttWer)),
medianCer: median(completed.map((row) => row.sttCer)),
medianAsrMs: median(completed.map((row) => row.asrMs)),
medianFirstTokenMs: median(completed.map((row) => row.firstTokenMs)),
medianFirstAudioMs: median(completed.map((row) => row.firstAudioMs)),
medianSpeechEndToFirstAudioMs: median(completed.map((row) => row.speechEndToFirstAudioMs)),
medianSpeechEndToAudioEndMs: median(completed.map((row) => row.speechEndToAudioEndMs)),
identityPasses: completed.filter((row) => row.llmQualityPass).length,
transcripts,
errors: errors.map((row) => `${row.id ?? "?"}: ${row.error}`),
},
rows,
rawSummary: raw?.summary ?? null,
rawCandidates: raw?.candidates ?? [],
};
}
function median(values) {
const finite = values.filter(Number.isFinite).sort((a, b) => a - b);
if (finite.length === 0) return null;
const middle = Math.floor(finite.length / 2);
if (finite.length % 2 === 1) return finite[middle];
return (finite[middle - 1] + finite[middle]) / 2;
}
function printSummary(payload) {
const summary = payload.summary;
console.log(
[
`${summary.completedRuns}/${summary.requestedRuns} completed`,
`${summary.exactTranscriptRuns}/${summary.completedRuns} exact transcripts`,
`median WER ${formatPercent(summary.medianWer)}`,
`median speech-end-to-audio ${formatMs(summary.medianSpeechEndToFirstAudioMs)}`,
].join(", "),
);
if (summary.transcripts.length > 0) {
console.log(`Transcripts: ${summary.transcripts.map((text) => JSON.stringify(text)).join(", ")}`);
}
for (const error of summary.errors) console.log(`Loopback error: ${error}`);
}
function formatMs(value) {
if (!Number.isFinite(value)) return "-";
if (value < 1000) return `${Math.round(value)} ms`;
return `${(value / 1000).toFixed(2)} s`;
}
function formatPercent(value) {
if (!Number.isFinite(value)) return "-";
return `${Math.round(value * 100)}%`;
}
main().catch((error) => {
console.error(error.stack ?? error.message);
process.exitCode = 1;
});