Separate config models from loader
- configs/models.py: AppConfig and all section dataclasses - configs/load.py: pure loading logic (yaml, env overrides) - config = load_config() singleton for consumers
This commit is contained in:
+63
-73
@@ -1,97 +1,87 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
import dataclasses
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from configs.models import (
|
||||||
|
AppConfig,
|
||||||
|
AudioConfig,
|
||||||
|
DeadBugConfig,
|
||||||
|
LoggingConfig,
|
||||||
|
ModelConfig,
|
||||||
|
ServerConfig,
|
||||||
|
VideoConfig,
|
||||||
|
)
|
||||||
|
|
||||||
@dataclass
|
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
class ServerConfig:
|
|
||||||
host: str = "0.0.0.0"
|
|
||||||
port: int = 8765
|
|
||||||
max_ws_size: int = 10_485_760
|
|
||||||
|
|
||||||
|
_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"),
|
||||||
|
}
|
||||||
|
|
||||||
@dataclass
|
_SECTION_CLASS = {
|
||||||
class VideoConfig:
|
"server": ServerConfig,
|
||||||
process_every_n_frames: int = 1
|
"video": VideoConfig,
|
||||||
|
"model": ModelConfig,
|
||||||
|
"dead_bug": DeadBugConfig,
|
||||||
@dataclass
|
"audio": AudioConfig,
|
||||||
class ModelConfig:
|
"logging": LoggingConfig,
|
||||||
path: str = ""
|
}
|
||||||
prefer_gpu: bool = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def resolved_path(self) -> str:
|
|
||||||
if self.path:
|
|
||||||
return self.path
|
|
||||||
return str(Path(__file__).resolve().parent.parent / "pose_models" / "pose_landmarker_full.task")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DeadBugConfig:
|
|
||||||
visibility_threshold: float = 0.45
|
|
||||||
extension_confirm_frames: int = 4
|
|
||||||
reset_confirm_frames: int = 3
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AudioConfig:
|
|
||||||
rep_announcer_enabled: bool = True
|
|
||||||
rep_announcer_rate: int = 185
|
|
||||||
rep_announcer_volume: float = 1.0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LoggingConfig:
|
|
||||||
dir: str = "logs"
|
|
||||||
rotation: str = "20 MB"
|
|
||||||
retention: str = "14 days"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dir_path(self) -> Path:
|
|
||||||
return Path(__file__).resolve().parent.parent / self.dir
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AppConfig:
|
|
||||||
server: ServerConfig = field(default_factory=ServerConfig)
|
|
||||||
video: VideoConfig = field(default_factory=VideoConfig)
|
|
||||||
model: ModelConfig = field(default_factory=ModelConfig)
|
|
||||||
dead_bug: DeadBugConfig = field(default_factory=DeadBugConfig)
|
|
||||||
audio: AudioConfig = field(default_factory=AudioConfig)
|
|
||||||
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
|
||||||
|
|
||||||
|
|
||||||
def _dict_to_dataclass(cls: type, data: dict[str, Any] | None) -> dict[str, Any]:
|
def _dict_to_dataclass(cls: type, data: dict[str, Any] | None) -> dict[str, Any]:
|
||||||
"""Convert a dict to dataclass constructor kwargs, using only known fields."""
|
|
||||||
import dataclasses
|
|
||||||
if data is None:
|
if data is None:
|
||||||
return {}
|
return {}
|
||||||
fields = {f.name for f in dataclasses.fields(cls)}
|
field_names = {f.name for f in dataclasses.fields(cls)}
|
||||||
return {k: v for k, v in data.items() if k in fields}
|
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:
|
def load_config(config_path: str | Path | None = None) -> AppConfig:
|
||||||
if config_path is None:
|
if config_path is None:
|
||||||
config_path = Path(__file__).resolve().parent.parent / "config.yaml"
|
config_path = _PROJECT_ROOT / "config.yaml"
|
||||||
|
|
||||||
raw: dict[str, Any] = {}
|
raw = _read_yaml(Path(config_path))
|
||||||
if Path(config_path).exists():
|
_apply_env_overrides(raw)
|
||||||
with open(config_path, encoding="utf-8") as f:
|
|
||||||
raw = yaml.safe_load(f) or {}
|
|
||||||
|
|
||||||
return AppConfig(
|
return AppConfig(**{
|
||||||
server=ServerConfig(**_dict_to_dataclass(ServerConfig, raw.get("server"))),
|
section: cls(**_dict_to_dataclass(cls, raw.get(section)))
|
||||||
video=VideoConfig(**_dict_to_dataclass(VideoConfig, raw.get("video"))),
|
for section, cls in _SECTION_CLASS.items()
|
||||||
model=ModelConfig(**_dict_to_dataclass(ModelConfig, raw.get("model"))),
|
})
|
||||||
dead_bug=DeadBugConfig(**_dict_to_dataclass(DeadBugConfig, raw.get("dead_bug"))),
|
|
||||||
audio=AudioConfig(**_dict_to_dataclass(AudioConfig, raw.get("audio"))),
|
|
||||||
logging=LoggingConfig(**_dict_to_dataclass(LoggingConfig, raw.get("logging"))),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ServerConfig:
|
||||||
|
host: str = "0.0.0.0"
|
||||||
|
port: int = 8765
|
||||||
|
max_ws_size: int = 10_485_760
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VideoConfig:
|
||||||
|
process_every_n_frames: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ModelConfig:
|
||||||
|
path: str = ""
|
||||||
|
prefer_gpu: bool = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def resolved_path(self) -> str:
|
||||||
|
if self.path:
|
||||||
|
return self.path
|
||||||
|
return str(Path(__file__).resolve().parent.parent / "pose_models" / "pose_landmarker_full.task")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeadBugConfig:
|
||||||
|
visibility_threshold: float = 0.45
|
||||||
|
extension_confirm_frames: int = 4
|
||||||
|
reset_confirm_frames: int = 3
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AudioConfig:
|
||||||
|
rep_announcer_enabled: bool = True
|
||||||
|
rep_announcer_rate: int = 185
|
||||||
|
rep_announcer_volume: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LoggingConfig:
|
||||||
|
dir: str = "logs"
|
||||||
|
rotation: str = "20 MB"
|
||||||
|
retention: str = "14 days"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dir_path(self) -> Path:
|
||||||
|
return Path(__file__).resolve().parent.parent / self.dir
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AppConfig:
|
||||||
|
server: ServerConfig = field(default_factory=ServerConfig)
|
||||||
|
video: VideoConfig = field(default_factory=VideoConfig)
|
||||||
|
model: ModelConfig = field(default_factory=ModelConfig)
|
||||||
|
dead_bug: DeadBugConfig = field(default_factory=DeadBugConfig)
|
||||||
|
audio: AudioConfig = field(default_factory=AudioConfig)
|
||||||
|
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
||||||
Reference in New Issue
Block a user