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
160 lines
6.0 KiB
TypeScript
160 lines
6.0 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* Mixed local + cloud voice round-trip — REAL services, measured (#8785).
|
|
*
|
|
* Answers "can we mix local STT with the really fast cloud LLM?" with real
|
|
* numbers. One utterance flows through:
|
|
*
|
|
* cloud TTS (ElevenLabs, to make the question audio)
|
|
* → LOCAL STT (eliza-1-asr via the fused libelizainference + Metal)
|
|
* → cloud LLM (Cerebras — the fast inference)
|
|
* → cloud TTS (ElevenLabs — spoken reply)
|
|
*
|
|
* and prints the per-stage latency + the hybrid time-to-first-audio. This is the
|
|
* production "local STT + fast cloud LLM + cloud TTS" topology.
|
|
*
|
|
* Inputs (env): ELIZA_INFERENCE_LIBRARY (fused dylib), ELIZA_ASR_BUNDLE
|
|
* (asr/eliza-1-asr.gguf), ELEVENLABS_API_KEY, CEREBRAS_API_KEY. Exits 2 (skip)
|
|
* when an artifact/key is missing; 1 on failure; 0 on a clean round-trip.
|
|
*/
|
|
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { decodeMonoPcm16Wav } from "../src/services/voice/engine-bridge";
|
|
import { loadElizaInferenceFfi } from "../src/services/voice/ffi-bindings";
|
|
|
|
const EL_VOICE = "21m00Tcm4TlvDq8ikWAM"; // a standard ElevenLabs voice
|
|
const QUESTION = "What time is it right now in San Francisco";
|
|
|
|
function skip(m: string): never {
|
|
console.log(`[mixed-roundtrip] SKIP: ${m}`);
|
|
process.exit(2);
|
|
}
|
|
function fail(m: string): never {
|
|
console.error(`[mixed-roundtrip] FAIL: ${m}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const lib = process.env.ELIZA_INFERENCE_LIBRARY?.trim();
|
|
const bundle = process.env.ELIZA_ASR_BUNDLE?.trim();
|
|
const elKey = process.env.ELEVENLABS_API_KEY?.trim();
|
|
const cbKey = process.env.CEREBRAS_API_KEY?.trim();
|
|
if (!lib || !existsSync(lib)) skip("set ELIZA_INFERENCE_LIBRARY to the fused dylib");
|
|
if (!bundle || !existsSync(`${bundle}/asr`)) skip("set ELIZA_ASR_BUNDLE");
|
|
if (!elKey) skip("set ELEVENLABS_API_KEY");
|
|
if (!cbKey) skip("set CEREBRAS_API_KEY");
|
|
|
|
/** Wrap raw mono PCM16LE @ sampleRate in a 44-byte WAV header. */
|
|
function wavFromPcm16(pcm: Uint8Array, sampleRate: number): Uint8Array {
|
|
const header = new ArrayBuffer(44);
|
|
const v = new DataView(header);
|
|
const w = (off: number, s: string) => {
|
|
for (let i = 0; i < s.length; i++) v.setUint8(off + i, s.charCodeAt(i));
|
|
};
|
|
const byteRate = sampleRate * 2;
|
|
w(0, "RIFF");
|
|
v.setUint32(4, 36 + pcm.length, true);
|
|
w(8, "WAVE");
|
|
w(12, "fmt ");
|
|
v.setUint32(16, 16, true);
|
|
v.setUint16(20, 1, true);
|
|
v.setUint16(22, 1, true);
|
|
v.setUint32(24, sampleRate, true);
|
|
v.setUint32(28, byteRate, true);
|
|
v.setUint16(32, 2, true);
|
|
v.setUint16(34, 16, true);
|
|
w(36, "data");
|
|
v.setUint32(40, pcm.length, true);
|
|
const out = new Uint8Array(44 + pcm.length);
|
|
out.set(new Uint8Array(header), 0);
|
|
out.set(pcm, 44);
|
|
return out;
|
|
}
|
|
|
|
async function elevenTts(text: string, fmt: string): Promise<Uint8Array> {
|
|
const r = await fetch(
|
|
`https://api.elevenlabs.io/v1/text-to-speech/${EL_VOICE}?output_format=${fmt}`,
|
|
{
|
|
method: "POST",
|
|
headers: { "xi-api-key": elKey!, "content-type": "application/json" },
|
|
body: JSON.stringify({ text, model_id: "eleven_turbo_v2_5" }),
|
|
},
|
|
);
|
|
if (!r.ok) fail(`ElevenLabs TTS ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
|
return new Uint8Array(await r.arrayBuffer());
|
|
}
|
|
|
|
async function cerebras(userText: string): Promise<string> {
|
|
const r = await fetch("https://api.cerebras.ai/v1/chat/completions", {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${cbKey}`, "content-type": "application/json" },
|
|
body: JSON.stringify({
|
|
model: process.env.CEREBRAS_MODEL?.trim() || "gpt-oss-120b",
|
|
// gpt-oss is a reasoning model — leave room for reasoning + the reply.
|
|
max_tokens: 400,
|
|
messages: [
|
|
{
|
|
role: "system",
|
|
content: "You are a concise voice assistant. Reply in one short sentence.",
|
|
},
|
|
{ role: "user", content: userText },
|
|
],
|
|
}),
|
|
});
|
|
if (!r.ok) fail(`Cerebras ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
|
const j = (await r.json()) as {
|
|
choices?: Array<{ message?: { content?: string } }>;
|
|
};
|
|
return (j.choices?.[0]?.message?.content ?? "").trim();
|
|
}
|
|
|
|
console.log(`[mixed-roundtrip] question: "${QUESTION}"`);
|
|
|
|
// 1) cloud TTS → raw 16 kHz PCM (the spoken question)
|
|
const t0 = performance.now();
|
|
const pcm16 = await elevenTts(QUESTION, "pcm_16000");
|
|
const tQ = Math.round(performance.now() - t0);
|
|
console.log(`[mixed-roundtrip] (cloud TTS question) ${tQ}ms, ${pcm16.length} bytes pcm`);
|
|
|
|
// 2) LOCAL STT via the fused engine + Metal
|
|
const ffi = loadElizaInferenceFfi(lib);
|
|
const ctx = ffi.create(bundle);
|
|
ffi.mmapAcquire(ctx, "asr");
|
|
let transcript = "";
|
|
let asrMs = 0;
|
|
try {
|
|
const { pcm, sampleRate } = decodeMonoPcm16Wav(wavFromPcm16(pcm16, 16000));
|
|
const a0 = performance.now();
|
|
const res = ffi.asrTranscribeTimed({ ctx, pcm, sampleRateHz: sampleRate });
|
|
asrMs = Math.round(performance.now() - a0);
|
|
transcript = (res.text ?? "").trim();
|
|
} finally {
|
|
ffi.mmapEvict(ctx, "asr");
|
|
ffi.destroy(ctx);
|
|
ffi.close();
|
|
}
|
|
console.log(`[mixed-roundtrip] (LOCAL STT, eliza-1-asr) ${asrMs}ms → "${transcript}"`);
|
|
if (!transcript) fail("local STT produced an empty transcript");
|
|
|
|
// 3) cloud LLM (Cerebras — fast)
|
|
const l0 = performance.now();
|
|
const reply = await cerebras(transcript);
|
|
const llmMs = Math.round(performance.now() - l0);
|
|
const cbModel = process.env.CEREBRAS_MODEL?.trim() || "gpt-oss-120b";
|
|
console.log(`[mixed-roundtrip] (cloud LLM, Cerebras ${cbModel}) ${llmMs}ms → "${reply}"`);
|
|
if (!reply) fail("LLM produced an empty reply");
|
|
|
|
// 4) cloud TTS (spoken reply) — measure to first audio bytes
|
|
const r0 = performance.now();
|
|
const replyAudio = await elevenTts(reply, "mp3_44100_128");
|
|
const ttsMs = Math.round(performance.now() - r0);
|
|
console.log(`[mixed-roundtrip] (cloud TTS reply) ${ttsMs}ms, ${replyAudio.length} bytes mp3`);
|
|
|
|
const hybridTtfa = asrMs + llmMs + ttsMs;
|
|
console.log("");
|
|
console.log("[mixed-roundtrip] ── latency breakdown (real services) ──");
|
|
console.log(` local STT (eliza-1-asr + Metal): ${asrMs} ms`);
|
|
console.log(` cloud LLM (Cerebras): ${llmMs} ms`);
|
|
console.log(` cloud TTS (ElevenLabs first audio):${ttsMs} ms`);
|
|
console.log(` ── hybrid round-trip (STT+LLM+TTS): ${hybridTtfa} ms`);
|
|
console.log("[mixed-roundtrip] PASS");
|