Files
Panama/config/dot/quickshell/modules/settings/SoundPage.qml
T
Gabriel Brown 7cd4131327 Hold a key, speak, and the words are typed
Super+D holds the microphone open, releasing it transcribes on the GPU and
types the result wherever the cursor is. Roughly 150ms for a normal utterance
once the model is resident, measured rather than hoped for.

Getting there meant discarding two approaches. Fedora 44 cannot install any
GPU-capable Whisper for Python -- openai-whisper needs a numba that needs an
llvmlite that does not exist for 3.14, and faster-whisper needs a ctranslate2
nobody packaged. The whisper-cpp package IS built with HIP but ships libraries
with no binary and no bindings, and hand-writing ctypes for a large by-value
struct is a segfault waiting for a version bump. So a container, as suggested.

Vulkan rather than ROCm, and upstream's image rather than one built here. ROCm
is seven gigabytes and serves AMD alone; Vulkan compute runs on the AMD, Intel
and NVIDIA machines this config is used on, in a twentieth of the space. The
Vulkan tag already contains whisper-server, so there is no Containerfile to keep
working -- an earlier draft of this commit had one, and it was strictly worse.

Two bugs found by using it rather than by reading it. Whisper describes silence
as the literal text "[BLANK_AUDIO]", and the first working version pasted that
string into the clipboard; a transcription that is nothing but such markers is
now discarded. And the server answers with a line per segment, which typed into
a window is an Enter press -- sending the half-written message, submitting the
form. Whitespace is collapsed to one line.

Neither the image nor the model is installed by ./install. Together they are
over two gigabytes that want the network, and Settings offers both as one
action instead. Nothing starts at login either: whisper-server holds the model
from the moment it starts, so the first press of the key is what brings it up.

The contract pins both text bugs, that the server stays on loopback, and that it
does not start at login. Reverting the [BLANK_AUDIO] guard did not fail it at
first -- the check was still correct, it had simply stopped being called -- so
it now checks the call site too.

Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
2026-08-21 12:22:19 -04:00

146 lines
4.7 KiB
QML

import QtQuick
import qs.config
import qs.services
SettingsPage {
title: "Sound"
lede: "Live PipeWire output, input, and device selection."
SettingsCard {
title: "Output"
subtitle: AudioDevices.current(true)?.description ?? "No output device"
SoundDeviceList {
width: parent.width
output: true
}
AudioBalance {
width: parent.width
node: AudioDevices.current(true)
}
}
SettingsCard {
title: "Input"
subtitle: AudioDevices.current(false)?.description ?? "No input device"
SoundDeviceList {
width: parent.width
output: false
}
}
// Beside Input on purpose: dictation listens through whichever device that
// card selects, and putting the two together is what makes that obvious.
SettingsCard {
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."
// Not a toggle: the keybind exists either way, and a switch would imply
// dictation can be turned off rather than simply not set up.
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"
value: Dictation.imageBuilt ? "Ready" : "Missing"
}
ActionRow {
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()
}
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"
}
TextRow {
visible: !Dictation.typingAvailable
label: "Typing"
detail: "wtype is missing, so dictated text would go to the clipboard instead of being typed."
value: "Missing"
}
TextRow {
visible: Dictation.lastError !== ""
label: "Problem"
detail: Dictation.lastError
value: ""
divider: false
}
}
SettingsCard {
title: "Applications"
subtitle: "Control each application currently playing through PipeWire."
ApplicationMixer {
width: parent.width
}
}
SettingsCard {
title: "Sound feedback"
subtitle: "Use the same event preferences as GTK and GNOME applications."
SettingRow {
label: "Event sounds"
detail: "Play alerts and interface event sounds"
controlWidth: 42
SettingsToggle {
anchors.fill: parent
checked: SoundFeedback.eventSounds
enabled: !SoundFeedback.busy
onToggled: checked => SoundFeedback.setEventSounds(checked)
}
}
SettingRow {
label: "Input feedback"
detail: "Play sounds for supported typing and input events"
controlWidth: 42
divider: false
SettingsToggle {
anchors.fill: parent
checked: SoundFeedback.inputFeedback
enabled: !SoundFeedback.busy
onToggled: checked => SoundFeedback.setInputFeedback(checked)
}
}
}
SettingsCard {
title: "Advanced sound"
ActionRow {
label: "Device profiles"
detail: "Open Fedora's complete device profile panel"
divider: false
action: "Open panel"
onTriggered: SystemSettings.openGnomePanel("sound")
}
}
}