settings: Speech page — STT/TTS endpoints with live health probe
speech.stt/speech.tts land in Config so the settings page, the souveraine-stt CLI, and the keyboard mic key all read one source of truth. STT defaults to the VPN faster-whisper bridge; the page probes /health so configured and answering are visibly different states. TTS is a field waiting for a server.
This commit is contained in:
parent
ad5be0a661
commit
402e887dfc
4 changed files with 159 additions and 0 deletions
|
|
@ -59,6 +59,7 @@ modules/settings/DockConfig.qml souveraine/modules/settings/DockConfig.qml
|
|||
modules/settings/NavigationConfig.qml souveraine/modules/settings/NavigationConfig.qml
|
||||
modules/settings/KeyboardConfig.qml souveraine/modules/settings/KeyboardConfig.qml
|
||||
modules/settings/IdleConfig.qml souveraine/modules/settings/IdleConfig.qml
|
||||
modules/settings/SpeechConfig.qml souveraine/modules/settings/SpeechConfig.qml
|
||||
modules/ii/polkit/Polkit.qml souveraine/modules/ii/polkit/Polkit.qml
|
||||
modules/ii/dock/Dock.qml souveraine/modules/ii/dock/Dock.qml
|
||||
modules/ii/dock/DockManifest.qml souveraine/modules/ii/dock/DockManifest.qml
|
||||
|
|
|
|||
|
|
@ -536,6 +536,24 @@ Singleton {
|
|||
property bool pinnedOnStartup: false
|
||||
}
|
||||
|
||||
// Speech services — network STT/TTS on Casey's VPN. The shell
|
||||
// and the souveraine-stt/tts CLIs read the same config file, so
|
||||
// the settings page here is the single source of truth for both.
|
||||
property JsonObject speech: JsonObject {
|
||||
property JsonObject stt: JsonObject {
|
||||
property bool enable: true
|
||||
// faster-whisper REST bridge: POST multipart 'audio'
|
||||
// (WAV) -> {"text": ...}. /health for the status probe.
|
||||
property string endpoint: "http://10.10.20.19:7862/transcribe"
|
||||
}
|
||||
property JsonObject tts: JsonObject {
|
||||
property bool enable: false
|
||||
// Speech-synthesis endpoint: POST {"text": ...} -> audio
|
||||
// stream. Empty until a TTS server exists on the VPN.
|
||||
property string endpoint: ""
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject overlay: JsonObject {
|
||||
property bool openingZoomAnimation: true
|
||||
property bool darkenScreen: true
|
||||
|
|
|
|||
135
surfaces/quickshell/modules/settings/SpeechConfig.qml
Normal file
135
surfaces/quickshell/modules/settings/SpeechConfig.qml
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
// Speech services — dictation (STT) and speech synthesis (TTS).
|
||||
// The endpoints live in Config so the souveraine-stt CLI (and the
|
||||
// keyboard's mic key that shells out to it) read exactly what's set
|
||||
// here. The probe hits the STT /health route so "it's configured" and
|
||||
// "it's answering" are visibly different states.
|
||||
|
||||
ContentPage {
|
||||
forceWidth: true
|
||||
|
||||
ContentSection {
|
||||
icon: "mic"
|
||||
title: Translation.tr("Dictation (speech to text)")
|
||||
|
||||
ConfigSwitch {
|
||||
buttonIcon: "record_voice_over"
|
||||
text: Translation.tr("Enable dictation")
|
||||
checked: Config.options.speech.stt.enable
|
||||
onCheckedChanged: {
|
||||
Config.options.speech.stt.enable = checked;
|
||||
}
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Voice input through the transcription server. The keyboard's mic key and the souveraine-stt command both use this.")
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTextField {
|
||||
Layout.fillWidth: true
|
||||
text: Config.options.speech.stt.endpoint
|
||||
placeholderText: Translation.tr("Transcription endpoint (http://host:port/transcribe)")
|
||||
onEditingFinished: {
|
||||
if (text !== Config.options.speech.stt.endpoint) {
|
||||
Config.options.speech.stt.endpoint = text;
|
||||
healthProbe.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 8
|
||||
|
||||
StyledText {
|
||||
text: healthProbe.statusText
|
||||
color: healthProbe.healthy ? Appearance.colors.colOnLayer1
|
||||
: Appearance.colors.colSubtext
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
}
|
||||
|
||||
RippleButton {
|
||||
implicitWidth: 32
|
||||
implicitHeight: 32
|
||||
buttonRadius: Appearance.rounding.full
|
||||
onClicked: healthProbe.refresh()
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "refresh"
|
||||
iconSize: 18
|
||||
}
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Check the server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentSection {
|
||||
icon: "text_to_speech"
|
||||
title: Translation.tr("Speech synthesis (text to speech)")
|
||||
|
||||
ConfigSwitch {
|
||||
buttonIcon: "campaign"
|
||||
text: Translation.tr("Enable speech output")
|
||||
checked: Config.options.speech.tts.enable
|
||||
onCheckedChanged: {
|
||||
Config.options.speech.tts.enable = checked;
|
||||
}
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Spoken responses through the synthesis server. Off until a TTS endpoint exists.")
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTextField {
|
||||
Layout.fillWidth: true
|
||||
text: Config.options.speech.tts.endpoint
|
||||
placeholderText: Translation.tr("Synthesis endpoint (empty = none yet)")
|
||||
onEditingFinished: {
|
||||
if (text !== Config.options.speech.tts.endpoint)
|
||||
Config.options.speech.tts.endpoint = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STT /health probe. Derives the health URL from the transcribe
|
||||
// endpoint (…/transcribe -> …/health) rather than storing a second URL.
|
||||
QtObject {
|
||||
id: healthProbe
|
||||
property bool healthy: false
|
||||
property string statusText: Translation.tr("Checking server…")
|
||||
|
||||
function refresh() {
|
||||
statusText = Translation.tr("Checking server…");
|
||||
healthy = false;
|
||||
probeProc.running = false;
|
||||
probeProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: probeProc
|
||||
running: true
|
||||
command: ["curl", "-s", "--max-time", "5",
|
||||
Config.options.speech.stt.endpoint.replace(/\/[^\/]*$/, "/health")]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const h = JSON.parse(text);
|
||||
healthProbe.healthy = h.status === "ok";
|
||||
healthProbe.statusText = healthProbe.healthy
|
||||
? Translation.tr("Server up · %1 on %2").arg(h.model ?? "?").arg(h.device ?? "?")
|
||||
: Translation.tr("Server answered but not ok");
|
||||
} catch (e) {
|
||||
healthProbe.healthy = false;
|
||||
healthProbe.statusText = Translation.tr("Server unreachable");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -68,6 +68,11 @@ ApplicationWindow {
|
|||
icon: "keyboard",
|
||||
component: "modules/settings/KeyboardConfig.qml"
|
||||
},
|
||||
{
|
||||
name: Translation.tr("Speech"),
|
||||
icon: "mic",
|
||||
component: "modules/settings/SpeechConfig.qml"
|
||||
},
|
||||
{
|
||||
name: Translation.tr("About"),
|
||||
icon: "info",
|
||||
|
|
|
|||
Loading…
Reference in a new issue