feat: add scan UI controls and start_scan handler

Add Scan button, threshold spinner, mode combobox, and reference source
combobox to the settings row. Implement handler methods for starting scans,
handling results/errors, cleanup of workers, and reference folder selection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 08:57:56 +02:00
parent fd42791c9f
commit afda9b2d9f
+104
View File
@@ -1559,6 +1559,29 @@ class MainWindow(QMainWindow):
lambda v: self._settings.setValue("track_subject", "true" if v else "false") lambda v: self._settings.setValue("track_subject", "true" if v else "false")
) )
# ── audio scan controls ──────────────────────────────────────
self._btn_scan = QPushButton("Scan")
self._btn_scan.setToolTip("Scan current video for audio segments matching reference clips")
self._btn_scan.clicked.connect(self._start_scan)
self._sld_threshold = QDoubleSpinBox()
self._sld_threshold.setRange(0.0, 1.0)
self._sld_threshold.setSingleStep(0.05)
self._sld_threshold.setValue(0.7)
self._sld_threshold.setPrefix("Thr: ")
self._sld_threshold.setToolTip("Similarity threshold (0=match everything, 1=exact match)")
self._cmb_scan_mode = QComboBox()
self._cmb_scan_mode.addItems(["Average", "Nearest"])
self._cmb_scan_mode.setToolTip("Average: compare to mean profile\nNearest: compare to closest clip")
self._cmb_scan_ref = QComboBox()
self._cmb_scan_ref.addItems(["Current Profile", "Custom Folder"])
self._cmb_scan_ref.currentIndexChanged.connect(self._on_scan_ref_changed)
self._scan_folder: str = ""
self._scan_worker: ScanWorker | None = None
cpu_count = os.cpu_count() or 2 cpu_count = os.cpu_count() or 2
self._spn_workers = QSpinBox() self._spn_workers = QSpinBox()
self._spn_workers.setRange(1, cpu_count) self._spn_workers.setRange(1, cpu_count)
@@ -1691,6 +1714,10 @@ class MainWindow(QMainWindow):
settings_row.addWidget(self._chk_rand_portrait) settings_row.addWidget(self._chk_rand_portrait)
settings_row.addWidget(self._chk_rand_square) settings_row.addWidget(self._chk_rand_square)
settings_row.addWidget(self._chk_track) settings_row.addWidget(self._chk_track)
settings_row.addWidget(self._btn_scan)
settings_row.addWidget(self._sld_threshold)
settings_row.addWidget(self._cmb_scan_mode)
settings_row.addWidget(self._cmb_scan_ref)
settings_row.addStretch() settings_row.addStretch()
self._lbl_status = QLabel() self._lbl_status = QLabel()
self._lbl_status.setStyleSheet("color: #888; font-size: 11px;") self._lbl_status.setStyleSheet("color: #888; font-size: 11px;")
@@ -2468,6 +2495,83 @@ class MainWindow(QMainWindow):
return return
self._step_cursor(markers[0][0] - self._cursor) # wrap to first self._step_cursor(markers[0][0] - self._cursor) # wrap to first
def _on_scan_ref_changed(self, index: int) -> None:
if index == 1: # Custom Folder
folder = QFileDialog.getExistingDirectory(self, "Select reference clip folder")
if folder:
self._scan_folder = folder
else:
self._cmb_scan_ref.setCurrentIndex(0)
def _cleanup_scan_worker(self) -> None:
"""Disconnect signals and schedule deletion of old scan worker."""
if self._scan_worker is not None:
try:
self._scan_worker.finished.disconnect()
self._scan_worker.error.disconnect()
self._scan_worker.progress.disconnect()
except TypeError:
pass # already disconnected
self._scan_worker.deleteLater()
self._scan_worker = None
def _start_scan(self) -> None:
if not self._file_path:
self._show_status("No video loaded")
return
if self._scan_worker and self._scan_worker.isRunning():
self._show_status("Scan already running")
return
# Clean up previous worker
self._cleanup_scan_worker()
# Collect reference clip paths
if self._cmb_scan_ref.currentIndex() == 0:
# Current profile — all exports across all files in this profile
clip_paths = [p for p in self._db.get_all_export_paths(self._profile)
if os.path.exists(p)]
else:
# Custom folder
if not self._scan_folder:
self._show_status("No reference folder selected")
return
exts = (".mp4", ".mkv", ".avi", ".mov", ".wav", ".mp3", ".flac")
clip_paths = [
os.path.join(self._scan_folder, f)
for f in sorted(os.listdir(self._scan_folder))
if f.lower().endswith(exts)
]
if not clip_paths:
self._show_status("No reference clips found")
return
mode = self._cmb_scan_mode.currentText().lower()
threshold = self._sld_threshold.value()
self._btn_scan.setEnabled(False)
self._scan_file_path = self._file_path # remember which file we're scanning
self._show_status(f"Scanning with {len(clip_paths)} reference clips...")
self._scan_worker = ScanWorker(self._file_path, clip_paths, mode, threshold)
self._scan_worker.finished.connect(self._on_scan_done)
self._scan_worker.error.connect(self._on_scan_error)
self._scan_worker.progress.connect(self._show_status)
self._scan_worker.start()
def _on_scan_done(self, regions: list) -> None:
self._btn_scan.setEnabled(True)
# Ignore stale results if the user switched files during scan
if self._file_path != getattr(self, '_scan_file_path', None):
return
self._timeline.set_scan_regions(regions)
self._show_status(f"Scan complete: {len(regions)} matching regions")
def _on_scan_error(self, msg: str) -> None:
self._btn_scan.setEnabled(True)
self._show_status(f"Scan error: {msg}")
# --- Export --- # --- Export ---
def _pick_folder(self): def _pick_folder(self):