infinicode 2.8.64 → 2.8.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -433,6 +433,31 @@ class PreviewAgent:
433
433
  self._motion_capture_lock = asyncio.Lock()
434
434
  self._mesh_bootstrap_ready = False
435
435
  self._next_mesh_bootstrap = 0.0
436
+ self._reported_pipeline: set[tuple[str, str]] = set()
437
+
438
+ async def _report_pipeline(self, stage: str, status: str = "ok", message: str = "",
439
+ details: Optional[dict] = None, once: bool = True) -> None:
440
+ """Report a real robot-side transition for the Park test timeline."""
441
+ if not self._session or not self.device_id or not self.device_token:
442
+ return
443
+ session_id = self._vision_session_id or self._current_state.session_id
444
+ key = (session_id or "idle", stage)
445
+ if once and key in self._reported_pipeline:
446
+ return
447
+ try:
448
+ response = await self._session.post(
449
+ f"{self.scheduler_url.rstrip('/')}/api/devices/{self.device_id}/pipeline-events",
450
+ headers={"Authorization": f"Bearer {self.device_token}"},
451
+ json={"stage": stage, "status": status, "message": message,
452
+ "session_id": session_id, "source": "preview_agent",
453
+ "details": details or {}},
454
+ timeout=5.0,
455
+ )
456
+ response.raise_for_status()
457
+ if once:
458
+ self._reported_pipeline.add(key)
459
+ except Exception as exc:
460
+ logger.debug("pipeline event %s was not accepted: %s", stage, exc)
436
461
 
437
462
  async def _ensure_mesh_identity(self) -> bool:
438
463
  if not _mesh_proxy_headers():
@@ -744,7 +769,7 @@ class PreviewAgent:
744
769
  dt = dt.replace(tzinfo=timezone.utc)
745
770
  return dt.timestamp()
746
771
 
747
- async def _end_vision_session(self) -> None:
772
+ async def _end_vision_session(self) -> None:
748
773
  # Tell the scheduler too, not just the local publisher/LiveKit
749
774
  # connection — otherwise the sessions row never gets ended_at set,
750
775
  # and the server's active_sessions count climbs forever until it
@@ -763,7 +788,8 @@ class PreviewAgent:
763
788
  )
764
789
  except Exception as e:
765
790
  logger.warning(f"failed to notify scheduler of session end: {e}")
766
- self._vision_session_id = None
791
+ await self._report_pipeline("session_ended", "ok", "Robot conversation loop stopped")
792
+ self._vision_session_id = None
767
793
  self._vision_trigger_in_flight = False
768
794
  self._vision_hard_deadline = 0.0
769
795
  self._vision_last_activity = 0.0
@@ -784,8 +810,16 @@ class PreviewAgent:
784
810
  self._mesh_bootstrap_ready = False
785
811
  self._next_mesh_bootstrap = 0.0
786
812
  if production_mode is not None:
787
- self.production_mode = production_mode
788
- if not production_mode and self._vision_session_id:
813
+ self.production_mode = production_mode
814
+ await self._report_pipeline("robot_online", message="Robot heartbeat accepted")
815
+ inventory = _get_device_inventory()
816
+ real_video = [d for d in inventory.get("video", []) if str(d.get("id", "")).lower() not in ("auto", "none")]
817
+ real_inputs = [d for d in inventory.get("audio_input", []) if str(d.get("id", "")).lower() not in ("default", "none")]
818
+ real_outputs = [d for d in inventory.get("audio_output", []) if str(d.get("id", "")).lower() not in ("default", "none")]
819
+ await self._report_pipeline("camera_ready", "ok" if real_video else "blocked", f"{len(real_video)} camera device(s) detected")
820
+ await self._report_pipeline("microphone_ready", "ok" if real_inputs else "blocked", f"{len(real_inputs)} microphone device(s) detected")
821
+ await self._report_pipeline("speaker_ready", "ok" if real_outputs else "blocked", f"{len(real_outputs)} speaker device(s) detected")
822
+ if not production_mode and self._vision_session_id:
789
823
  await self._end_vision_session()
790
824
  try:
791
825
  await asyncio.wait_for(self._shutdown.wait(), timeout=self.heartbeat_interval)
@@ -923,8 +957,9 @@ class PreviewAgent:
923
957
  if not self.device_id or not self.device_token or not self._session:
924
958
  logger.warning("vision motion event received but not enrolled yet — ignoring")
925
959
  return
926
- self._vision_trigger_in_flight = True
927
- logger.info("motion detected by RoboVisionAI_PI requesting a session")
960
+ self._vision_trigger_in_flight = True
961
+ await self._report_pipeline("motion_detected", "ok", f"Motion received from {payload.get('source', 'camera')}", once=False)
962
+ logger.info("motion detected by RoboVisionAI_PI — requesting a session")
928
963
  # The cue is feedback only; never block session allocation on speaker
929
964
  await self._stop_motion_capture()
930
965
  # DirectShow can return a stale handle for a short interval after
@@ -943,9 +978,10 @@ class PreviewAgent:
943
978
  )
944
979
  r.raise_for_status()
945
980
  data = r.json()
