routing by preset name; right-aligned General switches; About copyright
- Spoken presets: route() now also matches a preset's name as an implicit keyword, so presets with no configured keywords (Nicer email, Calm down, Add emojis) are triggerable by voice — say the name. Explicit keywords still win; names are added to STT hotwords too. Tests added. - General tab: switches moved to the far right of each row with an inline grey description (new _switch_row helper) so each toggle is self-explanatory. - About tab: add "Copyright: 2026 mARTin Bierschenk - Design". - MANUAL + CHANGELOG updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8cfc006296
commit
d00064b6cd
@ -35,7 +35,7 @@ Use the dropdown at the top to pick a preset to edit, **+ Add** to create one, o
|
||||
| **Name** | `name` | Short name for the action, shown in the main panel. |
|
||||
| **Icon (emoji)** | `icon` | Emoji shown next to this preset in the "matched preset" notification — give each a distinct one to tell them apart at a glance. Default `⚡`. |
|
||||
| **Description** | `description` | One line explaining what the preset does (shown in the panel). |
|
||||
| **Keywords (comma)** | `keywords` | Spoken trigger words, comma-separated. Say one at the **start or end** of your speech to select this preset (fuzzy-matched, e.g. `nicer email, bessere email`). |
|
||||
| **Keywords (comma)** | `keywords` | Spoken trigger words, comma-separated. Say one at the **start or end** of your speech to select this preset (fuzzy-matched, e.g. `nicer email, bessere email`). The preset's **name is always an implicit trigger**, so it works by voice even with no keywords here; add keywords for alternate/foreign-language phrasings. |
|
||||
| **Hotkey (optional)** | `hotkey` | A direct keyboard shortcut for this preset. Click **Set** and press the combo, or type it (e.g. `<ctrl>+<alt>+e`). Leave blank for keyword-only. |
|
||||
| **Mode** | `mode` | `transcribe` types your words as-is · `rewrite` sends them to the language model first · `stream` shows live text from a realtime STT engine. |
|
||||
| **LLM model (opt.)** | `model` | Override the language model for *this preset only*. Blank = use the active LLM engine's model. |
|
||||
@ -212,7 +212,7 @@ Read-only information:
|
||||
|
||||
- **Version** and a link to the source repository
|
||||
(`github.com/mARTin-B78/blitztext-app-linux`).
|
||||
- **License: MIT**.
|
||||
- **License: MIT** · **Copyright: 2026 mARTin Bierschenk - Design**.
|
||||
- Sub-tabs with the full **Changelog** and **License** text.
|
||||
|
||||
---
|
||||
|
||||
@ -9,6 +9,15 @@ The version is defined in [`blitztext/__init__.py`](blitztext/__init__.py).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- **Presets are speakable by name**: voice routing now matches a preset's *name*
|
||||
as an implicit keyword, so a preset works by voice even with no keywords
|
||||
configured (e.g. just say "nicer email …"). Explicit keywords still take
|
||||
precedence, and preset names also bias the STT for better recognition.
|
||||
- **General settings switches** moved to the far right of each row, each with an
|
||||
inline description so it's clear what the toggle does without hovering.
|
||||
- **About**: added a "Copyright: 2026 mARTin Bierschenk - Design" line.
|
||||
|
||||
## [1.4.0] - 2026-06-07
|
||||
|
||||
### Added
|
||||
|
||||
@ -126,6 +126,8 @@ class Config:
|
||||
out: list[str] = []
|
||||
for w in self.workflows:
|
||||
out.extend(w.keywords)
|
||||
if w.name and w.name not in out:
|
||||
out.append(w.name) # names are implicit triggers — bias STT for them too
|
||||
return out
|
||||
|
||||
@property
|
||||
|
||||
@ -92,6 +92,44 @@ def _labeled(parent: Gtk.Box, label: str, widget: Gtk.Widget, width: int = 130,
|
||||
return widget
|
||||
|
||||
|
||||
def _switch_row(parent: Gtk.Box, label: str, switch: Gtk.Switch, description: str = "",
|
||||
width: int = 130) -> Gtk.Switch:
|
||||
"""A settings row: [label] [grey description ……………] [switch, far right].
|
||||
|
||||
The description tells the user what the switch does without hovering.
|
||||
"""
|
||||
row = Gtk.Box(spacing=10)
|
||||
row.set_margin_top(3); row.set_margin_bottom(3)
|
||||
|
||||
lbl = Gtk.Label(label=label, xalign=0.0); lbl.set_size_request(width, -1)
|
||||
row.pack_start(lbl, False, False, 0)
|
||||
|
||||
desc = Gtk.Label(label=description, xalign=0.0)
|
||||
desc.set_line_wrap(True)
|
||||
desc.get_style_context().add_class("dim-label")
|
||||
row.pack_start(desc, True, True, 0)
|
||||
|
||||
switch.set_halign(Gtk.Align.END)
|
||||
switch.set_valign(Gtk.Align.CENTER)
|
||||
row.pack_end(switch, False, False, 0)
|
||||
|
||||
# ATK accessibility + tooltips
|
||||
if hasattr(lbl, "set_mnemonic_widget"):
|
||||
lbl.set_mnemonic_widget(switch)
|
||||
atk = switch.get_accessible()
|
||||
if atk and label:
|
||||
atk.set_name(label.replace("_", ""))
|
||||
if description:
|
||||
atk.set_description(description)
|
||||
if description:
|
||||
switch.set_tooltip_text(description)
|
||||
lbl.set_tooltip_text(description)
|
||||
desc.set_tooltip_text(description)
|
||||
|
||||
parent.pack_start(row, False, False, 0)
|
||||
return switch
|
||||
|
||||
|
||||
def _infobox(parent: Gtk.Box, text: str) -> Gtk.Box:
|
||||
"""A plain-language help box at the top of a tab (read by screen readers)."""
|
||||
box = Gtk.Box(spacing=8)
|
||||
@ -860,15 +898,15 @@ class SettingsDialog:
|
||||
tooltip="‘type’ types the text key by key. ‘paste’ copies it and presses Ctrl+V (faster for long text).")
|
||||
self.gen_lang = _labeled(page, "Language hint", _entry(self.cfg.language, placeholder="de, en, … (blank = autodetect)"),
|
||||
tooltip="Spoken language code (de, en, …). Leave blank to auto-detect.")
|
||||
self.gen_notify = Gtk.Switch(); self.gen_notify.set_active(self.cfg.notify); self.gen_notify.set_halign(Gtk.Align.START)
|
||||
_labeled(page, "Notifications", self.gen_notify)
|
||||
self.gen_notify_routing = Gtk.Switch(); self.gen_notify_routing.set_active(self.cfg.notify_routing); self.gen_notify_routing.set_halign(Gtk.Align.START)
|
||||
_labeled(page, "Announce matched preset", self.gen_notify_routing,
|
||||
tooltip="Pop a notification showing which preset (and spoken keyword) a voice command "
|
||||
"matched. Shown even for hands-free wakeword sessions, so you can always see "
|
||||
"what you triggered. Each preset's emoji icon appears in the notification.")
|
||||
self.gen_boot = Gtk.Switch(); self.gen_boot.set_active(autostart.is_enabled()); self.gen_boot.set_halign(Gtk.Align.START)
|
||||
_labeled(page, "Launch on login", self.gen_boot)
|
||||
self.gen_notify = Gtk.Switch(); self.gen_notify.set_active(self.cfg.notify)
|
||||
_switch_row(page, "Notifications", self.gen_notify,
|
||||
"Desktop pop-ups for recording, transcription, and errors (manual dictation).")
|
||||
self.gen_notify_routing = Gtk.Switch(); self.gen_notify_routing.set_active(self.cfg.notify_routing)
|
||||
_switch_row(page, "Announce matched preset", self.gen_notify_routing,
|
||||
"After a voice command, show which preset and keyword matched — even hands-free.")
|
||||
self.gen_boot = Gtk.Switch(); self.gen_boot.set_active(autostart.is_enabled())
|
||||
_switch_row(page, "Launch on login", self.gen_boot,
|
||||
"Start Blitztext automatically when you log in.")
|
||||
self._start_meter()
|
||||
|
||||
def _selected_mic_name(self) -> str:
|
||||
@ -1071,6 +1109,10 @@ class SettingsDialog:
|
||||
license_label.set_selectable(True)
|
||||
page.pack_start(license_label, False, False, 2)
|
||||
|
||||
copyright_label = Gtk.Label(label="Copyright: 2026 mARTin Bierschenk - Design", xalign=0.0)
|
||||
copyright_label.set_selectable(True)
|
||||
page.pack_start(copyright_label, False, False, 2)
|
||||
|
||||
nb = Gtk.Notebook()
|
||||
nb.set_margin_top(8)
|
||||
page.pack_start(nb, True, True, 0)
|
||||
|
||||
@ -85,7 +85,13 @@ def route(
|
||||
|
||||
best = None # (score, span_len, position, preset_name, keyword)
|
||||
for preset in presets:
|
||||
for keyword in getattr(preset, "keywords", None) or []:
|
||||
# The preset's own name is always an implicit trigger, so a preset is
|
||||
# speakable by name even when it has no keywords configured.
|
||||
candidates = list(getattr(preset, "keywords", None) or [])
|
||||
name = getattr(preset, "name", "")
|
||||
if name:
|
||||
candidates.append(name)
|
||||
for keyword in candidates:
|
||||
kw_tokens = normalize(keyword)
|
||||
m = _match_window(tokens, kw_tokens, threshold)
|
||||
if m is None:
|
||||
|
||||
@ -51,3 +51,23 @@ def test_strip_span():
|
||||
# 'nicer e-mail' normalizes to 3 tokens
|
||||
res = _strip_span("Nicer e-mail can you send me the report", 3, "start")
|
||||
assert res == "can you send me the report"
|
||||
|
||||
|
||||
def test_route_matches_preset_name_without_keywords():
|
||||
"""A preset with no keywords is still speakable by its name."""
|
||||
presets = [
|
||||
DummyPreset("Transcribe", []),
|
||||
DummyPreset("Nicer email", []), # no keywords configured
|
||||
]
|
||||
res = route("nicer email please call me back tomorrow", presets)
|
||||
assert res.preset_name == "Nicer email"
|
||||
assert res.text == "please call me back tomorrow"
|
||||
|
||||
|
||||
def test_explicit_keyword_still_wins_over_name():
|
||||
presets = [
|
||||
DummyPreset("Add emojis", ["emoji"]),
|
||||
]
|
||||
res = route("emoji this is great", presets)
|
||||
assert res.preset_name == "Add emojis"
|
||||
assert res.text == "this is great"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user