feat: initialize managed portal
This commit is contained in:
148
managed/store_dwell_alert/app/main.py
Normal file
148
managed/store_dwell_alert/app/main.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from argparse import ArgumentParser
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
|
||||
from app.config import (
|
||||
AppConfig,
|
||||
load_config,
|
||||
resolve_config_path,
|
||||
resolve_project_path,
|
||||
resolve_project_root,
|
||||
)
|
||||
from app.modules.detector_tracker import YOLOTrackerAdapter
|
||||
from app.modules.dwell_engine import DwellEngine
|
||||
from app.modules.identity_resolver import IdentityResolver
|
||||
from app.modules.notifier import append_json_event
|
||||
from app.modules.staff_filter import StaffMatcher, load_staff_gallery
|
||||
from app.modules.stream_reader import RTSPFrameReader
|
||||
|
||||
|
||||
def build_app(config_path: str | Path | None = None) -> dict:
|
||||
resolved_config_path = resolve_config_path(config_path)
|
||||
config = load_config(resolved_config_path)
|
||||
project_root = resolve_project_root(resolved_config_path)
|
||||
event_sink_path = resolve_project_path(project_root, config.event_sink.path)
|
||||
gallery_dir = resolve_project_path(project_root, config.staff.gallery_dir)
|
||||
|
||||
return {
|
||||
"config": config,
|
||||
"config_path": resolved_config_path,
|
||||
"stream_reader": RTSPFrameReader(
|
||||
rtsp_url=config.stream.rtsp_url,
|
||||
sample_fps=config.stream.sample_fps,
|
||||
reconnect_backoff_seconds=config.stream.reconnect_backoff_seconds,
|
||||
),
|
||||
"tracker": YOLOTrackerAdapter(),
|
||||
"identity_resolver": IdentityResolver(
|
||||
pause_timeout_seconds=config.thresholds.pause_timeout_seconds,
|
||||
),
|
||||
"staff_matcher": StaffMatcher(
|
||||
gallery=load_staff_gallery(gallery_dir),
|
||||
similarity_threshold=config.staff.similarity_threshold,
|
||||
min_hits=config.staff.min_hits,
|
||||
),
|
||||
"dwell_engine": DwellEngine(
|
||||
camera_id=config.camera_id,
|
||||
min_people=config.thresholds.min_people,
|
||||
min_dwell_seconds=config.thresholds.min_dwell_seconds,
|
||||
pause_timeout_seconds=config.thresholds.pause_timeout_seconds,
|
||||
alert_cooldown_seconds=config.thresholds.alert_cooldown_seconds,
|
||||
),
|
||||
"notifier": append_json_event,
|
||||
"event_sink_path": event_sink_path,
|
||||
}
|
||||
|
||||
|
||||
def process_observations(
|
||||
app: dict,
|
||||
observations: list[dict],
|
||||
when,
|
||||
) -> list[dict]:
|
||||
events = app["dwell_engine"].process_observations(observations, when)
|
||||
for event in events:
|
||||
app["notifier"](app["event_sink_path"], event)
|
||||
return events
|
||||
|
||||
|
||||
def process_frame(app: dict, frame, when: datetime | None = None) -> list[dict]:
|
||||
observation_time = when or datetime.now().astimezone()
|
||||
tracks = app["tracker"].track(frame)
|
||||
observations = app["identity_resolver"].resolve(tracks, observation_time)
|
||||
observations = app["staff_matcher"].classify(observations)
|
||||
return process_observations(app, observations, observation_time)
|
||||
|
||||
|
||||
def run_once(app: dict) -> list[dict]:
|
||||
frame = app["stream_reader"].read()
|
||||
if frame is None:
|
||||
return []
|
||||
return process_frame(app, frame)
|
||||
|
||||
|
||||
def run_forever(app: dict, max_frames: int | None = None) -> int:
|
||||
processed_frames = 0
|
||||
try:
|
||||
while True:
|
||||
frame = app["stream_reader"].read()
|
||||
if frame is None:
|
||||
sleep(0.2)
|
||||
continue
|
||||
process_frame(app, frame)
|
||||
processed_frames += 1
|
||||
if max_frames is not None and processed_frames >= max_frames:
|
||||
break
|
||||
finally:
|
||||
app["stream_reader"].close()
|
||||
return processed_frames
|
||||
|
||||
|
||||
def parse_args() -> ArgumentParser:
|
||||
parser = ArgumentParser(description="Store dwell alert service bootstrap")
|
||||
parser.add_argument("--config", help="Path to YAML config file")
|
||||
parser.add_argument("--once", action="store_true", help="Read and process one frame")
|
||||
parser.add_argument(
|
||||
"--manage-api",
|
||||
action="store_true",
|
||||
help="Start the management API instead of the RTSP worker loop",
|
||||
)
|
||||
parser.add_argument("--host", default="0.0.0.0", help="Host for the management API")
|
||||
parser.add_argument("--port", type=int, default=18081, help="Port for the management API")
|
||||
parser.add_argument(
|
||||
"--max-frames",
|
||||
type=int,
|
||||
help="Maximum frames to process before exiting",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = parse_args()
|
||||
args = parser.parse_args()
|
||||
if args.manage_api:
|
||||
from app.manage_api import run_manage_api
|
||||
|
||||
run_manage_api(args.config, host=args.host, port=args.port)
|
||||
return 0
|
||||
|
||||
app = build_app(args.config)
|
||||
config: AppConfig = app["config"]
|
||||
print(f"Loaded config from {app['config_path']}")
|
||||
print(f"Camera: {config.camera_id}")
|
||||
print(f"RTSP: {config.stream.rtsp_url}")
|
||||
print(f"Event sink: {app['event_sink_path']}")
|
||||
print(f"Loaded {len(app['staff_matcher'].gallery)} staff gallery embedding(s)")
|
||||
if args.once:
|
||||
events = run_once(app)
|
||||
print(f"Generated {len(events)} event(s)")
|
||||
app["stream_reader"].close()
|
||||
return 0
|
||||
processed_frames = run_forever(app, max_frames=args.max_frames)
|
||||
print(f"Processed {processed_frames} frame(s)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user