const assert = require('assert'); const { execFileSync } = require('child_process'); const { mkdtempSync, readFileSync, writeFileSync } = require('fs'); const { join } = require('path'); const { tmpdir } = require('os'); const N = require('./noko.js'); // stereo averaging + clamp assert.deepStrictEqual(Array.from(N.interleavedToMonoFloat32([1, -1, 0.5, 0.25], 2)), [0, 0.375]); assert.deepStrictEqual(Array.from(N.interleavedToMonoFloat32([2, -2], 1)), [1, -1]); // pcm16le -> mono float32 const pcm = Buffer.from([0x00, 0x80, 0xff, 0x7f]); // -32768, 32767 const mono = N.pcm16leToMonoFloat32(pcm, 1); assert.strictEqual(mono[0], -1); assert.ok(Math.abs(mono[1] - 32767 / 32768) < 1e-8); assert.throws(() => N.pcm16leToMonoFloat32(Buffer.from([1]), 1), /even/); const f32 = Buffer.alloc(8); f32.writeFloatLE(-0.25, 0); f32.writeFloatLE(0.75, 4); assert.deepStrictEqual(Array.from(N.float32leToFloat32(f32, 1)), [-0.25, 0.75]); assert.throws(() => N.float32leToFloat32(Buffer.from([0, 1]), 1), /divisible by 4/); // resample 48 kHz -> 16 kHz reduces by 3x and preserves endpoints const ramp = new Float32Array([0, 1, 2, 3, 4, 5]); const down = N.resampleLinear(ramp, 48000, 16000); assert.strictEqual(down.length, 2); assert.strictEqual(down[0], 0); assert.strictEqual(down[1], 5); // exact 30 ms frames at 16 kHz const samples = new Float32Array(480 * 3 + 12).fill(0.25); const frames = N.splitFrames(samples, N.FRAME_SAMPLES); assert.strictEqual(frames.length, 3); assert.strictEqual(frames[0].length, 480); // VAD smoothing: 2 speech frames trigger pre-roll flush, then 1 hangover noise frame kept. const tinyFrames = [0, 1, 2, 3, 4].map(v => new Float32Array([v])); const gated = N.vadGate(tinyFrames, [0, 0.4, 0.5, 0, 0], { threshold: 0.3, preRoll: 1, onset: 2, hangover: 1 }); assert.deepStrictEqual(gated.frameIndexes, [0, 1, 2, 3]); assert.deepStrictEqual(Array.from(gated.samples), [0, 1, 2, 3]); // A second speech burst after hangover must preserve typed pre-roll entries. const twoBursts = N.vadGate([0, 1, 2, 3, 4, 5, 6, 7].map(v => new Float32Array([v])), [0, 0.4, 0.5, 0, 0, 0.4, 0.5, 0], { threshold: 0.3, preRoll: 1, onset: 2, hangover: 1 }); assert.deepStrictEqual(twoBursts.frameIndexes, [0, 1, 2, 3, 4, 5, 6, 7]); assert.deepStrictEqual(Array.from(twoBursts.samples), [0, 1, 2, 3, 4, 5, 6, 7]); // postprocess: trim, collapse spaces, remove junk tokens. assert.strictEqual(N.postprocess(' hello world ', ['']), 'hello world'); // model dir concept validation. assert.deepStrictEqual(N.validateParakeetModelFiles([ 'nemo128.onnx', 'encoder.int8.onnx', 'decoder_joint.int8.onnx', 'vocab.txt' ]), { ok: true, missing: [] }); assert.deepStrictEqual(N.validateParakeetModelFiles(['encoder.onnx']).missing, [ 'preprocessor', 'decoder-or-joint', 'vocabulary-or-tokenizer' ]); // End-to-end core flow with injected VAD and Parakeet engine. const input = new Float32Array(480 * 6).fill(0); for (let i = 480 * 2; i < 480 * 4; i++) input[i] = 0.6; const writes = []; const state = new N.EngineState(); const result = N.transcribePipeline({ samples: input, sampleRate: 16000, channels: 1 }, { probability: (_frame, index) => index >= 2 && index <= 3 ? 0.9 : 0.01 }, { transcribe: (speech, ctx) => { assert.strictEqual(ctx.sampleRate, 16000); assert.ok(speech.length > 0); return { text: ' hello noko ', meta: { model: 'fake-parakeet', quantization: 'int8', runtimeProvider: 'fake-onnx' } }; } }, { write: record => writes.push(record) }, { state: state, vad: { preRoll: 1, onset: 2, hangover: 1 } }); assert.strictEqual(result.text, 'hello noko'); assert.strictEqual(result.state, 'ready'); assert.strictEqual(state.state, 'ready'); assert.strictEqual(writes.length, 1); assert.strictEqual(writes[0].transcript, 'hello noko'); assert.strictEqual(writes[0].model, 'fake-parakeet'); assert.ok(writes[0].duration > 0); assert.throws(() => N.transcribePipeline({ samples: new Float32Array(480 * 2), sampleRate: 16000 }, { probability: () => 0 }, { transcribe: () => 'nope' }), /empty speech buffer after vad/); // CLI fixture proves the command path saves JSONL history after a full injected flow. const dir = mkdtempSync(join(tmpdir(), 'noko-')); const fixture = join(dir, 'fixture.json'); const history = join(dir, 'history.jsonl'); writeFileSync(fixture, JSON.stringify({ samples: Array.from(input), sampleRate: 16000, channels: 1, probabilities: [0, 0, 0.9, 0.9, 0, 0], transcript: ' cli noko ', options: { vad: { preRoll: 1, onset: 2, hangover: 1 } } })); const cliOut = JSON.parse(execFileSync('node', ['noko.mjs', 'transcribe-json', fixture, history], { cwd: __dirname, encoding: 'utf8' })); assert.strictEqual(cliOut.text, 'cli noko'); assert.strictEqual(JSON.parse(readFileSync(history, 'utf8').trim()).transcript, 'cli noko'); assert.throws(() => execFileSync('node', ['noko.mjs', 'transcribe-real-json', fixture, join(dir, 'real.jsonl')], { cwd: __dirname, encoding: 'utf8', stdio: 'pipe', env: { ...process.env, NOKO_DATA_DIR: join(dir, 'missing-data') } }), /vad model missing/); const customDataDir = join(dir, 'noko-data'); const assets = JSON.parse(execFileSync('node', ['noko.mjs', 'assets-json'], { cwd: __dirname, encoding: 'utf8', env: { ...process.env, NOKO_DATA_DIR: customDataDir } })); assert.strictEqual(assets.dataDir, customDataDir); assert.ok(assets.vad.path.startsWith(customDataDir)); assert.ok(assets.parakeet.path.startsWith(customDataDir)); assert.ok(assets.runtime.path.startsWith(customDataDir)); assert.equal(assets.runtime.package, 'onnxruntime-node@1.27.0'); assert.match(assets.parakeet.files.encoder.url, /^https:\/\/huggingface\.co\//); assert.doesNotMatch(JSON.stringify(assets), /com\.pais\.handy|Handy/); for (const file of [assets.vad, ...Object.values(assets.parakeet.files)]) { require('fs').mkdirSync(require('path').dirname(file.path), { recursive: true }); writeFileSync(file.path, 'x'); } require('fs').mkdirSync(assets.runtime.path, { recursive: true }); const progressLines = execFileSync('node', ['noko.mjs', 'install-assets-progress'], { cwd: __dirname, encoding: 'utf8', env: { ...process.env, NOKO_DATA_DIR: customDataDir } }).trim().split('\n').map(JSON.parse); assert.equal(progressLines.at(-1).event, 'done'); assert.equal(progressLines.at(-1).progress, 1); const deps = JSON.parse(execFileSync('node', ['noko.mjs', 'system-deps-json'], { cwd: __dirname, encoding: 'utf8' })); assert.ok(deps.dependencies.some(dep => dep.command === 'pw-record' && dep.required === true)); assert.ok(deps.dependencies.some(dep => dep.command === 'wtype' && dep.required === true)); assert.ok(Array.isArray(deps.missing)); assert.ok(deps.installCommand.length === 0 || deps.installCommand[0] === 'pkexec'); const doctor = JSON.parse(execFileSync('node', ['noko.mjs', 'doctor'], { cwd: __dirname, encoding: 'utf8', env: { ...process.env, NOKO_DATA_DIR: customDataDir } })); assert.strictEqual(typeof doctor.capture.pwRecord, 'boolean'); assert.ok(Array.isArray(doctor.systemDependencies)); assert.ok(doctor.systemDependencies.some(dep => dep.command === 'pw-record' && dep.required === true)); assert.ok(doctor.systemDependencies.some(dep => dep.command === 'wtype' && dep.required === true)); assert.strictEqual(typeof doctor.missingSystemDependencies.length, 'number'); assert.strictEqual(typeof doctor.ok, 'boolean'); assert.strictEqual(doctor.vad.path, assets.vad.path); assert.strictEqual(doctor.parakeet.path, assets.parakeet.path); assert.doesNotMatch(JSON.stringify(doctor), /com\.pais\.handy|Handy/); execFileSync('node', ['--input-type=module', '-e', ` import assert from 'node:assert/strict'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { createServer } from 'node:http'; import { mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; const dir = join(tmpdir(), 'noko-update-check-' + process.pid); const assetDir = join(dir, 'assets'); mkdirSync(join(assetDir, 'vad'), { recursive: true }); mkdirSync(join(assetDir, 'parakeet-tdt-0.6b-v3-int8'), { recursive: true }); writeFileSync(join(assetDir, 'vad', 'silero_vad.onnx'), 'old'); for (const name of ['config.json', 'nemo128.onnx', 'encoder-model.int8.onnx', 'decoder_joint-model.int8.onnx', 'vocab.txt']) writeFileSync(join(assetDir, 'parakeet-tdt-0.6b-v3-int8', name), name === 'encoder-model.int8.onnx' ? 'old' : 'same'); const payload = { '/silero_vad.onnx': 'old', '/config.json': 'same', '/nemo128.onnx': 'same', '/encoder-model.int8.onnx': 'newer-remote', '/decoder_joint-model.int8.onnx': 'same', '/vocab.txt': 'same' }; const server = createServer((req, res) => { const body = payload[req.url] || ''; res.setHeader('content-length', Buffer.byteLength(body)); res.setHeader('connection', 'close'); res.end(req.method === 'HEAD' ? '' : body); }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const base = 'http://127.0.0.1:' + server.address().port; const env = { ...process.env, NOKO_DATA_DIR: assetDir, NOKO_VAD_URL: base + '/silero_vad.onnx', NOKO_PARAKEET_REPO: base }; const run = promisify(execFile); const { stdout } = await run('node', ['noko.mjs', 'check-assets-json'], { cwd: process.cwd(), encoding: 'utf8', env }); const check = JSON.parse(stdout); assert.equal(check.updateAvailable, true); assert.equal(check.files.encoder.updateAvailable, true); assert.equal(check.files.vad.updateAvailable, false); server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); `], { cwd: __dirname, encoding: 'utf8' }); // Real VAD bridge shape test: the public API accepts 480-sample frames, then // prepares a Silero-compatible ONNX feed with context/state/sr when required. execFileSync('node', ['--input-type=module', '-e', ` import assert from 'node:assert/strict'; import { OnnxVad, vadRuntimeReport } from './noko-vad.mjs'; class Tensor { constructor(type, data, dims) { this.type = type; this.data = data; this.dims = dims; } } const calls = []; const session = { inputNames: ['input', 'state', 'sr'], outputNames: ['output', 'stateN'], async run(feeds) { calls.push(feeds); assert.deepEqual(feeds.input.dims, [1, 576]); assert.deepEqual(feeds.state.dims, [2, 1, 128]); assert.deepEqual(feeds.sr.dims, []); return { output: new Tensor('float32', new Float32Array([0.7]), [1, 1]), stateN: new Tensor('float32', new Float32Array(256).fill(0.1), [2, 1, 128]) }; } }; const vad = new OnnxVad({ Tensor }, session); assert.ok(Math.abs(await vad.probability(new Float32Array(480).fill(0.2)) - 0.7) < 1e-6); assert.equal(calls.length, 1); assert.throws(() => vad.feeds(new Float32Array(479)), /exactly 480/); const report = await vadRuntimeReport('./noko.test.js', { ort: { Tensor }, session }); assert.equal(report.loadable, true); assert.deepEqual(report.inputNames, ['input', 'state', 'sr']); assert.deepEqual(report.outputNames, ['output', 'stateN']); assert.equal(report.stateful, true); assert.equal(report.srInput, true); assert.ok(Math.abs(report.smokeProbability - 0.7) < 1e-6); `], { cwd: __dirname, encoding: 'utf8' }); execFileSync('node', ['--input-type=module', '-e', ` import assert from 'node:assert/strict'; import { mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { decodeTokens, discoverParakeetDir, ParakeetOnnx } from './noko-parakeet.mjs'; const dir = join(tmpdir(), 'noko-parakeet-' + process.pid); mkdirSync(join(dir, 'onnx_int8'), { recursive: true }); writeFileSync(join(dir, 'onnx_int8', '._encoder.int8.onnx'), ''); writeFileSync(join(dir, 'onnx_int8', 'nemo128.onnx'), ''); writeFileSync(join(dir, 'onnx_int8', 'encoder.int8.onnx'), ''); writeFileSync(join(dir, 'onnx_int8', 'decoder_joint.int8.onnx'), ''); writeFileSync(join(dir, 'onnx_int8', 'tokenizer.model'), ''); const found = discoverParakeetDir(dir); assert.equal(found.ok, true); assert.equal(found.files.preprocessor, 'onnx_int8/nemo128.onnx'); assert.equal(found.files.encoder, 'onnx_int8/encoder.int8.onnx'); assert.deepEqual(discoverParakeetDir(join(dir, 'missing')).missing, ['preprocessor', 'encoder', 'decoder-or-joint', 'vocabulary-or-tokenizer']); await assert.rejects(() => ParakeetOnnx.create(join(dir, 'missing'), { ort: {} }), /model dir incomplete/); assert.equal(decodeTokens({ decodeIds: ids => ids.join(',') }, [1, 2, 3]), '1,2,3'); assert.throws(() => decodeTokens(null, [1]), /tokenizer missing/); const runtime = new ParakeetOnnx({}, {}, found, { size: 1, blankIdx: 0, decodeIds: () => '' }); await assert.rejects(() => runtime.transcribe(new Float32Array()), /empty speech buffer/); runtime.busy = true; await assert.rejects(() => runtime.transcribe(new Float32Array([0])), /already running/); runtime.busy = false; assert.equal(runtime.state, 'ready'); class Tensor { constructor(type, data, dims) { this.type = type; this.data = data; this.dims = dims; } } const stateTensor = new Tensor('float32', new Float32Array(2 * 1 * 640), [2, 1, 640]); const fake = new ParakeetOnnx({ Tensor }, { preprocessor: { async run() { return { features: new Tensor('float32', new Float32Array([0]), [1, 1, 1]), features_lens: new Tensor('int64', BigInt64Array.from([1n]), [1]) }; } }, encoder: { async run() { return { outputs: new Tensor('float32', new Float32Array([0.1, 0.2]), [1, 2, 1]), encoded_lengths: new Tensor('int64', BigInt64Array.from([1n]), [1]) }; } }, decoderJoint: { inputMetadata: [ { name: 'input_states_1', shape: [2, 'batch', 640] }, { name: 'input_states_2', shape: [2, 'batch', 640] } ], async run() { return { outputs: new Tensor('float32', new Float32Array([0.9, 0.1, 0, 1]), [1, 1, 1, 4]), output_states_1: stateTensor, output_states_2: stateTensor }; } } }, { files: { encoder: 'encoder-model.int8.onnx' } }, { size: 2, blankIdx: 1, decodeIds: ids => ids.map(id => id === 0 ? 'hello' : '').join('') }); const fakeResult = await fake.transcribe(new Float32Array([0.4])); assert.equal(fakeResult.text, 'hello'); assert.equal(fakeResult.meta.runtimeProvider, 'onnxruntime-node'); `], { cwd: __dirname, encoding: 'utf8' }); console.log('noko.test.js: ALL PASS');