mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(cli): use a healthy pinned Sherpa copy beside the CLI (#5095)
* docs(cli): map the pinned Sherpa runtime loading contract * fix(cli): use a healthy pinned Sherpa copy beside the CLI * chore(cli): keep runtime loader comments within the ratchet * docs(cli): wrap the runtime contract palette declarations * chore(cli): keep the Sherpa contract note out of the repo
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { CliRuntimeError, consumeCommandResult } from "../utils/commandResult.js";
|
||||
|
||||
const cachedRuntimePath =
|
||||
"/cache/optional/sherpa-onnx-node@1.13.8/node_modules/sherpa-onnx-node/index.js";
|
||||
const sherpa = {
|
||||
SHERPA_RUNTIME_DIR: "/cache/optional/sherpa-onnx-node@1.13.8",
|
||||
PARAKEET_MODEL_DIR: "/cache/parakeet/parakeet-tdt-0.6b-v3-int8",
|
||||
@@ -44,7 +46,10 @@ describe("models install parakeet --json", () => {
|
||||
consumeCommandResult();
|
||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
sherpa.sherpaUnsupportedReason.mockReturnValue(null);
|
||||
sherpa.installSherpaRuntime.mockResolvedValue(true);
|
||||
sherpa.installSherpaRuntime.mockResolvedValue({
|
||||
installed: true,
|
||||
runtimePath: cachedRuntimePath,
|
||||
});
|
||||
sherpa.ensureParakeetModel.mockResolvedValue(false);
|
||||
});
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
@@ -58,6 +63,7 @@ describe("models install parakeet --json", () => {
|
||||
model: "parakeet-tdt-0.6b-v3",
|
||||
changed: true,
|
||||
runtimeDir: sherpa.SHERPA_RUNTIME_DIR,
|
||||
runtimePath: cachedRuntimePath,
|
||||
modelDir: sherpa.PARAKEET_MODEL_DIR,
|
||||
},
|
||||
});
|
||||
@@ -65,10 +71,24 @@ describe("models install parakeet --json", () => {
|
||||
});
|
||||
|
||||
it("reports changed: false when the runtime loads and the model verifies", async () => {
|
||||
sherpa.installSherpaRuntime.mockResolvedValue(false);
|
||||
sherpa.installSherpaRuntime.mockResolvedValue({
|
||||
installed: false,
|
||||
runtimePath: cachedRuntimePath,
|
||||
});
|
||||
expect((await install()).out).toMatchObject({ ok: true, changed: false });
|
||||
});
|
||||
|
||||
it("reports a selected beside copy while retaining the cache destination", async () => {
|
||||
const runtimePath = "/bundle/node_modules/sherpa-onnx-node/index.js";
|
||||
sherpa.installSherpaRuntime.mockResolvedValue({ installed: false, runtimePath });
|
||||
expect((await install()).out).toMatchObject({
|
||||
ok: true,
|
||||
changed: false,
|
||||
runtimeDir: sherpa.SHERPA_RUNTIME_DIR,
|
||||
runtimePath,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports a failed install as ok:false with exit 1", async () => {
|
||||
sherpa.ensureParakeetModel.mockRejectedValue(new Error("tokens.txt did not match"));
|
||||
expect(await install()).toEqual({
|
||||
|
||||
@@ -43,18 +43,18 @@ function downloadProgress(spin: Spinner) {
|
||||
type Sherpa = typeof import("../whisper/sherpa.js");
|
||||
type Spinner = ReturnType<typeof clack.spinner> | null;
|
||||
|
||||
/** Installs what is missing; true when anything changed. */
|
||||
/** Installs missing pieces and carries the selected runtime provenance into the result. */
|
||||
async function installMissing(sherpa: Sherpa, spin: Spinner, signal: AbortSignal) {
|
||||
const runtimeInstalled = await sherpa.installSherpaRuntime({ signal });
|
||||
const runtime = await sherpa.installSherpaRuntime({ signal });
|
||||
spin?.message("Verifying the Parakeet model...");
|
||||
const modelFetched = await sherpa.ensureParakeetModel({
|
||||
signal,
|
||||
onBytes: downloadProgress(spin),
|
||||
});
|
||||
return runtimeInstalled || modelFetched;
|
||||
return { changed: runtime.installed || modelFetched, runtimePath: runtime.runtimePath };
|
||||
}
|
||||
|
||||
/** A synchronous child's Ctrl-C shows as its error before the scope's listener runs. */
|
||||
/** Cancellation can reach the child before the scope observes the signal. */
|
||||
const wasCancelled = (err: unknown, signal: AbortSignal, sherpa: Sherpa) =>
|
||||
signal.aborted || err instanceof sherpa.DecodeCancelled;
|
||||
|
||||
@@ -68,12 +68,19 @@ async function installParakeet(json: boolean): Promise<void> {
|
||||
const cancellation = createRenderCancellationScope();
|
||||
spin?.start("Checking the sherpa-onnx runtime (installing it from npm if it does not load)...");
|
||||
try {
|
||||
const changed = await installMissing(sherpa, spin, cancellation.signal);
|
||||
const { changed, runtimePath } = await installMissing(sherpa, spin, cancellation.signal);
|
||||
spin?.stop(c.success(changed ? "Parakeet installed" : "Parakeet is already installed"));
|
||||
if (json) {
|
||||
const { SHERPA_RUNTIME_DIR: runtimeDir, PARAKEET_MODEL_DIR: modelDir } = sherpa;
|
||||
console.log(
|
||||
JSON.stringify({ ok: true, model: PARAKEET_MODEL_LABEL, changed, runtimeDir, modelDir }),
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
model: PARAKEET_MODEL_LABEL,
|
||||
changed,
|
||||
runtimeDir,
|
||||
runtimePath,
|
||||
modelDir,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { runCommand } from "citty";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { chmodSync, existsSync, writeFileSync, readFileSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import {
|
||||
chmodSync,
|
||||
mkdirSync,
|
||||
existsSync,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
mkdtempSync,
|
||||
rmSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { WhisperUnavailableError } from "../whisper/manager.js";
|
||||
import { CliRuntimeError, consumeCommandResult } from "../utils/commandResult.js";
|
||||
|
||||
@@ -18,10 +27,24 @@ vi.mock("../whisper/transcribe.js", () => ({
|
||||
|
||||
// Engine selection: which runners look installed, and the sherpa decode child it spawns.
|
||||
const runners = { sherpa: false, mlx: false };
|
||||
vi.mock("../whisper/sherpa.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../whisper/sherpa.js")>()),
|
||||
sherpaParakeetInstalled: () => runners.sherpa,
|
||||
}));
|
||||
let runtimeDir: string;
|
||||
vi.mock("../whisper/sherpa.js", async (importOriginal) => {
|
||||
const sherpa = await importOriginal<typeof import("../whisper/sherpa.js")>();
|
||||
return {
|
||||
...sherpa,
|
||||
sherpaParakeetInstalled: () => runners.sherpa,
|
||||
transcribeWithSherpa: (
|
||||
wavPath: string,
|
||||
dir: string,
|
||||
options: Parameters<typeof sherpa.transcribeWithSherpa>[2],
|
||||
) =>
|
||||
sherpa.transcribeWithSherpa(wavPath, dir, {
|
||||
...options,
|
||||
runtimeDir,
|
||||
cliUrl: pathToFileURL(join(runtimeDir, "dist", "cli.js")).href,
|
||||
}),
|
||||
};
|
||||
});
|
||||
const mlxMock = vi.fn();
|
||||
vi.mock("../whisper/parakeet.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../whisper/parakeet.js")>()),
|
||||
@@ -165,6 +188,12 @@ describe("transcribe command", () => {
|
||||
|
||||
describe("engine selection", () => {
|
||||
beforeEach(() => {
|
||||
runtimeDir = mkdtempSync(join(tmpdir(), "hf-transcribe-runtime-"));
|
||||
dirs.push(runtimeDir);
|
||||
const pkg = join(runtimeDir, "node_modules", "sherpa-onnx-node");
|
||||
mkdirSync(pkg, { recursive: true });
|
||||
writeFileSync(join(pkg, "package.json"), JSON.stringify({ main: "index.js" }));
|
||||
writeFileSync(join(pkg, "index.js"), "module.exports = {};");
|
||||
transcribeMock.mockImplementation(async (_in: string, dir: string) =>
|
||||
fakeTranscript(dir, "whisper"),
|
||||
);
|
||||
|
||||
@@ -132,9 +132,9 @@ describe("loadOptionalPackage", () => {
|
||||
|
||||
describe("a copy installed beside the CLI", () => {
|
||||
// node_modules/hyperframes/dist/cli.js with onnxruntime-node installed next to hyperframes.
|
||||
function layout(version: string) {
|
||||
function layout(version: string, name = "onnxruntime-node") {
|
||||
const root = mkdtempSync(join(tmpdir(), "hf-beside-"));
|
||||
const pkg = join(root, "node_modules", "onnxruntime-node");
|
||||
const pkg = join(root, "node_modules", name);
|
||||
mkdirSync(pkg, { recursive: true });
|
||||
writeFileSync(join(pkg, "package.json"), JSON.stringify({ version, main: "index.js" }));
|
||||
writeFileSync(join(pkg, "index.js"), `module.exports = { copy: "beside ${version}" };`);
|
||||
@@ -153,6 +153,27 @@ describe("a copy installed beside the CLI", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["1.13.8", "1.13.7"])("accepts only the Sherpa 1.13.8 pin, received %s", (version) => {
|
||||
const { root, cliUrl } = layout(version, "sherpa-onnx-node");
|
||||
try {
|
||||
const result = loadBesideCli("sherpa-onnx-node", cliUrl);
|
||||
if (version === "1.13.8") expect(result).toEqual({ copy: "beside 1.13.8" });
|
||||
else expect(result).toBeNull();
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a missing optional-package entry eligible for cache fallback", () => {
|
||||
const { root, cliUrl } = layout("1.21.1");
|
||||
try {
|
||||
rmSync(join(root, "node_modules", "onnxruntime-node", "index.js"));
|
||||
expect(loadBesideCli("onnxruntime-node", cliUrl)).toBeNull();
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores it when its manifest is unreadable, instead of throwing", () => {
|
||||
const { root, cliUrl } = layout(OPTIONAL_PACKAGES["onnxruntime-node"]);
|
||||
writeFileSync(join(root, "node_modules", "onnxruntime-node", "package.json"), "{ version: 1");
|
||||
|
||||
@@ -30,6 +30,13 @@ export const OPTIONAL_PACKAGES = {
|
||||
"@google/genai": "1.52.0",
|
||||
} as const satisfies Record<OptionalPackage, string>;
|
||||
|
||||
export const PINNED_PACKAGES = {
|
||||
...OPTIONAL_PACKAGES,
|
||||
"sherpa-onnx-node": "1.13.8",
|
||||
} as const;
|
||||
|
||||
type PinnedPackage = keyof typeof PINNED_PACKAGES;
|
||||
|
||||
export const CACHE_DIR = join(homedir(), ".cache", "hyperframes", "optional");
|
||||
|
||||
export interface OptionalPackageDeps {
|
||||
@@ -53,7 +60,7 @@ export function installedOptionalPackageVersion(
|
||||
cacheDir = CACHE_DIR,
|
||||
cliUrl = import.meta.url,
|
||||
): string | null {
|
||||
if (pinnedCopyBesideCli(name, cliUrl)) return OPTIONAL_PACKAGES[name];
|
||||
if (pinnedPackageBesideCli(name, cliUrl)?.resolution === "entry") return OPTIONAL_PACKAGES[name];
|
||||
const dir = optionalPackageDir(name, cacheDir);
|
||||
if (!isInstalled(dir, name)) return null;
|
||||
return (JSON.parse(readFileSync(manifestPath(dir, name), "utf-8")) as { version: string })
|
||||
@@ -113,28 +120,51 @@ export function isInstalled(dir: string, name: string): boolean {
|
||||
return existsSync(manifestPath(dir, name));
|
||||
}
|
||||
|
||||
export function loadInstalled(dir: string, name: string): unknown | null {
|
||||
export function installedPackagePath(dir: string, name: string): string | null {
|
||||
if (!isInstalled(dir, name)) return null;
|
||||
return createRequire(join(dir, "package.json"))(name);
|
||||
return createRequire(join(dir, "package.json")).resolve(name);
|
||||
}
|
||||
|
||||
function pinnedCopyBesideCli(name: OptionalPackage, cliUrl: string): boolean {
|
||||
function loadInstalled(dir: string, name: string): unknown | null {
|
||||
const entry = installedPackagePath(dir, name);
|
||||
return entry === null ? null : createRequire(join(dir, "package.json"))(entry);
|
||||
}
|
||||
|
||||
export function pinnedPackageBesideCli(
|
||||
name: PinnedPackage,
|
||||
cliUrl = import.meta.url,
|
||||
): { resolution: "entry" | "package"; path: string } | null {
|
||||
const req = createRequire(cliUrl);
|
||||
try {
|
||||
const entry = realpathSync(req.resolve(name));
|
||||
let entry: string;
|
||||
try {
|
||||
entry = realpathSync(req.resolve(name));
|
||||
} catch {
|
||||
const copy = (req.resolve.paths(name) ?? [])
|
||||
.map((dir) => join(dir, name))
|
||||
.find((dir) => existsSync(join(dir, "package.json")));
|
||||
return copy && hasPinnedVersion(copy, name)
|
||||
? { resolution: "package", path: realpathSync(copy) }
|
||||
: null;
|
||||
}
|
||||
const copy = (req.resolve.paths(name) ?? [])
|
||||
.map((dir) => join(dir, name))
|
||||
.find((dir) => existsSync(dir) && entry.startsWith(realpathSync(dir) + sep));
|
||||
if (!copy) return false;
|
||||
const manifest = readFileSync(join(copy, "package.json"), "utf-8");
|
||||
return (JSON.parse(manifest) as { version?: string }).version === OPTIONAL_PACKAGES[name];
|
||||
if (!copy) return null;
|
||||
return hasPinnedVersion(copy, name) ? { resolution: "entry", path: entry } : null;
|
||||
} catch {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadBesideCli(name: OptionalPackage, cliUrl = import.meta.url): unknown | null {
|
||||
return pinnedCopyBesideCli(name, cliUrl) ? createRequire(cliUrl)(name) : null;
|
||||
function hasPinnedVersion(copy: string, name: PinnedPackage): boolean {
|
||||
const manifest = readFileSync(join(copy, "package.json"), "utf-8");
|
||||
return (JSON.parse(manifest) as { version?: string }).version === PINNED_PACKAGES[name];
|
||||
}
|
||||
|
||||
export function loadBesideCli(name: PinnedPackage, cliUrl = import.meta.url): unknown | null {
|
||||
const copy = pinnedPackageBesideCli(name, cliUrl);
|
||||
return copy?.resolution === "entry" ? createRequire(cliUrl)(copy.path) : null;
|
||||
}
|
||||
|
||||
export function runNpm(args: string[], signal?: AbortSignal): Promise<void> {
|
||||
|
||||
@@ -4,13 +4,15 @@ import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
truncateSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { DownloadOptions } from "../cloud/download.js";
|
||||
import {
|
||||
ensureParakeetModel,
|
||||
@@ -20,6 +22,7 @@ import {
|
||||
DecodeCancelled,
|
||||
sherpaRuntimeLoadError,
|
||||
sherpaUnsupportedReason,
|
||||
transcribeWithSherpa,
|
||||
type ModelFile,
|
||||
} from "./sherpa.js";
|
||||
|
||||
@@ -41,6 +44,19 @@ function fakeDownload(served: Record<string, string>) {
|
||||
});
|
||||
}
|
||||
|
||||
/** A stand-in runtime in `root` whose entry point loads, or throws as a missing library does. */
|
||||
const LOADS = "module.exports = {};";
|
||||
const BROKEN = 'throw new Error("libonnxruntime.so: cannot open\\n shared object");';
|
||||
function fakeRuntime(root: string, body: string, version = "1.13.8") {
|
||||
const pkg = join(root, "node_modules", "sherpa-onnx-node");
|
||||
mkdirSync(pkg, { recursive: true });
|
||||
writeFileSync(
|
||||
join(pkg, "package.json"),
|
||||
JSON.stringify({ name: "sherpa-onnx-node", main: "index.js", version }),
|
||||
);
|
||||
writeFileSync(join(pkg, "index.js"), body);
|
||||
}
|
||||
|
||||
describe("installSherpaRuntime", () => {
|
||||
it.each([
|
||||
["darwin", "arm64", "sherpa-onnx-darwin-arm64@1.13.8"],
|
||||
@@ -66,28 +82,23 @@ describe("installSherpaRuntime", () => {
|
||||
expect(args).toContain(sherpaPlatformPackage());
|
||||
});
|
||||
|
||||
/** A stand-in runtime in `root` whose entry point loads, or throws as a missing library does. */
|
||||
const LOADS = "module.exports = {};";
|
||||
const BROKEN = 'throw new Error("libonnxruntime.so: cannot open\\n shared object");';
|
||||
function fakeRuntime(root: string, body: string) {
|
||||
const pkg = join(root, "node_modules", "sherpa-onnx-node");
|
||||
mkdirSync(pkg, { recursive: true });
|
||||
writeFileSync(join(pkg, "package.json"), '{"name":"sherpa-onnx-node","main":"index.js"}');
|
||||
writeFileSync(join(pkg, "index.js"), body);
|
||||
}
|
||||
|
||||
it("reinstalls a runtime that is present but does not load", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-sherpa-runtime-"));
|
||||
try {
|
||||
fakeRuntime(dir, BROKEN);
|
||||
expect(sherpaRuntimeLoadError(dir)).toBe("libonnxruntime.so: cannot open shared object");
|
||||
expect(await sherpaRuntimeLoadError(dir)).toBe(
|
||||
"libonnxruntime.so: cannot open shared object",
|
||||
);
|
||||
const run = vi.fn(async (args: string[]) =>
|
||||
fakeRuntime(args[args.indexOf("--prefix") + 1]!, LOADS),
|
||||
);
|
||||
|
||||
expect(await installSherpaRuntime({ run, dir })).toBe(true);
|
||||
expect(await installSherpaRuntime({ run, dir })).toEqual({
|
||||
installed: true,
|
||||
runtimePath: join(dir, "node_modules", "sherpa-onnx-node", "index.js"),
|
||||
});
|
||||
expect(run).toHaveBeenCalledTimes(1);
|
||||
expect(sherpaRuntimeLoadError(dir)).toBeNull();
|
||||
expect(await sherpaRuntimeLoadError(dir)).toBeNull();
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -98,7 +109,10 @@ describe("installSherpaRuntime", () => {
|
||||
try {
|
||||
fakeRuntime(dir, LOADS);
|
||||
const run = vi.fn();
|
||||
expect(await installSherpaRuntime({ run, dir })).toBe(false);
|
||||
expect(await installSherpaRuntime({ run, dir })).toEqual({
|
||||
installed: false,
|
||||
runtimePath: join(dir, "node_modules", "sherpa-onnx-node", "index.js"),
|
||||
});
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
@@ -135,11 +149,24 @@ describe("installSherpaRuntime", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("reads its own probe timeout as a broken runtime, not a cancel, and says so", () => {
|
||||
it("reads its own probe timeout as a broken runtime, not a cancel, and says so", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-sherpa-runtime-"));
|
||||
try {
|
||||
fakeRuntime(dir, "Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);");
|
||||
expect(sherpaRuntimeLoadError(dir, 300)).toBe("loading it timed out after 0.3 s");
|
||||
expect(await sherpaRuntimeLoadError(dir, 300)).toBe("loading it timed out after 0.3 s");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("reports timeout even when the runtime handles SIGTERM and exits zero", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-sherpa-runtime-"));
|
||||
try {
|
||||
fakeRuntime(
|
||||
dir,
|
||||
'process.on("SIGTERM", () => process.exit(0)); require("node:net").createServer().listen(0, "127.0.0.1");',
|
||||
);
|
||||
expect(await sherpaRuntimeLoadError(dir, 300)).toBe("loading it timed out after 0.3 s");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -152,33 +179,179 @@ describe("installSherpaRuntime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sherpaParakeetInstalled", () => {
|
||||
it("counts a runtime that does not load, so transcribe reports it instead of skipping it", async () => {
|
||||
const home = mkdtempSync(join(tmpdir(), "hf-sherpa-home-"));
|
||||
vi.stubEnv("HOME", home);
|
||||
vi.resetModules();
|
||||
try {
|
||||
const sherpa = await import("./sherpa.js");
|
||||
const manifest = join(sherpa.SHERPA_RUNTIME_DIR, "node_modules", "sherpa-onnx-node");
|
||||
mkdirSync(manifest, { recursive: true });
|
||||
writeFileSync(join(manifest, "package.json"), "{}");
|
||||
mkdirSync(sherpa.PARAKEET_MODEL_DIR, { recursive: true });
|
||||
for (const [name, bytes] of [
|
||||
["encoder.int8.onnx", 652_184_281],
|
||||
["decoder.int8.onnx", 11_845_275],
|
||||
["joiner.int8.onnx", 6_355_277],
|
||||
["tokens.txt", 93_939],
|
||||
] as const) {
|
||||
writeFileSync(join(sherpa.PARAKEET_MODEL_DIR, name), "");
|
||||
truncateSync(join(sherpa.PARAKEET_MODEL_DIR, name), bytes);
|
||||
}
|
||||
expect(sherpa.sherpaRuntimeLoadError()).not.toBeNull();
|
||||
expect(sherpa.sherpaParakeetInstalled()).toBe(true);
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
}
|
||||
describe("a pinned Sherpa copy beside the CLI", () => {
|
||||
let root: string;
|
||||
beforeEach(() => {
|
||||
root = mkdtempSync(join(tmpdir(), "hf-sherpa-beside-"));
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function layout(besideBody: string, cachedBody?: string) {
|
||||
const bundle = join(root, "bundle");
|
||||
const cache = join(root, "cache");
|
||||
fakeRuntime(bundle, besideBody);
|
||||
if (cachedBody !== undefined) fakeRuntime(cache, cachedBody);
|
||||
const cliUrl = pathToFileURL(join(bundle, "dist", "cli.js")).href;
|
||||
const pinnedPath = join(bundle, "node_modules", "sherpa-onnx-node", "index.js");
|
||||
return { cache, cliUrl, pinnedPath };
|
||||
}
|
||||
|
||||
function decoder() {
|
||||
const parentMarker = join(root, "loaded-in-parent");
|
||||
return `
|
||||
if (process.pid === ${process.pid}) require("node:fs").writeFileSync(${JSON.stringify(parentMarker)}, "");
|
||||
module.exports = {
|
||||
readWave() { return { sampleRate: 100, samples: new Float32Array(100).fill(0.5) }; },
|
||||
OfflineRecognizer: class {
|
||||
createStream() { return { acceptWaveform() {} }; }
|
||||
decode() {}
|
||||
getResult() { return { tokens: [" hello"], timestamps: [0], durations: [1] }; }
|
||||
}
|
||||
};`;
|
||||
}
|
||||
|
||||
async function transcript(cache: string, cliUrl: string) {
|
||||
const out = join(root, "out");
|
||||
mkdirSync(out, { recursive: true });
|
||||
const onEvent = vi.fn();
|
||||
await transcribeWithSherpa(join(root, "speech.wav"), out, {
|
||||
signal: new AbortController().signal,
|
||||
runtimeDir: cache,
|
||||
cliUrl,
|
||||
onEvent,
|
||||
});
|
||||
const result = JSON.parse(readFileSync(join(out, "transcript.json"), "utf-8"));
|
||||
expect(result).toEqual([{ text: "hello", start: 0, end: 1 }]);
|
||||
const event = { type: "progress", phase: "transcription", model: "parakeet-tdt-0.6b-v3" };
|
||||
expect(onEvent.mock.calls).toEqual([
|
||||
[{ ...event, status: "started" }],
|
||||
[{ ...event, status: "completed" }],
|
||||
]);
|
||||
expect(existsSync(join(root, "loaded-in-parent"))).toBe(false);
|
||||
}
|
||||
|
||||
it("loads a pinned adjacent copy without a cache or npm install", async () => {
|
||||
const { cache, cliUrl, pinnedPath } = layout(LOADS);
|
||||
const run = vi.fn();
|
||||
expect(await sherpaRuntimeLoadError(cache, 60_000, cliUrl)).toBeNull();
|
||||
expect(await installSherpaRuntime({ dir: cache, cliUrl, run })).toEqual({
|
||||
installed: false,
|
||||
runtimePath: pinnedPath,
|
||||
});
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
expect(existsSync(cache)).toBe(false);
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"cancels an in-flight probe without inspecting the cache",
|
||||
async () => {
|
||||
const cacheMarker = join(root, "cache-loaded");
|
||||
const { cache, cliUrl } = layout(
|
||||
'require("node:net").createServer().listen(0, "127.0.0.1", () => process.kill(process.ppid, "SIGUSR2"));',
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(cacheMarker)}, ""); module.exports = {};`,
|
||||
);
|
||||
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const controller = new AbortController();
|
||||
const abort = () => controller.abort();
|
||||
process.once("SIGUSR2", abort);
|
||||
try {
|
||||
await expect(
|
||||
sherpaRuntimeLoadError(cache, 300, cliUrl, controller.signal),
|
||||
).rejects.toBeInstanceOf(DecodeCancelled);
|
||||
expect(existsSync(cacheMarker)).toBe(false);
|
||||
expect(log).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
process.removeListener("SIGUSR2", abort);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("passes the pinned adjacent copy to the real decode worker", async () => {
|
||||
const { cache, cliUrl } = layout(decoder());
|
||||
await transcript(cache, cliUrl);
|
||||
});
|
||||
|
||||
it("logs the broken pinned path once and decodes with the healthy cache", async () => {
|
||||
const { cache, cliUrl, pinnedPath } = layout(BROKEN, decoder());
|
||||
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
await transcript(cache, cliUrl);
|
||||
expect(log).toHaveBeenCalledTimes(1);
|
||||
expect(log.mock.calls[0]?.[0]).toContain(pinnedPath);
|
||||
expect(log.mock.calls[0]?.[0]).toContain("libonnxruntime.so: cannot open shared object");
|
||||
expect(String(log.mock.calls[0]?.[0])).not.toContain("\n");
|
||||
});
|
||||
|
||||
it("reports an accepted package with a missing entry and uses the healthy cache", async () => {
|
||||
const { cache, cliUrl, pinnedPath } = layout(LOADS, decoder());
|
||||
rmSync(pinnedPath);
|
||||
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
await transcript(cache, cliUrl);
|
||||
expect(log).toHaveBeenCalledTimes(1);
|
||||
expect(log.mock.calls[0]?.[0]).toContain(dirname(pinnedPath));
|
||||
expect(log.mock.calls[0]?.[0]).toContain("Cannot find module");
|
||||
});
|
||||
|
||||
it.each([undefined, BROKEN])(
|
||||
"logs the broken pinned path once and reports repair when no cache loads (%s)",
|
||||
async (cachedBody) => {
|
||||
const { cache, cliUrl, pinnedPath } = layout(BROKEN, cachedBody);
|
||||
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const out = join(root, "out");
|
||||
mkdirSync(out);
|
||||
await expect(
|
||||
transcribeWithSherpa(join(root, "speech.wav"), out, {
|
||||
signal: new AbortController().signal,
|
||||
runtimeDir: cache,
|
||||
cliUrl,
|
||||
}),
|
||||
).rejects.toThrow(/hyperframes models install parakeet/);
|
||||
expect(log).toHaveBeenCalledTimes(1);
|
||||
expect(log.mock.calls[0]?.[0]).toContain(pinnedPath);
|
||||
expect(log.mock.calls[0]?.[0]).toContain("libonnxruntime.so: cannot open shared object");
|
||||
expect(readdirSync(out)).toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("sherpaParakeetInstalled", () => {
|
||||
it.each(["cache", "beside CLI"])(
|
||||
"counts a broken runtime in %s, so transcribe reports it instead of skipping it",
|
||||
async (location) => {
|
||||
const home = mkdtempSync(join(tmpdir(), "hf-sherpa-home-"));
|
||||
vi.stubEnv("HOME", home);
|
||||
vi.resetModules();
|
||||
try {
|
||||
const sherpa = await import("./sherpa.js");
|
||||
const bundle = join(home, "bundle");
|
||||
const cliUrl = pathToFileURL(join(bundle, "dist", "cli.js")).href;
|
||||
const manifest = join(
|
||||
location === "cache" ? sherpa.SHERPA_RUNTIME_DIR : bundle,
|
||||
"node_modules",
|
||||
"sherpa-onnx-node",
|
||||
);
|
||||
mkdirSync(manifest, { recursive: true });
|
||||
if (location === "cache") writeFileSync(join(manifest, "package.json"), "{}");
|
||||
else fakeRuntime(bundle, BROKEN);
|
||||
mkdirSync(sherpa.PARAKEET_MODEL_DIR, { recursive: true });
|
||||
for (const [name, bytes] of [
|
||||
["encoder.int8.onnx", 652_184_281],
|
||||
["decoder.int8.onnx", 11_845_275],
|
||||
["joiner.int8.onnx", 6_355_277],
|
||||
["tokens.txt", 93_939],
|
||||
] as const) {
|
||||
writeFileSync(join(sherpa.PARAKEET_MODEL_DIR, name), "");
|
||||
truncateSync(join(sherpa.PARAKEET_MODEL_DIR, name), bytes);
|
||||
}
|
||||
expect(sherpa.sherpaParakeetInstalled(cliUrl)).toBe(true);
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("ensureParakeetModel", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { execFile, spawnSync, type ExecFileException } from "node:child_process";
|
||||
import { execFile, spawn, type ExecFileException } from "node:child_process";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { createReadStream, mkdirSync, renameSync, rmSync, statSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
@@ -8,6 +8,9 @@ import { downloadToFile } from "../cloud/download.js";
|
||||
import { stoppedByCancelSignal } from "../utils/renderCancellation.js";
|
||||
import {
|
||||
CACHE_DIR,
|
||||
PINNED_PACKAGES,
|
||||
pinnedPackageBesideCli,
|
||||
installedPackagePath,
|
||||
install,
|
||||
isInstalled,
|
||||
runNpm,
|
||||
@@ -29,7 +32,7 @@ import {
|
||||
} from "./transcribe.js";
|
||||
|
||||
const RUNTIME = "sherpa-onnx-node";
|
||||
const RUNTIME_VERSION = "1.13.8";
|
||||
const RUNTIME_VERSION = PINNED_PACKAGES[RUNTIME];
|
||||
/** Per platform and arch, like the native package inside, so Rosetta never shadows arm64. */
|
||||
export const SHERPA_RUNTIME_DIR = join(
|
||||
CACHE_DIR,
|
||||
@@ -121,36 +124,109 @@ const nativePackageName = (platform: string, arch: string) =>
|
||||
`sherpa-onnx-${platform === "win32" ? "win" : platform}-${arch}`;
|
||||
|
||||
const LOAD_RUNTIME = `try {
|
||||
require("node:module").createRequire(process.env.HF_SHERPA_MANIFEST)("${RUNTIME}");
|
||||
require(process.env.HF_SHERPA_RUNTIME_PATH);
|
||||
} catch (e) {
|
||||
process.stderr.write(String(e && e.message).replace(/\\s+/g, " "));
|
||||
process.exitCode = 1;
|
||||
}`;
|
||||
|
||||
/** Loads it in a child as the decode worker does: null when it loads, else the loader's error. */
|
||||
export function sherpaRuntimeLoadError(
|
||||
/** Native bindings are probed only in a child, as they are loaded in the decode worker. */
|
||||
function runtimeLoadError(
|
||||
runtimePath: string,
|
||||
timeoutMs: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string | null> {
|
||||
if (signal?.aborted)
|
||||
return Promise.reject(new DecodeCancelled("Parakeet runtime check cancelled"));
|
||||
return new Promise((resolve, reject) => {
|
||||
const probe = spawn(process.execPath, ["-e", LOAD_RUNTIME], {
|
||||
env: { ...process.env, HF_SHERPA_RUNTIME_PATH: runtimePath },
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
timeout: timeoutMs,
|
||||
signal,
|
||||
});
|
||||
let stderr = "";
|
||||
probe.stderr.setEncoding("utf8");
|
||||
probe.stderr.on("data", (chunk: string) => {
|
||||
stderr = bounded(stderr + chunk);
|
||||
});
|
||||
probe.on("error", (error) => {
|
||||
if (signal?.aborted) reject(new DecodeCancelled("Parakeet runtime check cancelled"));
|
||||
else resolve(bounded(error.message));
|
||||
});
|
||||
probe.on("close", (code, childSignal) => {
|
||||
if (
|
||||
signal?.aborted ||
|
||||
stoppedByCancelSignal({ code, signal: childSignal, killed: probe.killed })
|
||||
) {
|
||||
reject(new DecodeCancelled("Parakeet runtime check cancelled"));
|
||||
return;
|
||||
}
|
||||
if (code === 0 && !probe.killed) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
let reason: string;
|
||||
if (probe.killed) reason = `loading it timed out after ${timeoutMs / 1000} s`;
|
||||
else if (childSignal) reason = `loading it crashed (${childSignal})`;
|
||||
else reason = `it exited ${code} with no output`;
|
||||
resolve(bounded(stderr.trim() || reason));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
type RuntimeSelection =
|
||||
| { status: "ready"; path: string }
|
||||
| { status: "unavailable"; error: string };
|
||||
|
||||
async function cachedRuntime(
|
||||
dir: string,
|
||||
timeoutMs: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RuntimeSelection> {
|
||||
let path: string | null;
|
||||
try {
|
||||
path = installedPackagePath(dir, RUNTIME);
|
||||
} catch (err) {
|
||||
return {
|
||||
status: "unavailable",
|
||||
error: bounded(err instanceof Error ? err.message : String(err)),
|
||||
};
|
||||
}
|
||||
if (path === null)
|
||||
return { status: "unavailable", error: `${RUNTIME} is not installed in ${dir}` };
|
||||
const error = await runtimeLoadError(path, timeoutMs, signal);
|
||||
return error === null ? { status: "ready", path } : { status: "unavailable", error };
|
||||
}
|
||||
|
||||
async function selectRuntime(
|
||||
dir: string,
|
||||
cliUrl: string,
|
||||
timeoutMs = 60_000,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RuntimeSelection> {
|
||||
if (signal?.aborted) throw new DecodeCancelled("Parakeet runtime check cancelled");
|
||||
const copy = pinnedPackageBesideCli(RUNTIME, cliUrl);
|
||||
if (copy !== null) {
|
||||
const path = copy.path;
|
||||
const error = await runtimeLoadError(path, timeoutMs, signal);
|
||||
if (error === null) return { status: "ready", path };
|
||||
console.error(
|
||||
`Parakeet runtime at ${path.replace(/\s+/g, " ")} did not load: ${error.replace(/\s+/g, " ")}`,
|
||||
);
|
||||
}
|
||||
return cachedRuntime(dir, timeoutMs, signal);
|
||||
}
|
||||
|
||||
/** Returns the load failure after checking the pinned copy beside the CLI, then its cache. */
|
||||
export async function sherpaRuntimeLoadError(
|
||||
dir = SHERPA_RUNTIME_DIR,
|
||||
timeoutMs = 60_000,
|
||||
): string | null {
|
||||
if (!isInstalled(dir, RUNTIME)) return `${RUNTIME} is not installed in ${dir}`;
|
||||
const env = { ...process.env, HF_SHERPA_MANIFEST: join(dir, "package.json") };
|
||||
const probe = spawnSync(process.execPath, ["-e", LOAD_RUNTIME], {
|
||||
env,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
if (probe.status === 0) return null;
|
||||
// Ctrl-C reaches the probe too: a cancel, never proof that a working runtime is broken.
|
||||
if (stoppedByCancelSignal(probe)) throw new DecodeCancelled("Parakeet install cancelled");
|
||||
const timedOut = (probe.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
|
||||
return bounded(
|
||||
probe.stderr?.trim() ||
|
||||
(timedOut ? `loading it timed out after ${timeoutMs / 1000} s` : probe.error?.message) ||
|
||||
(probe.signal
|
||||
? `loading it crashed (${probe.signal})`
|
||||
: `it exited ${probe.status} with no output`),
|
||||
);
|
||||
cliUrl = import.meta.url,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string | null> {
|
||||
const selected = await selectRuntime(dir, cliUrl, timeoutMs, signal);
|
||||
return selected.status === "ready" ? null : selected.error;
|
||||
}
|
||||
|
||||
/** The native package, pinned too: the runtime's own optionalDependencies accept any 1.13.x. */
|
||||
@@ -158,33 +234,41 @@ export function sherpaPlatformPackage(platform = process.platform, arch = proces
|
||||
return `${nativePackageName(platform, arch)}@${RUNTIME_VERSION}`;
|
||||
}
|
||||
|
||||
/** Installs the runtime unless it already loads; true when it installed. */
|
||||
/** Installs if needed and returns the healthy entry selected for native child loading. */
|
||||
export async function installSherpaRuntime({
|
||||
run = runNpm,
|
||||
signal,
|
||||
dir = SHERPA_RUNTIME_DIR,
|
||||
}: { run?: typeof runNpm; signal?: AbortSignal; dir?: string } = {}): Promise<boolean> {
|
||||
if (sherpaRuntimeLoadError(dir) === null) return false;
|
||||
cliUrl = import.meta.url,
|
||||
}: {
|
||||
run?: typeof runNpm;
|
||||
signal?: AbortSignal;
|
||||
dir?: string;
|
||||
cliUrl?: string;
|
||||
} = {}): Promise<{ installed: boolean; runtimePath: string }> {
|
||||
const present = await selectRuntime(dir, cliUrl, 60_000, signal);
|
||||
if (present.status === "ready") return { installed: false, runtimePath: present.path };
|
||||
// install() keeps any dir holding the runtime manifest, so a broken one goes first.
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
const native = sherpaPlatformPackage();
|
||||
await install(dir, RUNTIME, RUNTIME_VERSION, (args) => run([...args, native], signal));
|
||||
const stillBroken = sherpaRuntimeLoadError(dir);
|
||||
if (stillBroken) {
|
||||
const selected = await cachedRuntime(dir, 60_000, signal);
|
||||
if (selected.status === "unavailable") {
|
||||
throw new Error(
|
||||
`The sherpa-onnx runtime was reinstalled but still does not load (${stillBroken}). Use --engine whisper for now.`,
|
||||
`The sherpa-onnx runtime was reinstalled but still does not load (${selected.error}). Use --engine whisper for now.`,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
return { installed: true, runtimePath: selected.path };
|
||||
}
|
||||
|
||||
/**
|
||||
* Model sizes only (install verified the hashes). The runtime counts once its manifest is there, even
|
||||
* if broken: transcribe must then fail with the repair, not quietly pick whisper.
|
||||
*/
|
||||
export function sherpaParakeetInstalled(): boolean {
|
||||
export function sherpaParakeetInstalled(cliUrl = import.meta.url): boolean {
|
||||
return (
|
||||
isInstalled(SHERPA_RUNTIME_DIR, RUNTIME) &&
|
||||
(pinnedPackageBesideCli(RUNTIME, cliUrl) !== null ||
|
||||
isInstalled(SHERPA_RUNTIME_DIR, RUNTIME)) &&
|
||||
PARAKEET_MODEL_FILES.every(
|
||||
(f) =>
|
||||
statSync(join(PARAKEET_MODEL_DIR, f.name), { throwIfNoEntry: false })?.size === f.bytes,
|
||||
@@ -306,11 +390,15 @@ function failureReason(err: ExecFileException | null, stderr: string): string {
|
||||
}
|
||||
|
||||
/** Decodes in a child process: onnxruntime aborts the whole process on some inputs, uncatchably. */
|
||||
function decode(wavPath: string, signal: AbortSignal): Promise<SherpaWindow[]> {
|
||||
function decode(
|
||||
wavPath: string,
|
||||
runtimePath: string,
|
||||
signal: AbortSignal,
|
||||
): Promise<SherpaWindow[]> {
|
||||
const sourceMode = import.meta.url.endsWith(".ts");
|
||||
const worker = new URL(sourceMode ? "./sherpaWorker.ts" : "./sherpaWorker.js", import.meta.url);
|
||||
const args = [...(sourceMode ? ["--import", "tsx"] : []), fileURLToPath(worker)];
|
||||
const input = { wavPath, runtimeDir: SHERPA_RUNTIME_DIR, config: recognizerConfig() };
|
||||
const input = { wavPath, runtimePath, config: recognizerConfig() };
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
@@ -356,6 +444,8 @@ export async function transcribeWithSherpa(
|
||||
options: {
|
||||
signal: AbortSignal;
|
||||
onProgress?: (message: string) => void;
|
||||
runtimeDir?: string;
|
||||
cliUrl?: string;
|
||||
onEvent?: (event: TranscribeProgress) => void;
|
||||
},
|
||||
): Promise<TranscribeResult> {
|
||||
@@ -366,7 +456,19 @@ export async function transcribeWithSherpa(
|
||||
model: PARAKEET_MODEL_LABEL,
|
||||
status: "started",
|
||||
});
|
||||
const windows = await decode(wavPath, options.signal);
|
||||
if (options.signal.aborted) throw new DecodeCancelled("Transcription cancelled");
|
||||
const selected = await selectRuntime(
|
||||
options.runtimeDir ?? SHERPA_RUNTIME_DIR,
|
||||
options.cliUrl ?? import.meta.url,
|
||||
60_000,
|
||||
options.signal,
|
||||
);
|
||||
if (selected.status === "unavailable") {
|
||||
throw new Error(
|
||||
`Parakeet runtime does not load (${selected.error}). Run hyperframes models install parakeet to repair it, or use --engine whisper.`,
|
||||
);
|
||||
}
|
||||
const windows = await decode(wavPath, selected.path, options.signal);
|
||||
const result = writeParakeetTranscript(dir, mergeWindowsToWords(windows));
|
||||
options.onEvent?.({
|
||||
type: "progress",
|
||||
|
||||
@@ -41,7 +41,11 @@ module.exports = {
|
||||
`;
|
||||
|
||||
function runWorker(runtimeDir: string, wavPath: string) {
|
||||
const input = JSON.stringify({ wavPath, runtimeDir, config: {} });
|
||||
const input = JSON.stringify({
|
||||
wavPath,
|
||||
runtimePath: join(runtimeDir, "node_modules", "sherpa-onnx-node", "index.js"),
|
||||
config: {},
|
||||
});
|
||||
return new Promise<{ code: number | null; stdout: string; stderr: string }>((resolve) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
@@ -102,7 +106,11 @@ describe("sherpaWorker", () => {
|
||||
"stops between windows once the CLI that spawned it is gone",
|
||||
async () => {
|
||||
const started = join(fakeRuntime(), "started");
|
||||
const input = JSON.stringify({ wavPath: "long.wav", runtimeDir: root, config: {} });
|
||||
const input = JSON.stringify({
|
||||
wavPath: "long.wav",
|
||||
runtimePath: join(root, "node_modules", "sherpa-onnx-node", "index.js"),
|
||||
config: {},
|
||||
});
|
||||
// The shell waits for the first window, then exits, orphaning a 10-window decode.
|
||||
const shell =
|
||||
`"${process.execPath}" --import tsx "${WORKER}" >/dev/null 2>&1 & echo $!; ` +
|
||||
@@ -136,6 +144,8 @@ describe("sherpaWorker", () => {
|
||||
root = mkdtempSync(join(tmpdir(), "hf-sherpa-worker-"));
|
||||
const { code, stderr } = await runWorker(root, "speech.wav");
|
||||
expect(code).toBe(1);
|
||||
expect(stderr).toContain(`${SHERPA_ERROR_PREFIX}sherpa-onnx-node is not installed in ${root}`);
|
||||
expect(stderr).toContain(
|
||||
`${SHERPA_ERROR_PREFIX}Cannot find module '${join(root, "node_modules", "sherpa-onnx-node", "index.js")}'`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { loadInstalled } from "../utils/optionalPackages.js";
|
||||
import { createRequire } from "node:module";
|
||||
import {
|
||||
droppedSpeechGaps,
|
||||
silenceCuts,
|
||||
@@ -22,7 +22,7 @@ interface SherpaOnnx {
|
||||
};
|
||||
}
|
||||
|
||||
const { wavPath, runtimeDir, config } = JSON.parse(process.env.HYPERFRAMES_PARAKEET_INPUT ?? "{}");
|
||||
const { wavPath, runtimePath, config } = JSON.parse(process.env.HYPERFRAMES_PARAKEET_INPUT ?? "{}");
|
||||
const parentPid = process.ppid;
|
||||
|
||||
/** Leading silence moves the frame grid; 0.5 s recovered the dropped clause at every length tried. */
|
||||
@@ -46,8 +46,7 @@ function decodeWindow(
|
||||
}
|
||||
|
||||
try {
|
||||
const sherpa = loadInstalled(runtimeDir, "sherpa-onnx-node") as SherpaOnnx | null;
|
||||
if (!sherpa) throw new Error(`sherpa-onnx-node is not installed in ${runtimeDir}`);
|
||||
const sherpa = createRequire(import.meta.url)(runtimePath) as SherpaOnnx;
|
||||
const recognizer = new sherpa.OfflineRecognizer(config);
|
||||
const wave = sherpa.readWave(wavPath);
|
||||
const cuts = silenceCuts(wave.samples, wave.sampleRate);
|
||||
|
||||
Reference in New Issue
Block a user