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