feat: wakeword benchmark — draggable split pane for results table (v2.03.22)

Controls (TTS config, engine checkboxes, run button) are in the top pane;
the results table is in the bottom pane. Drag the divider to see more rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-06-10 01:57:31 +02:00
parent 8b26bc3379
commit 012a069ad0
3 changed files with 42 additions and 25 deletions

View File

@ -9,6 +9,13 @@ The version is defined in [`blitztext/__init__.py`](blitztext/__init__.py).
## [Unreleased]
## [2.03.22] - 2026-06-10
### Changed
- **Wakeword benchmark uses a split pane.** The TTS config / engine selector
controls sit in the top pane; the results table sits in the bottom pane.
Drag the divider to give the table as much vertical space as needed.
## [2.03.21] - 2026-06-10
### Added

View File

@ -6,4 +6,4 @@ counterpart to the macOS Blitztext menu bar app: it runs natively on the host
(not in a container) so it can type into any application via xdotool.
"""
__version__ = "2.03.21"
__version__ = "2.03.22"

View File

@ -2355,26 +2355,27 @@ notebook.bt-nb tab:checked label {
"TTS server (Kokoro, XTTS, OpenAI …) — set its URL, model and voices, then "
"Connect to test it.")
# URL with a ⟳ reload that loads models + voices from the server (like the
# Engines tab). Set the saved URL after building the field.
# ---- Controls pane (top half of the Paned) ----
ctrl = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
# URL with a ⟳ reload that loads models + voices from the server
self.tts_dot = Gtk.Label(); self.tts_dot.set_markup(_dot(GREY))
self.tts_dot.set_tooltip_text("TTS server: green = reachable, red = unreachable")
self.wwb_url = _url_field(page, "TTS URL", "http://localhost:8880/v1",
self.wwb_url = _url_field(ctrl, "TTS URL", "http://localhost:8880/v1",
self._tts_reload, dot=self.tts_dot)
self.wwb_url.set_text(self.cfg.tts_url)
self.wwb_url.connect("focus-out-event", self._on_tts_url_leave)
self._probe_dot(self.tts_dot, self.cfg.tts_url, 80)
self.wwb_key = _labeled(
page, "API key env", _entry(self.cfg.tts_api_key_env, placeholder="(optional, e.g. OPENAI_API_KEY)"),
ctrl, "API key env", _entry(self.cfg.tts_api_key_env, placeholder="(optional, e.g. OPENAI_API_KEY)"),
tooltip="Name of an environment variable holding a bearer token. Leave empty for "
"no-auth local servers. The key itself is never stored in the config.")
self.wwb_model = _model_combo("press ⟳ to load, or type a model id")
self.wwb_model.set_text(self.cfg.tts_model)
_labeled(page, "TTS model", self.wwb_model,
_labeled(ctrl, "TTS model", self.wwb_model,
tooltip="The TTS model id your endpoint serves (from {url}/models). Required.")
self.wwb_voices = MultiPicker("press ⟳ to load voices, or type names")
self.wwb_voices.set_text(", ".join(self.cfg.tts_voices))
_labeled(page, "Voices (comma)", self.wwb_voices,
_labeled(ctrl, "Voices (comma)", self.wwb_voices,
tooltip="Voices to cycle through at random so the test covers different timbres. "
"⟳ fills these from the server; the ▾ dropdown appends one at a time.")
@ -2385,10 +2386,10 @@ notebook.bt-nb tab:checked label {
connect_box = Gtk.Box(spacing=10)
connect_box.pack_start(self.wwb_test_btn, False, False, 0)
connect_box.pack_start(self.wwb_test_lbl, False, False, 0)
_labeled(page, "", connect_box)
_labeled(ctrl, "", connect_box)
# ---- Wakeword engine checkboxes ----
_section_title(page, "Engines to test", margin_top=10,
_section_title(ctrl, "Engines to test", margin_top=10,
icon="audio-input-microphone-symbolic")
self._wwb_checks: dict[str, Gtk.CheckButton] = {}
eng_row = Gtk.Box(spacing=16)
@ -2401,14 +2402,14 @@ notebook.bt-nb tab:checked label {
if not self.cfg.wakeword_engines:
eng_row.pack_start(Gtk.Label(label="(no wakeword engines configured — add one in the Input tab)"),
False, False, 0)
page.pack_start(eng_row, False, False, 0)
ctrl.pack_start(eng_row, False, False, 0)
# ---- Wakeword model selector ----
all_ww_models = list(dict.fromkeys(
e.model for e in self.cfg.wakeword_engines if e.model))
self.wwb_wakeword = _model_combo("auto — each engine's own model")
self.wwb_wakeword.set_models(all_ww_models)
_labeled(page, "Wakeword", self.wwb_wakeword,
_labeled(ctrl, "Wakeword", self.wwb_wakeword,
tooltip="Which wakeword model to test. Leave empty to use each engine's own "
"configured model. Select a specific model to override all engines "
"and test that phrase (e.g. okay_computer on all servers).")
@ -2417,16 +2418,15 @@ notebook.bt-nb tab:checked label {
adj = Gtk.Adjustment(value=12, lower=1, upper=200, step_increment=1, page_increment=10)
self.wwb_count = Gtk.SpinButton(adjustment=adj, climb_rate=1, digits=0)
self.wwb_count.set_halign(Gtk.Align.START); self.wwb_count.set_size_request(90, -1)
_labeled(page, "Wakeword samples", self.wwb_count,
_labeled(ctrl, "Wakeword samples", self.wwb_count,
tooltip="How many wakeword utterances to synthesize and test (filler-only "
"utterances for false-fire checking are added on top).")
wrun = Gtk.Button(label="Run wakeword benchmark"); wrun.connect("clicked", self._run_wakeword_bench)
wrun.set_halign(Gtk.Align.START)
page.pack_start(wrun, False, False, 6)
ctrl.pack_start(wrun, False, False, 6)
# Results table: Engine | Wakeword | Voice | Detected | Total | Recall% | False fires | Time
# Col indices: 0 1 2 3 4 5 6 7
# ---- Results pane (bottom half of the Paned) ----
# col 8 = foreground colour (not displayed)
self.wwb_store = Gtk.ListStore(str, str, str, str, str, str, str, str, str)
wwb_sort = Gtk.TreeModelSort(model=self.wwb_store)
@ -2460,9 +2460,17 @@ notebook.bt-nb tab:checked label {
if i in (3, 4, 5, 6, 7):
wwb_sort.set_sort_func(i, _num_cmp_wwb, i)
ww_sw = Gtk.ScrolledWindow()
ww_sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
ww_sw.add(self.wwb_tree)
self.wwb_summary = Gtk.Label(xalign=0.0); self.wwb_summary.set_line_wrap(True)
self.wwb_summary.set_selectable(True)
self.wwb_summary.set_margin_top(4); self.wwb_summary.set_margin_bottom(2)
# Toolbar: Copy CSV + Save CSV
tb = Gtk.Box(spacing=6)
tb.set_margin_top(6)
tb.set_margin_bottom(4)
_copy_btn = Gtk.Button(label="Copy as CSV")
_copy_btn.set_tooltip_text("Copy results to clipboard as comma-separated values")
_copy_btn.connect("clicked", self._wwbench_copy_csv)
@ -2471,17 +2479,19 @@ notebook.bt-nb tab:checked label {
_save_btn.connect("clicked", self._wwbench_save_csv)
tb.pack_start(_copy_btn, False, False, 0)
tb.pack_start(_save_btn, False, False, 0)
page.pack_start(tb, False, False, 0)
ww_sw = Gtk.ScrolledWindow()
ww_sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
ww_sw.set_min_content_height(150)
ww_sw.add(self.wwb_tree)
page.pack_start(ww_sw, True, True, 2)
results_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
results_box.pack_start(tb, False, False, 0)
results_box.pack_start(ww_sw, True, True, 0)
results_box.pack_start(self.wwb_summary, False, False, 0)
self.wwb_summary = Gtk.Label(xalign=0.0); self.wwb_summary.set_line_wrap(True)
self.wwb_summary.set_selectable(True)
page.pack_start(self.wwb_summary, False, False, 4)
# ---- Paned: drag the divider to give more space to the table ----
ww_paned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
ww_paned.pack1(ctrl, resize=False, shrink=False)
ww_paned.pack2(results_box, resize=True, shrink=False)
ww_paned.set_position(340)
ww_paned.set_size_request(-1, 260)
page.pack_start(ww_paned, True, True, 4)
def _run_bench(self, _b) -> None:
wav = self.bench_wav.get_filename()