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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user