diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/radar.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/radar.py index 130ffac..0041031 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/radar.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/radar.py @@ -206,6 +206,7 @@ def generate_radar_json( return { "radar_points": radar_points, "radar_size": [radar_w, radar_h], + "detection_count": len(radar_points), } @@ -287,6 +288,7 @@ def radar_json_with_annotated_frame( return { "radar_points": radar_points, "radar_size": [radar_w, radar_h], + "detection_count": len(radar_points), "radar_base64": _encode_radar_to_base64(radar_image), } diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/manifest.json b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/manifest.json index fc1b54a..b6bba96 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/manifest.json +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/manifest.json @@ -96,7 +96,8 @@ }, "outputs": { "frames": "array", - "total_frames": "integer" + "total_frames": "integer", + "summary": "object" } }, { diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/plugin.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/plugin.py index ba28cae..2020e09 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/plugin.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/plugin.py @@ -59,6 +59,7 @@ def on_unload(self) -> None: # pragma: no cover # noqa: B027 from forgesyte_yolo_tracker.inference.radar import radar_json_with_annotated_frame from forgesyte_yolo_tracker.configs import load_model_config from forgesyte_yolo_tracker.utils.json_sanitize import sanitize_json +from forgesyte_yolo_tracker.utils.summary import compute_video_summary logger = logging.getLogger(__name__) @@ -310,12 +311,14 @@ def _run_video_tool( logger.info(f"Completed: {frame_index} frames") # v0.10.0: Sanitize output for JSON serialization + sanitized = sanitize_json({ + "total_frames": frame_index, + "frames": frame_results, + }) + sanitized["summary"] = compute_video_summary(sanitized["frames"]) return { "success": True, - "result": sanitize_json({ - "total_frames": frame_index, - "frames": frame_results, - }) + "result": sanitized, } @@ -486,10 +489,12 @@ def _tool_video_player_tracking( frame_index += 1 # v0.10.0: Sanitize output for JSON serialization - return sanitize_json({ + sanitized = sanitize_json({ "total_frames": frame_index, "frames": frame_results, }) + sanitized["summary"] = compute_video_summary(sanitized["frames"]) + return sanitized class Plugin(BasePlugin): # type: ignore[misc] diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/utils/__init__.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/utils/__init__.py index 7f00067..ed68e4b 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/utils/__init__.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/utils/__init__.py @@ -10,9 +10,11 @@ Custom forgeSYTE modules: - ball.py - Ball tracking (not annotating) - soccer_pitch.py - Soccer pitch drawing utilities +- summary.py - Video summary computation utilities """ from . import ball, soccer_pitch +from .summary import compute_video_summary # Lazy import to avoid torch/transformers at module load time def __getattr__(name: str): @@ -33,6 +35,8 @@ def __getattr__(name: str): "create_batches", "TeamClassifier", "ViewTransformer", + # Video summary + "compute_video_summary", # Custom forgeSYTE modules "ball", "soccer_pitch", diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/utils/summary.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/utils/summary.py new file mode 100644 index 0000000..d863699 --- /dev/null +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/utils/summary.py @@ -0,0 +1,59 @@ +"""Video summary computation utilities. + +Provides functions to compute aggregated metadata from video tool results. +""" + +import logging +from typing import Any, Dict, List + +logger = logging.getLogger(__name__) + + +# Registry pattern for detection extraction (avoid elif chains) +_DETECTION_FIELD_MAP: Dict[str, str] = { + "detections": "detections", + "tracked_objects": "tracked_objects", + "radar_points": "radar_points", +} + + +def _get_detections_from_frame(frame: Dict[str, Any]) -> List[Dict[str, Any]]: + """Extract detections list from frame using registry lookup. + + Args: + frame: Frame result dictionary + + Returns: + List of detection dicts, or empty list if not found + """ + for field_name in _DETECTION_FIELD_MAP.values(): + if field_name in frame: + return frame[field_name] + return [] + + +def compute_video_summary(frames: List[Dict[str, Any]]) -> Dict[str, int]: + """Compute aggregated summary metadata from video tool frames. + + Args: + frames: List of frame results from video processing + + Returns: + Dictionary with: + - detection_count: Total detections across all frames + - frame_count: Number of frames processed + """ + total_detection_count = 0 + + for frame in frames: + detections = _get_detections_from_frame(frame) + total_detection_count += len(detections) + + logger.info( + f"Video summary: {total_detection_count} detections across {len(frames)} frames" + ) + + return { + "detection_count": total_detection_count, + "frame_count": len(frames), + }