diff --git a/config/dot/quickshell/modules/settings/SoundPage.qml b/config/dot/quickshell/modules/settings/SoundPage.qml index 02c9cef..da3170d 100644 --- a/config/dot/quickshell/modules/settings/SoundPage.qml +++ b/config/dot/quickshell/modules/settings/SoundPage.qml @@ -37,41 +37,47 @@ SettingsPage { title: "Dictation" subtitle: Dictation.ready ? "Hold Super+D, speak, and release. The words are typed where the cursor is." - : "Speech to text, on the GPU. Two pieces have to be in place first — neither ships with Panama, because both are large and want the network." + : "Speech to text, on the GPU. Hold Super+D once it is set up. The one-time setup below fetches a speech server and a ~490 MB model — neither ships with Panama, because both are large and want the network." - // Not a toggle: the keybind exists either way, and a switch would imply - // dictation can be turned off rather than simply not set up. + // Status of the two pieces, read from the machine rather than guessed. TextRow { label: "Speech server" - detail: Dictation.imageBuilt - ? (Dictation.serverReady - ? "Built and running" - : "Built. Starts on the first dictation and stays loaded.") - : "Not built — run: panama app whisper-vulkan" + detail: Dictation.serverReady + ? "Running" + : (Dictation.imageBuilt ? "Ready — starts on the first dictation" : "Not set up yet") value: Dictation.imageBuilt ? "Ready" : "Missing" } - ActionRow { + TextRow { label: "Speech model" - detail: Dictation.downloading - ? (Dictation.downloadTotalBytes > 0 - ? Math.round(Dictation.downloadFraction * 100) + "% of " - + Math.round(Dictation.downloadTotalBytes / 1048576) + " MB" - : "Downloading…") - : (Dictation.modelInstalled - ? Math.round(Dictation.modelBytes / 1048576) + " MB, in place" - : "About 490 MB, downloaded once and kept") - action: Dictation.downloading ? "Downloading…" : "Download" - enabled: !Dictation.downloading && !Dictation.modelInstalled - visible: !Dictation.modelInstalled || Dictation.downloading - onTriggered: Dictation.download() + detail: Dictation.modelInstalled + ? "Kept across rebuilds of the server" + : "About 490 MB, downloaded once" + value: Dictation.modelInstalled ? Math.round(Dictation.modelBytes / 1048576) + " MB" : "Missing" } - TextRow { - visible: Dictation.modelInstalled && !Dictation.downloading - label: "Speech model" - detail: "Downloaded once and kept across rebuilds of the server." - value: Math.round(Dictation.modelBytes / 1048576) + " MB" + // ONE action that actually works: panama-dictate setup pulls the + // server image and downloads the model together. This card used to + // offer a Download button wired to a command the helper does not have, + // and told you to build a "panama app whisper-vulkan" that does not + // exist -- so nothing here did anything. It does now. + ActionRow { + visible: !Dictation.ready || Dictation.downloading + label: "Set up dictation" + detail: { + if (!Dictation.downloading) + return "Fetches the speech server and the model. Runs once, keeps both."; + if (Dictation.phase === "pulling") + return "Fetching the speech server…"; + if (Dictation.downloadTotalBytes > 0) + return "Downloading model — " + Math.round(Dictation.downloadFraction * 100) + + "% of " + Math.round(Dictation.downloadTotalBytes / 1048576) + " MB"; + return "Setting up…"; + } + action: Dictation.downloading ? "Working…" : "Set up" + enabled: !Dictation.downloading + onTriggered: Dictation.setup() + divider: !Dictation.typingAvailable || Dictation.lastError !== "" } TextRow { @@ -79,6 +85,7 @@ SettingsPage { label: "Typing" detail: "wtype is missing, so dictated text would go to the clipboard instead of being typed." value: "Missing" + divider: Dictation.lastError !== "" } TextRow { diff --git a/config/dot/quickshell/services/Dictation.qml b/config/dot/quickshell/services/Dictation.qml index 133e724..d792dbb 100644 --- a/config/dot/quickshell/services/Dictation.qml +++ b/config/dot/quickshell/services/Dictation.qml @@ -33,6 +33,9 @@ Singleton { property int modelBytes: 0 property bool downloading: false + // "pulling" while the server image comes down, "downloading" while the + // model does. Empty when idle. Lets the button say which half is running. + property string phase: "" property int downloadedBytes: 0 property int downloadTotalBytes: 0 property string lastError: "" @@ -76,7 +79,12 @@ Singleton { Process { id: downloadRun - command: [root.helper, "download"] + // "setup", not "download": the helper has no "download" command -- it + // has one "setup" that pulls the server image AND downloads the model, + // because being told to fetch half a gigabyte only to hear an image is + // also needed would be two trips. Calling the wrong name is why this + // button did nothing for so long. + command: [root.helper, "setup"] stdout: SplitParser { // One JSON object per line, roughly once a second, so the progress // bar moves without the helper flooding a pipe nobody drains. @@ -87,6 +95,8 @@ Singleton { root.lastError = "The model could not be downloaded."; return; } + if (update.state !== undefined) + root.phase = String(update.state); if (update.bytes !== undefined) root.downloadedBytes = update.bytes; if (update.total !== undefined) @@ -99,8 +109,9 @@ Singleton { } onExited: (exitCode, exitStatus) => { root.downloading = false; + root.phase = ""; if (exitCode !== 0 && root.lastError === "") - root.lastError = "The model could not be downloaded."; + root.lastError = "Dictation setup did not finish. Check that podman is installed and the network is up."; root.refresh(); } } @@ -110,10 +121,13 @@ Singleton { statusRun.running = true; } - function download(): void { + // Pulls the speech server image and downloads the model, with progress. + // One action because the helper's one "setup" command does both. + function setup(): void { if (downloadRun.running) return; root.lastError = ""; + root.phase = ""; root.downloadedBytes = 0; root.downloadTotalBytes = 0; root.downloading = true;