release: bump version to 0.50.0

This commit is contained in:
rayd1o
2026-05-10 22:06:01 +08:00
parent e1984c7a35
commit 455b8360d0
80 changed files with 10936 additions and 298 deletions

118
motion_agent/recognizer.py Normal file
View File

@@ -0,0 +1,118 @@
"""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,
)