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] ## [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 ## [2.03.21] - 2026-06-10
### Added ### 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. (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 " "TTS server (Kokoro, XTTS, OpenAI …) — set its URL, model and voices, then "
"Connect to test it.") "Connect to test it.")
# URL with a ⟳ reload that loads models + voices from the server (like the # ---- Controls pane (top half of the Paned) ----
# Engines tab). Set the saved URL after building the field. 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 = Gtk.Label(); self.tts_dot.set_markup(_dot(GREY))
self.tts_dot.set_tooltip_text("TTS server: green = reachable, red = unreachable") 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._tts_reload, dot=self.tts_dot)
self.wwb_url.set_text(self.cfg.tts_url) self.wwb_url.set_text(self.cfg.tts_url)
self.wwb_url.connect("focus-out-event", self._on_tts_url_leave) self.wwb_url.connect("focus-out-event", self._on_tts_url_leave)
self._probe_dot(self.tts_dot, self.cfg.tts_url, 80) self._probe_dot(self.tts_dot, self.cfg.tts_url, 80)
self.wwb_key = _labeled( 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 " 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.") "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 = _model_combo("press ⟳ to load, or type a model id")
self.wwb_model.set_text(self.cfg.tts_model) 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.") 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 = MultiPicker("press ⟳ to load voices, or type names")
self.wwb_voices.set_text(", ".join(self.cfg.tts_voices)) 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. " 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.") "⟳ 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 = Gtk.Box(spacing=10)
connect_box.pack_start(self.wwb_test_btn, False, False, 0) connect_box.pack_start(self.wwb_test_btn, False, False, 0)
connect_box.pack_start(self.wwb_test_lbl, 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 ---- # ---- 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") icon="audio-input-microphone-symbolic")
self._wwb_checks: dict[str, Gtk.CheckButton] = {} self._wwb_checks: dict[str, Gtk.CheckButton] = {}
eng_row = Gtk.Box(spacing=16) eng_row = Gtk.Box(spacing=16)
@ -2401,14 +2402,14 @@ notebook.bt-nb tab:checked label {
if not self.cfg.wakeword_engines: if not self.cfg.wakeword_engines:
eng_row.pack_start(Gtk.Label(label="(no wakeword engines configured — add one in the Input tab)"), eng_row.pack_start(Gtk.Label(label="(no wakeword engines configured — add one in the Input tab)"),
False, False, 0) False, False, 0)
page.pack_start(eng_row, False, False, 0) ctrl.pack_start(eng_row, False, False, 0)
# ---- Wakeword model selector ---- # ---- Wakeword model selector ----
all_ww_models = list(dict.fromkeys( all_ww_models = list(dict.fromkeys(
e.model for e in self.cfg.wakeword_engines if e.model)) 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 = _model_combo("auto — each engine's own model")
self.wwb_wakeword.set_models(all_ww_models) 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 " tooltip="Which wakeword model to test. Leave empty to use each engine's own "
"configured model. Select a specific model to override all engines " "configured model. Select a specific model to override all engines "
"and test that phrase (e.g. okay_computer on all servers).") "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) 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 = 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) 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 " tooltip="How many wakeword utterances to synthesize and test (filler-only "
"utterances for false-fire checking are added on top).") "utterances for false-fire checking are added on top).")
wrun = Gtk.Button(label="Run wakeword benchmark"); wrun.connect("clicked", self._run_wakeword_bench) wrun = Gtk.Button(label="Run wakeword benchmark"); wrun.connect("clicked", self._run_wakeword_bench)
wrun.set_halign(Gtk.Align.START) 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 # ---- Results pane (bottom half of the Paned) ----
# Col indices: 0 1 2 3 4 5 6 7
# col 8 = foreground colour (not displayed) # col 8 = foreground colour (not displayed)
self.wwb_store = Gtk.ListStore(str, str, str, str, str, str, str, str, str) self.wwb_store = Gtk.ListStore(str, str, str, str, str, str, str, str, str)
wwb_sort = Gtk.TreeModelSort(model=self.wwb_store) 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): if i in (3, 4, 5, 6, 7):
wwb_sort.set_sort_func(i, _num_cmp_wwb, i) 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 # Toolbar: Copy CSV + Save CSV
tb = Gtk.Box(spacing=6) 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 = Gtk.Button(label="Copy as CSV")
_copy_btn.set_tooltip_text("Copy results to clipboard as comma-separated values") _copy_btn.set_tooltip_text("Copy results to clipboard as comma-separated values")
_copy_btn.connect("clicked", self._wwbench_copy_csv) _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) _save_btn.connect("clicked", self._wwbench_save_csv)
tb.pack_start(_copy_btn, False, False, 0) tb.pack_start(_copy_btn, False, False, 0)
tb.pack_start(_save_btn, False, False, 0) tb.pack_start(_save_btn, False, False, 0)
page.pack_start(tb, False, False, 0)
ww_sw = Gtk.ScrolledWindow() results_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
ww_sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) results_box.pack_start(tb, False, False, 0)
ww_sw.set_min_content_height(150) results_box.pack_start(ww_sw, True, True, 0)
ww_sw.add(self.wwb_tree) results_box.pack_start(self.wwb_summary, False, False, 0)
page.pack_start(ww_sw, True, True, 2)
self.wwb_summary = Gtk.Label(xalign=0.0); self.wwb_summary.set_line_wrap(True) # ---- Paned: drag the divider to give more space to the table ----
self.wwb_summary.set_selectable(True) ww_paned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
page.pack_start(self.wwb_summary, False, False, 4) 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: def _run_bench(self, _b) -> None:
wav = self.bench_wav.get_filename() wav = self.bench_wav.get_filename()