426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
223 lines
7.8 KiB
TypeScript
223 lines
7.8 KiB
TypeScript
#!/usr/bin/env bun
|
||
/**
|
||
* STT quality benchmark across the published eliza-1-asr GGUF quants (#10726
|
||
* scope item: "STT quality benchmarks per model/quant, documented per-device
|
||
* selection"). Real weights, real fused-lib transcription — no mocks.
|
||
*
|
||
* For every `eliza-1-asr-<quant>.gguf` in ELIZA_ASR_QUANT_DIR (paired with the
|
||
* dir's shared `eliza-1-asr-mmproj.gguf`), plus optionally the shipped bundle
|
||
* ASR (ELIZA_ASR_BUNDLE — what mobile provisioning actually stages), this lane
|
||
* transcribes the fixed-transcript Kokoro corpus and reports per-quant:
|
||
* WER (vs the known reference texts), transcription latency, RTF, and load
|
||
* time. Corpus caveat: the reference audio is TTS speech, so absolute WER
|
||
* carries the TTS pronunciation floor; the CROSS-QUANT comparison is the
|
||
* signal (identical audio for every quant).
|
||
*
|
||
* Exit 0 pass / 1 fail / 2 skip. STT_BENCH_REQUIRE=1 turns skips into failures.
|
||
*
|
||
* Env:
|
||
* ELIZA_INFERENCE_LIBRARY / ELIZA_INFERENCE_LIB_DIR — fused lib
|
||
* ELIZA_ASR_QUANT_DIR — dir with eliza-1-asr-<quant>.gguf + eliza-1-asr-mmproj.gguf
|
||
* ELIZA_ASR_BUNDLE — optional shipped-bundle row (asr/eliza-1-asr.gguf layout)
|
||
* ELIZA_KOKORO_MODEL_DIR — Kokoro model for one-time corpus synthesis
|
||
* STT_BENCH_MAX_BEST_WER — sanity ceiling for the BEST quant's mean WER (default 0.5)
|
||
* ELIZA_VOICE_BENCH_OUT — report dir (default <plugin>/voice-bench-output)
|
||
*/
|
||
|
||
import { existsSync, readdirSync, statSync } from "node:fs";
|
||
import path from "node:path";
|
||
import { wordErrorRate } from "@elizaos/shared/voice-wer";
|
||
import { STT_BENCH_CORPUS } from "../src/services/voice/bench-utils";
|
||
import {
|
||
BENCH_SAMPLE_RATE,
|
||
bootFusedFfi,
|
||
type CorpusItem,
|
||
defaultReportDir,
|
||
ensureKokoroCorpus,
|
||
makeBenchGates,
|
||
makeQuantBundle,
|
||
mean,
|
||
median,
|
||
writeBenchReport,
|
||
} from "./voice-bench-shared";
|
||
|
||
const TAG = "stt-quant-bench";
|
||
const gates = makeBenchGates(TAG, "STT_BENCH_REQUIRE");
|
||
const log = (msg: string) => console.log(`[${TAG}] ${msg}`);
|
||
|
||
interface Variant {
|
||
name: string;
|
||
bundleDir: string;
|
||
sizeBytes: number;
|
||
cleanup?: () => void;
|
||
}
|
||
|
||
interface VariantResult {
|
||
name: string;
|
||
sizeBytes: number;
|
||
loadMs: number;
|
||
meanWer: number;
|
||
medianWer: number;
|
||
meanTranscribeMs: number;
|
||
firstUttMs: number;
|
||
/** total transcribe time / total audio time — lower is better, <1 = faster than realtime. */
|
||
rtf: number;
|
||
/** 1/rtf — "× realtime", higher is better (asr_bench.ts convention). */
|
||
xRealtime: number;
|
||
utterances: Array<{
|
||
id: string;
|
||
reference: string;
|
||
transcript: string;
|
||
wer: number;
|
||
ms: number;
|
||
audioSeconds: number;
|
||
}>;
|
||
}
|
||
|
||
const { ffi, libPath } = bootFusedFfi(gates);
|
||
log(`lib=${libPath}`);
|
||
|
||
// --- discover variants -------------------------------------------------------
|
||
const quantDir = process.env.ELIZA_ASR_QUANT_DIR?.trim();
|
||
const variants: Variant[] = [];
|
||
if (quantDir && existsSync(quantDir)) {
|
||
const mmproj = path.join(quantDir, "eliza-1-asr-mmproj.gguf");
|
||
if (!existsSync(mmproj)) {
|
||
gates.skip(`ELIZA_ASR_QUANT_DIR has no eliza-1-asr-mmproj.gguf (${quantDir})`);
|
||
}
|
||
const quantFiles = readdirSync(quantDir)
|
||
.filter((f) => /^eliza-1-asr-(?!mmproj).+\.gguf$/.test(f))
|
||
.sort();
|
||
for (const f of quantFiles) {
|
||
const quantPath = path.join(quantDir, f);
|
||
const { dir, cleanup } = makeQuantBundle(quantPath, mmproj);
|
||
variants.push({
|
||
name: f.replace(/^eliza-1-asr-/, "").replace(/\.gguf$/, ""),
|
||
bundleDir: dir,
|
||
sizeBytes: statSync(quantPath).size + statSync(mmproj).size,
|
||
cleanup,
|
||
});
|
||
}
|
||
}
|
||
const shippedBundle = process.env.ELIZA_ASR_BUNDLE?.trim();
|
||
if (shippedBundle && existsSync(path.join(shippedBundle, "asr", "eliza-1-asr.gguf"))) {
|
||
const main = path.join(shippedBundle, "asr", "eliza-1-asr.gguf");
|
||
const mm = path.join(shippedBundle, "asr", "eliza-1-asr-mmproj.gguf");
|
||
variants.push({
|
||
name: "bundle-2b (shipped)",
|
||
bundleDir: shippedBundle,
|
||
sizeBytes: statSync(main).size + (existsSync(mm) ? statSync(mm).size : 0),
|
||
});
|
||
}
|
||
if (variants.length === 0) {
|
||
gates.skip(
|
||
"no ASR variants found — set ELIZA_ASR_QUANT_DIR (eliza-1-asr-<q>.gguf + mmproj) " +
|
||
"and/or ELIZA_ASR_BUNDLE (shipped bundle layout)",
|
||
);
|
||
}
|
||
log(`variants: ${variants.map((v) => v.name).join(", ")}`);
|
||
|
||
// --- corpus -------------------------------------------------------------------
|
||
const corpus: CorpusItem[] = await ensureKokoroCorpus("clean", STT_BENCH_CORPUS, gates, log);
|
||
const totalAudioSec = corpus.reduce((a, c) => a + c.seconds, 0);
|
||
log(`corpus: ${corpus.length} utterances, ${totalAudioSec.toFixed(1)}s audio total`);
|
||
|
||
// --- benchmark ----------------------------------------------------------------
|
||
const results: VariantResult[] = [];
|
||
for (const variant of variants) {
|
||
log(`--- ${variant.name} (${(variant.sizeBytes / 1e6).toFixed(0)} MB) ---`);
|
||
const tLoad0 = performance.now();
|
||
const ctx = ffi.create(variant.bundleDir);
|
||
ffi.mmapAcquire(ctx, "asr");
|
||
const loadMs = Math.round(performance.now() - tLoad0);
|
||
const utterances: VariantResult["utterances"] = [];
|
||
try {
|
||
for (const item of corpus) {
|
||
const t0 = performance.now();
|
||
const { text } = ffi.asrTranscribeTimed({
|
||
ctx,
|
||
pcm: item.pcm,
|
||
sampleRateHz: BENCH_SAMPLE_RATE,
|
||
});
|
||
const ms = Math.round(performance.now() - t0);
|
||
const transcript = (text ?? "").trim();
|
||
const wer = wordErrorRate(item.text, transcript);
|
||
utterances.push({
|
||
id: item.id,
|
||
reference: item.text,
|
||
transcript,
|
||
wer,
|
||
ms,
|
||
audioSeconds: item.seconds,
|
||
});
|
||
log(` ${item.id} ${ms}ms WER=${wer.toFixed(3)} "${transcript}"`);
|
||
}
|
||
} finally {
|
||
ffi.mmapEvict(ctx, "asr");
|
||
ffi.destroy(ctx);
|
||
variant.cleanup?.();
|
||
}
|
||
const totalMs = utterances.reduce((a, u) => a + u.ms, 0);
|
||
const rtf = totalMs / 1000 / totalAudioSec;
|
||
results.push({
|
||
name: variant.name,
|
||
sizeBytes: variant.sizeBytes,
|
||
loadMs,
|
||
meanWer: mean(utterances.map((u) => u.wer)),
|
||
medianWer: median(utterances.map((u) => u.wer)),
|
||
meanTranscribeMs: mean(utterances.map((u) => u.ms)),
|
||
firstUttMs: utterances[0]?.ms ?? 0,
|
||
rtf,
|
||
xRealtime: rtf > 0 ? 1 / rtf : 0,
|
||
utterances,
|
||
});
|
||
}
|
||
|
||
// --- report -------------------------------------------------------------------
|
||
const header =
|
||
"| variant | size (MB) | load (ms) | mean WER | median WER | mean ms/utt | 1st utt (ms) | RTF | × realtime |";
|
||
const sep = "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |";
|
||
const rows = results.map(
|
||
(r) =>
|
||
`| ${r.name} | ${(r.sizeBytes / 1e6).toFixed(0)} | ${r.loadMs} | ${r.meanWer.toFixed(3)} | ${r.medianWer.toFixed(3)} | ${Math.round(r.meanTranscribeMs)} | ${r.firstUttMs} | ${r.rtf.toFixed(3)} | ${r.xRealtime.toFixed(1)}× |`,
|
||
);
|
||
const table = [header, sep, ...rows].join("\n");
|
||
console.log(`\n${table}\n`);
|
||
|
||
const md = [
|
||
"# STT quant benchmark — eliza-1-asr (real weights, fused lib, CPU)",
|
||
"",
|
||
`Host: ${process.platform}-${process.arch}. Corpus: ${corpus.length} fixed-transcript Kokoro utterances (${totalAudioSec.toFixed(1)}s). ` +
|
||
"Absolute WER includes the TTS pronunciation floor; cross-quant deltas are the signal.",
|
||
"",
|
||
table,
|
||
"",
|
||
].join("\n");
|
||
const { jsonPath, mdPath } = writeBenchReport(
|
||
defaultReportDir(),
|
||
"stt-quant-bench",
|
||
{
|
||
schemaVersion: 1,
|
||
generatedAt: new Date().toISOString(),
|
||
host: `${process.platform}-${process.arch}`,
|
||
lib: libPath,
|
||
corpus: { utterances: corpus.length, totalAudioSec },
|
||
results,
|
||
},
|
||
md,
|
||
);
|
||
log(`report: ${jsonPath}`);
|
||
log(`report: ${mdPath}`);
|
||
|
||
// --- gate ---------------------------------------------------------------------
|
||
// Sanity, not vanity: if even the best quant cannot transcribe the corpus, the
|
||
// published weights (or the ASR path) are broken and the lane must go RED.
|
||
const maxBestWer = Number(process.env.STT_BENCH_MAX_BEST_WER ?? "0.5");
|
||
const bestWer = Math.min(...results.map((r) => r.meanWer));
|
||
if (bestWer > maxBestWer) {
|
||
gates.fail(
|
||
`best-quant mean WER ${bestWer.toFixed(3)} > ${maxBestWer} — published ASR weights or decode path are broken`,
|
||
);
|
||
}
|
||
log(`PASS (best mean WER ${bestWer.toFixed(3)} ≤ ${maxBestWer})`);
|