Upload
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user