// noko.js — pure Noko speech-to-text pipeline helpers. // NO I/O, NO native audio, NO ONNX imports. Receives samples + injected VAD/engine. // Dual-mode: node `require` (tests) + QML import. var TARGET_RATE = 16000; var FRAME_MS = 30; var FRAME_SAMPLES = 480; function finiteNumber(value, fallback) { var n = Number(value); return isFinite(n) ? n : fallback; } function assertPositiveInt(value, name) { var n = Number(value); if (!Number.isInteger(n) || n <= 0) throw new Error(name + " must be a positive integer"); return n; } function interleavedToMonoFloat32(samples, channels) { var ch = assertPositiveInt(channels || 1, "channels"); var input = samples || []; var frames = Math.floor(input.length / ch); var out = new Float32Array(frames); for (var i = 0; i < frames; i++) { var sum = 0; for (var c = 0; c < ch; c++) sum += finiteNumber(input[i * ch + c], 0); out[i] = Math.max(-1, Math.min(1, sum / ch)); } return out; } function pcm16leToMonoFloat32(buffer, channels) { var ch = assertPositiveInt(channels || 1, "channels"); var bytes = buffer || []; if (bytes.length % 2 !== 0) throw new Error("pcm16le byte length must be even"); var sampleCount = Math.floor(bytes.length / 2); var tmp = new Float32Array(sampleCount); for (var i = 0; i < sampleCount; i++) { var lo = bytes[i * 2]; var hi = bytes[i * 2 + 1]; var v = (hi << 8) | lo; if (v & 0x8000) v -= 0x10000; tmp[i] = Math.max(-1, Math.min(1, v / 32768)); } return interleavedToMonoFloat32(tmp, ch); } function float32leToFloat32(buffer, channels) { var ch = assertPositiveInt(channels || 1, "channels"); var bytes = buffer || []; if (bytes.length % 4 !== 0) throw new Error("float32le byte length must be divisible by 4"); var sampleCount = Math.floor(bytes.length / 4); var tmp = new Float32Array(sampleCount); for (var i = 0; i < sampleCount; i++) { var value = typeof bytes.readFloatLE === "function" ? bytes.readFloatLE(i * 4) : new DataView(bytes.buffer, bytes.byteOffset || 0, bytes.byteLength || bytes.length).getFloat32(i * 4, true); tmp[i] = Math.max(-1, Math.min(1, finiteNumber(value, 0))); } return interleavedToMonoFloat32(tmp, ch); } function resampleLinear(samples, fromRate, toRate) { var srcRate = assertPositiveInt(fromRate, "fromRate"); var dstRate = assertPositiveInt(toRate || TARGET_RATE, "toRate"); var input = samples || []; if (input.length === 0 || srcRate === dstRate) return new Float32Array(input); if (input.length === 1) return new Float32Array([finiteNumber(input[0], 0)]); var outLen = Math.max(1, Math.round(input.length * dstRate / srcRate)); var out = new Float32Array(outLen); var scale = (input.length - 1) / Math.max(1, outLen - 1); for (var i = 0; i < outLen; i++) { var pos = i * scale; var left = Math.floor(pos); var right = Math.min(input.length - 1, left + 1); var frac = pos - left; out[i] = finiteNumber(input[left], 0) * (1 - frac) + finiteNumber(input[right], 0) * frac; } return out; } function splitFrames(samples, frameSamples) { var size = assertPositiveInt(frameSamples || FRAME_SAMPLES, "frameSamples"); var input = samples || []; var count = Math.floor(input.length / size); var out = []; for (var i = 0; i < count; i++) out.push(new Float32Array(Array.prototype.slice.call(input, i * size, (i + 1) * size))); return out; } function concatFrames(frames) { var list = frames || []; var total = 0; for (var i = 0; i < list.length; i++) total += list[i].length; var out = new Float32Array(total); var offset = 0; for (var j = 0; j < list.length; j++) { out.set(list[j], offset); offset += list[j].length; } return out; } function vadGate(frames, probabilities, options) { var opts = options || {}; var threshold = finiteNumber(opts.threshold, 0.3); var preRollLimit = assertPositiveInt(opts.preRoll === undefined ? 15 : opts.preRoll + 1, "preRoll") - 1; var onsetLimit = assertPositiveInt(opts.onset || 2, "onset"); var hangoverLimit = assertPositiveInt(opts.hangover === undefined ? 15 : opts.hangover + 1, "hangover") - 1; var list = frames || []; var probs = probabilities || []; var out = []; var kept = []; var pre = []; var onset = []; var inSpeech = false; var hangover = 0; for (var i = 0; i < list.length; i++) { var speech = finiteNumber(probs[i], 0) >= threshold; if (inSpeech) { if (speech) { out.push(list[i]); kept.push(i); hangover = hangoverLimit; } else if (hangover > 0) { out.push(list[i]); kept.push(i); hangover--; } else { inSpeech = false; pre = [{ frame: list[i], index: i }]; onset = []; } continue; } if (speech) { onset.push({ frame: list[i], index: i }); if (onset.length >= onsetLimit) { for (var p = 0; p < pre.length; p++) out.push(pre[p].frame); for (var pi = 0; pi < pre.length; pi++) kept.push(pre[pi].index); for (var o = 0; o < onset.length; o++) out.push(onset[o].frame); for (var oi = 0; oi < onset.length; oi++) kept.push(onset[oi].index); inSpeech = true; hangover = hangoverLimit; pre = []; onset = []; } } else { onset = []; pre.push({ frame: list[i], index: i }); if (pre.length > preRollLimit) pre.shift(); } } return { samples: concatFrames(out), frameIndexes: kept, speechFrameCount: out.length }; } function postprocess(text, junkTokens) { var out = String(text || ""); var junk = junkTokens || []; for (var i = 0; i < junk.length; i++) out = out.split(String(junk[i])).join(" "); return out.replace(/\s+/g, " ").trim(); } function validateParakeetModelFiles(names) { var files = (names || []).map(function (n) { return String(n).toLowerCase(); }); function has(part) { for (var i = 0; i < files.length; i++) if (files[i].indexOf(part) >= 0) return true; return false; } var missing = []; if (!has("preprocessor") && !has("nemo")) missing.push("preprocessor"); if (!has("encoder")) missing.push("encoder"); if (!has("decoder") && !has("joint")) missing.push("decoder-or-joint"); if (!has("vocab") && !has("token") && !has("model_config")) missing.push("vocabulary-or-tokenizer"); return { ok: missing.length === 0, missing: missing }; } function EngineState() { this.state = "unloaded"; this.error = ""; } EngineState.prototype.set = function (next, error) { this.state = String(next); this.error = String(error || ""); return this.state; }; function historyRecord(transcript, speechSamples, meta) { var info = meta || {}; return { timestamp: info.timestamp || new Date().toISOString(), transcript: String(transcript || ""), sampleCount: speechSamples ? speechSamples.length : 0, duration: speechSamples ? Math.round(speechSamples.length / TARGET_RATE * 1000) / 1000 : 0, model: info.model || "parakeet", quantization: info.quantization || "unknown", runtimeProvider: info.runtimeProvider || "unknown", audioPath: info.audioPath || "", error: info.error || "" }; } function transcribePipeline(input, vad, engine, history, options) { var opts = options || {}; var state = opts.state || new EngineState(); if (!input || !input.samples) throw new Error("no input samples"); if (!vad || typeof vad.probability !== "function") throw new Error("vad probability function missing"); if (!engine || typeof engine.transcribe !== "function") throw new Error("parakeet engine missing"); state.set("transcribing"); try { var mono = interleavedToMonoFloat32(input.samples, input.channels || 1); var resampled = resampleLinear(mono, input.sampleRate || TARGET_RATE, TARGET_RATE); var frameList = splitFrames(resampled, FRAME_SAMPLES); var probabilities = []; for (var i = 0; i < frameList.length; i++) probabilities.push(Number(vad.probability(frameList[i], i)) || 0); var gated = vadGate(frameList, probabilities, opts.vad || {}); if (gated.samples.length === 0) throw new Error("empty speech buffer after vad"); var raw = engine.transcribe(gated.samples, { sampleRate: TARGET_RATE }); var text = postprocess(raw && raw.text !== undefined ? raw.text : raw, opts.junkTokens || []); if (!text) throw new Error("empty transcript"); var record = historyRecord(text, gated.samples, Object.assign({}, opts.history || {}, raw && raw.meta ? raw.meta : {})); if (history && typeof history.write === "function") history.write(record); state.set("ready"); return { text: text, sampleRate: TARGET_RATE, speechSamples: gated.samples, frameIndexes: gated.frameIndexes, history: record, state: state.state }; } catch (error) { state.set("failed", String(error && error.message ? error.message : error)); throw error; } } if (typeof module !== 'undefined' && module.exports) { module.exports = { TARGET_RATE: TARGET_RATE, FRAME_MS: FRAME_MS, FRAME_SAMPLES: FRAME_SAMPLES, interleavedToMonoFloat32: interleavedToMonoFloat32, pcm16leToMonoFloat32: pcm16leToMonoFloat32, float32leToFloat32: float32leToFloat32, resampleLinear: resampleLinear, splitFrames: splitFrames, concatFrames: concatFrames, vadGate: vadGate, postprocess: postprocess, validateParakeetModelFiles: validateParakeetModelFiles, EngineState: EngineState, historyRecord: historyRecord, transcribePipeline: transcribePipeline }; }