diff --git a/packages/server/src/server/speech/providers/local/sherpa/model-downloader.test.ts b/packages/server/src/server/speech/providers/local/sherpa/model-downloader.test.ts index bc2e462a53..b4d66e808a 100644 --- a/packages/server/src/server/speech/providers/local/sherpa/model-downloader.test.ts +++ b/packages/server/src/server/speech/providers/local/sherpa/model-downloader.test.ts @@ -8,6 +8,7 @@ import { existsSync, truncateSync, readdirSync, + symlinkSync, } from "node:fs"; import { execFileSync } from "node:child_process"; import { tmpdir } from "node:os"; @@ -23,7 +24,71 @@ function makeTmpDir(): string { const logger = pino({ level: "silent" }); +// A tar.bz2 containing the three Kokoro files ("complete model data") and espeak-ng-data/. +// Generated with Python tarfile + bz2, so testing extraction needs no system compressor. +const compressedModelArchive = Buffer.from( + "QlpoOTFBWSZTWYGlJLIAAOjfgcqBQAP/ICEAAAD+r99AABAICDAA1mQkopo0P1JkeoMTBDE9RgyiADQZAAAACSVPTU9TT1B6g0aGJ6hkyafQaedKK3SkotKCAUvkgkMWhnICQFABoykPAQBIYAZHteVO8PnEPulpWBWogKIMAi5XOBFWQPCYTsU1U7snVwYKXg/axQ0QgDBKDgsClwZB7z0TLGwJV+3hIFscrlfwje/SLh4QDkqFxkXTaE+x7Zh/DDrAILu8MdygDAzOzD88IBQpa8wkDqd3cEH8XckU4UJCBpSSyA==", + "base64", +); + describe("sherpa model downloader", () => { + test.skipIf(process.platform !== "linux")( + "installs a cached bzip2 model without a system bzip2 executable", + async () => { + const root = makeTmpDir(); + const modelsDir = path.join(root, "models"); + const spec = getSherpaOnnxModelSpec("kokoro-en-v0_19"); + const archive = path.join( + modelsDir, + ".downloads", + path.basename(new URL(spec.archiveUrl).pathname), + ); + const originalPath = process.env.PATH; + try { + const binDir = path.join(root, "bin"); + mkdirSync(binDir); + symlinkSync( + execFileSync("which", ["tar"], { encoding: "utf8" }).trim(), + path.join(binDir, "tar"), + ); + mkdirSync(path.dirname(archive), { recursive: true }); + writeFileSync(archive, compressedModelArchive); + // GNU tar is available, but the bzip2 executable it invokes is not. + process.env.PATH = binDir; + const modelDir = await ensureSherpaOnnxModel({ modelsDir, modelId: spec.id, logger }); + expect(readFileSync(path.join(modelDir, "model.onnx"), "utf8")).toBe("complete model data"); + expect(existsSync(path.join(modelDir, "espeak-ng-data"))).toBe(true); + expect(existsSync(archive)).toBe(false); + } finally { + if (originalPath === undefined) delete process.env.PATH; + else process.env.PATH = originalPath; + rmSync(root, { recursive: true, force: true }); + } + }, + ); + test.skipIf(process.platform !== "linux")( + "installs the same cached bzip2 archive when system tools are available", + async () => { + const root = makeTmpDir(); + const modelsDir = path.join(root, "models"); + const spec = getSherpaOnnxModelSpec("kokoro-en-v0_19"); + const archive = path.join( + modelsDir, + ".downloads", + path.basename(new URL(spec.archiveUrl).pathname), + ); + try { + mkdirSync(path.dirname(archive), { recursive: true }); + writeFileSync(archive, compressedModelArchive); + const modelDir = await ensureSherpaOnnxModel({ modelsDir, modelId: spec.id, logger }); + expect(readFileSync(path.join(modelDir, "model.onnx"), "utf8")).toBe("complete model data"); + expect(existsSync(archive)).toBe(false); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }, + ); + test("does not expose a partial model when tar extraction fails", async () => { const root = makeTmpDir(); const modelsDir = path.join(root, "models");