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); } } } }