from __future__ import annotations import asyncio from collections.abc import Mapping, Sequence from concurrent.futures import ThreadPoolExecutor from io import BytesIO from typing import Any import pytest from cmvr_edge_ai.application import create_default_registry from cmvr_edge_ai.compiler import compile_pipeline from cmvr_edge_ai.config import load_config_data from cmvr_edge_ai.contracts import ( BoundingBox, Detection, DetectionAlert, ImageFrame, ) from cmvr_edge_ai.core import Envelope, Sink from cmvr_edge_ai.detection import ( DetectionModel, DetectionModelRegistry, DetectionModelSpec, ) from cmvr_edge_ai.plugins import PluginKind, PluginSpec class _AlwaysViolationModel(DetectionModel): def __init__(self) -> None: self.load_calls = 0 self.predict_calls = 0 self.close_calls = 0 def load(self) -> None: self.load_calls += 1 def predict( self, frame: ImageFrame, labels: tuple[str, ...], confidence: float, ) -> Sequence[Detection]: del frame, confidence self.predict_calls += 1 if "No-Helmet" not in labels: return () return ( Detection( "No-Helmet", 0.95, BoundingBox(1.0, 2.0, 20.0, 40.0), ), ) def close(self) -> None: self.close_calls += 1 class _CollectAlertSink(Sink): def __init__(self) -> None: self.alerts: list[Envelope[Any]] = [] async def consume( self, envelope: Envelope[Any], input_port: str = "alerts" ) -> None: del input_port self.alerts.append(envelope) async def _keep_event_loop_responsive() -> None: while True: await asyncio.sleep(0.01) def test_decoded_frames_flow_through_registered_model_and_repeat_gate() -> None: pillow_image = pytest.importorskip("PIL.Image") model = _AlwaysViolationModel() models = DetectionModelRegistry() models.register( DetectionModelSpec( model_id="fake-ppe@1", name="Fake PPE", supported_labels=("No-Helmet",), factory=lambda options: model, backend="fake", ) ) registry = create_default_registry( discover_entry_points=False, model_registry=models, ) sink = _CollectAlertSink() def sink_factory(node_id: str, params: Mapping[str, Any]) -> Sink: del node_id, params return sink registry.register( PluginSpec( plugin_id="test.alert_sink@1", kind=PluginKind.SINK, factory=sink_factory, inputs={"alerts": "DetectionAlert/v1"}, ) ) width, height = 64, 48 frames = ( ImageFrame(bytes((0, 0, 255)) * (width * height), width, height, "BGR8"), ImageFrame(bytes((0, 255, 0)) * (width * height), width, height, "BGR8"), ImageFrame(bytes((255, 0, 0)) * (width * height), width, height, "BGR8"), ) config = load_config_data( { "api_version": "cmvr.edge.ai/v1", "pipelines": { "ppe": { "nodes": { "frames": { "uses": "core.sequence_source@1", "with": { "items": list(frames), "schema_name": "ImageFrame", }, }, "detector": { "uses": "detection.model@1", "with": { "model": "fake-ppe@1", "detect_labels": ["No-Helmet"], "attach_frame": True, }, }, "gate": { "uses": "detection.repeat_gate@1", "with": { "time_source": "received", "alert_image": { "enabled": True, "jpeg_quality": 85, }, "rules": [ { "id": "no-helmet", "labels": ["No-Helmet"], "min_hits": 2, "window_ms": 10_000, "cooldown_ms": 30_000, } ], }, }, "sink": {"uses": "test.alert_sink@1"}, }, "edges": [ {"from": "frames.output", "to": "detector.frames"}, { "from": "detector.detections", "to": "gate.detections", }, {"from": "gate.alerts", "to": "sink.alerts"}, ], } }, } ) gate_health_details: list[str] = [] async def scenario() -> None: loop = asyncio.get_running_loop() executor = ThreadPoolExecutor(max_workers=2) loop.set_default_executor(executor) ticker = asyncio.create_task(_keep_event_loop_responsive()) try: compiled = compile_pipeline( config, "ppe", registry, metadata={"thread_executor": executor}, ) await compiled.runtime.run() gate_health = await compiled.runtime.nodes["gate"].health() gate_health_details.append(gate_health.detail) finally: ticker.cancel() await asyncio.gather(ticker, return_exceptions=True) executor.shutdown(wait=True, cancel_futures=True) loop._default_executor = None # type: ignore[attr-defined] asyncio.run(scenario()) assert model.load_calls == 1 assert model.predict_calls == 3 assert model.close_calls == 1 assert len(sink.alerts) == 1 alert = sink.alerts[0].payload assert isinstance(alert, DetectionAlert) assert alert.rule_id == "no-helmet" assert alert.hit_count == 2 assert alert.labels == ("No-Helmet",) assert sink.alerts[0].sequence == 1 assert alert.image is not None assert alert.image.media_type == "image/jpeg" assert (alert.image.width, alert.image.height) == (width, height) assert alert.image.data.startswith(b"\xff\xd8") with pillow_image.open(BytesIO(alert.image.data)) as decoded: decoded.load() assert decoded.format == "JPEG" assert decoded.mode == "RGB" assert decoded.size == (width, height) # This pixel lies outside the annotation and proves that the evidence # image came from sequence 1 (green), the frame that reached min_hits. red, green, blue = decoded.getpixel((width - 1, height - 1)) assert green > 200 assert red < 40 assert blue < 40 assert gate_health_details assert "processed_frames=3" in gate_health_details[0] assert "alerts_emitted=1" in gate_health_details[0] assert "alert_images_encoded=1" in gate_health_details[0]