Refactor HVideoTool to support project-based workflow: introduced project management features, updated UI for project handling, and enhanced documentation in README and CLAUDE.md. The tool now organizes images and settings into projects, improving usability and detection caching.
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
"""A HVideoTool *project*: a self-contained folder on disk.
|
||||
|
||||
This replaces the old "open a bare folder of images" model. A project is a folder
|
||||
that holds:
|
||||
|
||||
```
|
||||
MyProject/
|
||||
├── project.json # version, name, created, source, settings{detector/model/threshold/restore}
|
||||
├── frames/ # the images (what used to be "the folder")
|
||||
├── detections.json # the detection cache (basename -> detections, tagged with detector key)
|
||||
└── collections/ # curation sub-folders (created lazily)
|
||||
```
|
||||
|
||||
The project file carries the **per-project** settings (detector, model, overlay
|
||||
threshold, restore engine). Global ``settings.json`` only seeds the defaults for
|
||||
*new* projects; once a project exists it remembers how it was last inspected.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from ..config import AppConfig
|
||||
|
||||
PROJECT_FILE = "project.json"
|
||||
FRAMES_DIR = "frames"
|
||||
CACHE_FILE = "detections.json"
|
||||
COLLECTIONS_DIR = "collections"
|
||||
FAVORITES_DIR = "Избранное" # the single default collection ("в избранное")
|
||||
_VERSION = 1
|
||||
|
||||
# The subset of AppConfig fields a project remembers (mirrored to/from project.json).
|
||||
_SETTING_KEYS = (
|
||||
"detector",
|
||||
"model_path",
|
||||
"default_threshold",
|
||||
"restorer",
|
||||
"dm_dir",
|
||||
"dm_model",
|
||||
"dm_python",
|
||||
"dm_gpu",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Project:
|
||||
"""A HVideoTool project rooted at ``root`` — the single source of truth for its
|
||||
on-disk layout and its per-project settings."""
|
||||
|
||||
root: Path
|
||||
name: str
|
||||
settings: dict = field(default_factory=dict) # subset of AppConfig fields
|
||||
source: str | None = None # originating video/folder, if any
|
||||
created: str | None = None
|
||||
|
||||
# ----------------------------------------------------------------- paths
|
||||
@property
|
||||
def project_file(self) -> Path:
|
||||
return self.root / PROJECT_FILE
|
||||
|
||||
@property
|
||||
def frames_dir(self) -> Path:
|
||||
return self.root / FRAMES_DIR
|
||||
|
||||
@property
|
||||
def cache_path(self) -> Path:
|
||||
return self.root / CACHE_FILE
|
||||
|
||||
@property
|
||||
def collections_dir(self) -> Path:
|
||||
return self.root / COLLECTIONS_DIR
|
||||
|
||||
@property
|
||||
def favorites_dir(self) -> Path:
|
||||
"""The single default collection — frames moved "to favorites" land here."""
|
||||
return self.collections_dir / FAVORITES_DIR
|
||||
|
||||
# ------------------------------------------------------------- lifecycle
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
root: Path | str,
|
||||
name: str | None = None,
|
||||
settings: dict | None = None,
|
||||
source: str | None = None,
|
||||
) -> "Project":
|
||||
"""Create a new project folder (with ``frames/``) and write ``project.json``."""
|
||||
root = Path(root)
|
||||
proj = cls(
|
||||
root=root,
|
||||
name=name or root.name,
|
||||
settings={k: v for k, v in (settings or {}).items() if k in _SETTING_KEYS},
|
||||
source=source,
|
||||
created=datetime.now().isoformat(timespec="seconds"),
|
||||
)
|
||||
proj.frames_dir.mkdir(parents=True, exist_ok=True)
|
||||
proj.save()
|
||||
return proj
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path | str) -> "Project":
|
||||
"""Load a project from its folder or directly from its ``project.json``."""
|
||||
path = Path(path)
|
||||
root = path.parent if path.name == PROJECT_FILE else path
|
||||
data = json.loads((root / PROJECT_FILE).read_text(encoding="utf-8"))
|
||||
return cls(
|
||||
root=root,
|
||||
name=data.get("name", root.name),
|
||||
settings={k: v for k, v in data.get("settings", {}).items() if k in _SETTING_KEYS},
|
||||
source=data.get("source"),
|
||||
created=data.get("created"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_project(path: Path | str) -> bool:
|
||||
"""True if ``path`` is a project folder (or a ``project.json``)."""
|
||||
path = Path(path)
|
||||
if path.name == PROJECT_FILE:
|
||||
return path.is_file()
|
||||
return (path / PROJECT_FILE).is_file()
|
||||
|
||||
def save(self) -> None:
|
||||
"""Write ``project.json``."""
|
||||
payload = {
|
||||
"version": _VERSION,
|
||||
"name": self.name,
|
||||
"created": self.created,
|
||||
"source": self.source,
|
||||
"settings": self.settings,
|
||||
}
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
self.project_file.write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
|
||||
# ----------------------------------------------------- settings <-> config
|
||||
def apply_to_config(self, cfg: AppConfig) -> None:
|
||||
"""Overlay this project's stored settings onto ``cfg`` (mutates it)."""
|
||||
for key in _SETTING_KEYS:
|
||||
if key in self.settings:
|
||||
setattr(cfg, key, self.settings[key])
|
||||
|
||||
def update_from_config(self, cfg: AppConfig) -> None:
|
||||
"""Capture the per-project settings from ``cfg`` into ``self.settings``."""
|
||||
self.settings = {key: getattr(cfg, key) for key in _SETTING_KEYS}
|
||||
Reference in New Issue
Block a user