Add clap detection sensitivity settings (Tools menu)

Threshold and min-interval (the librosa delta/wait knobs) were only
ever tunable via the CLI's -t/-m flags. Added a Clap Detection
Settings dialog (Tools menu) with both as spin boxes, an explicit
Reset to Defaults button, and Cancel that leaves MainWindow's current
settings untouched.

Exposed DEFAULT_THRESHOLD=0.7/DEFAULT_MIN_INTERVAL=0.3 as named
constants in clap_detection.py so the CLI script, DetectWorker's
defaults, and the dialog's reset button all reference the same source
of truth instead of three copies of the same magic numbers that could
drift apart.
This commit is contained in:
ThePendulum
2026-07-26 05:56:50 +02:00
parent 97cbf38d97
commit ff127a8c21
4 changed files with 186 additions and 10 deletions
+78 -3
View File
@@ -9,13 +9,65 @@ from PyQt6.QtGui import QShortcut, QKeySequence, QAction
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QDoubleSpinBox, QFileDialog,
QMessageBox, QPlainTextEdit, QGroupBox, QTabWidget,
QMessageBox, QPlainTextEdit, QGroupBox, QTabWidget, QDialog, QDialogButtonBox, QFormLayout,
)
from player_panel import PlayerPanel, format_timestamp
from sync_logic import compute_offset, SyncOffset
from ffmpeg_export import export_synced, FfmpegExportError, ExportResult
from clap_detection import detect_claps, ClapDetectionUnavailable, ClapEvent
from clap_detection import (
detect_claps, ClapDetectionUnavailable, ClapEvent,
DEFAULT_THRESHOLD, DEFAULT_MIN_INTERVAL,
)
class ClapSettingsDialog(QDialog):
"""Lets the threshold/min-interval knobs from clap_detection.py be
tuned from the GUI instead of only via the CLI's -t/-m flags. Keeps its
own pending values so Cancel doesn't touch the caller's settings."""
def __init__(self, threshold: float, min_interval: float, parent=None):
super().__init__(parent)
self.setWindowTitle("Clap Detection Settings")
self.threshold_spin = QDoubleSpinBox()
self.threshold_spin.setRange(0.1, 2.0)
self.threshold_spin.setSingleStep(0.05)
self.threshold_spin.setDecimals(2)
self.threshold_spin.setValue(threshold)
self.min_interval_spin = QDoubleSpinBox()
self.min_interval_spin.setRange(0.05, 5.0)
self.min_interval_spin.setSingleStep(0.05)
self.min_interval_spin.setDecimals(2)
self.min_interval_spin.setValue(min_interval)
form = QFormLayout()
form.addRow("Sensitivity threshold (higher = less sensitive):", self.threshold_spin)
form.addRow("Minimum seconds between claps:", self.min_interval_spin)
reset_button = QPushButton("Reset to Defaults")
reset_button.clicked.connect(self._reset_to_defaults)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
button_row = QHBoxLayout()
button_row.addWidget(reset_button)
button_row.addStretch(1)
button_row.addWidget(buttons)
layout = QVBoxLayout(self)
layout.addLayout(form)
layout.addLayout(button_row)
def _reset_to_defaults(self):
self.threshold_spin.setValue(DEFAULT_THRESHOLD)
self.min_interval_spin.setValue(DEFAULT_MIN_INTERVAL)
def values(self):
return self.threshold_spin.value(), self.min_interval_spin.value()
class ExportWorker(QThread):
@@ -45,12 +97,15 @@ class DetectWorker(QThread):
finished_ok = pyqtSignal(list) # List[ClapEvent]
finished_err = pyqtSignal(str)
def __init__(self, input_path, start_offset=0.0, max_results=None, audio_stream_index=None):
def __init__(self, input_path, start_offset=0.0, max_results=None, audio_stream_index=None,
threshold=DEFAULT_THRESHOLD, min_interval=DEFAULT_MIN_INTERVAL):
super().__init__()
self.input_path = input_path
self.start_offset = start_offset
self.max_results = max_results
self.audio_stream_index = audio_stream_index
self.threshold = threshold
self.min_interval = min_interval
def run(self):
try:
@@ -59,6 +114,8 @@ class DetectWorker(QThread):
start_offset=self.start_offset,
max_results=self.max_results,
audio_stream_index=self.audio_stream_index,
threshold=self.threshold,
min_interval=self.min_interval,
)
self.finished_ok.emit(events)
except (ClapDetectionUnavailable, RuntimeError) as e:
@@ -80,6 +137,8 @@ class MainWindow(QMainWindow):
self._preview_worker: Optional[ExportWorker] = None
self._preview_temp_path: Optional[str] = None # currently loaded in preview_panel
self._pending_preview_path: Optional[str] = None # being built by _preview_worker
self.clap_threshold: float = DEFAULT_THRESHOLD
self.clap_min_interval: float = DEFAULT_MIN_INTERVAL
# Two independent monitors (Kdenlive-style project/clip monitor
# tabs): each is a full player with its own timeline and transport,
@@ -202,6 +261,21 @@ class MainWindow(QMainWindow):
self.detect_next_action.triggered.connect(self._on_detect_next_clicked)
tools_menu.addAction(self.detect_next_action)
tools_menu.addSeparator()
settings_action = QAction("Clap Detection Settings…", self)
settings_action.triggered.connect(self._on_clap_settings_clicked)
tools_menu.addAction(settings_action)
def _on_clap_settings_clicked(self):
dialog = ClapSettingsDialog(self.clap_threshold, self.clap_min_interval, parent=self)
if dialog.exec() == QDialog.DialogCode.Accepted:
self.clap_threshold, self.clap_min_interval = dialog.values()
self.statusBar().showMessage(
f"Clap detection settings updated: threshold={self.clap_threshold:.2f}, "
f"min_interval={self.clap_min_interval:.2f}s", 4000,
)
def _wire_shortcuts(self):
# These act on whichever tab (Source/Preview) is currently active --
# each is a fully independent monitor with its own transport, so the
@@ -385,6 +459,7 @@ class MainWindow(QMainWindow):
self._detect_worker = DetectWorker(
self.input_path, start_offset=start_offset, max_results=max_results,
audio_stream_index=self._current_audio_stream_index(),
threshold=self.clap_threshold, min_interval=self.clap_min_interval,
)
self._detect_worker.finished_ok.connect(self._on_detect_done)
self._detect_worker.finished_err.connect(self._on_detect_failed)