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. |
|
| **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 `⚡`. |
|
| **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). |
|
| **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. |
|
| **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. |
|
| **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. |
|
| **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
|
- **Version** and a link to the source repository
|
||||||
(`github.com/mARTin-B78/blitztext-app-linux`).
|
(`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.
|
- 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]
|
## [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
|
## [1.4.0] - 2026-06-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -126,6 +126,8 @@ class Config:
|
|||||||
out: list[str] = []
|
out: list[str] = []
|
||||||
for w in self.workflows:
|
for w in self.workflows:
|
||||||
out.extend(w.keywords)
|
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
|
return out
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@ -92,6 +92,44 @@ def _labeled(parent: Gtk.Box, label: str, widget: Gtk.Widget, width: int = 130,
|
|||||||
return widget
|
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:
|
def _infobox(parent: Gtk.Box, text: str) -> Gtk.Box:
|
||||||
"""A plain-language help box at the top of a tab (read by screen readers)."""
|
"""A plain-language help box at the top of a tab (read by screen readers)."""
|
||||||
box = Gtk.Box(spacing=8)
|
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).")
|
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)"),
|
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.")
|
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)
|
self.gen_notify = Gtk.Switch(); self.gen_notify.set_active(self.cfg.notify)
|
||||||
_labeled(page, "Notifications", self.gen_notify)
|
_switch_row(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)
|
"Desktop pop-ups for recording, transcription, and errors (manual dictation).")
|
||||||
_labeled(page, "Announce matched preset", self.gen_notify_routing,
|
self.gen_notify_routing = Gtk.Switch(); self.gen_notify_routing.set_active(self.cfg.notify_routing)
|
||||||
tooltip="Pop a notification showing which preset (and spoken keyword) a voice command "
|
_switch_row(page, "Announce matched preset", self.gen_notify_routing,
|
||||||
"matched. Shown even for hands-free wakeword sessions, so you can always see "
|
"After a voice command, show which preset and keyword matched — even hands-free.")
|
||||||
"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 = Gtk.Switch(); self.gen_boot.set_active(autostart.is_enabled()); self.gen_boot.set_halign(Gtk.Align.START)
|
_switch_row(page, "Launch on login", self.gen_boot,
|
||||||
_labeled(page, "Launch on login", self.gen_boot)
|
"Start Blitztext automatically when you log in.")
|
||||||
self._start_meter()
|
self._start_meter()
|
||||||
|
|
||||||
def _selected_mic_name(self) -> str:
|
def _selected_mic_name(self) -> str:
|
||||||
@ -1071,6 +1109,10 @@ class SettingsDialog:
|
|||||||
license_label.set_selectable(True)
|
license_label.set_selectable(True)
|
||||||
page.pack_start(license_label, False, False, 2)
|
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 = Gtk.Notebook()
|
||||||
nb.set_margin_top(8)
|
nb.set_margin_top(8)
|
||||||
page.pack_start(nb, True, True, 0)
|
page.pack_start(nb, True, True, 0)
|
||||||
|
|||||||
@ -85,7 +85,13 @@ def route(
|
|||||||
|
|
||||||
best = None # (score, span_len, position, preset_name, keyword)
|
best = None # (score, span_len, position, preset_name, keyword)
|
||||||
for preset in presets:
|
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)
|
kw_tokens = normalize(keyword)
|
||||||
m = _match_window(tokens, kw_tokens, threshold)
|
m = _match_window(tokens, kw_tokens, threshold)
|
||||||
if m is None:
|
if m is None:
|
||||||
|
|||||||
@ -51,3 +51,23 @@ def test_strip_span():
|
|||||||
# 'nicer e-mail' normalizes to 3 tokens
|
# 'nicer e-mail' normalizes to 3 tokens
|
||||||
res = _strip_span("Nicer e-mail can you send me the report", 3, "start")
|
res = _strip_span("Nicer e-mail can you send me the report", 3, "start")
|
||||||
assert res == "can you send me the report"
|
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