feat: scan export queuing and threshold default 0.50 in UI
Queue scan exports back-to-back: when an export is running, new batches are queued and drain automatically on completion. Each batch snapshots its state (file path, jobs, settings) so the user can switch videos while exports run. Also updates ScanWorker default and slider initial value to 0.50 to match the core threshold change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -195,7 +195,7 @@ class ScanWorker(QThread):
|
||||
progress = pyqtSignal(str) # status message
|
||||
|
||||
def __init__(self, video_path: str, model: dict,
|
||||
threshold: float = 0.30,
|
||||
threshold: float = 0.50,
|
||||
prefetched_audio=None):
|
||||
super().__init__()
|
||||
self._video_path = video_path
|
||||
@@ -2694,6 +2694,7 @@ class MainWindow(QMainWindow):
|
||||
self._cursor: float = 0.0
|
||||
self._export_counter: int = 1
|
||||
self._export_worker: ExportWorker | None = None
|
||||
self._export_queue: list[dict] = []
|
||||
self._last_export_path: str = ""
|
||||
self._overwrite_path: str = "" # set when a marker is selected for re-export
|
||||
self._overwrite_group: list[str] = [] # all output_paths in the selected group
|
||||
@@ -2959,7 +2960,7 @@ class MainWindow(QMainWindow):
|
||||
self._sld_threshold.setDecimals(2)
|
||||
self._sld_threshold.setRange(0.0, 1.0)
|
||||
self._sld_threshold.setSingleStep(0.01)
|
||||
self._sld_threshold.setValue(0.30)
|
||||
self._sld_threshold.setValue(0.50)
|
||||
self._sld_threshold.setPrefix("Thr: ")
|
||||
self._sld_threshold.setToolTip("Similarity threshold (0=match everything, 1=exact match)")
|
||||
|
||||
@@ -4491,9 +4492,6 @@ class MainWindow(QMainWindow):
|
||||
if not self._file_path:
|
||||
self._show_status("No video loaded")
|
||||
return
|
||||
if self._export_worker and self._export_worker.isRunning():
|
||||
self._show_status("Export already running…")
|
||||
return
|
||||
if self._scan_worker and self._scan_worker.isRunning():
|
||||
self._show_status("Scan already running")
|
||||
return
|
||||
@@ -4614,38 +4612,70 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# Clips go flat inside vid folder, numbered by video
|
||||
jobs = []
|
||||
self._auto_export_positions = []
|
||||
positions = []
|
||||
for area_idx, group in enumerate(groups):
|
||||
group_name = f"{name}_{vid_num:03d}_a{area_idx + 1}"
|
||||
for sub, start_t in enumerate(group):
|
||||
fname = f"{group_name}_{sub}{ext}"
|
||||
out = os.path.join(vid_folder, fname)
|
||||
jobs.append((start_t, out, None, 0.5))
|
||||
self._auto_export_positions.append((start_t, out))
|
||||
|
||||
self._show_status(f"Auto: exporting {len(jobs)} clips...")
|
||||
positions.append((start_t, out))
|
||||
|
||||
short_side = self._spn_resize.value() or None
|
||||
self._export_short_side = short_side
|
||||
self._export_portrait = "Off"
|
||||
self._export_crop_center = 0.5
|
||||
self._export_format = fmt
|
||||
self._export_clip_count = 1
|
||||
self._export_spread = spread
|
||||
self._export_folder = folder
|
||||
self._export_folder_suffix = ""
|
||||
self._export_profile = self._profile
|
||||
|
||||
hw_on = self._chk_hw.isChecked() and self._hw_encoders
|
||||
encoder = self._hw_encoders[0] if hw_on else "libx264"
|
||||
max_workers = min(self._spn_workers.value(), 3) if hw_on else self._spn_workers.value()
|
||||
is_scan = getattr(self, '_auto_export_no_markers', False)
|
||||
|
||||
batch = {
|
||||
"jobs": jobs,
|
||||
"positions": positions,
|
||||
"file_path": self._file_path,
|
||||
"short_side": short_side,
|
||||
"image_sequence": image_sequence,
|
||||
"max_workers": max_workers,
|
||||
"encoder": encoder,
|
||||
"spread": spread,
|
||||
"folder": folder,
|
||||
"format": fmt,
|
||||
"profile": self._profile,
|
||||
"is_scan": is_scan,
|
||||
}
|
||||
|
||||
if self._export_worker and self._export_worker.isRunning():
|
||||
self._export_queue.append(batch)
|
||||
n = len(self._export_queue)
|
||||
self._show_status(f"Auto: queued ({n} pending)")
|
||||
self._btn_auto_export.setEnabled(True)
|
||||
return
|
||||
|
||||
self._start_export_batch(batch)
|
||||
|
||||
def _start_export_batch(self, batch: dict) -> None:
|
||||
"""Start an export batch immediately."""
|
||||
self._auto_export_positions = batch["positions"]
|
||||
self._export_short_side = batch["short_side"]
|
||||
self._export_portrait = "Off"
|
||||
self._export_crop_center = 0.5
|
||||
self._export_format = batch["format"]
|
||||
self._export_clip_count = 1
|
||||
self._export_spread = batch["spread"]
|
||||
self._export_folder = batch["folder"]
|
||||
self._export_folder_suffix = ""
|
||||
self._export_profile = batch["profile"]
|
||||
self._auto_export_no_markers = batch["is_scan"]
|
||||
self._export_batch_file = batch["file_path"]
|
||||
|
||||
n_queued = len(self._export_queue)
|
||||
q_msg = f" ({n_queued} queued)" if n_queued else ""
|
||||
self._show_status(f"Auto: exporting {len(batch['jobs'])} clips...{q_msg}")
|
||||
|
||||
self._export_worker = ExportWorker(
|
||||
self._file_path, jobs,
|
||||
short_side=short_side,
|
||||
image_sequence=image_sequence,
|
||||
max_workers=max_workers,
|
||||
encoder=encoder,
|
||||
batch["file_path"], batch["jobs"],
|
||||
short_side=batch["short_side"],
|
||||
image_sequence=batch["image_sequence"],
|
||||
max_workers=batch["max_workers"],
|
||||
encoder=batch["encoder"],
|
||||
)
|
||||
self._export_worker.finished.connect(self._on_auto_clip_done)
|
||||
self._export_worker.all_done.connect(self._on_auto_batch_done)
|
||||
@@ -4664,10 +4694,11 @@ class MainWindow(QMainWindow):
|
||||
start_t = t
|
||||
break
|
||||
is_scan = getattr(self, '_auto_export_no_markers', False)
|
||||
batch_file = getattr(self, '_export_batch_file', self._file_path)
|
||||
label = self._txt_label.currentText().strip()
|
||||
category = self._cmb_category.currentText()
|
||||
self._db.add(
|
||||
os.path.basename(self._file_path),
|
||||
os.path.basename(batch_file),
|
||||
start_t,
|
||||
path,
|
||||
label=label,
|
||||
@@ -4679,27 +4710,45 @@ class MainWindow(QMainWindow):
|
||||
clip_count=1,
|
||||
spread=self._export_spread,
|
||||
profile=self._export_profile,
|
||||
source_path=self._file_path,
|
||||
source_path=batch_file,
|
||||
scan_export=is_scan,
|
||||
)
|
||||
if not is_scan:
|
||||
upsert_clip_annotation(self._export_folder, path, label)
|
||||
self._show_status(f"Auto: {os.path.basename(path)}")
|
||||
n_queued = len(self._export_queue)
|
||||
q_msg = f" ({n_queued} queued)" if n_queued else ""
|
||||
self._show_status(f"Auto: {os.path.basename(path)}{q_msg}")
|
||||
_log(f" auto clip done: {os.path.basename(path)}")
|
||||
|
||||
def _on_auto_batch_done(self):
|
||||
n = len(self._auto_export_positions)
|
||||
batch_file = getattr(self, '_export_batch_file', self._file_path)
|
||||
batch_profile = self._export_profile
|
||||
|
||||
# Mark the batch's video as done in playlist
|
||||
n_clips = self._db.get_clip_count(os.path.basename(batch_file), batch_profile)
|
||||
self._playlist.mark_done(batch_file, n_clips)
|
||||
|
||||
# If current video matches the batch, refresh its markers
|
||||
if self._file_path == batch_file:
|
||||
self._refresh_markers()
|
||||
self._update_next_label()
|
||||
|
||||
_log(f"Auto export complete: {n} clips ({os.path.basename(batch_file)})")
|
||||
|
||||
# Drain queue
|
||||
if self._export_queue:
|
||||
next_batch = self._export_queue.pop(0)
|
||||
self._show_status(f"Auto: starting next batch ({len(self._export_queue)} remaining)")
|
||||
self._start_export_batch(next_batch)
|
||||
return
|
||||
|
||||
self._btn_auto_export.setEnabled(True)
|
||||
self._btn_cancel.setEnabled(False)
|
||||
self._btn_export.setEnabled(True)
|
||||
self._set_subprofile_btns_enabled(True)
|
||||
self._auto_export_no_markers = False
|
||||
self._refresh_markers()
|
||||
n_clips = self._db.get_clip_count(os.path.basename(self._file_path), self._profile)
|
||||
self._playlist.mark_done(self._file_path, n_clips)
|
||||
self._update_next_label()
|
||||
self._show_status(f"Auto export complete: {n} clips")
|
||||
_log(f"Auto export complete: {n} clips")
|
||||
|
||||
def _jump_to_next_scan_region(self) -> None:
|
||||
regions = sorted(self._timeline._scan_regions, key=lambda r: r[0])
|
||||
@@ -5005,7 +5054,9 @@ class MainWindow(QMainWindow):
|
||||
self._show_status("Cancelling export…")
|
||||
|
||||
def _on_export_cancelled(self):
|
||||
_log("Export cancelled")
|
||||
n_dropped = len(self._export_queue)
|
||||
self._export_queue.clear()
|
||||
_log(f"Export cancelled (dropped {n_dropped} queued)")
|
||||
self._btn_export.setEnabled(True)
|
||||
self._btn_auto_export.setEnabled(True)
|
||||
self._set_subprofile_btns_enabled(True)
|
||||
@@ -5016,7 +5067,10 @@ class MainWindow(QMainWindow):
|
||||
n_clips = self._db.get_clip_count(os.path.basename(self._file_path), self._profile)
|
||||
if n_clips:
|
||||
self._playlist.mark_done(self._file_path, n_clips)
|
||||
self._show_status("Export cancelled", 4000)
|
||||
msg = "Export cancelled"
|
||||
if n_dropped:
|
||||
msg += f" ({n_dropped} queued batches dropped)"
|
||||
self._show_status(msg, 4000)
|
||||
|
||||
def changeEvent(self, event):
|
||||
super().changeEvent(event)
|
||||
|
||||
Reference in New Issue
Block a user