149 lines
5.0 KiB
Python
149 lines
5.0 KiB
Python
"""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}
|