diff --git a/core/audio_scan.py b/core/audio_scan.py index dd01224..6a4dcc2 100644 --- a/core/audio_scan.py +++ b/core/audio_scan.py @@ -489,16 +489,27 @@ def list_model_versions(profile_name: str = "default", def restore_model_version(version_path: str, profile_name: str = "default", embed_model: str | None = None) -> None: """Restore a backup version as the active model.""" - import shutil + import filecmp, shutil from datetime import datetime current = default_model_path(profile_name, embed_model) if version_path == current: return - # Back up current before replacing + # Back up current before replacing — but only if no identical backup exists if os.path.exists(current): stem, ext = os.path.splitext(current) - ts = datetime.now().strftime("%Y%m%d_%H%M%S") - shutil.move(current, f"{stem}_{ts}{ext}") + already_saved = False + if os.path.isdir(_MODEL_DIR): + import re + pat = re.compile(re.escape(os.path.basename(stem)) + r"_\d{8}_\d{6}" + re.escape(ext) + "$") + for fname in os.listdir(_MODEL_DIR): + if pat.match(fname): + candidate = os.path.join(_MODEL_DIR, fname) + if filecmp.cmp(current, candidate, shallow=False): + already_saved = True + break + if not already_saved: + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + shutil.move(current, f"{stem}_{ts}{ext}") shutil.copy2(version_path, current) _log(f"audio_scan: restored {os.path.basename(version_path)} as active model") diff --git a/main.py b/main.py index a97ecb6..9427a83 100755 --- a/main.py +++ b/main.py @@ -1218,10 +1218,16 @@ class TimelineWidget(QWidget): p.drawText(mx + 1, rh + 2, 13, 12, Qt.AlignmentFlag.AlignCenter, str(num)) - # ── scan mode cursor line ───────────────────────────────────── + # ── scan mode cursor + playback line ───────────────────────── if self._scan_mode: - p.setPen(QPen(QColor(255, 255, 255, 200), 2)) + # Export cursor (dim) + p.setPen(QPen(QColor(255, 255, 255, 80), 1)) p.drawLine(x_start, rh, x_start, h) + # Playback position (bright green) + if self._play_pos is not None and self._play_pos >= 0: + px = int(self._play_pos / self._duration * w) + p.setPen(QPen(QColor(80, 255, 80, 220), 2)) + p.drawLine(px, rh, px, h) # ── crop keyframe diamonds ──────────────────────────────────── if self._crop_keyframes and self._duration > 0: @@ -2429,10 +2435,16 @@ class MainWindow(QMainWindow): self._scan_all_queue: list[str] = [] self._cmb_scan_model = QComboBox() - self._cmb_scan_model.setToolTip("Trained embedding model to use for scanning\nRight-click to rollback to a previous version") + self._cmb_scan_model.setToolTip("Trained embedding model to use for scanning") self._cmb_scan_model.setMinimumWidth(120) self._cmb_scan_model.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self._cmb_scan_model.customContextMenuRequested.connect(self._show_model_versions_menu) + self._btn_model_history = QPushButton("\u23f2") + self._btn_model_history.setFixedWidth(28) + self._btn_model_history.setToolTip("Rollback to a previous model version") + self._btn_model_history.clicked.connect( + lambda: self._show_model_versions_menu(None) + ) self._spn_auto_fuse = QDoubleSpinBox() self._spn_auto_fuse.setDecimals(1) @@ -2591,6 +2603,7 @@ class MainWindow(QMainWindow): settings_row.addWidget(self._chk_rand_square) settings_row.addWidget(self._chk_track) settings_row.addWidget(self._cmb_scan_model) + settings_row.addWidget(self._btn_model_history) settings_row.addWidget(self._btn_scan) settings_row.addWidget(self._btn_scan_mode) settings_row.addWidget(self._btn_auto_export) @@ -3480,10 +3493,13 @@ class MainWindow(QMainWindow): display = f"{label[:4]}-{label[4:6]}-{label[6:8]} {label[9:11]}:{label[11:13]}" act = menu.addAction(f"Restore {display}") act.setData(path) - chosen = menu.exec(self._cmb_scan_model.mapToGlobal(pos)) + global_pos = (self._btn_model_history.mapToGlobal(self._btn_model_history.rect().bottomLeft()) + if pos is None + else self._cmb_scan_model.mapToGlobal(pos)) + chosen = menu.exec(global_pos) if chosen and chosen.data(): restore_model_version(chosen.data(), self._profile, embed_name) - self._show_status(f"Restored model version — rescan to use it") + self._start_scan() def _cleanup_scan_worker(self) -> None: """Disconnect signals, cancel, and schedule deletion of old scan worker."""