946
- except Exception as e:
947
- logger.error(f"request-session failed: {e}")
948
- self._vision_trigger_in_flight = False
981
+ except Exception as e:
982
+ logger.error(f"request-session failed: {e}")
983
+ await self._report_pipeline("scheduler_session", "failed", f"Session request failed: {type(e).__name__}", once=False)
984
+ self._vision_trigger_in_flight = False
949
985
  return
950
986
  state = PreviewState(
951
987
  active=True,
@@ -960,8 +996,9 @@ class PreviewAgent:
960
996
  self._vision_trigger_in_flight = False
961
997
  self._vision_hard_deadline = now + self.vision_session_seconds
962
998
  self._vision_last_activity = now
963
- self._remote_session_ended = False
964
- self._current_state = state
999
+ self._remote_session_ended = False
1000
+ self._current_state = state
1001
+ await self._report_pipeline("scheduler_session", "ok", "Scheduler allocated a conversation session")
965
1002
  # If the scheduler returned a voice config, let the agent know by
966
1003
  # posting it to our local webhook endpoint. The preview agent itself
967
1004
  # does not consume it, but this makes the config observable locally
@@ -1006,9 +1043,10 @@ class LiveKitPublisher:
1006
1043
  # the camera is being handed between preview and voice sessions.
1007
1044
  self._last_motion_sample = 0.0
1008
1045
 
1009
- async def start(self) -> None:
1010
- self.room = self.rtc.Room()
1011
- await self.room.connect(self.url, self.token)
1046
+ async def start(self) -> None:
1047
+ self.room = self.rtc.Room()
1048
+ await self.room.connect(self.url, self.token)
1049
+ await self.agent._report_pipeline("livekit_join", "ok", "Robot joined the LiveKit room")
1012
1050
 
1013
1051
  # Register after signaling. Registering during Room.connect can invoke
1014
1052
  # callbacks while the native LiveKit participant state is incomplete;
@@ -1019,9 +1057,10 @@ class LiveKitPublisher:
1019
1057
  logger.info(f"track_subscribed: kind={track.kind} from={participant.identity} sid={publication.sid}")
1020
1058
  if track.kind != self.rtc.TrackKind.KIND_AUDIO:
1021
1059
  return
1022
- if participant.identity == self.room.local_participant.identity:
1023
- return
1024
- self._tasks.append(asyncio.create_task(self._play_remote_audio(track, publication.sid)))
1060
+ if participant.identity == self.room.local_participant.identity:
1061
+ return
1062
+ self._tasks.append(asyncio.create_task(self.agent._report_pipeline("tts_subscribed", "ok", "Subscribed to remote voice audio")))
1063
+ self._tasks.append(asyncio.create_task(self._play_remote_audio(track, publication.sid)))
1025
1064
 
1026
1065
  self.room.on("track_subscribed", _on_track_subscribed)
1027
1066
  logger.info("audio out: track_subscribed listener registered")
@@ -1073,15 +1112,17 @@ class LiveKitPublisher:
1073
1112
  self.video_track = self.rtc.LocalVideoTrack.create_video_track("camera", self.video_source)
1074
1113
  vopts = self.rtc.TrackPublishOptions()
1075
1114
  vopts.source = self.rtc.TrackSource.SOURCE_CAMERA
1076
- await self.room.local_participant.publish_track(self.video_track, vopts)
1077
- self.video_source.capture_frame(first_frame)
1115
+ await self.room.local_participant.publish_track(self.video_track, vopts)
1116
+ self.video_source.capture_frame(first_frame)
1117
+ await self.agent._report_pipeline("camera_published", "ok", "Camera track published")
1078
1118
 
1079
1119
  if audio_enabled:
1080
1120
  self.audio_source = self.rtc.AudioSource(48000, 1)
1081
1121
  self.audio_track = self.rtc.LocalAudioTrack.create_audio_track("microphone", self.audio_source)
1082
1122
  aopts = self.rtc.TrackPublishOptions()
1083
1123
  aopts.source = self.rtc.TrackSource.SOURCE_MICROPHONE
1084
- await self.room.local_participant.publish_track(self.audio_track, aopts)
1124
+ await self.room.local_participant.publish_track(self.audio_track, aopts)
1125
+ await self.agent._report_pipeline("microphone_published", "ok", "Microphone track published")
1085
1126
 
1086
1127
  # Register speaker playback (subscribe to the voice agent's TTS audio
1087
1128
  # track) BEFORE opening the camera. Camera open is a slow/occasionally
@@ -1217,11 +1258,12 @@ class LiveKitPublisher:
1217
1258
  async for frame in stream:
1218
1259
  af = frame.frame if hasattr(frame, "frame") else frame
1219
1260
  frame_count += 1
1220
- if frame_count == 1:
1221
- logger.info(
1222
- f"audio out: first frame received for {sid} "
1223
- f"(rate={af.sample_rate}, channels={getattr(af, 'num_channels', 1)})"
1224
- )
1261
+ if frame_count == 1:
1262
+ logger.info(
1263
+ f"audio out: first frame received for {sid} "
1264
+ f"(rate={af.sample_rate}, channels={getattr(af, 'num_channels', 1)})"
1265
+ )
1266
+ await self.agent._report_pipeline("playback_started", "ok", "First TTS audio frame reached the robot")
1225
1267
  in_rate = af.sample_rate
1226
1268
  in_channels = int(getattr(af, "num_channels", 1) or 1)
1227
1269
  raw = bytes(af.data)