From c8fd057129d5c880d901427b7d7221f05dd0721c Mon Sep 17 00:00:00 2001 From: hjwang <2392948297@qq.com> Date: Wed, 10 Jun 2026 10:19:41 +0800 Subject: [PATCH] Centralize configuration into config.yaml - All settings moved to config.yaml - configs/load.py reads from config.yaml with env var overrides - Environment variables still work for backward compatibility - Added pyyaml to requirements --- app/core/lifecycle.py | 2 +- app/core/logging.py | 2 +- app/main.py | 6 ++- app/signaling/websocket_server.py | 2 +- app/webrtc/video_receiver.py | 2 +- config.yaml | 29 +++++++++++ configs/default.py | 32 ------------ configs/load.py | 83 +++++++++++++++++++++++++++++++ requirements.txt | 1 + tests/test_dead_bug_rules.py | 4 +- 10 files changed, 123 insertions(+), 40 deletions(-) create mode 100644 config.yaml delete mode 100644 configs/default.py create mode 100644 configs/load.py diff --git a/app/core/lifecycle.py b/app/core/lifecycle.py index 71637a4..ce01dab 100644 --- a/app/core/lifecycle.py +++ b/app/core/lifecycle.py @@ -1,7 +1,7 @@ from __future__ import annotations from app.diagnostics.crash_handler import enable_crash_handler -from configs.default import LOG_DIR +from configs.load import LOG_DIR def startup() -> None: diff --git a/app/core/logging.py b/app/core/logging.py index 533b11c..06cef3c 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -4,7 +4,7 @@ from pathlib import Path from loguru import logger -from configs.default import LOG_DIR, LOG_RETENTION, LOG_ROTATION +from configs.load import LOG_DIR, LOG_RETENTION, LOG_ROTATION def setup_logging() -> None: diff --git a/app/main.py b/app/main.py index d21a230..351aa21 100644 --- a/app/main.py +++ b/app/main.py @@ -13,7 +13,11 @@ from app.core.lifecycle import startup from app.signaling.websocket_server import main as serve -if __name__ == "__main__": +def main(): startup() logger.info("Starting server...") asyncio.run(serve()) + + +if __name__ == "__main__": + main() diff --git a/app/signaling/websocket_server.py b/app/signaling/websocket_server.py index ccc44de..a2707cc 100644 --- a/app/signaling/websocket_server.py +++ b/app/signaling/websocket_server.py @@ -7,7 +7,7 @@ import websockets from loguru import logger from app.webrtc.peer_session import PeerSession -from configs.default import WS_HOST, WS_MAX_SIZE, WS_PORT +from configs.load import WS_HOST, WS_MAX_SIZE, WS_PORT async def handle_client(websocket): diff --git a/app/webrtc/video_receiver.py b/app/webrtc/video_receiver.py index 766f509..bb8230b 100644 --- a/app/webrtc/video_receiver.py +++ b/app/webrtc/video_receiver.py @@ -10,7 +10,7 @@ from loguru import logger from app.audio.rep_announcer import RepAnnouncer from app.exercises.dead_bug.detector import DeadBugDetector from app.rendering.window_display import close_window, is_esc_pressed, show_frame -from configs.default import ( +from configs.load import ( EXTENSION_CONFIRM_FRAMES, MODEL_PATH, PREFER_GPU, diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..04f3d04 --- /dev/null +++ b/config.yaml @@ -0,0 +1,29 @@ +# PoseFit Server Configuration +# ============================ + +server: + host: "0.0.0.0" + port: 8765 + max_ws_size: 10485760 # 10 MB + +video: + process_every_n_frames: 1 + +model: + path: "" # empty = auto-detect pose_models/pose_landmarker_full.task + prefer_gpu: true + +dead_bug: + visibility_threshold: 0.45 + extension_confirm_frames: 4 + reset_confirm_frames: 3 + +audio: + rep_announcer_enabled: true + rep_announcer_rate: 185 + rep_announcer_volume: 1.0 + +logging: + dir: logs + rotation: "20 MB" + retention: "14 days" diff --git a/configs/default.py b/configs/default.py deleted file mode 100644 index 0c2a7ba..0000000 --- a/configs/default.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -import os -from pathlib import Path - -# ── Server ────────────────────────────────────────────────────────────────── -WS_HOST = os.getenv("POSEFIT_WS_HOST", "0.0.0.0") -WS_PORT = int(os.getenv("POSEFIT_WS_PORT", "8765")) -WS_MAX_SIZE = int(os.getenv("POSEFIT_WS_MAX_SIZE", str(10 * 1024 * 1024))) - -# ── Video processing ──────────────────────────────────────────────────────── -PROCESS_EVERY_N_FRAMES = max(1, int(os.getenv("POSEFIT_PROCESS_EVERY_N_FRAMES", "1"))) - -# ── Model ─────────────────────────────────────────────────────────────────── -MODEL_DIR: Path = Path(__file__).resolve().parent.parent / "pose_models" -MODEL_PATH = os.getenv("POSEFIT_MODEL_PATH", str(MODEL_DIR / "pose_landmarker_full.task")) -PREFER_GPU = os.getenv("POSEFIT_PREFER_GPU", "1") not in ("0", "false", "False") - -# ── Dead bug exercise ─────────────────────────────────────────────────────── -VISIBILITY_THRESHOLD = float(os.getenv("POSEFIT_VISIBILITY_THRESHOLD", "0.45")) -EXTENSION_CONFIRM_FRAMES = int(os.getenv("POSEFIT_EXTENSION_CONFIRM_FRAMES", "4")) -RESET_CONFIRM_FRAMES = int(os.getenv("POSEFIT_RESET_CONFIRM_FRAMES", "3")) - -# ── Audio ─────────────────────────────────────────────────────────────────── -REP_ANNOUNCER_ENABLED = os.getenv("POSEFIT_REP_ANNOUNCER_ENABLED", "1") not in ("0", "false", "False") -REP_ANNOUNCER_RATE = int(os.getenv("POSEFIT_REP_ANNOUNCER_RATE", "185")) -REP_ANNOUNCER_VOLUME = float(os.getenv("POSEFIT_REP_ANNOUNCER_VOLUME", "1.0")) - -# ── Logging ───────────────────────────────────────────────────────────────── -LOG_DIR: Path = Path(__file__).resolve().parent.parent / "logs" -LOG_ROTATION = os.getenv("POSEFIT_LOG_ROTATION", "20 MB") -LOG_RETENTION = os.getenv("POSEFIT_LOG_RETENTION", "14 days") diff --git a/configs/load.py b/configs/load.py new file mode 100644 index 0000000..2cd5453 --- /dev/null +++ b/configs/load.py @@ -0,0 +1,83 @@ +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") diff --git a/requirements.txt b/requirements.txt index 64b1034..6a5fef2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ numpy>=1.26,<2 loguru>=0.7.0 mediapipe==0.10.21 pyttsx3>=2.99 +pyyaml>=6.0 diff --git a/tests/test_dead_bug_rules.py b/tests/test_dead_bug_rules.py index 8923e1e..86d48d9 100644 --- a/tests/test_dead_bug_rules.py +++ b/tests/test_dead_bug_rules.py @@ -1,9 +1,7 @@ from __future__ import annotations -from app.exercises.dead_bug.metrics import calculate_metrics from app.exercises.dead_bug.rules import detect_diagonal_extension, has_required_visibility, is_ready_position -from app.exercises.dead_bug.state_machine import DeadBugStateMachine -from app.exercises.dead_bug.types import DeadBugMetrics, DeadBugPhase, Point +from app.exercises.dead_bug.types import DeadBugMetrics, Point class TestDeadBugRules: