from __future__ import annotations import dataclasses import os from pathlib import Path from typing import Any import yaml from configs.models import ( AppConfig, AudioConfig, DeadBugConfig, LoggingConfig, ModelConfig, ServerConfig, VideoConfig, ) _PROJECT_ROOT = Path(__file__).resolve().parent.parent _ENV_MAP = { "POSEFIT_WS_HOST": ("server", "host"), "POSEFIT_WS_PORT": ("server", "port", int), "POSEFIT_WS_MAX_SIZE": ("server", "max_ws_size", int), "POSEFIT_PROCESS_EVERY_N_FRAMES": ("video", "process_every_n_frames", int), "POSEFIT_MODEL_PATH": ("model", "path"), "POSEFIT_PREFER_GPU": ("model", "prefer_gpu", lambda v: v not in ("0", "false", "False")), "POSEFIT_VISIBILITY_THRESHOLD": ("dead_bug", "visibility_threshold", float), "POSEFIT_EXTENSION_CONFIRM_FRAMES": ("dead_bug", "extension_confirm_frames", int), "POSEFIT_RESET_CONFIRM_FRAMES": ("dead_bug", "reset_confirm_frames", int), "POSEFIT_REP_ANNOUNCER_ENABLED": ("audio", "rep_announcer_enabled", lambda v: v not in ("0", "false", "False")), "POSEFIT_REP_ANNOUNCER_RATE": ("audio", "rep_announcer_rate", int), "POSEFIT_REP_ANNOUNCER_VOLUME": ("audio", "rep_announcer_volume", float), "POSEFIT_LOG_ROTATION": ("logging", "rotation"), "POSEFIT_LOG_RETENTION": ("logging", "retention"), "POSEFIT_LOG_DIR": ("logging", "dir"), } _SECTION_CLASS = { "server": ServerConfig, "video": VideoConfig, "model": ModelConfig, "dead_bug": DeadBugConfig, "audio": AudioConfig, "logging": LoggingConfig, } def _dict_to_dataclass(cls: type, data: dict[str, Any] | None) -> dict[str, Any]: if data is None: return {} field_names = {f.name for f in dataclasses.fields(cls)} return {k: v for k, v in data.items() if k in field_names} def _read_yaml(path: Path) -> dict[str, Any]: if not path.exists(): return {} with open(path, encoding="utf-8") as f: return yaml.safe_load(f) or {} def _apply_env_overrides(raw: dict[str, Any]) -> None: for env_var, (section, key, *rest) in _ENV_MAP.items(): value = os.getenv(env_var) if value is None: continue if rest: value = rest[0](value) raw.setdefault(section, {})[key] = value def load_config(config_path: str | Path | None = None) -> AppConfig: if config_path is None: config_path = _PROJECT_ROOT / "config.yaml" raw = _read_yaml(Path(config_path)) _apply_env_overrides(raw) return AppConfig(**{ section: cls(**_dict_to_dataclass(cls, raw.get(section))) for section, cls in _SECTION_CLASS.items() }) config = load_config()