amical/packages/whisper-wrapper/scripts/test-addon.js

160 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
// Quick smoke-test runner for the whisper.cpp Node addon build.
//
// Usage:
// node scripts/test-addon.js [--model /path/to/model.bin] [--audio /path/to/audio.wav]
//
// If no flags are provided the script will grab the first *.bin model from
// "~/Library/Application Support/amical/models" and the bundled jfk sample.
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
function resolveBinding() {
const nativeRoot = path.resolve(__dirname, "..", "native");
const { platform, arch } = process;
const candidates = [
`${platform}-${arch}-metal`,
`${platform}-${arch}-openblas`,
`${platform}-${arch}-cuda`,
`${platform}-${arch}`,
"cpu-fallback",
];
for (const dir of candidates) {
const bindingPath = path.join(nativeRoot, dir, "whisper.node");
if (fs.existsSync(bindingPath)) {
return bindingPath;
}
}
throw new Error(
`Unable to locate a whisper.node binary for ${platform}-${arch}. ` +
`Expected one of: ${candidates.join(", ")}`,
);
}
function defaultModelPath() {
const modelsDir = path.join(
os.homedir(),
"Library",
"Application Support",
"amical",
"models",
);
if (!fs.existsSync(modelsDir)) {
throw new Error(
`Model directory not found at ${modelsDir}. Pass --model to override.`,
);
}
const candidates = fs
.readdirSync(modelsDir)
.filter((f) => f.toLowerCase().endsWith(".bin"))
.map((name) => {
const fullPath = path.join(modelsDir, name);
const stats = fs.statSync(fullPath);
return { name, fullPath, size: stats.size };
})
.sort((a, b) => - a.size + b.size);
if (candidates.length === 0) {
throw new Error(
`No .bin model files found in ${modelsDir}. Pass --model to override.`,
);
}
return candidates[0].fullPath;
}
function defaultAudioPath() {
const audio = path.resolve(
__dirname,
"..",
"whisper.cpp",
"samples",
"jfk.wav",
);
if (!fs.existsSync(audio)) {
throw new Error(
`Sample audio not found at ${audio}. Pass --audio to override.`,
);
}
return audio;
}
function parseArgs() {
const args = process.argv.slice(2);
const options = {};
for (const arg of args) {
if (!arg.startsWith("--")) continue;
const [key, value] = arg.slice(2).split("=");
if (!value) {
throw new Error(`Flag '${arg}' must be provided as --${key}=<value>`);
}
options[key] = value;
}
return options;
}
async function main() {
const opts = parseArgs();
const modelPath = path.resolve(opts.model || defaultModelPath());
const audioPath = path.resolve(opts.audio || defaultAudioPath());
if (!fs.existsSync(modelPath)) {
throw new Error(`Model file not found at ${modelPath}`);
}
if (!fs.existsSync(audioPath)) {
throw new Error(`Audio file not found at ${audioPath}`);
}
const bindingPath = resolveBinding();
console.log(`> Using addon: ${bindingPath}`);
console.log(`> Using model: ${modelPath}`);
console.log(`> Using audio: ${audioPath}`);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const binding = require(bindingPath);
if (typeof binding.init !== "function" ||
typeof binding.full !== "function" ||
typeof binding.free !== "function") {
throw new Error(`Addon at ${bindingPath} does not expose init/full/free APIs.`);
}
const handle = binding.init({ model: modelPath, gpu: true });
try {
const segments = binding.full(handle, {
fname_inp: audioPath,
language: "en",
no_timestamps: false,
suppress_blank: true,
suppress_non_speech_tokens: true,
});
console.log("Transcription segments:\n");
for (const segment of segments) {
const from = typeof segment.from === "number" ? segment.from : "?";
const to = typeof segment.to === "number" ? segment.to : "?";
console.log(` [${from} -> ${to}] ${segment.text}`);
}
console.log("\nDone.");
} finally {
binding.free(handle);
}
}
main().catch((err) => {
console.error("Test run failed:", err);
process.exitCode = 1;
});