Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/Noko.qml

Raw
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io

Item {
    id: root

    property string status: "checking"
    property string lastError: ""
    property string lastChecked: ""
    property string lastTranscript: ""
    property string lastDuration: ""
    property string lastInsertError: ""
    property var historyEntries: []
    property bool ready: false
    property bool listening: false
    property bool transcribing: false
    property bool finished: false
    property bool installing: false
    property real installProgress: 0
    property string installLabel: ""
    property bool checkingUpdates: false
    property bool updateAvailable: false
    property bool updateUnknown: false
    property string updateChecked: ""
    property var updateReport: ({})
    property var assets: ({})
    property var doctor: ({})
    readonly property bool blocked: status === "blocked" || status === "error"
    readonly property bool active: blocked || listening || transcribing || installing
    readonly property bool strong: blocked || installing
    readonly property string label: blocked ? "noko!" : "noko"
    readonly property string role: blocked || installing ? "warning" : (listening || transcribing || finished ? "ui.accent" : "ui.focus")
    readonly property string overlayState: listening ? "capturing" : (transcribing ? "transcribing" : (finished ? "finished" : ""))
    readonly property string cliPath: Qt.resolvedUrl("noko.mjs").toString().replace(/^file:\/\//, "")
    readonly property string historyPath: Quickshell.statePath("noko-history.jsonl")
    readonly property string capturePath: Quickshell.cachePath("noko-capture.f32")

    function refresh(): string {
        if (!installing && !listening && !transcribing)
            status = "checking";
        assetsProc.exec(["node", cliPath, "assets-json"]);
        doctorProc.exec(["node", cliPath, "doctor"]);
        return stateJson();
    }

    function checkUpdates(): string {
        checkingUpdates = true;
        updateUnknown = false;
        updateProc.exec(["node", cliPath, "check-assets-json"]);
        return stateJson();
    }

    function install(): string {
        installing = true;
        listening = false;
        transcribing = false;
        installProgress = 0;
        installLabel = "starting";
        status = "installing";
        installProc.exec(["node", cliPath, "install-assets-progress"]);
        return stateJson();
    }

    function update(): string {
        installing = true;
        listening = false;
        transcribing = false;
        installProgress = 0;
        installLabel = "updating";
        status = "installing";
        installProc.exec(["node", cliPath, "update-assets-progress"]);
        return stateJson();
    }

    function trigger(): string {
        if (listening)
            return stopCapture();
        return startCapture();
    }

    function startCapture(): string {
        if (!ready)
            return refresh();
        installing = false;
        listening = true;
        transcribing = false;
        finished = false;
        lastError = "";
        status = "listening";
        captureProc.exec(["sh", "-c", "exec pw-record --raw --rate 16000 --channels 1 --format f32 - > \"$1\"", "noko-capture", capturePath]);
        return stateJson();
    }

    function stopCapture(): string {
        if (!listening)
            return stateJson();
        listening = false;
        transcribing = true;
        finished = false;
        status = "transcribing";
        captureProc.signal(2);
        return stateJson();
    }

    function transcribe(seconds): string {
        return trigger();
    }

    function beginListening(): string {
        listening = true;
        transcribing = false;
        finished = false;
        status = "listening";
        return stateJson();
    }

    function beginTranscribing(): string {
        listening = false;
        transcribing = true;
        finished = false;
        status = "transcribing";
        return stateJson();
    }

    function finish(): string {
        listening = false;
        transcribing = false;
        finished = false;
        installing = false;
        refresh();
        return stateJson();
    }

    function parseJson(text, fallback): var {
        try {
            return JSON.parse(String(text || "{}"));
        } catch (error) {
            lastError = fallback;
            status = "error";
            return null;
        }
    }

    function applyAssets(text): void {
        const report = parseJson(text, "assets-json");
        if (report)
            assets = report;
    }

    function applyUpdateCheck(text): void {
        checkingUpdates = false;
        const report = parseJson(text, "update-check-json");
        if (!report)
            return;
        updateReport = report;
        updateAvailable = !!report.updateAvailable;
        updateUnknown = report.unknown && report.unknown.length > 0;
        updateChecked = report.checkedAt || new Date().toISOString();
    }

    function applyDoctor(text): void {
        const report = parseJson(text, "doctor-json");
        if (!report)
            return;
        doctor = report;
        ready = !!report.ok;
        lastChecked = new Date().toISOString();
        if (ready) {
            lastError = "";
            if (!installing && !listening && !transcribing)
                status = "ready";
            if (!checkingUpdates && (!updateChecked || updateUnknown))
                checkUpdates();
            return;
        }
        const missing = [];
        const systemMissing = report.missingSystemDependencies || [];
        for (let i = 0; i < systemMissing.length; i++)
            missing.push(String(systemMissing[i]));
        if (!report.vad || !report.vad.readable)
            missing.push("vad");
        if (!report.parakeet || !report.parakeet.ok)
            missing.push("parakeet");
        if (!report.runtime || !report.runtime.onnxruntimeNode)
            missing.push("onnxruntime");
        if (report.parakeet && report.parakeet.needsSentencePiece && (!report.runtime || !report.runtime.sentencepieceJs))
            missing.push("sentencepiece");
        lastError = missing.join(",") || "not-ready";
        if (!installing && !listening && !transcribing)
            status = "blocked";
    }

    function applyInstallEvent(text): void {
        const event = parseJson(text, "install-progress");
        if (!event)
            return;
        if (event.label)
            installLabel = String(event.label);
        if (event.progress !== undefined)
            installProgress = Math.max(0, Math.min(1, Number(event.progress)));
        if (event.assets)
            assets = event.assets;
        if (event.event === "done") {
            installing = false;
            installProgress = 1;
            installLabel = "done";
            updateChecked = "";
            updateAvailable = false;
            updateUnknown = false;
            refresh();
        }
    }

    function loadHistoryText(text): void {
        const rows = [];
        const lines = String(text || "").trim().split("\n").filter(line => line.length > 0);
        for (let i = Math.max(0, lines.length - 20); i < lines.length; i++) {
            const row = parseJson(lines[i], "history-json");
            if (row && row.transcript)
                rows.unshift(row);
        }
        historyEntries = rows;
        if (rows.length > 0) {
            lastTranscript = String(rows[0].transcript || "");
            lastDuration = rows[0].duration !== undefined ? `${rows[0].duration}s` : "";
        }
    }

    function applyTranscribe(text): void {
        listening = false;
        transcribing = false;
        const report = parseJson(text, "transcribe-json");
        if (!report)
            return;
        lastTranscript = String(report.text || "");
        lastDuration = report.history && report.history.duration !== undefined ? `${report.history.duration}s` : "";
        lastInsertError = "";
        if (lastTranscript.length > 0)
            insertProc.exec(["wtype", "--", lastTranscript]);
        if (report.history)
            historyEntries = [report.history].concat(historyEntries).slice(0, 20);
        status = "ready";
        finished = true;
        finishedTimer.restart();
        refresh();
    }

    function fail(phase, text): void {
        installing = false;
        listening = false;
        transcribing = false;
        finished = false;
        if (phase !== "capture" && phase !== "transcribe")
            ready = false;
        lastError = String(text || phase).trim() || phase;
        status = "error";
    }

    function stateJson(): string {
        return JSON.stringify({
            "backend": "Noko.qml",
            "status": status,
            "ready": ready,
            "active": active,
            "strong": strong,
            "label": label,
            "role": role,
            "overlayState": overlayState,
            "lastError": lastError,
            "lastChecked": lastChecked,
            "lastTranscript": lastTranscript,
            "lastDuration": lastDuration,
            "lastInsertError": lastInsertError,
            "historyEntries": historyEntries,
            "capturePath": capturePath,
            "installing": installing,
            "installProgress": installProgress,
            "installLabel": installLabel,
            "checkingUpdates": checkingUpdates,
            "updateAvailable": updateAvailable,
            "updateUnknown": updateUnknown,
            "updateChecked": updateChecked,
            "historyPath": historyPath,
            "cliPath": cliPath,
            "assets": assets,
            "doctor": doctor
        });
    }

    Component.onCompleted: refresh()

    FileView {
        id: historyFile
        path: root.historyPath
        blockWrites: true
        printErrors: false
        onLoaded: root.loadHistoryText(historyFile.text())
        onLoadFailed: root.historyEntries = []
    }

    Timer {
        id: finishedTimer

        interval: 1400
        onTriggered: root.finished = false
    }

    Timer {
        interval: 60000
        repeat: true
        running: true
        onTriggered: root.refresh()
    }

    Process {
        id: assetsProc
        running: false
        stdout: StdioCollector {
            waitForEnd: true
            onStreamFinished: root.applyAssets(this.text)
        }
        stderr: StdioCollector {
            waitForEnd: true
            onStreamFinished: {
                if (this.text.length > 0)
                    root.fail("assets", this.text);
            }
        }
    }

    Process {
        id: doctorProc
        running: false
        stdout: StdioCollector {
            waitForEnd: true
            onStreamFinished: root.applyDoctor(this.text)
        }
        stderr: StdioCollector {
            waitForEnd: true
            onStreamFinished: {
                if (this.text.length > 0 && root.status === "checking")
                    root.fail("doctor", this.text);
            }
        }
    }

    Process {
        id: updateProc
        running: false
        stdout: StdioCollector {
            waitForEnd: true
            onStreamFinished: root.applyUpdateCheck(this.text)
        }
        stderr: StdioCollector {
            waitForEnd: true
            onStreamFinished: {
                if (this.text.length > 0) {
                    root.checkingUpdates = false;
                    root.updateUnknown = true;
                }
            }
        }
    }

    Process {
        id: installProc
        running: false
        stdout: SplitParser {
            onRead: data => root.applyInstallEvent(data)
        }
        stderr: StdioCollector {
            waitForEnd: true
            onStreamFinished: {
                if (this.text.length > 0)
                    root.fail("install", this.text);
            }
        }
    }

    Process {
        id: captureProc
        running: false
        onRunningChanged: {
            if (!running && root.status === "transcribing")
                transcribeProc.exec(["node", root.cliPath, "transcribe-raw-json", root.capturePath, root.historyPath]);
        }
        stderr: StdioCollector {
            waitForEnd: true
            onStreamFinished: {
                if (this.text.length > 0 && root.listening)
                    root.fail("capture", this.text);
            }
        }
    }

    Process {
        id: insertProc
        running: false
        stderr: StdioCollector {
            waitForEnd: true
            onStreamFinished: root.lastInsertError = String(this.text || "").trim()
        }
    }

    Process {
        id: transcribeProc
        running: false
        stdout: StdioCollector {
            waitForEnd: true
            onStreamFinished: root.applyTranscribe(this.text)
        }
        stderr: StdioCollector {
            waitForEnd: true
            onStreamFinished: {
                if (this.text.length > 0)
                    root.fail("transcribe", this.text);
            }
        }
    }
}