feat(v1): add local alerts, screenshot and JSONL event log

AlertDispatcher de-duplicates by event_id and fires each side effect once
per CONFIRMED: annotated screenshot, one JSONL line (config_version,
source_id, no rtsp/credentials), sound and popup via injectable sink.
44 tests pass; real-frame artifact smoke verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-21 19:49:20 +08:00
co-authored by Claude Opus 4.8
parent 861532ffb0
commit afe7a5e781
8 changed files with 347 additions and 7 deletions
+9
View File
@@ -14,6 +14,7 @@ from typing import Optional
from PyQt5 import QtCore, QtWidgets
from v1.alerts import AlertDispatcher, EventArtifactWriter
from v1.config import AppConfig, EventConfig, load_config
from v1.pipeline import FallPipeline
from v1.pose import PoseAdapter
@@ -93,12 +94,17 @@ class ApplicationController:
self.window.monitor.start_requested.connect(self.start)
self.window.monitor.stop_requested.connect(self.stop)
self._worker: Optional[FrameWorker] = None
self._dispatcher: Optional[AlertDispatcher] = None
def start(self) -> None:
if self._worker is not None:
return
from v1.gui import QtAlertSink
self._draft.start_monitoring()
running = _running_config(self._config, self._draft)
writer = EventArtifactWriter(running.event_dir, running.source_id)
self._dispatcher = AlertDispatcher(writer, QtAlertSink(self.window))
worker = FrameWorker(running, self._pose_adapter)
worker.frame_ready.connect(self._on_frame)
worker.finished.connect(self._on_finished)
@@ -115,9 +121,12 @@ class ApplicationController:
analysis, self._draft.running_values["keypoint_confidence_threshold"]
)
self.window.monitor.render_view(analysis.packet.image, view)
if self._dispatcher is not None:
self._dispatcher.dispatch(analysis.events, analysis.packet.image, view)
def _on_finished(self) -> None:
self._worker = None
self._dispatcher = None
self.window.monitor.set_running(False)