"""Gesture recognizer interfaces. The production recognizer is intentionally thin in this skeleton. It validates that optional CV dependencies are available and keeps the protocol independent from any specific model implementation. """ from __future__ import annotations from dataclasses import dataclass from typing import Any, Protocol from .cameras import MotionAgentDependencyError from .events import GestureName, SkeletonEvent, SkeletonJoint, now_ms @dataclass(frozen=True) class GestureObservation: gesture: GestureName confidence: float intensity: float = 1.0 timestamp_ms: int | None = None class GestureRecognizer(Protocol): name: str def recognize(self, frame: Any) -> GestureObservation | None: """Return a gesture observation for the current frame.""" def debug_skeleton( self, frame: Any, *, camera_id: str, mode: str, matched_gesture: GestureName | None = None, confidence: float = 0.0, ) -> SkeletonEvent | None: """Return normalized skeleton debug data when available.""" class MediaPipeGestureRecognizer: name = "mediapipe-opencv" def __init__(self) -> None: try: import cv2 # noqa: F401 import mediapipe # noqa: F401 except ImportError as exc: raise MotionAgentDependencyError( "MediaPipe and OpenCV are required for live gesture recognition. " "Add mediapipe and opencv-python with uv, or use --dry-run for protocol testing." ) from exc def recognize(self, frame: Any) -> GestureObservation | None: _ = frame return None def debug_skeleton( self, frame: Any, *, camera_id: str, mode: str, matched_gesture: GestureName | None = None, confidence: float = 0.0, ) -> SkeletonEvent | None: _ = frame, camera_id, mode, matched_gesture, confidence return None class NullGestureRecognizer: name = "dry-run" def recognize(self, frame: Any) -> GestureObservation | None: _ = frame return None def debug_skeleton( self, frame: Any, *, camera_id: str, mode: str, matched_gesture: GestureName | None = None, confidence: float = 0.0, ) -> SkeletonEvent | None: _ = frame now = now_ms() sway = ((now // 250) % 6 - 2.5) * 0.015 joints = [ SkeletonJoint("head", 0.5, 0.18, 1.0), SkeletonJoint("neck", 0.5, 0.3, 1.0), SkeletonJoint("left_shoulder", 0.38, 0.34, 1.0), SkeletonJoint("right_shoulder", 0.62, 0.34, 1.0), SkeletonJoint("left_elbow", 0.31 + sway, 0.48, 0.95), SkeletonJoint("right_elbow", 0.69 - sway, 0.48, 0.95), SkeletonJoint("left_wrist", 0.24 + sway, 0.62, 0.9), SkeletonJoint("right_wrist", 0.76 - sway, 0.62, 0.9), ] bones = [ ("head", "neck"), ("neck", "left_shoulder"), ("neck", "right_shoulder"), ("left_shoulder", "left_elbow"), ("left_elbow", "left_wrist"), ("right_shoulder", "right_elbow"), ("right_elbow", "right_wrist"), ] return SkeletonEvent( joints=joints, bones=bones, matched_gesture=matched_gesture, confidence=confidence, camera_id=camera_id, mode=mode, )