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:
Leonid Pershin
2026-06-07 06:33:00 +03:00
parent 9c471ca701
commit ac02ca27a8
16 changed files with 81 additions and 105 deletions
+17 -23
View File
@@ -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)