dj_reactor / shared /vision /base.py
RyeCatcher's picture
Publish DJ Reactor v2 β€” shared layer + Beat Spectrum control panel
694b576 verified
Raw
History Blame Contribute Delete
2.11 kB
"""Pluggable detection layer β€” shared across all robot apps.
A `Detector` turns a camera frame into an `AttentionResult` (is a person
present, are they engaged, how sure). Apps pick a detector by config and never
care about the implementation, the same way Cardinal's agent picks a `Brain`.
Concrete detectors live alongside this file (motion.py, vlm.py); apps add their
own by implementing the protocol.
This is deliberately separate from `vision/detector.py`'s object/bbox
detection β€” that answers "what objects are in frame", this answers "is the user
present and engaged". An app can use both.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable
import numpy as np
@dataclass
class AttentionResult:
"""A detector's read on the user, normalized across implementations."""
present: bool # is a person at the desk at all
engaged: bool # are they attending to their work (vs looked away)
confidence: float = 0.5 # 0..1 β€” how sure the detector is
source: str = "" # detector name, for UI/telemetry
detail: str = "" # short human label ("typing", "looked away", "no face")
extra: dict[str, Any] = field(default_factory=dict)
@property
def focused(self) -> bool:
"""Convenience: present AND engaged."""
return self.present and self.engaged
@runtime_checkable
class Detector(Protocol):
"""Frame -> AttentionResult. Stateful detectors keep history internally."""
name: str
def detect(self, frame: np.ndarray) -> AttentionResult:
"""Analyze one BGR frame."""
...
def reset(self) -> None:
"""Clear any internal state (called on session start)."""
...
def release(self) -> None:
"""Free resources (models, buffers)."""
...
class BaseDetector:
"""Optional base with no-op lifecycle, so detectors only implement detect()."""
name: str = "base"
def reset(self) -> None: # noqa: D102
pass
def release(self) -> None: # noqa: D102
pass