2026-07-21 19:43:49 +08:00
|
|
|
"""Assemble configuration, the fall pipeline worker, and the PyQt window.
|
|
|
|
|
|
|
|
|
|
The GUI never computes events. A background worker owns the video source and the
|
|
|
|
|
``FallPipeline`` and emits already-decided ``FrameAnalysis`` objects; the window
|
|
|
|
|
only renders them. Settings edits become a new immutable runtime snapshot at the
|
|
|
|
|
next start, so screenshots and logs stay traceable to the actual config version.
|
|
|
|
|
"""
|
|
|
|
|
|
2026-07-21 22:44:09 +08:00
|
|
|
import json
|
2026-07-21 19:43:49 +08:00
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
from dataclasses import replace
|
|
|
|
|
from pathlib import Path
|
2026-07-21 22:44:09 +08:00
|
|
|
from typing import Dict, Optional
|
2026-07-21 19:43:49 +08:00
|
|
|
|
|
|
|
|
from PyQt5 import QtCore, QtWidgets
|
|
|
|
|
|
2026-07-21 19:49:20 +08:00
|
|
|
from v1.alerts import AlertDispatcher, EventArtifactWriter
|
2026-07-21 19:43:49 +08:00
|
|
|
from v1.config import AppConfig, EventConfig, load_config
|
|
|
|
|
from v1.pipeline import FallPipeline
|
|
|
|
|
from v1.pose import PoseAdapter
|
|
|
|
|
from v1.video_source import SourceMode, SourceStatus, VideoSource
|
|
|
|
|
from v1.view_model import SettingsDraft, build_monitor_view
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FrameWorker(QtCore.QThread):
|
|
|
|
|
"""Read one source, run the pipeline, and emit render-only analyses."""
|
|
|
|
|
|
|
|
|
|
frame_ready = QtCore.pyqtSignal(object)
|
|
|
|
|
|
|
|
|
|
def __init__(self, config: AppConfig, pose_adapter: PoseAdapter) -> None:
|
|
|
|
|
super().__init__()
|
|
|
|
|
self._config = config
|
|
|
|
|
self._pose_adapter = pose_adapter
|
|
|
|
|
self._stop = False
|
|
|
|
|
|
|
|
|
|
def run(self) -> None:
|
2026-07-21 22:56:14 +08:00
|
|
|
# OpenCV reads this env var when it opens the stream, so set it first.
|
|
|
|
|
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = self._config.ffmpeg_capture_options
|
2026-07-21 20:54:34 +08:00
|
|
|
mode = SourceMode.STREAM if self._config.source_mode == "stream" else SourceMode.REPLAY
|
2026-07-21 19:43:49 +08:00
|
|
|
source = VideoSource(self._config.source_url, mode=mode)
|
|
|
|
|
pipeline = FallPipeline.from_config(self._config, self._pose_adapter)
|
|
|
|
|
try:
|
|
|
|
|
while not self._stop:
|
|
|
|
|
packet = source.read()
|
|
|
|
|
analysis = pipeline.process(packet)
|
|
|
|
|
self.frame_ready.emit(analysis)
|
|
|
|
|
if packet.status in (SourceStatus.CLOSED, SourceStatus.EOF):
|
|
|
|
|
break
|
|
|
|
|
self.msleep(5)
|
|
|
|
|
finally:
|
|
|
|
|
source.close()
|
|
|
|
|
|
|
|
|
|
def stop(self) -> None:
|
|
|
|
|
self._stop = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _running_config(config: AppConfig, draft: SettingsDraft) -> AppConfig:
|
|
|
|
|
values = draft.running_values
|
|
|
|
|
event = EventConfig(
|
|
|
|
|
keypoint_confidence_threshold=values["keypoint_confidence_threshold"],
|
|
|
|
|
suspect_window_seconds=values["suspect_window_seconds"],
|
|
|
|
|
confirm_window_seconds=values["confirm_window_seconds"],
|
|
|
|
|
recovery_window_seconds=values["recovery_window_seconds"],
|
|
|
|
|
cooldown_seconds=values["cooldown_seconds"],
|
|
|
|
|
)
|
|
|
|
|
return replace(
|
|
|
|
|
config, event=event, confidence_threshold=values["model_confidence_threshold"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _draft_from_config(config: AppConfig) -> SettingsDraft:
|
|
|
|
|
return SettingsDraft(
|
|
|
|
|
{
|
|
|
|
|
"keypoint_confidence_threshold": config.event.keypoint_confidence_threshold,
|
|
|
|
|
"suspect_window_seconds": config.event.suspect_window_seconds,
|
|
|
|
|
"confirm_window_seconds": config.event.confirm_window_seconds,
|
|
|
|
|
"recovery_window_seconds": config.event.recovery_window_seconds,
|
|
|
|
|
"cooldown_seconds": config.event.cooldown_seconds,
|
|
|
|
|
"model_confidence_threshold": config.confidence_threshold,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApplicationController:
|
|
|
|
|
"""Own the window, draft and worker lifecycle without doing event logic."""
|
|
|
|
|
|
2026-07-21 22:44:09 +08:00
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
config: AppConfig,
|
|
|
|
|
pose_adapter: PoseAdapter,
|
|
|
|
|
config_path: Optional[str] = None,
|
|
|
|
|
camera: Optional[Dict] = None,
|
|
|
|
|
) -> None:
|
2026-07-21 19:43:49 +08:00
|
|
|
from v1.gui import MainWindow
|
|
|
|
|
|
|
|
|
|
self._config = config
|
|
|
|
|
self._pose_adapter = pose_adapter
|
|
|
|
|
self._draft = _draft_from_config(config)
|
|
|
|
|
env_ready = bool(config.source_url)
|
|
|
|
|
model_summary = "{0} · {1}…".format(config.model_path.name, config.model_sha256[:12])
|
2026-07-21 22:44:09 +08:00
|
|
|
self.window = MainWindow(
|
|
|
|
|
self._draft, env_ready, model_summary, config.source_id, config_path, camera
|
|
|
|
|
)
|
2026-07-21 19:43:49 +08:00
|
|
|
self.window.monitor.start_requested.connect(self.start)
|
|
|
|
|
self.window.monitor.stop_requested.connect(self.stop)
|
|
|
|
|
self._worker: Optional[FrameWorker] = None
|
2026-07-21 19:49:20 +08:00
|
|
|
self._dispatcher: Optional[AlertDispatcher] = None
|
2026-07-21 19:43:49 +08:00
|
|
|
|
|
|
|
|
def start(self) -> None:
|
|
|
|
|
if self._worker is not None:
|
|
|
|
|
return
|
2026-07-21 19:49:20 +08:00
|
|
|
from v1.gui import QtAlertSink
|
|
|
|
|
|
2026-07-21 19:43:49 +08:00
|
|
|
self._draft.start_monitoring()
|
|
|
|
|
running = _running_config(self._config, self._draft)
|
2026-07-21 20:54:34 +08:00
|
|
|
self._pose_adapter.set_confidence_threshold(running.confidence_threshold)
|
2026-07-21 19:49:20 +08:00
|
|
|
writer = EventArtifactWriter(running.event_dir, running.source_id)
|
|
|
|
|
self._dispatcher = AlertDispatcher(writer, QtAlertSink(self.window))
|
2026-07-21 19:43:49 +08:00
|
|
|
worker = FrameWorker(running, self._pose_adapter)
|
|
|
|
|
worker.frame_ready.connect(self._on_frame)
|
|
|
|
|
worker.finished.connect(self._on_finished)
|
|
|
|
|
self._worker = worker
|
|
|
|
|
self.window.monitor.set_running(True)
|
|
|
|
|
worker.start()
|
|
|
|
|
|
|
|
|
|
def stop(self) -> None:
|
|
|
|
|
if self._worker is not None:
|
|
|
|
|
self._worker.stop()
|
|
|
|
|
|
|
|
|
|
def _on_frame(self, analysis) -> None:
|
|
|
|
|
view = build_monitor_view(
|
|
|
|
|
analysis, self._draft.running_values["keypoint_confidence_threshold"]
|
|
|
|
|
)
|
|
|
|
|
self.window.monitor.render_view(analysis.packet.image, view)
|
2026-07-21 19:49:20 +08:00
|
|
|
if self._dispatcher is not None:
|
|
|
|
|
self._dispatcher.dispatch(analysis.events, analysis.packet.image, view)
|
2026-07-21 19:43:49 +08:00
|
|
|
|
|
|
|
|
def _on_finished(self) -> None:
|
|
|
|
|
self._worker = None
|
2026-07-21 19:49:20 +08:00
|
|
|
self._dispatcher = None
|
2026-07-21 19:43:49 +08:00
|
|
|
self.window.monitor.set_running(False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(config_path: Optional[str] = None) -> int:
|
|
|
|
|
path = Path(config_path or os.environ.get("SILVER_POSE_CONFIG", "v1/config.local.json"))
|
|
|
|
|
config = load_config(path)
|
|
|
|
|
pose_adapter = PoseAdapter(
|
|
|
|
|
config.model_path, config.model_sha256, config.confidence_threshold
|
|
|
|
|
)
|
2026-07-21 22:44:09 +08:00
|
|
|
camera = _camera_fields(path)
|
2026-07-21 19:43:49 +08:00
|
|
|
app = QtWidgets.QApplication(sys.argv)
|
2026-07-21 22:44:09 +08:00
|
|
|
controller = ApplicationController(config, pose_adapter, str(path), camera)
|
2026-07-21 19:43:49 +08:00
|
|
|
controller.window.show()
|
|
|
|
|
return app.exec_()
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 22:44:09 +08:00
|
|
|
def _camera_fields(path: Path) -> Dict:
|
|
|
|
|
"""Pre-fill the settings camera form from a structured local source, if any."""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
source = json.loads(Path(path).read_text(encoding="utf-8")).get("source", {})
|
|
|
|
|
except (OSError, ValueError):
|
|
|
|
|
return {}
|
|
|
|
|
if "host" not in source:
|
|
|
|
|
return {}
|
2026-07-21 22:56:14 +08:00
|
|
|
fields = ("host", "port", "channel", "username", "password", "transport", "timeout_seconds", "low_latency")
|
|
|
|
|
return {k: source.get(k) for k in fields if k in source}
|
2026-07-21 22:44:09 +08:00
|
|
|
|
|
|
|
|
|
2026-07-21 19:43:49 +08:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|