Add wakeword-specific audio cues (detected / captured)

Two optional WAVs in the Hands-free section: "Sound: detected" plays when the
wakeword fires (your cue to speak now) and "Sound: captured" plays when the
command is taken (silence/stop). For hands-free sessions these take precedence
over the general [sounds] cues, falling back to them and then to the built-in
system sound. The general cues are relabelled "Audio cues (manual dictation)".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-06-06 14:34:49 +02:00
parent 87eee3f103
commit 8e1419e828
3 changed files with 27 additions and 4 deletions

View File

@ -80,6 +80,8 @@ class Config:
wakeword_enabled: bool = False wakeword_enabled: bool = False
wakeword_uri: str = "tcp://127.0.0.1:10400" wakeword_uri: str = "tcp://127.0.0.1:10400"
wakeword_model: str = "okay_computer" wakeword_model: str = "okay_computer"
wakeword_sound_detected: str = "" # WAV played when the wakeword fires (speak now)
wakeword_sound_done: str = "" # WAV played when the command is captured
# workflows # workflows
workflows: list[Workflow] = field(default_factory=list) workflows: list[Workflow] = field(default_factory=list)
@ -172,6 +174,8 @@ def load(path: Path = CONFIG_PATH) -> Config:
wakeword_enabled=bool(ww.get("enabled", False)), wakeword_enabled=bool(ww.get("enabled", False)),
wakeword_uri=ww.get("uri", "tcp://127.0.0.1:10400"), wakeword_uri=ww.get("uri", "tcp://127.0.0.1:10400"),
wakeword_model=ww.get("model", "okay_computer"), wakeword_model=ww.get("model", "okay_computer"),
wakeword_sound_detected=ww.get("sound_detected", ""),
wakeword_sound_done=ww.get("sound_done", ""),
) )
for entry in data.get("workflow", []): for entry in data.get("workflow", []):
@ -279,6 +283,8 @@ def save(cfg: Config, path: Path = CONFIG_PATH) -> None:
"enabled": cfg.wakeword_enabled, "enabled": cfg.wakeword_enabled,
"uri": cfg.wakeword_uri, "uri": cfg.wakeword_uri,
"model": cfg.wakeword_model, "model": cfg.wakeword_model,
"sound_detected": cfg.wakeword_sound_detected,
"sound_done": cfg.wakeword_sound_done,
}, },
"stt": {"active": cfg.stt_active}, "stt": {"active": cfg.stt_active},
"stt_engine": [ "stt_engine": [
@ -398,6 +404,11 @@ threshold = 0.82 # 0..1 fuzzy-match strictness (higher = stricter)
enabled = false enabled = false
uri = "tcp://127.0.0.1:10400" uri = "tcp://127.0.0.1:10400"
model = "okay_computer" model = "okay_computer"
# Optional WAV cues for hands-free use (override the [sounds] cues when the
# wakeword triggers): played when the wakeword fires (speak now) and when the
# command is captured.
sound_detected = ""
sound_done = ""
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Speech-to-text engines (presets). The active one is used for transcription. # Speech-to-text engines (presets). The active one is used for transcription.

View File

@ -147,12 +147,15 @@ class Daemon:
sound.play(fallback=sound_name) sound.play(fallback=sound_name)
def _play_cue(self, cue: str) -> None: def _play_cue(self, cue: str) -> None:
"""Play the user's WAV for 'before'/'after', else a built-in system sound.""" """Play an audio cue. Hands-free (wakeword) sessions prefer the wakeword
sounds, then the general [sounds] cues, then a built-in system sound."""
from . import sound from . import sound
if cue == "before": if cue == "before":
sound.play(self.cfg.sound_before, fallback="device-added") custom = (self.cfg.wakeword_sound_detected if self._session_silent else "") or self.cfg.sound_before
sound.play(custom, fallback="device-added")
else: else:
sound.play(self.cfg.sound_after, fallback="complete") custom = (self.cfg.wakeword_sound_done if self._session_silent else "") or self.cfg.sound_after
sound.play(custom, fallback="complete")
# -- recording control ---------------------------------------------------- # -- recording control ----------------------------------------------------
def start_dictation(self, workflow: Workflow | None = None) -> None: def start_dictation(self, workflow: Workflow | None = None) -> None:

View File

@ -765,8 +765,15 @@ class SettingsDialog:
box = Gtk.Box(spacing=10); box.pack_start(self.ww_test_btn, False, False, 0); box.pack_start(self.ww_test_lbl, False, False, 0) box = Gtk.Box(spacing=10); box.pack_start(self.ww_test_btn, False, False, 0); box.pack_start(self.ww_test_lbl, False, False, 0)
_labeled(page, "", box) _labeled(page, "", box)
self.ww_snd_detected = self._sound_field(
page, "Sound: detected", self.cfg.wakeword_sound_detected,
"Played right after the wakeword is recognised — your cue that Blitztext is listening, speak now.")
self.ww_snd_done = self._sound_field(
page, "Sound: captured", self.cfg.wakeword_sound_done,
"Played when your command is captured (silence or stop) — confirms your input was taken.")
page.pack_start(Gtk.Separator(), False, False, 8) page.pack_start(Gtk.Separator(), False, False, 8)
page.pack_start(Gtk.Label(label="Audio cues", xalign=0.0), False, False, 2) page.pack_start(Gtk.Label(label="Audio cues (manual dictation)", xalign=0.0), False, False, 2)
self.snd_before = self._sound_field( self.snd_before = self._sound_field(
page, "Play before", self.cfg.sound_before, page, "Play before", self.cfg.sound_before,
"Sound played when recording starts — your confirmation that Blitztext is listening.") "Sound played when recording starts — your confirmation that Blitztext is listening.")
@ -1118,6 +1125,8 @@ class SettingsDialog:
c.wakeword_enabled = self.ww_enabled.get_active() c.wakeword_enabled = self.ww_enabled.get_active()
c.wakeword_uri = self.ww_uri.get_text().strip() c.wakeword_uri = self.ww_uri.get_text().strip()
c.wakeword_model = _combo_text(self.ww_model) c.wakeword_model = _combo_text(self.ww_model)
c.wakeword_sound_detected = self.ww_snd_detected.get_filename() or ""
c.wakeword_sound_done = self.ww_snd_done.get_filename() or ""
c.mic = self._selected_mic_name() c.mic = self._selected_mic_name()
c.output = self.gen_output.get_active_text() or "type" c.output = self.gen_output.get_active_text() or "type"
c.language = self.gen_lang.get_text().strip() c.language = self.gen_lang.get_text().strip()