from __future__ import annotations import json from copy import deepcopy from pathlib import Path from typing import Any APP_ROOT = Path(__file__).resolve().parent.parent CONFIG_PATH = APP_ROOT / "app_config.json" DEFAULT_CONFIG: dict[str, Any] = { "camera": { "index": 0, "width": 1920, "height": 1080, "fps": 30, "backend": "auto", "rotation_degrees": 0, "properties": { "brightness": None, "contrast": None, "saturation": None, "hue": None, "gain": None, "exposure": None, "sharpness": None, "auto_exposure": None, "focus": None, "auto_focus": None, }, }, "detection": { "model_path": "models/best.pt", "confidence_threshold": 0.25, "mode": "best", "frame_stride": 5, "image_size": 640, "device": "cpu", }, "ocr": { "enabled": True, "language": "eng", "tesseract_cmd": None, "threshold": True, "scale": 2.0, }, "capture": { "photos_dir": "captures/photos", "videos_dir": "captures/videos", "image_extension": "jpg", "video_extension": "mp4", "video_codec": "mp4v", }, "label_data": {"models": ["Regius", "Duvell"], "colors": ["T-NF-BLK-OUT-BST-G", "T-BLK-G"]}, } def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]: result = deepcopy(base) for key, value in override.items(): if isinstance(value, dict) and isinstance(result.get(key), dict): result[key] = deep_merge(result[key], value) else: result[key] = value return result class AppConfig: def __init__(self, path: Path = CONFIG_PATH) -> None: self.path = path self.data = self.load() def load(self) -> dict[str, Any]: if not self.path.exists(): self.path.parent.mkdir(parents=True, exist_ok=True) self.save(DEFAULT_CONFIG) return deepcopy(DEFAULT_CONFIG) with self.path.open("r", encoding="utf-8") as config_file: loaded = json.load(config_file) return deep_merge(DEFAULT_CONFIG, loaded) def save(self, data: dict[str, Any] | None = None) -> None: if data is not None: self.data = data self.path.parent.mkdir(parents=True, exist_ok=True) with self.path.open("w", encoding="utf-8") as config_file: json.dump(self.data, config_file, indent=2, ensure_ascii=False) config_file.write("\n") def resolve_path(self, configured_path: str) -> Path: path = Path(configured_path) if path.is_absolute(): return path return APP_ROOT / path