"""Persist a handful of user choices to ~/HVideoTool/settings.json. For the project-based tool this file holds the **defaults for new projects** (the detector, model path, overlay threshold, restore engine) plus app-level state: the last opened project, the recent-projects list, and the last directory used in file dialogs. Per-project settings live in each project's ``project.json``, not here. """ from __future__ import annotations import json from pathlib import Path from .config import AppConfig _PATH = Path.home() / "HVideoTool" / "settings.json" def _read() -> dict: try: return json.loads(_PATH.read_text(encoding="utf-8")) except (OSError, ValueError): return {} def _write(data: dict) -> None: _PATH.parent.mkdir(parents=True, exist_ok=True) _PATH.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") def apply(config: AppConfig) -> None: """Overlay persisted settings onto ``config`` (mutates it in place).""" data = _read() if data.get("detector"): config.detector = data["detector"] if isinstance(data.get("detector_models"), list): config.detector_models = [str(m) for m in data["detector_models"]] if "threshold" in data: config.default_threshold = float(data["threshold"]) if isinstance(data.get("model_thresholds"), dict): config.model_thresholds = { str(k): float(v) for k, v in data["model_thresholds"].items() } if "cross_model_nms" in data: config.cross_model_nms = bool(data["cross_model_nms"]) if "nms_iou" in data: config.nms_iou = float(data["nms_iou"]) if data.get("restorer"): config.restorer = data["restorer"] for key in ("dm_dir", "dm_model", "dm_gpu"): if key in data: setattr(config, key, data[key]) if "dm_feed_restored" in data: config.dm_feed_restored = bool(data["dm_feed_restored"]) # diffusion-inpaint engine for key in ("diff_backend", "diff_url", "diff_model", "diff_prompt", "diff_negative"): if key in data: setattr(config, key, data[key]) if "diff_steps" in data: config.diff_steps = int(data["diff_steps"]) if "diff_cfg" in data: config.diff_cfg = float(data["diff_cfg"]) if "diff_denoise" in data: config.diff_denoise = float(data["diff_denoise"]) if "diff_seed" in data: config.diff_seed = int(data["diff_seed"]) if "diff_mask_dilate" in data: config.diff_mask_dilate = int(data["diff_mask_dilate"]) if "diff_mask_blur" in data: config.diff_mask_blur = int(data["diff_mask_blur"]) def save(config: AppConfig) -> None: """Persist the configurable settings, preserving other keys (e.g. last_dir).""" data = _read() data.update( detector=config.detector, detector_models=list(config.detector_models), threshold=config.default_threshold, model_thresholds=dict(config.model_thresholds), cross_model_nms=config.cross_model_nms, nms_iou=config.nms_iou, restorer=config.restorer, dm_dir=config.dm_dir, dm_model=config.dm_model, dm_gpu=config.dm_gpu, dm_feed_restored=config.dm_feed_restored, diff_backend=config.diff_backend, diff_url=config.diff_url, diff_model=config.diff_model, diff_prompt=config.diff_prompt, diff_negative=config.diff_negative, diff_steps=config.diff_steps, diff_cfg=config.diff_cfg, diff_denoise=config.diff_denoise, diff_seed=config.diff_seed, diff_mask_dilate=config.diff_mask_dilate, diff_mask_blur=config.diff_mask_blur, ) _write(data) def last_dir() -> str | None: return _read().get("last_dir") def set_last_dir(path: str) -> None: data = _read() data["last_dir"] = path _write(data) def last_project() -> str | None: return _read().get("last_project") def set_last_project(path: str) -> None: data = _read() data["last_project"] = path _write(data) _RECENTS_CAP = 10 def recent_projects() -> list[str]: recents = _read().get("recent_projects", []) return [p for p in recents if isinstance(p, str)] def add_recent_project(path: str) -> None: """Push ``path`` to the front of the recent-projects list (deduped, capped).""" data = _read() recents = [p for p in data.get("recent_projects", []) if isinstance(p, str) and p != path] recents.insert(0, path) data["recent_projects"] = recents[:_RECENTS_CAP] _write(data)