"""Wake-word sensitivity tuner. WAKE_WORD_THRESHOLD is otherwise a number you guess at in .env, restart, and then test by saying "thunderbolt" at your computer repeatedly. This window makes it evidence-based: a live peak-score readout while you talk, a rolling list of near misses (frames that scored just under the threshold — i.e. the times it *nearly* heard you), and a slider that takes effect immediately, mid-listen, without a restart. The controller owns the threshold; this window is a view over it. """ from __future__ import annotations import time from typing import Callable from PySide6.QtCore import Qt, QTimer from PySide6.QtWidgets import ( QDialog, QHBoxLayout, QLabel, QListWidget, QPushButton, QSlider, QVBoxLayout, ) _SLIDER_SCALE = 100 # QSlider is integer-only; threshold is 0.00-1.00 class WakeTunerWindow(QDialog): def __init__( self, get_threshold: Callable[[], float], set_threshold: Callable[[float], None], get_stats: Callable[[], dict], on_reset: Callable[[], None], ): super().__init__() self._get_threshold = get_threshold self._set_threshold = set_threshold self._get_stats = get_stats self._on_reset = on_reset self.setWindowTitle("Bolt — wake word tuning") self.resize(460, 380) self._threshold_label = QLabel() self._slider = QSlider(Qt.Horizontal) self._slider.setRange(5, 99) self._slider.setValue(int(round(get_threshold() * _SLIDER_SCALE))) self._slider.valueChanged.connect(self._threshold_changed) self._peak_label = QLabel("Peak score since reset: —") self._peak_label.setToolTip( 'Say "thunderbolt" a few times and watch this. Set the threshold ' "just below the peak you can hit reliably." ) self._misses = QListWidget() reset_button = QPushButton("Reset stats") reset_button.clicked.connect(self._reset) close_button = QPushButton("Close") close_button.clicked.connect(self.close) close_button.setDefault(True) buttons = QHBoxLayout() buttons.addWidget(reset_button) buttons.addStretch(1) buttons.addWidget(close_button) layout = QVBoxLayout(self) layout.addWidget(self._threshold_label) layout.addWidget(self._slider) layout.addWidget(self._peak_label) layout.addWidget(QLabel("Near misses (heard something, didn't quite fire):")) layout.addWidget(self._misses) layout.addLayout(buttons) # Polled rather than signal-driven: scores arrive ~12x/second on the # audio thread, and a queued signal per frame to repaint a label is # more traffic than this is worth. self._timer = QTimer(self) self._timer.timeout.connect(self.refresh) self._update_threshold_label() def _threshold_changed(self, value: int) -> None: self._set_threshold(value / _SLIDER_SCALE) self._update_threshold_label() def _update_threshold_label(self) -> None: threshold = self._slider.value() / _SLIDER_SCALE self._threshold_label.setText( f"Threshold: {threshold:.2f} (lower = more sensitive, more false triggers)" ) def _reset(self) -> None: self._on_reset() self.refresh() def refresh(self) -> None: stats = self._get_stats() or {} peak = stats.get("peak", 0.0) self._peak_label.setText(f"Peak score since reset: {peak:.3f}") self._misses.clear() for timestamp, score, threshold in reversed(stats.get("near_misses", [])): when = time.strftime("%H:%M:%S", time.localtime(timestamp)) self._misses.addItem(f"{when} scored {score:.3f} (threshold {threshold:.2f})") if self._misses.count() == 0: self._misses.addItem("Nothing yet — say the wake phrase a few times.") def show_refreshed(self) -> None: self._slider.setValue(int(round(self._get_threshold() * _SLIDER_SCALE))) self.refresh() self.show() self.raise_() self.activateWindow() self._timer.start(500) def closeEvent(self, event) -> None: self._timer.stop() super().closeEvent(event)