#!/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}=`); } 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; });