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:
Leonid Pershin
2026-06-07 04:53:27 +03:00
parent 7f0121b7df
commit e27dfdf518
27 changed files with 2998 additions and 387 deletions
+77
View File
@@ -0,0 +1,77 @@
"""Persist detection results for a project.
The detection cache is a JSON file (``detections.json`` at the project root) that
maps each image to its detections so reopening a project doesn't have to re-run
the detector. The cache file lives apart from the images (which sit in the
project's ``frames/`` sub-folder), so the file location and the image base
directory are passed separately.
The cache is tagged with the detector identity (name + model + conf/imgsz); a
mismatch means the cache was produced by a different detector and is ignored
(``load_results`` returns ``None``) rather than shown as if current.
Keys are stored as **basenames**, so the cache survives moving/renaming the
project folder.
"""
from __future__ import annotations
import json
from pathlib import Path
from .types import Detection
_VERSION = 1
def make_key(detector: str, model_path: str | None, yolo_conf: float, yolo_imgsz: int) -> dict:
"""Identity of the detector that produced a cache; cache is only valid for a match."""
return {
"detector": detector,
"model_path": model_path or "",
"yolo_conf": round(float(yolo_conf), 4),
"yolo_imgsz": int(yolo_imgsz),
}
def save_results(cache_file: Path, key: dict, results: dict[str, list[Detection]]) -> bool:
"""Write the cache (basename -> detections) to ``cache_file``. False on failure."""
payload = {
"version": _VERSION,
"key": key,
"results": {
Path(p).name: [d.to_dict() for d in dets]
for p, dets in results.items()
},
}
try:
cache_file.parent.mkdir(parents=True, exist_ok=True)
cache_file.write_text(
json.dumps(payload, ensure_ascii=False), encoding="utf-8"
)
return True
except OSError:
return False
def load_results(cache_file: Path, key: dict, base_dir: Path) -> dict[str, list[Detection]] | None:
"""Load cached detections from ``cache_file`` if present and the detector matches.
Returns a dict keyed by **full path** (``base_dir / basename``), or ``None`` if
there is no cache, it's unreadable, or it was made by a different detector.
"""
if not cache_file.is_file():
return None
try:
payload = json.loads(cache_file.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
if payload.get("version") != _VERSION or payload.get("key") != key:
return None
out: dict[str, list[Detection]] = {}
for name, dets in payload.get("results", {}).items():
try:
out[str(base_dir / name)] = [Detection.from_dict(d) for d in dets]
except (KeyError, TypeError, ValueError):
continue # skip a corrupt entry, keep the rest
return out