Spaces:
Configuration error
Configuration error
| 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; | |
| }); | |