Refactor HVideoTool's configuration and project management: updated project handling in core/project.py, streamlined restoration settings in config.py, and improved documentation in CLAUDE.md. Removed unused parameters and enhanced type hints for better clarity.
This commit is contained in:
@@ -5,23 +5,24 @@ A project is a folder (``project.json`` + ``frames/`` + ``detections.json`` +
|
||||
model, threshold, restore engine) live in ``project.json``; the global
|
||||
``settings.json`` only seeds defaults for new projects.
|
||||
|
||||
Layout: a toolbar (new/open project · from-video · detector · model · calc-frame ·
|
||||
detect-all · threshold), then a splitter with three panes — left: collection
|
||||
controls + the file list; center: the image with overlays; right: a detail table
|
||||
of every detection. Collection controls sit by the file list (they act on its
|
||||
selection), keeping the toolbar to detection/entry actions only.
|
||||
Layout: a toolbar (new/open project · from-video · model · calc-frame · detect-all ·
|
||||
restore · threshold), then a splitter with three panes — left: collection controls +
|
||||
the file list; center: the image with overlays; right: a detail table of every
|
||||
detection. Collection controls sit by the file list (they act on its selection),
|
||||
keeping the toolbar to detection/entry actions only.
|
||||
|
||||
Viewing and detecting are decoupled, so browsing a big project stays instant even
|
||||
with a slow (CPU) detector:
|
||||
Detection is YOLO-only; restoration is DeepMosaics-only. Viewing and detecting are
|
||||
decoupled, so browsing a big project stays instant even with a slow (CPU) detector:
|
||||
- selecting a file just **shows** it (with its cached result, if any);
|
||||
- **double-clicking** a file, or "Рассчитать кадр", runs the detector on it;
|
||||
- "Детектировать все" runs the whole project.
|
||||
Both project loading and detect-all show a progress bar. Results are cached in the
|
||||
project; switching detector/model clears the cache.
|
||||
project; switching the model clears the cache.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
@@ -77,13 +78,12 @@ class MainWindow(QMainWindow):
|
||||
self._detector = None
|
||||
self._detector_key = None
|
||||
self._project: Project | None = None # the open project (None until one is opened)
|
||||
self._folder: Path | None = None # == project.frames_dir while a project is open
|
||||
self._files: list[Path] = []
|
||||
self._results: dict[str, list[Detection]] = {} # path -> detections (cache)
|
||||
self._current: Path | None = None
|
||||
self._restorer = None # un-censor engine, built lazily from config
|
||||
self._restorer_key = None
|
||||
self._restored: dict[str, "object"] = {} # path -> restored image (BGR ndarray)
|
||||
self._restored: dict[str, object] = {} # path -> restored image (BGR ndarray)
|
||||
self._showing_restored = False
|
||||
self._nav_sync = False # guard against slider<->list signal loops
|
||||
self._busy = False # a long operation is running
|
||||
@@ -547,7 +547,7 @@ class MainWindow(QMainWindow):
|
||||
if Project.is_project(p):
|
||||
try:
|
||||
self._open_project(Project.load(p))
|
||||
except (OSError, ValueError) as exc: # noqa: BLE001 - surface to the user
|
||||
except (OSError, ValueError) as exc:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось открыть проект:\n{exc}")
|
||||
elif p.is_dir():
|
||||
QMessageBox.information(
|
||||
@@ -562,10 +562,8 @@ class MainWindow(QMainWindow):
|
||||
"""On startup, reopen the last project if it still exists (best-effort)."""
|
||||
last = settings_store.last_project()
|
||||
if last and Project.is_project(last):
|
||||
try:
|
||||
with contextlib.suppress(OSError, ValueError):
|
||||
self._open_project(Project.load(last))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
def _new_project_root(self, default_name: str = "") -> Path | None:
|
||||
"""Prompt for a parent dir + name; return a fresh (empty) project root or None."""
|
||||
@@ -662,10 +660,8 @@ class MainWindow(QMainWindow):
|
||||
# Carry over an old sidecar detection cache (basename-keyed) if present.
|
||||
old_sidecar = src / ".hvideotool_detections.json"
|
||||
if old_sidecar.is_file():
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
shutil.copy2(str(old_sidecar), str(project.cache_path))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self.statusBar().showMessage(f"Импортировано {copied} картинок → {project.name}")
|
||||
self._open_project(project)
|
||||
@@ -764,7 +760,7 @@ class MainWindow(QMainWindow):
|
||||
str(video), str(out), step=step, keyframes_only=keyframes_only,
|
||||
max_dim=max_dim, progress=cb,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - surface decode errors to the user
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось извлечь кадры:\n{exc}")
|
||||
return
|
||||
finally:
|
||||
@@ -788,7 +784,6 @@ class MainWindow(QMainWindow):
|
||||
self.statusBar().showMessage(f"Сканирую папку: {folder}…")
|
||||
QApplication.processEvents()
|
||||
files = sorted(p for p in folder.iterdir() if p.suffix.lower() in _IMAGE_EXTS)
|
||||
self._folder = folder
|
||||
self._files = files
|
||||
self._results.clear()
|
||||
self._current = None
|
||||
@@ -850,7 +845,7 @@ class MainWindow(QMainWindow):
|
||||
img = imread_unicode(str(path))
|
||||
if img is None:
|
||||
raise RuntimeError(f"Не удалось прочитать: {Path(path).name}")
|
||||
dets = detector.detect(Frame(image=img, index=0, pts=0.0))
|
||||
dets = detector.detect(Frame(image=img))
|
||||
dets.sort(key=lambda d: d.score, reverse=True)
|
||||
return dets
|
||||
|
||||
@@ -1057,8 +1052,7 @@ class MainWindow(QMainWindow):
|
||||
self._start_job(fn, total, on_done=done)
|
||||
|
||||
def _make_restorer(self):
|
||||
key = (self._cfg.restorer, self._cfg.dm_dir, self._cfg.dm_model,
|
||||
self._cfg.dm_python, self._cfg.dm_gpu)
|
||||
key = (self._cfg.restorer, self._cfg.dm_dir, self._cfg.dm_model, self._cfg.dm_gpu)
|
||||
if key != self._restorer_key:
|
||||
self._restorer = build_restorer(self._cfg.restorer, self._cfg) # may raise
|
||||
self._restorer_key = key
|
||||
@@ -1266,7 +1260,7 @@ class MainWindow(QMainWindow):
|
||||
self.view.set_threshold(value)
|
||||
self._persist_settings()
|
||||
|
||||
def closeEvent(self, event) -> None: # noqa: N802 - Qt override
|
||||
def closeEvent(self, event) -> None:
|
||||
if self._job is not None: # stop a running background job before tearing down
|
||||
self._job.cancel()
|
||||
self._pool.waitForDone(3000)
|
||||
|
||||
Reference in New Issue
Block a user