infinicode 2.8.100 → 2.8.102
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.
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +322 -57
- package/dist/robopark/agent-ctl.js +42 -26
- package/dist/robopark/robot-runtime.js +9 -0
- package/docs/ROBOPARK_PRODUCTION_MEMORY.md +26 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +47 -0
- package/packages/robopark/scheduler/preview_agent.py +70 -17
|
@@ -5019,6 +5019,53 @@ async def delete_character_preset(preset_id: str):
|
|
|
5019
5019
|
return {"ok": True}
|
|
5020
5020
|
|
|
5021
5021
|
|
|
5022
|
+
class PresetPosition(BaseModel):
|
|
5023
|
+
id: str
|
|
5024
|
+
nx: float
|
|
5025
|
+
ny: float
|
|
5026
|
+
|
|
5027
|
+
|
|
5028
|
+
class PresetPositionBatch(BaseModel):
|
|
5029
|
+
positions: List[PresetPosition]
|
|
5030
|
+
|
|
5031
|
+
|
|
5032
|
+
@app.put("/api/character-presets/positions")
|
|
5033
|
+
async def update_preset_positions(payload: PresetPositionBatch):
|
|
5034
|
+
"""Batch-update (id, nx, ny) for the Park map. nx/ny are the
|
|
5035
|
+
normalized [0,1] pad coordinates the dashboard uses to place each
|
|
5036
|
+
robot on the iso park. Bounded + persisted so an operator can
|
|
5037
|
+
Ctrl+drag a robot on the Park map and have its position survive
|
|
5038
|
+
a refresh, server restart, or robot reconnect.
|
|
5039
|
+
|
|
5040
|
+
Designed for small, infrequent updates (a single drag releases
|
|
5041
|
+
one robot, plus an occasional "reset all" tool). The endpoint
|
|
5042
|
+
intentionally ignores all other preset fields — only the
|
|
5043
|
+
coordinates move; voice stack, name, system prompt, etc. are
|
|
5044
|
+
owned by the character-preset editor and are not touched here.
|
|
5045
|
+
"""
|
|
5046
|
+
if not payload.positions:
|
|
5047
|
+
return {"updated": 0}
|
|
5048
|
+
now = datetime.utcnow().isoformat()
|
|
5049
|
+
updated = 0
|
|
5050
|
+
async with aiosqlite.connect(DB_PATH) as db:
|
|
5051
|
+
for p in payload.positions:
|
|
5052
|
+
if not p.id:
|
|
5053
|
+
continue
|
|
5054
|
+
# Clamp nx/ny to the visible pad rectangle so a stray
|
|
5055
|
+
# drag can't park a robot off the island. A small inner
|
|
5056
|
+
# margin (0.04) keeps the avatar fully on the grass and
|
|
5057
|
+
# clear of the gold wall on the perimeter.
|
|
5058
|
+
nx = max(0.04, min(0.96, float(p.nx)))
|
|
5059
|
+
ny = max(0.04, min(0.96, float(p.ny)))
|
|
5060
|
+
cur = await db.execute(
|
|
5061
|
+
"UPDATE character_presets SET nx = ?, ny = ?, updated_at = ? WHERE id = ?",
|
|
5062
|
+
(nx, ny, now, p.id),
|
|
5063
|
+
)
|
|
5064
|
+
updated += cur.rowcount or 0
|
|
5065
|
+
await db.commit()
|
|
5066
|
+
return {"updated": updated}
|
|
5067
|
+
|
|
5068
|
+
|
|
5022
5069
|
class RobotVoiceConfigUpdate(BaseModel):
|
|
5023
5070
|
character_preset_id: Optional[str] = None
|
|
5024
5071
|
voice_stack_id: Optional[str] = None
|
|
@@ -541,12 +541,17 @@ class PreviewAgent:
|
|
|
541
541
|
self.vision_trigger_cooldown = float(cfg.get("vision_trigger_cooldown", os.getenv("VISION_TRIGGER_COOLDOWN", DEFAULT_VISION_TRIGGER_COOLDOWN)))
|
|
542
542
|
self.vision_session_seconds = float(cfg.get("vision_session_seconds", os.getenv("VISION_SESSION_SECONDS", DEFAULT_VISION_SESSION_SECONDS)))
|
|
543
543
|
self.vision_silence_timeout = float(cfg.get("vision_silence_timeout", os.getenv("VISION_SILENCE_TIMEOUT", DEFAULT_VISION_SILENCE_TIMEOUT)))
|
|
544
|
-
self.local_camera_motion = str(
|
|
544
|
+
self.local_camera_motion = str(
|
|
545
545
|
# Production needs an always-on detector. The preview publisher
|
|
546
546
|
# remains the single camera owner when a session starts, so this
|
|
547
547
|
# does not require the separate OpenCV vision server.
|
|
548
548
|
cfg.get("local_camera_motion", os.getenv("LOCAL_CAMERA_MOTION", "true"))
|
|
549
|
-
).lower() in ("1", "true", "yes", "on")
|
|
549
|
+
).lower() in ("1", "true", "yes", "on")
|
|
550
|
+
# The character greeting is the production acknowledgement. A local
|
|
551
|
+
# motion beep serializes on the same ALSA output and delays allocation.
|
|
552
|
+
self.motion_cue_enabled = str(
|
|
553
|
+
cfg.get("motion_cue_enabled", os.getenv("ROBOPARK_MOTION_CUE", "false"))
|
|
554
|
+
).lower() in ("1", "true", "yes", "on")
|
|
550
555
|
|
|
551
556
|
self._shutdown = asyncio.Event()
|
|
552
557
|
self._task: Optional[asyncio.Task] = None
|
|
@@ -1178,20 +1183,19 @@ class PreviewAgent:
|
|
|
1178
1183
|
logger.warning("vision motion event received but not enrolled yet — ignoring")
|
|
1179
1184
|
return
|
|
1180
1185
|
self._vision_trigger_in_flight = True
|
|
1181
|
-
|
|
1186
|
+
# Observability must not add a scheduler round trip before the actual
|
|
1187
|
+
# session request. The reporter handles and logs its own errors.
|
|
1188
|
+
asyncio.create_task(self._report_pipeline(
|
|
1189
|
+
"motion_detected", "ok",
|
|
1190
|
+
f"Motion received from {payload.get('source', 'camera')}", once=False,
|
|
1191
|
+
))
|
|
1182
1192
|
logger.info("motion detected by RoboVisionAI_PI — requesting a session")
|
|
1183
|
-
#
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
# I/O. Camera/mic join and the preset greeting are the critical path.
|
|
1190
|
-
# The cue and TTS share one physical ALSA output. Finish and release
|
|
1191
|
-
# the cue before LiveKit can deliver the greeting; background playback
|
|
1192
|
-
# races aplay and makes greeting delivery intermittently fail busy.
|
|
1193
|
-
async with self._speaker_operation_lock:
|
|
1194
|
-
await asyncio.to_thread(_play_audio_effect, self.audio_playback_device, "motion")
|
|
1193
|
+
# RoboVision owns the physical camera and exposes a shared stream, so
|
|
1194
|
+
# there is no capture handle to tear down and no reason to sleep here.
|
|
1195
|
+
if self.motion_cue_enabled:
|
|
1196
|
+
logger.warning("ROBOPARK_MOTION_CUE is enabled; the diagnostic cue adds greeting latency")
|
|
1197
|
+
async with self._speaker_operation_lock:
|
|
1198
|
+
await asyncio.to_thread(_play_audio_effect, self.audio_playback_device, "motion")
|
|
1195
1199
|
try:
|
|
1196
1200
|
r = await self._session.post(
|
|
1197
1201
|
f"{self.scheduler_url.rstrip('/')}/api/devices/{self.device_id}/request-session",
|
|
@@ -1294,9 +1298,25 @@ class LiveKitPublisher:
|
|
|
1294
1298
|
)
|
|
1295
1299
|
self._speaker_playback_active = threading.Event()
|
|
1296
1300
|
self._speaker_gate_until = 0.0
|
|
1301
|
+
self._speaker_started_at = 0.0
|
|
1302
|
+
self._barge_in_until = 0.0
|
|
1303
|
+
self._echo_mic_floor = 0.0
|
|
1304
|
+
self._barge_in_candidate_frames = 0
|
|
1297
1305
|
self._speaker_echo_tail = max(
|
|
1298
1306
|
0.1, min(float(os.getenv("ROBOPARK_ECHO_TAIL_MS", "700")) / 1000.0, 2.0)
|
|
1299
1307
|
)
|
|
1308
|
+
self._adaptive_barge_in = str(
|
|
1309
|
+
os.getenv("ROBOPARK_ADAPTIVE_BARGE_IN", "true")
|
|
1310
|
+
).lower() in ("1", "true", "yes", "on")
|
|
1311
|
+
self._barge_in_min_peak = max(
|
|
1312
|
+
256, min(int(os.getenv("ROBOPARK_BARGE_IN_MIN_PEAK", "2200")), 20000)
|
|
1313
|
+
)
|
|
1314
|
+
self._barge_in_ratio = max(
|
|
1315
|
+
1.25, min(float(os.getenv("ROBOPARK_BARGE_IN_ECHO_RATIO", "2.4")), 8.0)
|
|
1316
|
+
)
|
|
1317
|
+
self._barge_in_hold = max(
|
|
1318
|
+
0.4, min(float(os.getenv("ROBOPARK_BARGE_IN_HOLD_MS", "1400")) / 1000.0, 3.0)
|
|
1319
|
+
)
|
|
1300
1320
|
# Motion sampling state belongs to the publisher instance. Keeping it
|
|
1301
1321
|
# initialized here prevents shutdown/reopen paths from raising while
|
|
1302
1322
|
# the camera is being handed between preview and voice sessions.
|
|
@@ -1553,6 +1573,10 @@ class LiveKitPublisher:
|
|
|
1553
1573
|
def _open_echo_gate() -> None:
|
|
1554
1574
|
if not self._half_duplex:
|
|
1555
1575
|
return
|
|
1576
|
+
if not self._speaker_playback_active.is_set():
|
|
1577
|
+
self._speaker_started_at = time.monotonic()
|
|
1578
|
+
self._echo_mic_floor = 0.0
|
|
1579
|
+
self._barge_in_candidate_frames = 0
|
|
1556
1580
|
self._speaker_playback_active.set()
|
|
1557
1581
|
self._speaker_gate_until = time.monotonic() + self._speaker_echo_tail
|
|
1558
1582
|
|
|
@@ -1841,11 +1865,40 @@ class LiveKitPublisher:
|
|
|
1841
1865
|
_pub_start = time.monotonic()
|
|
1842
1866
|
for off in range(0, len(data) - bytes_per_frame + 1, bytes_per_frame):
|
|
1843
1867
|
chunk = data[off:off + bytes_per_frame]
|
|
1868
|
+
now = time.monotonic()
|
|
1844
1869
|
echo_gate_active = self._half_duplex and (
|
|
1845
1870
|
self._speaker_playback_active.is_set()
|
|
1846
|
-
or
|
|
1871
|
+
or now < self._speaker_gate_until
|
|
1847
1872
|
)
|
|
1848
|
-
|
|
1873
|
+
# Learn the microphone's speaker-echo floor while output is
|
|
1874
|
+
# active. A nearby visitor speaking produces a fast peak well
|
|
1875
|
+
# above that floor; reopen the mic briefly so LiveKit VAD can
|
|
1876
|
+
# cancel normal TTS. The initial 300 ms remains protected.
|
|
1877
|
+
_, raw_peak = _pcm16_scale_and_peak(chunk, 1.0)
|
|
1878
|
+
if echo_gate_active and self._adaptive_barge_in:
|
|
1879
|
+
if self._echo_mic_floor <= 0:
|
|
1880
|
+
self._echo_mic_floor = float(raw_peak)
|
|
1881
|
+
else:
|
|
1882
|
+
self._echo_mic_floor = self._echo_mic_floor * 0.92 + raw_peak * 0.08
|
|
1883
|
+
threshold = max(
|
|
1884
|
+
self._barge_in_min_peak,
|
|
1885
|
+
int(self._echo_mic_floor * self._barge_in_ratio),
|
|
1886
|
+
)
|
|
1887
|
+
warmed = now - self._speaker_started_at >= 0.3
|
|
1888
|
+
if warmed and raw_peak >= threshold:
|
|
1889
|
+
self._barge_in_candidate_frames += 1
|
|
1890
|
+
else:
|
|
1891
|
+
self._barge_in_candidate_frames = 0
|
|
1892
|
+
if self._barge_in_candidate_frames >= 3:
|
|
1893
|
+
if now >= self._barge_in_until:
|
|
1894
|
+
logger.info(
|
|
1895
|
+
"audio_loop: adaptive barge-in opened mic "
|
|
1896
|
+
"(peak=%d threshold=%d echo_floor=%d)",
|
|
1897
|
+
raw_peak, threshold, int(self._echo_mic_floor),
|
|
1898
|
+
)
|
|
1899
|
+
self._barge_in_until = now + self._barge_in_hold
|
|
1900
|
+
barge_in_active = self._adaptive_barge_in and now < self._barge_in_until
|
|
1901
|
+
if echo_gate_active and not barge_in_active:
|
|
1849
1902
|
# Preserve 20 ms frame cadence; only suppress content.
|
|
1850
1903
|
# Stopping publication would create gaps and destabilize
|
|
1851
1904
|
# VAD/endpointing when listening resumes.
|