repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/Noko.qml
Rawpragma 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);
}
}
}
}