elizaos--eliza
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
225 行
6.7 KiB
TypeScript
225 行
6.7 KiB
TypeScript
/**
|
|
* End-to-end test for packages/app-core/scripts/omnivoice-fuse/freeze-voice.mjs.
|
|
*
|
|
* The CLI builds an ELZ2 v2 voice preset from a corpus directory. We
|
|
* exercise the corpus-discovery + selection + dry-run + skip-encode write
|
|
* paths against a synthesized minimal corpus (1 short WAV + manifest
|
|
* entry); we do NOT run the FFI encode path here (the fused
|
|
* libelizainference is not built in CI by default — that's covered by
|
|
* the dedicated FFI smoke tests).
|
|
*
|
|
* Coverage:
|
|
* - --dry-run lists clips without writing anything.
|
|
* - --skip-encode writes a parseable ELZ2 v2 file under <out>.
|
|
* - the written preset round-trips via readVoicePresetFile and the
|
|
* refText + instruct + metadata fields are preserved.
|
|
* - --voice rejects path-unsafe ids.
|
|
* - --max-seconds bounds the selected reference length.
|
|
*/
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { readVoicePresetFile } from "../src/services/voice/voice-preset-format";
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
// .../plugins/plugin-local-inference/__tests__ -> repo root
|
|
const REPO_ROOT = path.resolve(HERE, "../../..");
|
|
const SCRIPT = path.join(
|
|
REPO_ROOT,
|
|
"packages",
|
|
"app-core",
|
|
"scripts",
|
|
"voice",
|
|
"freeze-voice.mjs",
|
|
);
|
|
|
|
interface RunResult {
|
|
stdout: string;
|
|
stderr: string;
|
|
code: number;
|
|
}
|
|
|
|
function runFreeze(args: string[], opts: { allowFail?: boolean } = {}): RunResult {
|
|
try {
|
|
const stdout = execFileSync("bun", [SCRIPT, ...args], {
|
|
cwd: REPO_ROOT,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
return { stdout, stderr: "", code: 0 };
|
|
} catch (err) {
|
|
const e = err as { stdout?: Buffer | string; stderr?: Buffer | string; status?: number };
|
|
if (!opts.allowFail) {
|
|
throw err;
|
|
}
|
|
return {
|
|
stdout: typeof e.stdout === "string" ? e.stdout : e.stdout?.toString() ?? "",
|
|
stderr: typeof e.stderr === "string" ? e.stderr : e.stderr?.toString() ?? "",
|
|
code: e.status ?? 1,
|
|
};
|
|
}
|
|
}
|
|
|
|
/** Build a minimal 16-bit PCM mono RIFF/WAVE file at `srHz` for `durSec`. */
|
|
function writeSineWav(filePath: string, durSec: number, srHz: number) {
|
|
const n = Math.round(durSec * srHz);
|
|
const dataLen = n * 2;
|
|
const buf = Buffer.alloc(44 + dataLen);
|
|
buf.write("RIFF", 0, "ascii");
|
|
buf.writeUInt32LE(36 + dataLen, 4);
|
|
buf.write("WAVE", 8, "ascii");
|
|
buf.write("fmt ", 12, "ascii");
|
|
buf.writeUInt32LE(16, 16); // fmt size
|
|
buf.writeUInt16LE(1, 20); // PCM
|
|
buf.writeUInt16LE(1, 22); // channels
|
|
buf.writeUInt32LE(srHz, 24);
|
|
buf.writeUInt32LE(srHz * 2, 28); // byte rate
|
|
buf.writeUInt16LE(2, 32); // block align
|
|
buf.writeUInt16LE(16, 34); // bits/sample
|
|
buf.write("data", 36, "ascii");
|
|
buf.writeUInt32LE(dataLen, 40);
|
|
const f = 440;
|
|
for (let i = 0; i < n; i++) {
|
|
const s = Math.round(Math.sin((2 * Math.PI * f * i) / srHz) * 0.1 * 32767);
|
|
buf.writeInt16LE(s, 44 + i * 2);
|
|
}
|
|
writeFileSync(filePath, buf);
|
|
}
|
|
|
|
describe("freeze-voice.mjs CLI", () => {
|
|
let corpusDir: string;
|
|
let outDir: string;
|
|
|
|
beforeEach(() => {
|
|
corpusDir = mkdtempSync(path.join(tmpdir(), "freeze-voice-corpus-"));
|
|
outDir = mkdtempSync(path.join(tmpdir(), "freeze-voice-out-"));
|
|
mkdirSync(path.join(corpusDir, "audio"), { recursive: true });
|
|
// Two clips: a 3-second sample and a 5-second sample, both at 44.1 kHz.
|
|
writeSineWav(path.join(corpusDir, "audio", "same_001.wav"), 3.0, 44100);
|
|
writeSineWav(path.join(corpusDir, "audio", "same_003.wav"), 5.0, 44100);
|
|
const manifest = [
|
|
JSON.stringify({
|
|
id: "same_001",
|
|
audio_path: "audio/same_001.wav",
|
|
duration_s: 3.0,
|
|
sample_rate: 44100,
|
|
channels: 1,
|
|
bit_depth: 16,
|
|
transcript: "First reference sentence for the freeze test.",
|
|
excluded: false,
|
|
}),
|
|
JSON.stringify({
|
|
id: "same_003",
|
|
audio_path: "audio/same_003.wav",
|
|
duration_s: 5.0,
|
|
sample_rate: 44100,
|
|
channels: 1,
|
|
bit_depth: 16,
|
|
transcript: "Second reference sentence containing more words.",
|
|
excluded: false,
|
|
}),
|
|
].join("\n");
|
|
writeFileSync(path.join(corpusDir, "manifest.jsonl"), manifest);
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(corpusDir, { recursive: true, force: true });
|
|
rmSync(outDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("--dry-run lists selected clips without writing", () => {
|
|
const out = path.join(outDir, "voice-preset-test.bin");
|
|
const res = runFreeze([
|
|
"--voice",
|
|
"test",
|
|
"--corpus",
|
|
corpusDir,
|
|
"--out",
|
|
out,
|
|
"--dry-run",
|
|
"--skip-encode",
|
|
]);
|
|
expect(res.stdout).toContain("selected 2 clip(s)");
|
|
expect(res.stdout).toContain("dry-run: skipping load + encode + write");
|
|
expect(existsSync(out)).toBe(false);
|
|
});
|
|
|
|
it("--skip-encode writes a parseable ELZ2 v2 file with refText + instruct + metadata", () => {
|
|
const out = path.join(outDir, "voice-preset-test.bin");
|
|
const instruct = "young adult female, warm";
|
|
const res = runFreeze([
|
|
"--voice",
|
|
"test",
|
|
"--corpus",
|
|
corpusDir,
|
|
"--out",
|
|
out,
|
|
"--skip-encode",
|
|
"--instruct",
|
|
instruct,
|
|
]);
|
|
expect(res.code).toBe(0);
|
|
expect(existsSync(out)).toBe(true);
|
|
const bytes = new Uint8Array(readFileSync(out));
|
|
const parsed = readVoicePresetFile(bytes);
|
|
expect(parsed.version).toBe(2);
|
|
expect(parsed.instruct).toBe(instruct);
|
|
expect(parsed.refText).toContain("First reference sentence");
|
|
expect(parsed.refText).toContain("Second reference sentence");
|
|
expect(parsed.refAudioTokens.K).toBe(0); // --skip-encode left these empty
|
|
expect(parsed.refAudioTokens.refT).toBe(0);
|
|
expect(parsed.metadata.voiceId).toBe("test");
|
|
expect(parsed.metadata.generator).toBe("freeze-voice.mjs");
|
|
expect(Array.isArray(parsed.metadata.referenceClipIds)).toBe(true);
|
|
expect((parsed.metadata.referenceClipIds as string[]).length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("--max-seconds bounds selected reference length", () => {
|
|
const out = path.join(outDir, "voice-preset-test.bin");
|
|
const res = runFreeze([
|
|
"--voice",
|
|
"test",
|
|
"--corpus",
|
|
corpusDir,
|
|
"--out",
|
|
out,
|
|
"--skip-encode",
|
|
"--max-seconds",
|
|
"3.5",
|
|
]);
|
|
expect(res.code).toBe(0);
|
|
// Only the first 3 s clip fits in a 3.5 s budget.
|
|
expect(res.stdout).toContain("selected 1 clip(s)");
|
|
});
|
|
|
|
it("rejects path-unsafe voice ids", () => {
|
|
const out = path.join(outDir, "voice-preset-test.bin");
|
|
const res = runFreeze(
|
|
[
|
|
"--voice",
|
|
"../escape",
|
|
"--corpus",
|
|
corpusDir,
|
|
"--out",
|
|
out,
|
|
"--skip-encode",
|
|
"--dry-run",
|
|
],
|
|
{ allowFail: true },
|
|
);
|
|
expect(res.code).not.toBe(0);
|
|
expect(res.stderr + res.stdout).toMatch(/path-safe segment/i);
|
|
});
|
|
});
|