Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/noko.js

Raw
// 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
    };
}