104 lines
2.9 KiB
Python
104 lines
2.9 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class Thresholds:
|
|
min_people: int = 5
|
|
min_dwell_seconds: int = 600
|
|
pause_timeout_seconds: int = 300
|
|
alert_cooldown_seconds: int = 600
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class StreamConfig:
|
|
rtsp_url: str = ""
|
|
sample_fps: float = 2.0
|
|
reconnect_backoff_seconds: float = 5.0
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class StaffConfig:
|
|
gallery_dir: str = "data/staff_gallery"
|
|
min_hits: int = 3
|
|
similarity_threshold: float = 0.9
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class WebhookConfig:
|
|
alert_url: str = ""
|
|
report_url: str = ""
|
|
timeout_seconds: float = 5.0
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class EventSinkConfig:
|
|
path: str = "logs/events.jsonl"
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class AppConfig:
|
|
camera_id: str
|
|
timezone: str = "Asia/Shanghai"
|
|
thresholds: Thresholds = field(default_factory=Thresholds)
|
|
stream: StreamConfig = field(default_factory=StreamConfig)
|
|
staff: StaffConfig = field(default_factory=StaffConfig)
|
|
event_sink: EventSinkConfig = field(default_factory=EventSinkConfig)
|
|
webhook: WebhookConfig = field(default_factory=WebhookConfig)
|
|
|
|
|
|
def _load_section(raw: dict, key: str, cls):
|
|
return cls(**raw.get(key, {}))
|
|
|
|
|
|
def resolve_config_path(config_path: str | Path | None = None) -> Path:
|
|
if config_path is not None:
|
|
return Path(config_path).expanduser().resolve()
|
|
project_root = Path(__file__).resolve().parent.parent
|
|
local_path = project_root / "config" / "108.local.yaml"
|
|
if local_path.exists():
|
|
return local_path
|
|
return project_root / "config" / "config.example.yaml"
|
|
|
|
|
|
def resolve_project_root(config_path: str | Path) -> Path:
|
|
return Path(config_path).expanduser().resolve().parent.parent
|
|
|
|
|
|
def resolve_project_path(project_root: str | Path, raw_path: str | Path) -> Path:
|
|
path = Path(raw_path)
|
|
if path.is_absolute():
|
|
return path.resolve()
|
|
return (Path(project_root).resolve() / path).resolve()
|
|
|
|
|
|
def load_config_document(path: Path) -> dict:
|
|
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
|
|
|
|
def load_config(path: Path) -> AppConfig:
|
|
raw = load_config_document(path)
|
|
return AppConfig(
|
|
camera_id=raw["camera_id"],
|
|
timezone=raw.get("timezone", "Asia/Shanghai"),
|
|
thresholds=_load_section(raw, "thresholds", Thresholds),
|
|
stream=_load_section(raw, "stream", StreamConfig),
|
|
staff=_load_section(raw, "staff", StaffConfig),
|
|
event_sink=_load_section(raw, "event_sink", EventSinkConfig),
|
|
webhook=_load_section(raw, "webhook", WebhookConfig),
|
|
)
|
|
|
|
|
|
def save_config_document(path: Path, raw: dict) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
temp_path = path.with_suffix(path.suffix + ".tmp")
|
|
temp_path.write_text(
|
|
yaml.safe_dump(raw, allow_unicode=True, sort_keys=False),
|
|
encoding="utf-8",
|
|
)
|
|
temp_path.replace(path)
|