Commit ·
51ad4f0
1
Parent(s): a946134
fix: enable face tracking by default and improve tap detection logging
Browse files- Face tracking now enabled by default (was disabled)
- Face tracking stays enabled during idle (was being disabled)
- Improved tap detector logging with IMU data verification
- Improved camera server logging with FPS stats
- Better error messages for missing head tracker dependencies
reachy_mini_ha_voice/camera_server.py
CHANGED
|
@@ -76,7 +76,7 @@ class MJPEGCameraServer:
|
|
| 76 |
|
| 77 |
# Face tracking state
|
| 78 |
self._head_tracker = None
|
| 79 |
-
self._face_tracking_enabled =
|
| 80 |
self._face_tracking_offsets: List[float] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
| 81 |
self._face_tracking_lock = threading.Lock()
|
| 82 |
|
|
@@ -103,10 +103,16 @@ class MJPEGCameraServer:
|
|
| 103 |
try:
|
| 104 |
from .head_tracker import HeadTracker
|
| 105 |
self._head_tracker = HeadTracker()
|
| 106 |
-
_LOGGER.info("Face tracking enabled")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
except Exception as e:
|
| 108 |
_LOGGER.warning("Failed to initialize head tracker: %s", e)
|
| 109 |
self._head_tracker = None
|
|
|
|
|
|
|
| 110 |
|
| 111 |
# Start frame capture thread
|
| 112 |
self._capture_thread = threading.Thread(
|
|
@@ -144,7 +150,10 @@ class MJPEGCameraServer:
|
|
| 144 |
|
| 145 |
def _capture_frames(self) -> None:
|
| 146 |
"""Background thread to capture frames from Reachy Mini and do face tracking."""
|
| 147 |
-
_LOGGER.info("Starting camera capture thread")
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
while self._running:
|
| 150 |
try:
|
|
@@ -152,6 +161,8 @@ class MJPEGCameraServer:
|
|
| 152 |
frame = self._get_camera_frame()
|
| 153 |
|
| 154 |
if frame is not None:
|
|
|
|
|
|
|
| 155 |
# Encode frame as JPEG for streaming
|
| 156 |
encode_params = [cv2.IMWRITE_JPEG_QUALITY, self.quality]
|
| 157 |
success, jpeg_data = cv2.imencode('.jpg', frame, encode_params)
|
|
@@ -167,6 +178,14 @@ class MJPEGCameraServer:
|
|
| 167 |
|
| 168 |
# Handle smooth interpolation when face lost
|
| 169 |
self._process_face_lost_interpolation(current_time)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
# Sleep to maintain target FPS
|
| 172 |
time.sleep(self._frame_interval)
|
|
|
|
| 76 |
|
| 77 |
# Face tracking state
|
| 78 |
self._head_tracker = None
|
| 79 |
+
self._face_tracking_enabled = True # Enabled by default for always-on face tracking
|
| 80 |
self._face_tracking_offsets: List[float] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
| 81 |
self._face_tracking_lock = threading.Lock()
|
| 82 |
|
|
|
|
| 103 |
try:
|
| 104 |
from .head_tracker import HeadTracker
|
| 105 |
self._head_tracker = HeadTracker()
|
| 106 |
+
_LOGGER.info("Face tracking enabled with YOLO head tracker")
|
| 107 |
+
except ImportError as e:
|
| 108 |
+
_LOGGER.warning("Failed to import head tracker (missing dependencies): %s", e)
|
| 109 |
+
_LOGGER.warning("Install with: pip install ultralytics supervision huggingface_hub")
|
| 110 |
+
self._head_tracker = None
|
| 111 |
except Exception as e:
|
| 112 |
_LOGGER.warning("Failed to initialize head tracker: %s", e)
|
| 113 |
self._head_tracker = None
|
| 114 |
+
else:
|
| 115 |
+
_LOGGER.info("Face tracking disabled by configuration")
|
| 116 |
|
| 117 |
# Start frame capture thread
|
| 118 |
self._capture_thread = threading.Thread(
|
|
|
|
| 150 |
|
| 151 |
def _capture_frames(self) -> None:
|
| 152 |
"""Background thread to capture frames from Reachy Mini and do face tracking."""
|
| 153 |
+
_LOGGER.info("Starting camera capture thread (face_tracking=%s)", self._face_tracking_enabled)
|
| 154 |
+
|
| 155 |
+
frame_count = 0
|
| 156 |
+
last_log_time = time.time()
|
| 157 |
|
| 158 |
while self._running:
|
| 159 |
try:
|
|
|
|
| 161 |
frame = self._get_camera_frame()
|
| 162 |
|
| 163 |
if frame is not None:
|
| 164 |
+
frame_count += 1
|
| 165 |
+
|
| 166 |
# Encode frame as JPEG for streaming
|
| 167 |
encode_params = [cv2.IMWRITE_JPEG_QUALITY, self.quality]
|
| 168 |
success, jpeg_data = cv2.imencode('.jpg', frame, encode_params)
|
|
|
|
| 178 |
|
| 179 |
# Handle smooth interpolation when face lost
|
| 180 |
self._process_face_lost_interpolation(current_time)
|
| 181 |
+
|
| 182 |
+
# Log stats every 10 seconds
|
| 183 |
+
if current_time - last_log_time >= 10.0:
|
| 184 |
+
fps = frame_count / (current_time - last_log_time)
|
| 185 |
+
_LOGGER.debug("Camera: %.1f fps, face_tracking=%s, head_tracker=%s",
|
| 186 |
+
fps, self._face_tracking_enabled, self._head_tracker is not None)
|
| 187 |
+
frame_count = 0
|
| 188 |
+
last_log_time = current_time
|
| 189 |
|
| 190 |
# Sleep to maintain target FPS
|
| 191 |
time.sleep(self._frame_interval)
|
reachy_mini_ha_voice/motion.py
CHANGED
|
@@ -85,17 +85,14 @@ class ReachyMiniMotion:
|
|
| 85 |
"""Called when wake word is detected.
|
| 86 |
|
| 87 |
Non-blocking: command sent to MovementManager.
|
| 88 |
-
|
| 89 |
"""
|
| 90 |
_LOGGER.debug("on_wakeup called")
|
| 91 |
if self._movement_manager is None:
|
| 92 |
_LOGGER.warning("on_wakeup: movement_manager is None, skipping motion")
|
| 93 |
return
|
| 94 |
|
| 95 |
-
#
|
| 96 |
-
if self._camera_server is not None:
|
| 97 |
-
self._camera_server.set_face_tracking_enabled(True)
|
| 98 |
-
_LOGGER.info("Face tracking enabled on wake word")
|
| 99 |
|
| 100 |
# Set listening state - face tracking will handle looking at user
|
| 101 |
self._movement_manager.set_state(RobotState.LISTENING)
|
|
@@ -188,7 +185,7 @@ class ReachyMiniMotion:
|
|
| 188 |
"""Called when returning to idle state.
|
| 189 |
|
| 190 |
Non-blocking: command sent to MovementManager.
|
| 191 |
-
|
| 192 |
"""
|
| 193 |
if self._movement_manager is None:
|
| 194 |
return
|
|
@@ -197,10 +194,8 @@ class ReachyMiniMotion:
|
|
| 197 |
self._movement_manager.set_state(RobotState.IDLE)
|
| 198 |
self._movement_manager.reset_to_neutral(duration=0.5)
|
| 199 |
|
| 200 |
-
#
|
| 201 |
-
|
| 202 |
-
self._camera_server.set_face_tracking_enabled(False)
|
| 203 |
-
_LOGGER.info("Face tracking disabled on idle")
|
| 204 |
|
| 205 |
_LOGGER.debug("Reachy Mini: Idle pose")
|
| 206 |
|
|
|
|
| 85 |
"""Called when wake word is detected.
|
| 86 |
|
| 87 |
Non-blocking: command sent to MovementManager.
|
| 88 |
+
Face tracking is always enabled, so robot will look at user automatically.
|
| 89 |
"""
|
| 90 |
_LOGGER.debug("on_wakeup called")
|
| 91 |
if self._movement_manager is None:
|
| 92 |
_LOGGER.warning("on_wakeup: movement_manager is None, skipping motion")
|
| 93 |
return
|
| 94 |
|
| 95 |
+
# Face tracking is always enabled, no need to enable it here
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
# Set listening state - face tracking will handle looking at user
|
| 98 |
self._movement_manager.set_state(RobotState.LISTENING)
|
|
|
|
| 185 |
"""Called when returning to idle state.
|
| 186 |
|
| 187 |
Non-blocking: command sent to MovementManager.
|
| 188 |
+
Face tracking remains enabled for continuous tracking.
|
| 189 |
"""
|
| 190 |
if self._movement_manager is None:
|
| 191 |
return
|
|
|
|
| 194 |
self._movement_manager.set_state(RobotState.IDLE)
|
| 195 |
self._movement_manager.reset_to_neutral(duration=0.5)
|
| 196 |
|
| 197 |
+
# Note: Face tracking remains enabled for continuous tracking
|
| 198 |
+
# This allows the robot to always look at the user when they approach
|
|
|
|
|
|
|
| 199 |
|
| 200 |
_LOGGER.debug("Reachy Mini: Idle pose")
|
| 201 |
|
reachy_mini_ha_voice/tap_detector.py
CHANGED
|
@@ -103,6 +103,16 @@ class TapDetector:
|
|
| 103 |
"(only available on Wireless version)"
|
| 104 |
)
|
| 105 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
except Exception as e:
|
| 107 |
_LOGGER.warning("Failed to check IMU availability: %s", e)
|
| 108 |
return
|
|
@@ -114,7 +124,8 @@ class TapDetector:
|
|
| 114 |
name="tap-detector"
|
| 115 |
)
|
| 116 |
self._thread.start()
|
| 117 |
-
_LOGGER.info("TapDetector started (threshold=%.1fg
|
|
|
|
| 118 |
|
| 119 |
def stop(self) -> None:
|
| 120 |
"""Stop tap detection thread."""
|
|
|
|
| 103 |
"(only available on Wireless version)"
|
| 104 |
)
|
| 105 |
return
|
| 106 |
+
|
| 107 |
+
# Verify accelerometer data is present
|
| 108 |
+
if "accelerometer" not in imu_data:
|
| 109 |
+
_LOGGER.warning(
|
| 110 |
+
"IMU accelerometer data not available - tap detection disabled"
|
| 111 |
+
)
|
| 112 |
+
return
|
| 113 |
+
|
| 114 |
+
_LOGGER.info("IMU available, accelerometer data: %s", imu_data.get("accelerometer"))
|
| 115 |
+
|
| 116 |
except Exception as e:
|
| 117 |
_LOGGER.warning("Failed to check IMU availability: %s", e)
|
| 118 |
return
|
|
|
|
| 124 |
name="tap-detector"
|
| 125 |
)
|
| 126 |
self._thread.start()
|
| 127 |
+
_LOGGER.info("TapDetector started (threshold=%.1fg, cooldown=%.1fs)",
|
| 128 |
+
self._threshold_g, self._cooldown_seconds)
|
| 129 |
|
| 130 |
def stop(self) -> None:
|
| 131 |
"""Stop tap detection thread."""
|