from __future__ import annotations import os from pathlib import Path from typing import Any import yaml _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"), } def _load_yaml() -> dict[str, Any]: config_path = _PROJECT_ROOT / "config.yaml" if config_path.exists(): with open(config_path, encoding="utf-8") as f: return yaml.safe_load(f) or {} return {} def _apply_env_overrides(config: dict) -> 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) config.setdefault(section, {})[key] = value _cfg = _load_yaml() _apply_env_overrides(_cfg) def _get(section: str, key: str, default: Any = None) -> Any: return _cfg.get(section, {}).get(key, default) # ── Server ────────────────────────────────────────────────────────────────── WS_HOST = _get("server", "host", "0.0.0.0") WS_PORT = _get("server", "port", 8765) WS_MAX_SIZE = _get("server", "max_ws_size", 10485760) # ── Video processing ──────────────────────────────────────────────────────── PROCESS_EVERY_N_FRAMES = max(1, _get("video", "process_every_n_frames", 1)) # ── Model ─────────────────────────────────────────────────────────────────── MODEL_DIR: Path = _PROJECT_ROOT / "pose_models" _model_path = _get("model", "path", "") MODEL_PATH = _model_path if _model_path else str(MODEL_DIR / "pose_landmarker_full.task") PREFER_GPU = bool(_get("model", "prefer_gpu", True)) # ── Dead bug exercise ─────────────────────────────────────────────────────── VISIBILITY_THRESHOLD = float(_get("dead_bug", "visibility_threshold", 0.45)) EXTENSION_CONFIRM_FRAMES = int(_get("dead_bug", "extension_confirm_frames", 4)) RESET_CONFIRM_FRAMES = int(_get("dead_bug", "reset_confirm_frames", 3)) # ── Audio ─────────────────────────────────────────────────────────────────── REP_ANNOUNCER_ENABLED = bool(_get("audio", "rep_announcer_enabled", True)) REP_ANNOUNCER_RATE = int(_get("audio", "rep_announcer_rate", 185)) REP_ANNOUNCER_VOLUME = float(_get("audio", "rep_announcer_volume", 1.0)) # ── Logging ───────────────────────────────────────────────────────────────── LOG_DIR: Path = _PROJECT_ROOT / _get("logging", "dir", "logs") LOG_ROTATION = _get("logging", "rotation", "20 MB") LOG_RETENTION = _get("logging", "retention", "14 days")