feat: wakeword results table — sortable columns + CSV export (v2.03.21)
- All columns sortable by clicking header; numeric cols sort numerically - "Copy as CSV" → clipboard; "Save CSV…" → file chooser Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6f133771e7
commit
8b26bc3379
@ -9,6 +9,15 @@ The version is defined in [`blitztext/__init__.py`](blitztext/__init__.py).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.03.21] - 2026-06-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Wakeword results table: sortable columns.** Click any column header to sort
|
||||||
|
ascending/descending. Numeric columns (Detected, Total, Recall %, False fires,
|
||||||
|
Time) sort numerically.
|
||||||
|
- **Wakeword results table: CSV export.** "Copy as CSV" copies the table to the
|
||||||
|
clipboard; "Save CSV…" opens a file chooser to write a `.csv` file.
|
||||||
|
|
||||||
## [2.03.20] - 2026-06-10
|
## [2.03.20] - 2026-06-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -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.20"
|
__version__ = "2.03.21"
|
||||||
|
|||||||
@ -2427,9 +2427,21 @@ notebook.bt-nb tab:checked label {
|
|||||||
|
|
||||||
# Results table: Engine | Wakeword | Voice | Detected | Total | Recall% | False fires | Time
|
# Results table: Engine | Wakeword | Voice | Detected | Total | Recall% | False fires | Time
|
||||||
# Col indices: 0 1 2 3 4 5 6 7
|
# Col indices: 0 1 2 3 4 5 6 7
|
||||||
|
# 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)
|
||||||
ww_tree = Gtk.TreeView(model=self.wwb_store)
|
wwb_sort = Gtk.TreeModelSort(model=self.wwb_store)
|
||||||
ww_tree.set_grid_lines(Gtk.TreeViewGridLines.HORIZONTAL)
|
|
||||||
|
def _num_cmp_wwb(model, a, b, col):
|
||||||
|
def _f(v):
|
||||||
|
try:
|
||||||
|
return float(str(v).rstrip("%"))
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
return float("inf")
|
||||||
|
va, vb = _f(model.get_value(a, col)), _f(model.get_value(b, col))
|
||||||
|
return (va > vb) - (va < vb)
|
||||||
|
|
||||||
|
self.wwb_tree = Gtk.TreeView(model=wwb_sort)
|
||||||
|
self.wwb_tree.set_grid_lines(Gtk.TreeViewGridLines.HORIZONTAL)
|
||||||
for title, i, expand in [
|
for title, i, expand in [
|
||||||
("Engine", 0, False),
|
("Engine", 0, False),
|
||||||
("Wakeword", 1, False),
|
("Wakeword", 1, False),
|
||||||
@ -2443,12 +2455,29 @@ notebook.bt-nb tab:checked label {
|
|||||||
r.set_property("ellipsize", Pango.EllipsizeMode.END)
|
r.set_property("ellipsize", Pango.EllipsizeMode.END)
|
||||||
col = Gtk.TreeViewColumn(title, r, text=i, foreground=8)
|
col = Gtk.TreeViewColumn(title, r, text=i, foreground=8)
|
||||||
col.set_resizable(True); col.set_expand(expand)
|
col.set_resizable(True); col.set_expand(expand)
|
||||||
ww_tree.append_column(col)
|
col.set_sort_column_id(i)
|
||||||
|
self.wwb_tree.append_column(col)
|
||||||
|
if i in (3, 4, 5, 6, 7):
|
||||||
|
wwb_sort.set_sort_func(i, _num_cmp_wwb, i)
|
||||||
|
|
||||||
|
# Toolbar: Copy CSV + Save CSV
|
||||||
|
tb = Gtk.Box(spacing=6)
|
||||||
|
tb.set_margin_top(6)
|
||||||
|
_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)
|
||||||
|
_save_btn = Gtk.Button(label="Save CSV…")
|
||||||
|
_save_btn.set_tooltip_text("Save results to a .csv file")
|
||||||
|
_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 = Gtk.ScrolledWindow()
|
||||||
ww_sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
ww_sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||||
ww_sw.set_min_content_height(120)
|
ww_sw.set_min_content_height(150)
|
||||||
ww_sw.add(ww_tree)
|
ww_sw.add(self.wwb_tree)
|
||||||
page.pack_start(ww_sw, True, True, 4)
|
page.pack_start(ww_sw, True, True, 2)
|
||||||
|
|
||||||
self.wwb_summary = Gtk.Label(xalign=0.0); self.wwb_summary.set_line_wrap(True)
|
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_selectable(True)
|
||||||
@ -2748,6 +2777,50 @@ notebook.bt-nb tab:checked label {
|
|||||||
GLib.markup_escape_text(p) for p in summary_parts))
|
GLib.markup_escape_text(p) for p in summary_parts))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
_WWB_CSV_HEADERS = ["Engine", "Wakeword", "Voice", "Detected", "Total",
|
||||||
|
"Recall %", "False fires", "Time (s)"]
|
||||||
|
|
||||||
|
def _wwbench_csv_text(self) -> str:
|
||||||
|
import csv, io
|
||||||
|
out = io.StringIO()
|
||||||
|
w = csv.writer(out)
|
||||||
|
w.writerow(self._WWB_CSV_HEADERS)
|
||||||
|
store = self.wwb_store
|
||||||
|
it = store.get_iter_first()
|
||||||
|
while it:
|
||||||
|
w.writerow([store.get_value(it, c) for c in range(8)])
|
||||||
|
it = store.iter_next(it)
|
||||||
|
return out.getvalue()
|
||||||
|
|
||||||
|
def _wwbench_copy_csv(self, _b) -> None:
|
||||||
|
if not self.wwb_store.get_iter_first():
|
||||||
|
return
|
||||||
|
Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD).set_text(self._wwbench_csv_text(), -1)
|
||||||
|
|
||||||
|
def _wwbench_save_csv(self, _b) -> None:
|
||||||
|
if not self.wwb_store.get_iter_first():
|
||||||
|
return
|
||||||
|
dlg = Gtk.FileChooserDialog(
|
||||||
|
title="Save wakeword results as CSV",
|
||||||
|
transient_for=self.dlg,
|
||||||
|
action=Gtk.FileChooserAction.SAVE)
|
||||||
|
dlg.add_button("_Cancel", Gtk.ResponseType.CANCEL)
|
||||||
|
dlg.add_button("_Save", Gtk.ResponseType.ACCEPT)
|
||||||
|
dlg.set_do_overwrite_confirmation(True)
|
||||||
|
dlg.set_current_name("wakeword_benchmark.csv")
|
||||||
|
f = Gtk.FileFilter(); f.set_name("CSV files (*.csv)"); f.add_pattern("*.csv")
|
||||||
|
dlg.add_filter(f)
|
||||||
|
if dlg.run() == Gtk.ResponseType.ACCEPT:
|
||||||
|
path = dlg.get_filename()
|
||||||
|
if path and not path.endswith(".csv"):
|
||||||
|
path += ".csv"
|
||||||
|
try:
|
||||||
|
with open(path, "w", encoding="utf-8", newline="") as fh:
|
||||||
|
fh.write(self._wwbench_csv_text())
|
||||||
|
except OSError as exc:
|
||||||
|
self._error(f"Could not save: {exc}")
|
||||||
|
dlg.destroy()
|
||||||
|
|
||||||
# ===== Manual ===========================================================
|
# ===== Manual ===========================================================
|
||||||
def _build_manual(self, page: Gtk.Box) -> None:
|
def _build_manual(self, page: Gtk.Box) -> None:
|
||||||
text = _read_first(_app_paths()["manual"])
|
text = _read_first(_app_paths()["manual"])
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user