Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

1511 lines
57 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bun
/*
* e2e_loop_bench.mjs — Eliza-1 end-to-end mic→speech voice-loop benchmark.
*
* Drives the *real* fused runtime — the omnivoice-grafted `llama-server`
* (`/completion` + `/v1/audio/speech` + the in-process MTP speculative
* loop) and `libelizainference.{so,dylib}`'s ASR FFI — through one or more
* complete voice turns:
*
* WAV (mic) → ASR transcribe (FFI) → text generate w/ MTP spec decode
* → phrase chunker → OmniVoice TTS (HTTP /v1/audio/speech) → PCM out
*
* It measures, per turn and aggregated:
* - ASR latency + WER (vs the reference text the WAV was synthesized from)
* - first-token latency (ASR-done → first decoded text token, SSE)
* - decode tokens/sec
* - MTP acceptance rate (n_drafted_accepted / n_drafted from /metrics)
* - first-audio latency (first text token → first PCM sample of the first phrase)
* - TTS RTF (wall-seconds / audio-seconds)
* - total turn latency (mic-in → last PCM sample)
* - peak RSS (server VmHWM via /proc, when available)
* - barge-in cancel latency (in-flight TTS HTTP request abort → stop consuming)
*
* `--turns 30` runs the 30-turn endurance variant: 30 loops, asserts no
* crash / no monotone RSS leak, and asserts peak RSS stays inside the
* manifest's `ramBudgetMb.recommended`.
*
* Hard requirements (per packages/inference/AGENTS.md §4 / §8): one process,
* one llama.cpp build, one GGML pin — text + MTP + TTS all in the fused
* `llama-server`; ASR via the fused FFI library; no second model process.
*
* Honesty: a backend whose fused build is not installed, or a bundle whose
* artifacts are missing, produces `status: "needs-bundle"` / `"needs-build"`
* with a reason and null metrics — never a fabricated pass.
*
* bun packages/inference/verify/e2e_loop_bench.mjs \
* --bundle ~/.eliza/local-inference/models/eliza-1-2b.bundle \
* --tier 2b --backend cpu [--turns 1] [--wav a.wav,b.wav] [--report out.json]
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
// --------------------------------------------------------------------------
// CLI
// --------------------------------------------------------------------------
function parseArgs(argv) {
const args = {
bundle: process.env.ELIZA_E2E_BUNDLE || "",
tier: process.env.ELIZA_E2E_TIER || "",
backend: process.env.ELIZA_E2E_BACKEND || "cpu", // cpu | vulkan | cuda
binDir: process.env.ELIZA_E2E_BIN_DIR || "",
wavs: (process.env.ELIZA_E2E_WAVS || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean),
refs: (process.env.ELIZA_E2E_REFS || "")
.split("|")
.map((s) => s.trim())
.filter(Boolean),
turns: Number.parseInt(process.env.ELIZA_E2E_TURNS || "1", 10),
nPredict: Number.parseInt(process.env.ELIZA_E2E_N_PREDICT || "40", 10),
enduranceNPredict: Number.parseInt(process.env.ELIZA_E2E_ENDURANCE_N_PREDICT || "12", 10),
ttsAllPhrases: process.env.ELIZA_E2E_TTS_ALL_PHRASES === "1",
ttsSteps: Number.parseInt(process.env.ELIZA_E2E_TTS_STEPS || "32", 10),
micTtsSteps: Number.parseInt(process.env.ELIZA_E2E_MIC_TTS_STEPS || "32", 10),
ttsRoundtripAsr: process.env.ELIZA_E2E_TTS_ROUNDTRIP_ASR === "1",
ttsAudioDir: process.env.ELIZA_E2E_TTS_AUDIO_DIR || "",
phraseMaxTokens: Number.parseInt(process.env.ELIZA_E2E_PHRASE_MAX_TOKENS || "8", 10),
maxTtsPhrases: Number.parseInt(process.env.ELIZA_E2E_MAX_TTS_PHRASES || "3", 10),
threads: Number.parseInt(
process.env.ELIZA_E2E_THREADS || String(Math.min(os.cpus().length, 12)),
10,
),
ctx: Number.parseInt(process.env.ELIZA_E2E_CTX || "2048", 10),
ngl: process.env.ELIZA_E2E_NGL || "0",
startTimeoutS: Number.parseInt(
process.env.ELIZA_E2E_START_TIMEOUT || "180",
10,
),
turnTimeoutS: Number.parseInt(
process.env.ELIZA_E2E_TURN_TIMEOUT || "240",
10,
),
report: process.env.ELIZA_E2E_REPORT || "",
quiet: process.env.ELIZA_E2E_QUIET === "1",
};
for (let i = 0; i < argv.length; i += 1) {
const a = argv[i];
const next = () => {
i += 1;
if (i >= argv.length) throw new Error(`missing value for ${a}`);
return argv[i];
};
if (a === "--bundle" || a === "--bundle-dir") args.bundle = next();
else if (a === "--tier") args.tier = next();
else if (a === "--backend") args.backend = next();
else if (a === "--bin-dir") args.binDir = next();
else if (a === "--wav" || a === "--wavs")
args.wavs = next()
.split(",")
.map((s) => s.trim())
.filter(Boolean);
else if (a === "--ref" || a === "--refs")
args.refs = next()
.split("|")
.map((s) => s.trim())
.filter(Boolean);
else if (a === "--turns") args.turns = Number.parseInt(next(), 10);
else if (a === "--n-predict") args.nPredict = Number.parseInt(next(), 10);
else if (a === "--endurance-n-predict") args.enduranceNPredict = Number.parseInt(next(), 10);
else if (a === "--tts-all-phrases") args.ttsAllPhrases = true;
else if (a === "--tts-steps") args.ttsSteps = Number.parseInt(next(), 10);
else if (a === "--mic-tts-steps") args.micTtsSteps = Number.parseInt(next(), 10);
else if (a === "--tts-roundtrip-asr") args.ttsRoundtripAsr = true;
else if (a === "--tts-audio-dir") args.ttsAudioDir = next();
else if (a === "--phrase-max-tokens") args.phraseMaxTokens = Number.parseInt(next(), 10);
else if (a === "--max-tts-phrases") args.maxTtsPhrases = Number.parseInt(next(), 10);
else if (a === "--threads") args.threads = Number.parseInt(next(), 10);
else if (a === "--ctx") args.ctx = Number.parseInt(next(), 10);
else if (a === "--ngl") args.ngl = next();
else if (a === "--report") args.report = next();
else if (a === "--quiet") args.quiet = true;
else if (a === "--help" || a === "-h") {
console.log(
"Usage: bun packages/inference/verify/e2e_loop_bench.mjs --bundle <dir> --tier <id> [--backend cpu|vulkan|cuda|metal] [--turns N] [--wav a,b] [--report out.json]",
);
process.exit(0);
} else throw new Error(`unknown argument: ${a}`);
}
return args;
}
const log = (q) => (...m) => {
if (!q) console.log("[e2e-loop]", ...m);
};
// --------------------------------------------------------------------------
// Discovery
// --------------------------------------------------------------------------
function stateRoot() {
return (
process.env.ELIZA_STATE_DIR?.trim() ||
process.env.ELIZA_STATE_DIR?.trim() ||
path.join(os.homedir(), ".eliza")
);
}
function platformTag() {
const sys =
{ darwin: "darwin", linux: "linux", win32: "windows" }[process.platform] ||
process.platform;
const arch = { x64: "x64", arm64: "arm64" }[process.arch] || process.arch;
return `${sys}-${arch}`;
}
function libName() {
if (process.platform === "darwin") return "libelizainference.dylib";
if (process.platform === "win32") return "libelizainference.dll";
return "libelizainference.so";
}
/**
* Find the fused `llama-server` build dir for the requested backend. We
* require a *fused* build (`fused: true` in CAPABILITIES.json) because the
* AGENTS.md §4 contract is one fused process — a stock build can't serve
* `/v1/audio/speech`.
*/
function discoverEngine(backend, explicitBinDir) {
const root = path.join(stateRoot(), "local-inference", "bin", "mtp");
if (explicitBinDir) {
return validateEngineDir(explicitBinDir, backend);
}
if (!fs.existsSync(root)) {
return { ok: false, reason: `${root} does not exist (no managed builds)` };
}
const plat = platformTag();
const prefer = `${plat}-${backend}-fused`;
const dirs = fs
.readdirSync(root)
.filter((d) => fs.statSync(path.join(root, d)).isDirectory())
.filter((d) => d.startsWith(plat));
const anyFused = dirs.filter((d) => d.includes("-fused"));
// Honesty: a `--backend X` run must use an `X`-fused build, not silently
// fall back to a different backend's fused build (that would mislabel the
// numbers). Exact `${plat}-${backend}-fused` only, then a `-${backend}-`
// fused variant; if neither exists, this is `needs-build` for that backend.
const pick =
dirs.find((d) => d === prefer) || anyFused.find((d) => d.includes(`-${backend}-`)) || null;
if (!pick) {
return {
ok: false,
reason: `no fused ${backend} build dir for ${plat} under ${root} (have fused: ${anyFused.join(", ") || "none"}; pass --bin-dir to override)`,
};
}
return validateEngineDir(path.join(root, pick), backend);
}
function validateEngineDir(dir, backend) {
const server = path.join(dir, "llama-server");
const lib = path.join(dir, libName());
if (!fs.existsSync(server)) {
return { ok: false, reason: `${server} missing (not a built fused dir)` };
}
let caps = null;
const capsPath = path.join(dir, "CAPABILITIES.json");
if (fs.existsSync(capsPath)) {
try {
caps = JSON.parse(fs.readFileSync(capsPath, "utf8"));
} catch {
caps = null;
}
}
const fused = caps?.fused === true || (caps?.omnivoice ?? null) !== null || dir.includes("-fused");
if (!fused) {
return { ok: false, reason: `${dir} is not an omnivoice-fused build (no /v1/audio/speech route)` };
}
const actualBackend = caps?.backend || backend;
return {
ok: true,
dir,
server,
lib: fs.existsSync(lib) ? lib : null,
speculative: fs.existsSync(path.join(dir, "llama-speculative-simple"))
? path.join(dir, "llama-speculative-simple")
: null,
backend: actualBackend,
backendMismatch: actualBackend !== backend ? `requested ${backend}, build is ${actualBackend}` : null,
caps,
};
}
function firstExisting(...c) {
return c.find((p) => p && fs.existsSync(p)) || null;
}
function readJsonIfPresent(p) {
if (!p || !fs.existsSync(p)) return null;
try {
return JSON.parse(fs.readFileSync(p, "utf8"));
} catch {
return null;
}
}
function bundleFiles(bundleDir, tier) {
const text = firstExisting(
...fs
.readdirSync(path.join(bundleDir, "text"))
.filter((f) => f.endsWith(".gguf"))
.sort()
.map((f) => path.join(bundleDir, "text", f)),
);
const drafter = firstExisting(
...fs
.readdirSync(path.join(bundleDir, "mtp"))
.filter((f) => f.endsWith(".gguf"))
.map((f) => path.join(bundleDir, "mtp", f)),
);
const ttsDir = path.join(bundleDir, "tts");
const ttsGgufs = fs.existsSync(ttsDir)
? fs.readdirSync(ttsDir).filter((f) => f.endsWith(".gguf"))
: [];
const ttsTok = ttsGgufs.find((f) => /token/i.test(f));
const ttsBase = ttsGgufs.find((f) => !/token/i.test(f)) || ttsGgufs[0];
const manifestPath = path.join(bundleDir, "eliza-1.manifest.json");
const manifest = readJsonIfPresent(manifestPath);
const mtpDisabledPolicy = firstExisting(
path.join(bundleDir, "mtp", `mtp-disabled-${tier}.release-policy.json`),
path.join(bundleDir, "mtp", "mtp-disabled.release-policy.json"),
);
return {
text,
drafter,
mtpDisabledPolicy,
mtpDisabled: !!mtpDisabledPolicy,
ttsModel: ttsBase ? path.join(ttsDir, ttsBase) : null,
ttsCodec: ttsTok ? path.join(ttsDir, ttsTok) : null,
asr: firstExisting(
path.join(bundleDir, "asr", "eliza-1-asr.gguf"),
...(fs.existsSync(path.join(bundleDir, "asr"))
? fs
.readdirSync(path.join(bundleDir, "asr"))
.filter((f) => f.endsWith(".gguf") && !/mmproj/i.test(f))
.map((f) => path.join(bundleDir, "asr", f))
: []),
),
manifest,
tier: manifest?.tier || tier,
ramRecommendedMb: manifest?.ramBudgetMb?.recommended ?? null,
ramMinMb: manifest?.ramBudgetMb?.min ?? null,
};
}
function tierUsesOmniVoice(tier) {
return tier === "9b" || tier === "27b" || tier === "27b-256k";
}
function isRealGguf(p, minBytes = 1_000_000) {
if (!p || !fs.existsSync(p)) return false;
try {
if (fs.statSync(p).size < minBytes) return false;
const fd = fs.openSync(p, "r");
const buf = Buffer.alloc(4);
fs.readSync(fd, buf, 0, 4, 0);
fs.closeSync(fd);
return buf.toString("utf8") === "GGUF";
} catch {
return false;
}
}
// --------------------------------------------------------------------------
// WAV helpers (16-bit PCM read/write + simple linear resample)
// --------------------------------------------------------------------------
function readWav(file) {
const buf = fs.readFileSync(file);
if (buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") {
throw new Error(`${file}: not a RIFF/WAVE file`);
}
let off = 12;
let fmt = null;
let dataOff = -1;
let dataLen = 0;
while (off + 8 <= buf.length) {
const id = buf.toString("ascii", off, off + 4);
const sz = buf.readUInt32LE(off + 4);
const body = off + 8;
if (id === "fmt ") {
fmt = {
audioFormat: buf.readUInt16LE(body),
channels: buf.readUInt16LE(body + 2),
sampleRate: buf.readUInt32LE(body + 4),
bitsPerSample: buf.readUInt16LE(body + 14),
};
} else if (id === "data") {
dataOff = body;
dataLen = sz;
}
off = body + sz + (sz % 2);
}
if (!fmt || dataOff < 0) throw new Error(`${file}: missing fmt/data chunk`);
if (fmt.bitsPerSample !== 16) {
throw new Error(`${file}: only 16-bit PCM supported (got ${fmt.bitsPerSample})`);
}
const nFrames = Math.floor(dataLen / 2 / fmt.channels);
const mono = new Float32Array(nFrames);
for (let i = 0; i < nFrames; i += 1) {
let acc = 0;
for (let c = 0; c < fmt.channels; c += 1) {
acc += buf.readInt16LE(dataOff + (i * fmt.channels + c) * 2);
}
mono[i] = acc / fmt.channels / 32768;
}
return { sampleRate: fmt.sampleRate, samples: mono };
}
function resampleLinear(samples, fromHz, toHz) {
if (fromHz === toHz) return samples;
const ratio = toHz / fromHz;
const out = new Float32Array(Math.max(1, Math.round(samples.length * ratio)));
for (let i = 0; i < out.length; i += 1) {
const src = i / ratio;
const i0 = Math.floor(src);
const i1 = Math.min(samples.length - 1, i0 + 1);
const frac = src - i0;
out[i] = samples[i0] * (1 - frac) + samples[i1] * frac;
}
return out;
}
function writeWav16(file, samples, sampleRate) {
const n = samples.length;
const buf = Buffer.alloc(44 + n * 2);
buf.write("RIFF", 0, "ascii");
buf.writeUInt32LE(36 + n * 2, 4);
buf.write("WAVE", 8, "ascii");
buf.write("fmt ", 12, "ascii");
buf.writeUInt32LE(16, 16);
buf.writeUInt16LE(1, 20); // PCM
buf.writeUInt16LE(1, 22); // mono
buf.writeUInt32LE(sampleRate, 24);
buf.writeUInt32LE(sampleRate * 2, 28);
buf.writeUInt16LE(2, 32);
buf.writeUInt16LE(16, 34);
buf.write("data", 36, "ascii");
buf.writeUInt32LE(n * 2, 40);
for (let i = 0; i < n; i += 1) {
let s = Math.max(-1, Math.min(1, samples[i]));
buf.writeInt16LE(Math.round(s * 32767), 44 + i * 2);
}
fs.writeFileSync(file, buf);
}
// --------------------------------------------------------------------------
// WER (Levenshtein over normalized word lists)
// --------------------------------------------------------------------------
function normalizeWords(s) {
return (s || "")
.toLowerCase()
.replace(/[^\p{L}\p{N}\s']/gu, " ")
.replace(/\s+/g, " ")
.trim()
.split(" ")
.filter(Boolean);
}
function wordErrorRate(ref, hyp) {
const r = normalizeWords(ref);
const h = normalizeWords(hyp);
if (r.length === 0) return h.length === 0 ? 0 : 1;
const dp = Array.from({ length: r.length + 1 }, () => new Array(h.length + 1).fill(0));
for (let i = 0; i <= r.length; i += 1) dp[i][0] = i;
for (let j = 0; j <= h.length; j += 1) dp[0][j] = j;
for (let i = 1; i <= r.length; i += 1) {
for (let j = 1; j <= h.length; j += 1) {
const cost = r[i - 1] === h[j - 1] ? 0 : 1;
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
}
}
return dp[r.length][h.length] / r.length;
}
// --------------------------------------------------------------------------
// Phrase chunker — punctuation boundary + max-token cap (mirrors the runtime
// phrase-chunker contract: punctuation OR an N-token cap).
// --------------------------------------------------------------------------
function chunkPhrases(text, maxTokens = 8) {
const out = [];
const parts = text.split(/([.!?,;:]+)/);
let cur = "";
let tokCount = 0;
const flush = () => {
const t = cur.trim();
if (t) out.push(t);
cur = "";
tokCount = 0;
};
for (let i = 0; i < parts.length; i += 1) {
const seg = parts[i];
if (!seg) continue;
if (/^[.!?,;:]+$/.test(seg)) {
cur += seg;
flush();
continue;
}
const words = seg.split(/\s+/).filter(Boolean);
for (const w of words) {
cur += (cur && !/\s$/.test(cur) ? " " : "") + w;
tokCount += 1;
if (tokCount >= maxTokens) flush();
}
}
flush();
return out.length ? out : [text.trim()].filter(Boolean);
}
// --------------------------------------------------------------------------
// FFI: libelizainference ASR + TTS-cancel (bun:ffi)
// --------------------------------------------------------------------------
async function loadFfi(libPath, libDir) {
// Bun re-exports libllama symbols through libelizainference; the SONAMEd
// deps live next to it. Set DYLD/LD path so the loader resolves them.
const sep = path.delimiter;
const cur = process.env.LD_LIBRARY_PATH || "";
process.env.LD_LIBRARY_PATH = cur ? `${libDir}${sep}${cur}` : libDir;
if (process.platform === "darwin") {
const c2 = process.env.DYLD_LIBRARY_PATH || "";
process.env.DYLD_LIBRARY_PATH = c2 ? `${libDir}${sep}${c2}` : libDir;
}
const ffi = await import("bun:ffi");
const T = ffi.FFIType;
const lib = ffi.dlopen(libPath, {
eliza_inference_abi_version: { args: [], returns: T.cstring },
eliza_inference_create: { args: [T.cstring, T.ptr], returns: T.ptr },
eliza_inference_destroy: { args: [T.ptr], returns: T.void },
eliza_inference_mmap_acquire: { args: [T.ptr, T.cstring, T.ptr], returns: T.i32 },
eliza_inference_mmap_evict: { args: [T.ptr, T.cstring, T.ptr], returns: T.i32 },
eliza_inference_asr_transcribe: {
args: [T.ptr, T.ptr, T.usize, T.i32, T.ptr, T.usize, T.ptr],
returns: T.i32,
},
eliza_inference_tts_synthesize: {
args: [T.ptr, T.cstring, T.usize, T.ptr, T.ptr, T.usize, T.ptr],
returns: T.i32,
},
eliza_inference_tts_stream_supported: { args: [], returns: T.i32 },
eliza_inference_tts_synthesize_stream: {
args: [T.ptr, T.ptr, T.usize, T.ptr, T.usize, T.usize, T.ptr],
returns: T.i32,
},
eliza_inference_cancel_tts: { args: [T.ptr, T.ptr], returns: T.i32 },
eliza_inference_free_string: { args: [T.usize], returns: T.void },
});
const s = lib.symbols;
const abi = s.eliza_inference_abi_version();
return { ffi, lib, s, abi: typeof abi === "string" ? abi : String(abi) };
}
function readErrAndFree(ffi, s, ptrBuf) {
// ptrBuf is a 8-byte buffer holding the out_error char* the C side wrote.
let p;
try {
p = ffi.read.ptr(ptrBuf, 0);
} catch {
p = 0n;
}
if (!p || p === 0n) return "(no diagnostic)";
let msg = "(unreadable diagnostic)";
try {
msg = ffi.CString(p);
} catch {
/* leave default */
}
try {
s.eliza_inference_free_string(p);
} catch {
/* best-effort */
}
return msg;
}
function asrTranscribeFloat32(ffiCtx, ffi, s, samples, sampleRate) {
const pcm16k = resampleLinear(samples, sampleRate, 16000);
const pcmBuf = Buffer.from(pcm16k.buffer, pcm16k.byteOffset, pcm16k.byteLength);
const outBytes = 4096;
const outBuf = Buffer.alloc(outBytes);
const errBuf = Buffer.alloc(8);
errBuf.fill(0);
const t0 = performance.now();
const rc = s.eliza_inference_asr_transcribe(
ffiCtx,
ffi.ptr(pcmBuf),
BigInt(pcm16k.length),
16000,
ffi.ptr(outBuf),
BigInt(outBytes),
ffi.ptr(errBuf),
);
const latencyMs = performance.now() - t0;
if (rc < 0) {
throw new Error(`asr_transcribe rc=${rc}: ${readErrAndFree(ffi, s, errBuf)}`);
}
return {
latencyMs,
transcript: outBuf.toString("utf8", 0, rc).trim(),
};
}
// --------------------------------------------------------------------------
// HTTP helpers against the fused llama-server
// --------------------------------------------------------------------------
async function httpJson(url, body, signal) {
const res = await fetch(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
signal,
});
const text = await res.text();
let json = null;
try {
json = JSON.parse(text);
} catch {
/* not json */
}
return { status: res.status, json, text };
}
async function waitHealthy(port, timeoutS, child, logFn) {
const deadline = Date.now() + timeoutS * 1000;
while (Date.now() < deadline) {
if (child && child.exitCode !== null) {
throw new Error(`llama-server exited (code ${child.exitCode}) before becoming healthy`);
}
try {
const r = await fetch(`http://127.0.0.1:${port}/health`, {
signal: AbortSignal.timeout(2000),
});
if (r.status === 200) {
const j = await r.json().catch(() => ({}));
if (j.status === "ok") return true;
}
} catch {
/* not up yet */
}
await new Promise((r) => setTimeout(r, 500));
}
throw new Error(`llama-server not healthy after ${timeoutS}s`);
}
async function fetchSpecCounters(port) {
try {
const r = await fetch(`http://127.0.0.1:${port}/metrics`, {
signal: AbortSignal.timeout(3000),
});
if (r.status !== 200) return { drafted: null, accepted: null };
const t = await r.text();
const m = (re) => {
const x = t.match(re);
return x ? Number(x[1]) : null;
};
return {
drafted: m(/llamacpp:n_drafted_total\s+([\d.]+)/),
accepted: m(/llamacpp:n_drafted_accepted_total\s+([\d.]+)/),
};
} catch {
return { drafted: null, accepted: null };
}
}
function peakRssMb(pid) {
if (process.platform !== "linux") return null;
try {
const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
const m = status.match(/VmHWM:\s+(\d+)\s+kB/);
return m ? Math.round(Number(m[1]) / 1024) : null;
} catch {
return null;
}
}
// --------------------------------------------------------------------------
// SSE streaming completion — measures first-token + decode tok/s
// --------------------------------------------------------------------------
async function streamCompletion(port, prompt, nPredict, turnTimeoutS) {
const t0 = performance.now();
let firstTokenMs = null;
let content = "";
let tokensSeen = 0;
let timings = null;
const ctrl = new AbortController();
const to = setTimeout(() => ctrl.abort(), turnTimeoutS * 1000);
try {
const res = await fetch(`http://127.0.0.1:${port}/completion`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
prompt,
n_predict: nPredict,
temperature: 0,
stream: true,
cache_prompt: false,
}),
signal: ctrl.signal,
});
if (!res.ok || !res.body) {
const t = await res.text().catch(() => "");
throw new Error(`/completion HTTP ${res.status}: ${t.slice(0, 200)}`);
}
const decoder = new TextDecoder();
let buf = "";
for await (const chunk of res.body) {
buf += decoder.decode(chunk, { stream: true });
let idx;
while ((idx = buf.indexOf("\n\n")) >= 0) {
const ev = buf.slice(0, idx);
buf = buf.slice(idx + 2);
for (const line of ev.split("\n")) {
const l = line.trim();
if (!l.startsWith("data:")) continue;
const payload = l.slice(5).trim();
if (payload === "[DONE]") continue;
let obj;
try {
obj = JSON.parse(payload);
} catch {
continue;
}
if (typeof obj.content === "string" && obj.content.length) {
if (firstTokenMs === null) firstTokenMs = performance.now() - t0;
content += obj.content;
tokensSeen += 1;
}
if (obj.timings) timings = obj.timings;
if (obj.stop === true || obj.stopped_eos || obj.stopped_limit) {
if (obj.timings) timings = obj.timings;
}
}
}
}
} finally {
clearTimeout(to);
}
const wallMs = performance.now() - t0;
return {
content: content.trim(),
firstTokenMs,
tokensSeen,
wallMs,
decodeTokPerSec: timings?.predicted_per_second ?? (tokensSeen > 0 ? (tokensSeen * 1000) / wallMs : null),
predictedN: timings?.predicted_n ?? tokensSeen,
promptTokPerSec: timings?.prompt_per_second ?? null,
};
}
// --------------------------------------------------------------------------
// TTS phrase synthesis via /v1/audio/speech (raw f32 PCM)
// --------------------------------------------------------------------------
function estimateTtsDurationSec(text) {
const words = (String(text).trim().match(/[A-Za-z0-9']+/g) || []).length;
if (words <= 0) return null;
// OmniVoice's default duration predictor over-stretches terse assistant
// snippets ("The meeting is to" regularly becomes ~1.8s). A conservative
// word-rate hint keeps latency sane without clipping normal short phrases.
return Math.max(0.75, Math.min(12, 0.42 + words * 0.19));
}
async function synthPhrasePcm(port, text, turnTimeoutS, ttsSteps = 32, durationSec = null) {
const t0 = performance.now();
const ctrl = new AbortController();
const to = setTimeout(() => ctrl.abort(), turnTimeoutS * 1000);
try {
const body = { input: text, response_format: "pcm", num_step: ttsSteps, interactive: false };
if (Number.isFinite(durationSec) && durationSec > 0) body.duration = durationSec;
const res = await fetch(`http://127.0.0.1:${port}/v1/audio/speech`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
signal: ctrl.signal,
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(`/v1/audio/speech HTTP ${res.status}: ${t.slice(0, 200)}`);
}
const sr = Number(res.headers.get("x-sample-rate") || "24000");
const ab = await res.arrayBuffer();
const wallMs = performance.now() - t0;
const samples = new Float32Array(ab);
const audioSec = samples.length / sr;
return { sampleRate: sr, samples, audioSec, wallMs, rtf: audioSec > 0 ? wallMs / 1000 / audioSec : null };
} finally {
clearTimeout(to);
}
}
// --------------------------------------------------------------------------
// One voice turn
// --------------------------------------------------------------------------
async function runTurn(opts, turnIdx) {
const {
port,
ffiCtx,
ffi,
s,
wav,
refText,
nPredict,
turnTimeoutS,
logFn,
ttsAllPhrases,
ttsSteps,
maxTtsPhrases,
phraseMaxTokens,
ttsRoundtripAsr,
ttsAudioDir,
} = opts;
const turnT0 = performance.now();
// 1) ASR: feed the WAV's mono PCM (resampled to 16 kHz) to the FFI.
const inputAsr = asrTranscribeFloat32(ffiCtx, ffi, s, wav.samples, wav.sampleRate);
const asrMs = inputAsr.latencyMs;
const transcript = inputAsr.transcript;
const wer = refText ? wordErrorRate(refText, transcript) : null;
logFn(`turn ${turnIdx}: ASR ${asrMs.toFixed(0)}ms -> ${JSON.stringify(transcript)} (wer=${wer == null ? "n/a" : wer.toFixed(3)})`);
// 2) Text generation with MTP spec decoding (counters reset by diffing /metrics).
const before = await fetchSpecCounters(port);
const prompt = transcript.length > 0 ? transcript : (refText || "Hello.");
const gen = await streamCompletion(port, prompt, nPredict, turnTimeoutS);
const after = await fetchSpecCounters(port);
let mtpAccept = null;
let dDrafted = null;
let dAccepted = null;
if (
before.drafted != null &&
after.drafted != null &&
before.accepted != null &&
after.accepted != null
) {
dDrafted = after.drafted - before.drafted;
dAccepted = after.accepted - before.accepted;
if (dDrafted > 0) mtpAccept = dAccepted / dDrafted;
}
logFn(
`turn ${turnIdx}: gen firstTok=${gen.firstTokenMs == null ? "n/a" : gen.firstTokenMs.toFixed(0)}ms tok/s=${gen.decodeTokPerSec == null ? "n/a" : gen.decodeTokPerSec.toFixed(1)} n=${gen.predictedN} mtp=${mtpAccept == null ? "n/a" : `${dAccepted}/${dDrafted}=${mtpAccept.toFixed(3)}`}`,
);
// 3) Phrase chunker + 4) TTS per phrase. First-audio = ASR-done → first PCM
// sample of the first phrase (the streaming-handoff latency the runtime
// optimizes); also report it relative to the first text token.
const allPhrases = chunkPhrases(gen.content || prompt, phraseMaxTokens);
// In endurance mode the contract is "the loop completes without crash /
// leak" — synthesizing every phrase of every one of 30 turns at a >1×-RTF
// CPU TTS would take ~45 min for no extra signal. So unless --tts-all-phrases
// is requested, an endurance turn synthesizes only the first phrase (still
// exercises the full TTS forward pass + the streaming handoff). A 1-turn
// run synthesizes up to `maxTtsPhrases` (default 3) — more than 3 phrases
// adds no signal over the per-phrase RTF + first-audio it already measures,
// and a noisy base-model response can chunk into 10+ phrases which would
// dominate the wall clock on a >1×-RTF CPU TTS.
const phrases = ttsAllPhrases ? allPhrases.slice(0, Math.max(1, maxTtsPhrases || 3)) : allPhrases.slice(0, 1);
const ttsRuns = [];
let firstPhrasePcm = null;
const ttsLoopT0 = performance.now();
for (let i = 0; i < phrases.length; i += 1) {
const ph = phrases[i];
const durationSec = estimateTtsDurationSec(ph);
const r = await synthPhrasePcm(port, ph, turnTimeoutS, ttsSteps, durationSec);
const run = { phrase: ph, durationHintSec: durationSec, audioSec: r.audioSec, wallMs: r.wallMs, rtf: r.rtf };
if (ttsAudioDir) {
fs.mkdirSync(ttsAudioDir, { recursive: true });
const safe = String(turnIdx).padStart(2, "0") + "-" + String(i + 1).padStart(2, "0");
const file = path.join(ttsAudioDir, `turn-${safe}-steps${ttsSteps}.wav`);
writeWav16(file, r.samples, r.sampleRate);
run.audioPath = file;
}
if (ttsRoundtripAsr) {
const rt = asrTranscribeFloat32(ffiCtx, ffi, s, r.samples, r.sampleRate);
run.roundTripAsr = {
latencyMs: round1(rt.latencyMs),
transcript: rt.transcript,
wer: round4(wordErrorRate(ph, rt.transcript)),
};
}
ttsRuns.push(run);
if (firstPhrasePcm === null) firstPhrasePcm = r;
}
const ttsTotalWallMs = ttsRuns.reduce((a, r) => a + (r.wallMs || 0), 0);
const totalAudioSec = ttsRuns.reduce((a, r) => a + r.audioSec, 0);
const ttsRtf = totalAudioSec > 0 ? ttsTotalWallMs / 1000 / totalAudioSec : null;
// first-audio relative to mic-in (ASR start) and to first text token.
const firstAudioFromMicMs = ttsLoopT0 - turnT0 + (firstPhrasePcm ? firstPhrasePcm.wallMs : 0);
// Approximate: we can't tap the mid-stream PCM-sample timestamp through
// the batch HTTP route, so first-audio is "first phrase fully synthesized"
// — the conservative end of the streaming-handoff window. Documented.
const firstAudioFromTokenMs =
gen.firstTokenMs == null ? null : firstAudioFromMicMs - asrMs - gen.firstTokenMs;
const totalTurnMs = performance.now() - turnT0;
logFn(
`turn ${turnIdx}: ${phrases.length} phrase(s) audio=${totalAudioSec.toFixed(2)}s RTF=${ttsRtf == null ? "n/a" : ttsRtf.toFixed(2)} firstAudio≈${firstAudioFromMicMs.toFixed(0)}ms total=${totalTurnMs.toFixed(0)}ms`,
);
return {
turn: turnIdx,
asr: { latencyMs: round1(asrMs), transcript, wer: wer == null ? null : round4(wer) },
gen: {
firstTokenMs: gen.firstTokenMs == null ? null : round1(gen.firstTokenMs),
decodeTokPerSec: gen.decodeTokPerSec == null ? null : round2(gen.decodeTokPerSec),
predictedN: gen.predictedN,
promptTokPerSec: gen.promptTokPerSec == null ? null : round2(gen.promptTokPerSec),
content: gen.content,
},
mtp: {
drafted: dDrafted,
accepted: dAccepted,
acceptanceRate: mtpAccept == null ? null : round4(mtpAccept),
},
tts: {
phrases: phrases.length,
totalPhrases: allPhrases.length,
audioSec: round2(totalAudioSec),
wallMs: round1(ttsTotalWallMs),
rtf: ttsRtf == null ? null : round4(ttsRtf),
roundTripAsrWerMean: round4(mean(ttsRuns.map((r) => r.roundTripAsr?.wer))),
perPhrase: ttsRuns.map((r) => ({
...r,
durationHintSec: r.durationHintSec == null ? null : round2(r.durationHintSec),
audioSec: round2(r.audioSec),
wallMs: round1(r.wallMs),
rtf: r.rtf == null ? null : round4(r.rtf),
})),
},
firstAudioFromMicMs: round1(firstAudioFromMicMs),
firstAudioFromTokenMs: firstAudioFromTokenMs == null ? null : round1(firstAudioFromTokenMs),
totalTurnMs: round1(totalTurnMs),
};
}
// --------------------------------------------------------------------------
// Barge-in cancel latency. Three probes:
// (a) audio drain: start a TTS HTTP request and abort the client fetch, which
// is the playback-side PCM drain surrogate.
// (b) native TTS: use eliza_inference_tts_synthesize_stream and have the
// chunk callback request cancellation, measuring callback-request ->
// ELIZA_ERR_CANCELLED return. This is the gateable TTS cancel evidence.
// (c) LLM/MTP: start a streaming /completion request and abort it, which
// is the assembled local server's stream-cancel path.
// --------------------------------------------------------------------------
const ELIZA_ERR_CANCELLED = -7;
async function measureBargeIn(port, ffiCtx, ffi, s) {
const audioDrainMs = await measureHttpAudioDrain(port);
let ttsStreamSupported = false;
try {
ttsStreamSupported = s.eliza_inference_tts_stream_supported() === 1;
} catch {
ttsStreamSupported = false;
}
const nativeTts = ttsStreamSupported
? measureNativeTtsStreamCancel(ffiCtx, ffi, s)
: {
ttsCancelMs: null,
nativeTtsStreamRc: null,
nativeTtsChunksBeforeCancel: 0,
nativeTtsCancelError: "eliza_inference_tts_stream_supported() returned 0",
};
const llm = await measureLlmAbort(port);
const measured = [
nativeTts.ttsCancelMs,
llm.llmCancelMs,
audioDrainMs,
].filter((v) => v != null);
const bargeInCancelMs =
ttsStreamSupported &&
nativeTts.ttsCancelMs != null &&
llm.llmCancelMs != null &&
measured.length > 0
? Math.max(...measured)
: null;
return {
bargeInCancelMs: round2(bargeInCancelMs),
ttsCancelMs: round2(nativeTts.ttsCancelMs),
llmCancelMs: round2(llm.llmCancelMs),
audioDrainMs: round2(audioDrainMs),
httpAbortMs: round2(audioDrainMs),
nativeTtsStreamRc: nativeTts.nativeTtsStreamRc,
nativeTtsChunksBeforeCancel: nativeTts.nativeTtsChunksBeforeCancel,
nativeTtsCancelError: nativeTts.nativeTtsCancelError,
llmAbortError: llm.llmAbortError,
ttsStreamSupported,
note:
bargeInCancelMs != null
? "native streaming TTS cancel and LLM/MTP stream abort were both measured; bargeInCancelMs is max(ttsCancelMs, llmCancelMs, audioDrainMs)"
: ttsStreamSupported
? "native streaming TTS is advertised, but the harness did not get a complete native TTS + LLM cancel measurement"
: "batch-only TTS build (eliza_inference_tts_stream_supported()==0); barge-in gate is not recordable until native streaming TTS cancel is available",
};
}
async function measureHttpAudioDrain(port) {
const longText = "Here is a short answer that I will keep speaking so we can interrupt it.";
const ctrl = new AbortController();
const reqP = fetch(`http://127.0.0.1:${port}/v1/audio/speech`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ input: longText, response_format: "pcm" }),
signal: ctrl.signal,
}).catch((e) => ({ aborted: true, err: String(e) }));
// Let it get going, then "barge in".
await new Promise((r) => setTimeout(r, 60));
const cancelT0 = performance.now();
ctrl.abort();
await reqP; // resolves immediately on abort
return performance.now() - cancelT0;
}
function measureNativeTtsStreamCancel(ffiCtx, ffi, s) {
const T = ffi.FFIType;
const text = "Here is a short answer that the native streaming TTS callback will cancel after the first audio chunk.";
const textBytes = Buffer.from(text, "utf8");
const textBuf = Buffer.alloc(textBytes.byteLength + 1);
textBytes.copy(textBuf);
const errBuf = Buffer.alloc(8);
errBuf.fill(0);
let cancelRequestedAtMs = null;
let chunks = 0;
const cb = new ffi.JSCallback(
((pcmPtr, nSamples, isFinal) => {
const n = Number(nSamples);
if (isFinal === 0 && n > 0 && pcmPtr !== 0n) {
chunks += 1;
if (cancelRequestedAtMs === null) {
cancelRequestedAtMs = performance.now();
return 1;
}
}
return 0;
}),
{
args: [T.ptr, T.usize, T.i32, T.ptr],
returns: T.i32,
},
);
let rc = null;
let ttsCancelMs = null;
let err = null;
try {
rc = s.eliza_inference_tts_synthesize_stream(
ffiCtx,
ffi.ptr(textBuf),
BigInt(textBytes.byteLength),
0n,
BigInt(cb.ptr),
0n,
ffi.ptr(errBuf),
);
if (cancelRequestedAtMs != null) {
ttsCancelMs = performance.now() - cancelRequestedAtMs;
}
if (rc !== ELIZA_ERR_CANCELLED) {
err = `tts_synthesize_stream returned rc=${rc}, expected ${ELIZA_ERR_CANCELLED}`;
}
} catch (e) {
err = e instanceof Error ? e.message : String(e);
} finally {
cb.close();
}
return {
ttsCancelMs,
nativeTtsStreamRc: rc,
nativeTtsChunksBeforeCancel: chunks,
nativeTtsCancelError: err,
};
}
async function measureLlmAbort(port) {
const ctrl = new AbortController();
const reqP = fetch(`http://127.0.0.1:${port}/completion`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
prompt: "Write a long paragraph about local voice cancellation.",
n_predict: 256,
temperature: 0,
stream: true,
cache_prompt: false,
}),
signal: ctrl.signal,
})
.then(async (res) => {
if (res.body) {
const reader = res.body.getReader();
await reader.read().catch(() => null);
}
})
.catch((e) => ({ aborted: true, err: String(e) }));
await new Promise((r) => setTimeout(r, 60));
const cancelT0 = performance.now();
ctrl.abort();
const result = await reqP;
return {
llmCancelMs: performance.now() - cancelT0,
llmAbortError:
result && typeof result === "object" && "err" in result ? result.err : null,
};
}
const round1 = (x) => (x == null ? null : Math.round(x * 10) / 10);
const round2 = (x) => (x == null ? null : Math.round(x * 100) / 100);
const round4 = (x) => (x == null ? null : Math.round(x * 10000) / 10000);
function median(xs) {
const v = xs.filter((x) => x != null).slice().sort((a, b) => a - b);
if (!v.length) return null;
const m = Math.floor(v.length / 2);
return v.length % 2 ? v[m] : (v[m - 1] + v[m]) / 2;
}
function mean(xs) {
const v = xs.filter((x) => x != null);
return v.length ? v.reduce((a, b) => a + b, 0) / v.length : null;
}
function maxOf(xs) {
const v = xs.filter((x) => x != null);
return v.length ? Math.max(...v) : null;
}
function parseCodecBackendLog(logText) {
const text = logText || "";
const fallback = text.match(
/\[PipelineCodec\] Metal codec fallback: requested=([^\s]+) selected=([^\s]+) reason=([^\n]+)/,
);
const loaded = text.match(/\[PipelineCodec\] Loaded codec: sr=(\d+) hop=(\d+) backend=([^\s]+)/);
return {
metalCodecFallback: !!fallback,
requestedBackend: fallback?.[1] ?? null,
selectedBackend: fallback?.[2] ?? loaded?.[3] ?? null,
reason: fallback?.[3]?.trim() ?? null,
sampleRate: loaded ? Number(loaded[1]) : null,
hopLength: loaded ? Number(loaded[2]) : null,
};
}
// --------------------------------------------------------------------------
// main
// --------------------------------------------------------------------------
function nowTag() {
return process.env.ELIZA_E2E_DATE?.trim() || new Date().toISOString().slice(0, 10);
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const logFn = log(args.quiet);
if (typeof Bun === "undefined") {
throw new Error("e2e_loop_bench.mjs requires Bun (bun:ffi for the ASR/TTS-cancel calls)");
}
if (!args.bundle) throw new Error("--bundle <dir> is required");
const bundleDir = path.resolve(args.bundle);
if (!fs.existsSync(bundleDir)) throw new Error(`bundle dir not found: ${bundleDir}`);
const engine = discoverEngine(args.backend, args.binDir);
const files = bundleFiles(bundleDir, args.tier);
const tier = files.tier || args.tier || "unknown";
const baseReport = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
harness: path.relative(REPO_ROOT, __filename),
host: {
platform: process.platform,
arch: process.arch,
cpus: os.cpus().length,
cpuModel: os.cpus()[0]?.model || null,
totalMemMb: Math.round(os.totalmem() / 1024 / 1024),
},
request: {
tier,
backend: args.backend,
turns: args.turns,
nPredict: args.nPredict,
ttsSteps: args.ttsSteps,
micTtsSteps: args.micTtsSteps,
ttsRoundtripAsr: args.ttsRoundtripAsr,
phraseMaxTokens: args.phraseMaxTokens,
wavs: args.wavs.length,
refs: args.refs.length,
},
bundle: { dir: bundleDir, tier, ramBudgetMb: files.manifest?.ramBudgetMb ?? null },
engine: engine.ok ? { dir: engine.dir, backend: engine.backend, backendMismatch: engine.backendMismatch ?? null, fused: true, caps: engine.caps?.kernels ?? null } : null,
};
// Preconditions — produce honest needs-* statuses, not fake passes.
if (!engine.ok) {
const out = { ...baseReport, status: "needs-build", reason: `no fused ${args.backend} build: ${engine.reason}`, e2eLoopOk: false };
return finish(out, args, logFn);
}
if (!engine.lib) {
const out = { ...baseReport, status: "needs-build", reason: `fused build at ${engine.dir} has no ${libName()} (ASR FFI library missing)`, e2eLoopOk: false };
return finish(out, args, logFn);
}
if (!tierUsesOmniVoice(tier)) {
// Kokoro bundles use GGUF artifacts via the fork; discover by scanning tts/kokoro/
const kokoroDir = path.join(bundleDir, "tts", "kokoro");
const kokoroGgufs = fs.existsSync(kokoroDir)
? fs.readdirSync(kokoroDir).filter((f) => f.endsWith(".gguf"))
: [];
const kokoroModelPresent = kokoroGgufs.some((f) => !/token/i.test(f));
const out = {
...baseReport,
status: "needs-harness",
reason: "this tier is Kokoro-only for TTS; the fused OmniVoice+MTP e2e harness is not applicable and the app-core Kokoro voice-loop harness must drive the full ASR->text->Kokoro path",
bundleArtifacts: {
text: !!isRealGguf(files.text),
drafter: files.drafter ? !!isRealGguf(files.drafter, 10_000_000) : null,
kokoroModel: kokoroModelPresent,
ttsModel: null,
ttsCodec: null,
asr: !!isRealGguf(files.asr, 1_000_000),
},
e2eLoopOk: null,
};
return finish(out, args, logFn);
}
const drafterReady = files.mtpDisabled || isRealGguf(files.drafter, 10_000_000);
if (!isRealGguf(files.text) || !drafterReady || !isRealGguf(files.ttsModel) || !isRealGguf(files.ttsCodec) || !isRealGguf(files.asr, 1_000_000)) {
const out = {
...baseReport,
status: "needs-bundle",
reason: "bundle is missing one of text/drafter/tts-model/tts-codec/asr GGUFs (stand-in or incomplete)",
bundleArtifacts: {
text: !!isRealGguf(files.text),
drafter: files.mtpDisabled ? "disabled-by-policy" : !!isRealGguf(files.drafter, 10_000_000),
ttsModel: !!isRealGguf(files.ttsModel),
ttsCodec: !!isRealGguf(files.ttsCodec),
asr: !!isRealGguf(files.asr, 1_000_000),
},
e2eLoopOk: false,
};
return finish(out, args, logFn);
}
// --- Pick / generate the mic WAVs. If none supplied, synthesize reference
// phrases via the fused TTS route (24 kHz f32 → 16 kHz 16-bit WAV) and
// use them as the "mic" input. Reference text drives WER.
const REF_PHRASES = [
"What is the capital of France?",
"Schedule a meeting for tomorrow afternoon.",
"Tell me a short fact about the ocean.",
];
let micWavs = []; // { file, samples, sampleRate, refText }
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "eliza-e2e-mic-"));
let serverChild = null;
let port = 0;
let ffiState = null;
let ffiCtx = null;
try {
// --- start the fused llama-server (text + drafter + omnivoice) ---
port = 30000 + Math.floor(Math.random() * 20000);
const env = {
...process.env,
LD_LIBRARY_PATH: `${engine.dir}${path.delimiter}${process.env.LD_LIBRARY_PATH || ""}`,
DYLD_LIBRARY_PATH: `${engine.dir}${path.delimiter}${process.env.DYLD_LIBRARY_PATH || ""}`,
ELIZA_OMNIVOICE_MODEL: files.ttsModel,
ELIZA_OMNIVOICE_CODEC: files.ttsCodec,
ELIZA_MTP_SKIP_SERVER_STRUCTURED_OUTPUT: "1",
};
const serverArgs = [
"-m", files.text,
"--port", String(port),
"-c", String(args.ctx),
"-ngl", String(args.ngl),
"-t", String(args.threads),
"--no-warmup",
"--metrics",
];
if (!files.mtpDisabled) {
serverArgs.splice(2, 0,
"-md", files.drafter,
"--spec-type", "mtp",
"--spec-draft-n-min", "2", "--spec-draft-n-max", "6",
);
}
logFn(
`starting fused llama-server: ${path.basename(engine.dir)} port=${port} text=${path.basename(files.text)} ` +
(files.mtpDisabled ? `drafter=disabled (${path.basename(files.mtpDisabledPolicy)})` : `drafter=${path.basename(files.drafter)}`),
);
const srvLog = fs.createWriteStream(path.join(tmpDir, "server.log"));
serverChild = spawn(engine.server, serverArgs, { cwd: engine.dir, env, stdio: ["ignore", "pipe", "pipe"] });
serverChild.stdout.pipe(srvLog);
serverChild.stderr.pipe(srvLog);
await waitHealthy(port, args.startTimeoutS, serverChild, logFn);
logFn(`server healthy (pid ${serverChild.pid})`);
// --- synthesize mic WAVs if none supplied ---
if (args.wavs.length > 0) {
for (let i = 0; i < args.wavs.length; i += 1) {
const w = args.wavs[i];
const wav = readWav(w);
micWavs.push({
file: w,
samples: wav.samples,
sampleRate: wav.sampleRate,
refText: args.refs[i] || null,
});
}
} else {
for (let i = 0; i < REF_PHRASES.length; i += 1) {
const ph = REF_PHRASES[i];
const r = await synthPhrasePcm(port, ph, args.turnTimeoutS, args.micTtsSteps);
const w16 = resampleLinear(r.samples, r.sampleRate, 16000);
const file = path.join(tmpDir, `mic-${i}.wav`);
writeWav16(file, w16, 16000);
micWavs.push({ file, samples: w16, sampleRate: 16000, refText: ph });
logFn(`synthesized mic WAV ${i}: ${ph.slice(0, 40)}... (${r.audioSec.toFixed(2)}s)`);
}
}
if (micWavs.length === 0) throw new Error("no mic WAVs available (synthesis produced nothing)");
// --- FFI: create the inference context anchored at the bundle ---
ffiState = await loadFfi(engine.lib, engine.dir);
logFn(`libelizainference ABI=${ffiState.abi}`);
const { ffi, s } = ffiState;
{
const errBuf = Buffer.alloc(8);
errBuf.fill(0);
ffiCtx = s.eliza_inference_create(Buffer.from(`${bundleDir}\0`, "utf8"), ffi.ptr(errBuf));
if (!ffiCtx || (typeof ffiCtx === "bigint" && ffiCtx === 0n)) {
throw new Error(`eliza_inference_create failed: ${readErrAndFree(ffi, s, errBuf)}`);
}
}
// Acquire the ASR + TTS regions (voice-on). text/mtp are kept hot by
// the library itself; acquire is idempotent.
for (const region of ["text", "mtp", "asr", "tts"]) {
const errBuf = Buffer.alloc(8);
errBuf.fill(0);
const rc = s.eliza_inference_mmap_acquire(ffiCtx, Buffer.from(`${region}\0`, "utf8"), ffi.ptr(errBuf));
if (rc < 0) {
const msg = readErrAndFree(ffi, s, errBuf);
// text/mtp on the library are best-effort warm hints; only asr is mandatory for this bench.
if (region === "asr") throw new Error(`mmap_acquire("asr") rc=${rc}: ${msg}`);
logFn(`mmap_acquire("${region}") rc=${rc} (non-fatal): ${msg}`);
}
}
// --- run the turns ---
const turnReports = [];
let bargeIn = null;
const rssSamples = [];
const totalTurns = Math.max(1, args.turns);
const isEndurance = totalTurns >= 8;
for (let t = 0; t < totalTurns; t += 1) {
const mic = micWavs[t % micWavs.length];
// Turn 1 always runs "full" (all phrases, full nPredict) so the e2e
// metrics are complete; later turns in an endurance run are lighter.
const fullTurn = t === 0 || !isEndurance;
const turnReport = await runTurn(
{
port, ffiCtx, ffi, s, wav: mic, refText: mic.refText,
nPredict: fullTurn ? args.nPredict : args.enduranceNPredict,
turnTimeoutS: args.turnTimeoutS, logFn,
ttsAllPhrases: args.ttsAllPhrases ? true : fullTurn,
ttsSteps: args.ttsSteps,
ttsRoundtripAsr: args.ttsRoundtripAsr,
ttsAudioDir: args.ttsAudioDir,
phraseMaxTokens: args.phraseMaxTokens,
maxTtsPhrases: args.maxTtsPhrases,
},
t + 1,
);
turnReports.push(turnReport);
const rss = peakRssMb(serverChild.pid);
if (rss != null) {
rssSamples.push(rss);
turnReport.serverPeakRssMb = rss;
}
if (serverChild.exitCode !== null) {
throw new Error(`llama-server died mid-loop (exit ${serverChild.exitCode}) at turn ${t + 1}`);
}
}
// Measure barge-in after the loop. The current HTTP TTS route is batch
// only, so aborting the client fetch stops playback consumption but does
// not cancel the server-side OmniVoice forward pass; running it mid-loop
// would contaminate subsequent turn latency.
bargeIn = await measureBargeIn(port, ffiCtx, ffi, s);
const finalPeakRss = peakRssMb(serverChild.pid);
const peakRss = maxOf([...rssSamples, finalPeakRss]);
const ramRec = files.manifest?.ramBudgetMb?.recommended ?? null;
// Endurance leak heuristic: the RSS in the last quarter of the run should
// not be dramatically (>1.5×) above the RSS in the first quarter.
let leakSuspected = false;
if (totalTurns >= 8 && rssSamples.length >= 8) {
const q = Math.floor(rssSamples.length / 4);
const firstQ = mean(rssSamples.slice(0, q));
const lastQ = mean(rssSamples.slice(-q));
if (firstQ != null && lastQ != null && lastQ > firstQ * 1.5) leakSuspected = true;
}
const ramWithinBudget = ramRec == null ? null : peakRss == null ? null : peakRss <= ramRec;
const summary = {
turns: totalTurns,
asrLatencyMsMedian: round1(median(turnReports.map((r) => r.asr.latencyMs))),
asrWerMean: round4(mean(turnReports.map((r) => r.asr.wer))),
asrWerByTurn: turnReports.map((r) => r.asr.wer),
firstTokenMsMedian: round1(median(turnReports.map((r) => r.gen.firstTokenMs))),
firstTokenMsP50: round1(median(turnReports.map((r) => r.gen.firstTokenMs))),
decodeTokPerSecMedian: round2(median(turnReports.map((r) => r.gen.decodeTokPerSec))),
mtpAcceptanceRateMean: round4(mean(turnReports.map((r) => r.mtp.acceptanceRate))),
mtpDraftedTotal: turnReports.reduce((a, r) => a + (r.mtp.drafted || 0), 0),
mtpAcceptedTotal: turnReports.reduce((a, r) => a + (r.mtp.accepted || 0), 0),
firstAudioFromMicMsMedian: round1(median(turnReports.map((r) => r.firstAudioFromMicMs))),
firstAudioFromTokenMsMedian: round1(median(turnReports.map((r) => r.firstAudioFromTokenMs))),
ttsRtfMedian: round4(median(turnReports.map((r) => r.tts.rtf))),
ttsRtfMean: round4(mean(turnReports.map((r) => r.tts.rtf))),
ttsRoundTripAsrWerMean: round4(mean(turnReports.map((r) => r.tts.roundTripAsrWerMean))),
ttsRoundTripAsrWerByTurn: turnReports.map((r) => r.tts.roundTripAsrWerMean),
totalTurnMsMedian: round1(median(turnReports.map((r) => r.totalTurnMs))),
bargeInCancelMs: bargeIn ? bargeIn.bargeInCancelMs : null,
serverPeakRssMb: peakRss,
ramBudgetRecommendedMb: ramRec,
ramWithinBudget,
leakSuspected,
};
const mtpOverall =
summary.mtpDraftedTotal > 0 ? round4(summary.mtpAcceptedTotal / summary.mtpDraftedTotal) : null;
summary.mtpAcceptanceRateOverall = mtpOverall;
const flowCompletedOk = turnReports.length > 0 &&
turnReports.every((r) => r.gen.firstTokenMs != null && r.tts.audioSec != null && r.tts.audioSec > 0 && r.totalTurnMs != null);
const requiredOptimizations = {
mtpDraftingActive: files.mtpDisabled ? null : summary.mtpDraftedTotal > 0,
streamingTtsActive: bargeIn?.ttsStreamSupported === true,
};
const optimizationReadyOk = Object.values(requiredOptimizations).filter((v) => v !== null).every(Boolean);
const e2eLoopOk = flowCompletedOk;
const thirtyTurnOk = totalTurns >= 30 ? flowCompletedOk && !leakSuspected && (ramWithinBudget !== false) : null;
summary.flowCompletedOk = flowCompletedOk;
summary.optimizationReadyOk = optimizationReadyOk;
summary.requiredOptimizations = requiredOptimizations;
const fullServerLog = fs.existsSync(path.join(tmpDir, "server.log"))
? fs.readFileSync(path.join(tmpDir, "server.log"), "utf8")
: "";
summary.codecBackend = parseCodecBackendLog(fullServerLog);
const out = {
...baseReport,
status: e2eLoopOk ? (optimizationReadyOk ? "ok" : "needs-optimization") : "failed",
reason: e2eLoopOk
? optimizationReadyOk
? null
: "voice loop completed, but required Eliza-1 optimizations were not all active"
: "voice loop did not complete",
e2eLoopOk,
thirtyTurnOk,
flowCompletedOk,
optimizationReadyOk,
requiredOptimizations,
summary,
bargeIn,
turns: turnReports,
serverLog: fullServerLog ? fullServerLog.split("\n").slice(-300).join("\n") : null,
};
return finish(out, args, logFn);
} catch (err) {
const serverLogPath = path.join(tmpDir, "server.log");
const out = {
...baseReport,
status: "failed",
reason: err instanceof Error ? err.message : String(err),
e2eLoopOk: false,
thirtyTurnOk: null,
serverExitCode: serverChild?.exitCode ?? null,
serverLog: fs.existsSync(serverLogPath)
? fs.readFileSync(serverLogPath, "utf8").split("\n").slice(-80).join("\n")
: null,
};
return finish(out, args, logFn);
} finally {
// teardown FFI then server
try {
if (ffiCtx && ffiState) {
const { s } = ffiState;
try { s.eliza_inference_destroy(ffiCtx); } catch { /* best-effort */ }
}
if (ffiState?.lib?.close) ffiState.lib.close();
} catch { /* best-effort */ }
if (serverChild && serverChild.exitCode === null) {
try { serverChild.kill("SIGTERM"); } catch { /* best-effort */ }
await new Promise((r) => setTimeout(r, 300));
if (serverChild.exitCode === null) {
try { serverChild.kill("SIGKILL"); } catch { /* best-effort */ }
}
}
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* best-effort */ }
}
}
function finish(report, args, logFn) {
const reportPath =
args.report ||
path.join(__dirname, "bench_results", `e2e_loop_${nowTag()}.json`);
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
// Merge if a file already exists for the day (one file, multiple
// tier/backend runs keyed by tier+backend).
let merged = report;
if (!args.report && fs.existsSync(reportPath)) {
try {
const prev = JSON.parse(fs.readFileSync(reportPath, "utf8"));
const runs = Array.isArray(prev.runs) ? prev.runs : prev.run ? [prev] : [prev];
const key = (r) => `${r?.request?.tier}|${r?.request?.backend}|${r?.request?.turns}`;
const filtered = runs.filter((r) => key(r) !== key(report));
merged = { schemaVersion: 1, generatedAt: new Date().toISOString(), runs: [...filtered, report] };
} catch {
merged = { schemaVersion: 1, generatedAt: new Date().toISOString(), runs: [report] };
}
} else if (!args.report) {
merged = { schemaVersion: 1, generatedAt: new Date().toISOString(), runs: [report] };
}
fs.writeFileSync(reportPath, `${JSON.stringify(merged, null, 2)}\n`);
logFn(`wrote ${reportPath}`);
if (report.status === "ok") {
const s = report.summary;
logFn(
`RESULT tier=${report.request.tier} backend=${report.request.backend} turns=${s.turns} ` +
`asr=${s.asrLatencyMsMedian}ms/wer${s.asrWerMean} firstTok=${s.firstTokenMsMedian}ms ` +
`tok/s=${s.decodeTokPerSecMedian} mtp=${s.mtpAcceptanceRateOverall} ` +
`firstAudio≈${s.firstAudioFromMicMsMedian}ms ttsRTF=${s.ttsRtfMedian} total=${s.totalTurnMsMedian}ms ` +
`peakRSS=${s.serverPeakRssMb}MB bargeIn=${s.bargeInCancelMs}ms e2eOk=${report.e2eLoopOk}` +
(report.thirtyTurnOk != null ? ` thirtyTurnOk=${report.thirtyTurnOk}` : ""),
);
} else {
logFn(`RESULT status=${report.status}: ${report.reason}`);
}
// The bench exits 0 whenever it produced its output (including needs-*),
// matching the eval-suite honesty contract; a non-zero exit means a harness
// crash. Callers gate on `status` / `e2eLoopOk` in the JSON.
return report;
}
main().then(
(r) => process.exit(0),
(err) => {
console.error("[e2e-loop] FATAL:", err?.stack || String(err));
process.exit(1);
},
);