Files
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
ci / test (push) Waiting to run
ci / lint-and-format (push) Waiting to run
ci / build (push) Waiting to run
ci / dev-startup (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
gitleaks / gitleaks (push) Waiting to run
Markdown Links / Relative Markdown Links (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Quality (Extended) / Homepage Build (PR smoke) (push) Waiting to run
Quality (Extended) / Comment-only diff guard (push) Waiting to run
Quality (Extended) / Format + Type Safety Ratchet (push) Waiting to run
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Waiting to run
Quality (Extended) / Develop Gate (lint) (push) Waiting to run
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

228 lines
7.2 KiB
JavaScript

#!/usr/bin/env bun
/**
* Kokoro agent-voice verification — the goal explicitly asks for the
* agent to reply in a Kokoro TTS voice. This runs Kokoro v1.0 via the
* in-repo KokoroGgufRuntime against a Kokoro-capable llama-server
* (`/v1/audio/speech`, the fork path) and the bundled ASCII fallback
* phonemizer (no espeak-ng needed for ASCII agent lines). Each agent
* line is synthesized, written to WAV, then run BACK through eliza-1
* ASR to prove the Kokoro audio is intelligible.
*
* Point ELIZA_KOKORO_FORK_URL at a running Kokoro llama-server.
*
* Run:
* bun packages/benchmarks/voice/verify-kokoro-agent-voice.mjs
*/
import { mkdirSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { loadElizaInferenceFfi } from "../../../plugins/plugin-local-inference/src/services/voice/ffi-bindings.ts";
import { KokoroGgufRuntime } from "../../../plugins/plugin-local-inference/src/services/voice/kokoro/kokoro-runtime.ts";
import { resolvePhonemizer } from "../../../plugins/plugin-local-inference/src/services/voice/kokoro/phonemizer.ts";
const HOME = os.homedir();
const REPO_ROOT = path.resolve(import.meta.dir, "../../..");
const BUNDLE = path.join(
HOME,
".eliza/local-inference/models/eliza-1-0_8b.bundle",
);
const INFER_DYLIB = path.join(
HOME,
".eliza/local-inference/bin/mtp/darwin-arm64-metal-fused/libelizainference.dylib",
);
const WORK = "/tmp/three-voice-e2e";
const REPORTS = path.join(REPO_ROOT, "packages/benchmarks/voice/reports");
mkdirSync(WORK, { recursive: true });
mkdirSync(REPORTS, { recursive: true });
const KOKORO_SERVER_URL =
process.env.ELIZA_KOKORO_FORK_URL?.trim() || "http://127.0.0.1:8081";
const layout = { sampleRate: 24_000 };
// Agent voice = af_bella (American English female). Distinct, warm.
const AGENT_VOICE = {
id: "af_bella",
displayName: "Bella",
lang: "a",
file: "af_bella.bin",
dim: 256,
};
const AGENT_LINES = [
{
turn: 3,
text: "It looks sunny this morning, with a chance of rain this afternoon.",
},
{ turn: 7, text: "It's two fifteen in the afternoon." },
];
function writeWav16(pcm, sampleRate, outPath) {
const n = pcm.length;
const buf = Buffer.alloc(44 + n * 2);
buf.write("RIFF", 0);
buf.writeUInt32LE(36 + n * 2, 4);
buf.write("WAVE", 8);
buf.write("fmt ", 12);
buf.writeUInt32LE(16, 16);
buf.writeUInt16LE(1, 20);
buf.writeUInt16LE(1, 22);
buf.writeUInt32LE(sampleRate, 24);
buf.writeUInt32LE(sampleRate * 2, 28);
buf.writeUInt16LE(2, 32);
buf.writeUInt16LE(16, 34);
buf.write("data", 36);
buf.writeUInt32LE(n * 2, 40);
for (let i = 0; i < n; i++) {
const s = Math.max(-1, Math.min(1, pcm[i]));
buf.writeInt16LE((s * 32767) | 0, 44 + i * 2);
}
writeFileSync(outPath, buf);
}
function resampleTo16k(pcm, srcRate) {
if (srcRate === 16_000) return pcm;
const ratio = 16_000 / srcRate;
const out = new Float32Array(Math.floor(pcm.length * ratio));
for (let i = 0; i < out.length; i++) {
const p = i / ratio,
i0 = Math.floor(p),
a = pcm[i0] ?? 0,
b = pcm[i0 + 1] ?? a;
out[i] = a + (b - a) * (p - i0);
}
return out;
}
function wer(ref, hyp) {
const r = ref
.toLowerCase()
.replace(/[^a-z0-9 ]/g, "")
.split(/\s+/)
.filter(Boolean);
const h = hyp
.toLowerCase()
.replace(/[^a-z0-9 ]/g, "")
.split(/\s+/)
.filter(Boolean);
const dp = Array.from({ length: r.length + 1 }, () =>
new Array(h.length + 1).fill(0),
);
for (let i = 0; i <= r.length; i++) dp[i][0] = i;
for (let j = 0; j <= h.length; j++) dp[0][j] = j;
for (let i = 1; i <= r.length; i++)
for (let j = 1; j <= h.length; j++)
dp[i][j] =
r[i - 1] === h[j - 1]
? dp[i - 1][j - 1]
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
return r.length === 0 ? 0 : dp[r.length][h.length] / r.length;
}
const log = (...a) => console.log("[kokoro-agent]", ...a);
const runtime = new KokoroGgufRuntime({
serverUrl: KOKORO_SERVER_URL,
modelId: process.env.ELIZA_KOKORO_FORK_MODEL_ID?.trim() || "kokoro-v1.0",
sampleRate: layout.sampleRate,
});
const phonemizer = await resolvePhonemizer();
log("phonemizer:", phonemizer.id, "| voice:", AGENT_VOICE.id);
const synth = [];
for (const line of AGENT_LINES) {
const t0 = performance.now();
const phonemes = await phonemizer.phonemize(line.text, AGENT_VOICE.lang);
const chunks = [];
await runtime.synthesize({
phonemes,
voice: AGENT_VOICE,
cancelSignal: { cancelled: false },
onChunk: ({ pcm, isFinal }) => {
if (!isFinal && pcm.length) chunks.push(pcm);
return false;
},
});
const total = chunks.reduce((s, c) => s + c.length, 0);
const pcm = new Float32Array(total);
let off = 0;
for (const c of chunks) {
pcm.set(c, off);
off += c.length;
}
const ms = performance.now() - t0;
const out = path.join(WORK, `agent-kokoro-turn-${line.turn}.wav`);
writeWav16(pcm, layout.sampleRate, out);
const durSec = pcm.length / layout.sampleRate;
synth.push({
turn: line.turn,
text: line.text,
file: out,
durSec: Number(durSec.toFixed(2)),
wallMs: Math.round(ms),
rtf: Number((ms / 1000 / durSec).toFixed(3)),
phonemeIds: phonemes.ids.length,
pcm16: resampleTo16k(pcm, layout.sampleRate),
});
log(
`turn ${line.turn}: ${durSec.toFixed(2)}s in ${Math.round(ms)}ms (RTF ${(ms / 1000 / durSec).toFixed(2)}), ${phonemes.ids.length} phoneme ids → ${out}`,
);
}
runtime.dispose();
// ASR the Kokoro audio back to prove it is intelligible.
let asrOk = true;
const asr = [];
try {
const ffi = loadElizaInferenceFfi(INFER_DYLIB);
const ctx = ffi.create(BUNDLE);
ffi.mmapAcquire(ctx, "asr");
for (const s of synth) {
const hyp = String(
ffi.asrTranscribe({ ctx, pcm: s.pcm16, sampleRateHz: 16_000 }) ?? "",
).trim();
const w = Number(wer(s.text, hyp).toFixed(3));
asr.push({ turn: s.turn, ref: s.text, hyp, wer: w });
log(`ASR turn ${s.turn}: "${hyp}" (WER ${w})`);
}
ffi.mmapEvict?.(ctx, "asr");
ffi.close?.(ctx);
} catch (err) {
asrOk = false;
log("ASR unavailable:", String(err?.message ?? err));
}
const meanWer = asr.length
? Number((asr.reduce((s, a) => s + a.wer, 0) / asr.length).toFixed(3))
: null;
const report = {
generatedAt: new Date().toISOString(),
model:
"Kokoro v1.0 via KokoroGgufRuntime (fork llama-server /v1/audio/speech)",
phonemizer: phonemizer.id,
voice: AGENT_VOICE.id,
synth: synth.map(({ pcm16, ...rest }) => rest),
asr: { available: asrOk, results: asr, meanWer },
pass: synth.every((s) => s.durSec > 0.3) && asrOk && (meanWer ?? 1) <= 0.5,
};
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
writeFileSync(
path.join(REPORTS, `kokoro-agent-voice-${stamp}.json`),
JSON.stringify(report, null, 2),
);
console.log("\n[kokoro-agent] === SUMMARY ===");
console.log(
` Kokoro generated ${synth.length} agent lines (voice ${AGENT_VOICE.id}, phonemizer ${phonemizer.id})`,
);
console.log(
` Audio: ${synth.map((s) => `T${s.turn}=${s.durSec}s@RTF${s.rtf}`).join(", ")}`,
);
console.log(
` ASR round-trip: ${asrOk ? `mean WER ${meanWer}` : "UNAVAILABLE"}`,
);
console.log(
` ${report.pass ? "PASS: Kokoro agent voice generates intelligible audio" : "PARTIAL — see report"}`,
);
process.exit(report.pass ? 0 : 1);