infinicode 2.8.90 → 2.8.92
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/package.json
CHANGED
|
@@ -455,6 +455,9 @@ class PreviewAgent:
|
|
|
455
455
|
self._vision_session_id: Optional[str] = None
|
|
456
456
|
self._vision_trigger_in_flight = False
|
|
457
457
|
self._speaker_test_in_flight = False
|
|
458
|
+
# The USB adapter is an exclusive ALSA endpoint. Keep operator tests
|
|
459
|
+
# and motion cues from racing each other while LiveKit is paused.
|
|
460
|
+
self._speaker_operation_lock = asyncio.Lock()
|
|
458
461
|
self._vision_hard_deadline: float = 0.0
|
|
459
462
|
self._vision_last_activity: float = 0.0
|
|
460
463
|
self.production_mode = False
|
|
@@ -896,18 +899,20 @@ class PreviewAgent:
|
|
|
896
899
|
return
|
|
897
900
|
self._speaker_test_in_flight = True
|
|
898
901
|
from robot_supervisor import _speaker_roundtrip_test
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
902
|
+
async with self._speaker_operation_lock:
|
|
903
|
+
# LiveKit owns the USB microphone and speaker continuously.
|
|
904
|
+
# Pause it while holding the same lock as the motion cue so
|
|
905
|
+
# nothing can reopen ALSA before the explicit test starts.
|
|
906
|
+
restore_state = self._current_state if self._publisher else None
|
|
907
|
+
if restore_state:
|
|
908
|
+
await self._stop_publisher()
|
|
909
|
+
await asyncio.sleep(1.0)
|
|
910
|
+
try:
|
|
911
|
+
result = await asyncio.to_thread(_speaker_roundtrip_test, request.get("params") or {})
|
|
912
|
+
result["media_owner_paused"] = bool(restore_state)
|
|
913
|
+
finally:
|
|
914
|
+
if restore_state and restore_state.active:
|
|
915
|
+
await self._start_publisher(restore_state)
|
|
911
916
|
result_response = await self._session.post(
|
|
912
917
|
f"{self.scheduler_url.rstrip('/')}/api/devices/{self.device_id}/supervisor-output",
|
|
913
918
|
json={"kind": "speaker_test", "service": None, "payload": result,
|
|
@@ -1066,7 +1071,8 @@ class PreviewAgent:
|
|
|
1066
1071
|
# The cue and TTS share one physical ALSA output. Finish and release
|
|
1067
1072
|
# the cue before LiveKit can deliver the greeting; background playback
|
|
1068
1073
|
# races aplay and makes greeting delivery intermittently fail busy.
|
|
1069
|
-
|
|
1074
|
+
async with self._speaker_operation_lock:
|
|
1075
|
+
await asyncio.to_thread(_play_audio_effect, self.audio_playback_device, "motion")
|
|
1070
1076
|
try:
|
|
1071
1077
|
r = await self._session.post(
|
|
1072
1078
|
f"{self.scheduler_url.rstrip('/')}/api/devices/{self.device_id}/request-session",
|
|
@@ -1251,17 +1257,14 @@ class LiveKitPublisher:
|
|
|
1251
1257
|
# — until it finished, so a short "Hello friend!" greeting could be
|
|
1252
1258
|
# over and gone before we ever got a chance to subscribe to it.
|
|
1253
1259
|
if audio_enabled:
|
|
1254
|
-
self._tasks.append(asyncio.create_task(self.
|
|
1260
|
+
self._tasks.append(asyncio.create_task(self._audio_retry_loop()))
|
|
1255
1261
|
try:
|
|
1256
1262
|
await asyncio.wait_for(self._mic_streaming.wait(), timeout=5.0)
|
|
1257
|
-
await self.agent._report_pipeline(
|
|
1258
|
-
"microphone_published", "ok", "Microphone track published with live PCM frames"
|
|
1259
|
-
)
|
|
1260
1263
|
except asyncio.TimeoutError:
|
|
1261
1264
|
await self.agent._report_pipeline(
|
|
1262
1265
|
"microphone_published", "blocked", "Microphone track published but PCM capture did not start"
|
|
1263
1266
|
)
|
|
1264
|
-
|
|
1267
|
+
logger.warning("microphone PCM is not ready; keeping the session alive while capture retries")
|
|
1265
1268
|
|
|
1266
1269
|
if self._capture and self.video_source:
|
|
1267
1270
|
self._tasks.append(asyncio.create_task(self._video_loop()))
|
|
@@ -1317,6 +1320,11 @@ class LiveKitPublisher:
|
|
|
1317
1320
|
process.wait(timeout=2)
|
|
1318
1321
|
except subprocess.TimeoutExpired:
|
|
1319
1322
|
process.terminate()
|
|
1323
|
+
try:
|
|
1324
|
+
process.wait(timeout=1)
|
|
1325
|
+
except subprocess.TimeoutExpired:
|
|
1326
|
+
process.kill()
|
|
1327
|
+
process.wait(timeout=1)
|
|
1320
1328
|
|
|
1321
1329
|
def close(self) -> None:
|
|
1322
1330
|
return
|
|
@@ -1493,16 +1501,24 @@ class LiveKitPublisher:
|
|
|
1493
1501
|
else:
|
|
1494
1502
|
data = b"".join(raw[i:i + 2] * OUT_CHANNELS for i in range(0, len(raw), 2))
|
|
1495
1503
|
write_queue.put(data)
|
|
1496
|
-
except Exception as e:
|
|
1497
|
-
logger.warning(f"audio out stream error: {e}")
|
|
1498
|
-
finally:
|
|
1499
|
-
|
|
1500
|
-
|
|
1504
|
+
except Exception as e:
|
|
1505
|
+
logger.warning(f"audio out stream error: {e}")
|
|
1506
|
+
finally:
|
|
1507
|
+
cancelling = bool(asyncio.current_task() and asyncio.current_task().cancelling())
|
|
1508
|
+
if cancelling:
|
|
1509
|
+
# On publisher teardown, close ALSA first. Waiting for the
|
|
1510
|
+
# writer while aplay still owns the device leaves hw:X,Y busy
|
|
1511
|
+
# long enough for the queued dashboard test to fail.
|
|
1512
|
+
stream_failed.set()
|
|
1513
|
+
out.stop_stream()
|
|
1514
|
+
write_queue.put(None)
|
|
1515
|
+
writer_thread.join(timeout=2.0)
|
|
1501
1516
|
logger.info(
|
|
1502
1517
|
f"audio out: {sid} received {frame_count} frames, "
|
|
1503
1518
|
f"writer wrote {written_frames[0]} chunks, queue backlog at close={write_queue.qsize()}"
|
|
1504
1519
|
)
|
|
1505
|
-
|
|
1520
|
+
if not cancelling:
|
|
1521
|
+
out.stop_stream()
|
|
1506
1522
|
out.close()
|
|
1507
1523
|
if pa is not None:
|
|
1508
1524
|
pa.terminate()
|
|
@@ -1577,7 +1593,29 @@ class LiveKitPublisher:
|
|
|
1577
1593
|
except Exception as e:
|
|
1578
1594
|
logger.debug(f"preview motion sampling failed: {e}")
|
|
1579
1595
|
|
|
1580
|
-
async def
|
|
1596
|
+
async def _audio_retry_loop(self) -> None:
|
|
1597
|
+
"""Keep microphone capture alive across transient ALSA ownership errors."""
|
|
1598
|
+
while not self._stop_event.is_set():
|
|
1599
|
+
try:
|
|
1600
|
+
await self._audio_loop()
|
|
1601
|
+
except asyncio.CancelledError:
|
|
1602
|
+
raise
|
|
1603
|
+
except Exception as e:
|
|
1604
|
+
logger.warning(
|
|
1605
|
+
f"audio_loop: capture failed on {self.agent.audio_capture_device}: {e}; retrying"
|
|
1606
|
+
)
|
|
1607
|
+
finally:
|
|
1608
|
+
mic = self._mic_capture
|
|
1609
|
+
self._mic_capture = None
|
|
1610
|
+
if mic is not None:
|
|
1611
|
+
await asyncio.to_thread(mic.stop)
|
|
1612
|
+
if not self._stop_event.is_set():
|
|
1613
|
+
try:
|
|
1614
|
+
await asyncio.wait_for(self._stop_event.wait(), timeout=1.0)
|
|
1615
|
+
except asyncio.TimeoutError:
|
|
1616
|
+
pass
|
|
1617
|
+
|
|
1618
|
+
async def _audio_loop(self) -> None:
|
|
1581
1619
|
assert self.audio_source is not None
|
|
1582
1620
|
mic = create_audio_capture(self.agent.audio_capture_device)
|
|
1583
1621
|
if mic is None:
|
|
@@ -1623,7 +1661,12 @@ class LiveKitPublisher:
|
|
|
1623
1661
|
data=chunk, sample_rate=48000, num_channels=1, samples_per_channel=samples_per_frame,
|
|
1624
1662
|
)
|
|
1625
1663
|
await self.audio_source.capture_frame(frame)
|
|
1626
|
-
self._mic_streaming.
|
|
1664
|
+
if not self._mic_streaming.is_set():
|
|
1665
|
+
self._mic_streaming.set()
|
|
1666
|
+
await self.agent._report_pipeline(
|
|
1667
|
+
"microphone_published", "ok",
|
|
1668
|
+
"Microphone track published with live PCM frames",
|
|
1669
|
+
)
|
|
1627
1670
|
p = audioop.max(chunk, 2)
|
|
1628
1671
|
_diag_peak = max(_diag_peak, p)
|
|
1629
1672
|
_diag_count += 1
|
|
@@ -1833,7 +1876,7 @@ class AlsaAudioCapture(AudioCapture):
|
|
|
1833
1876
|
"-f", "S16_LE", "-r", "48000", "-c", "1",
|
|
1834
1877
|
],
|
|
1835
1878
|
stdout=subprocess.PIPE,
|
|
1836
|
-
stderr=subprocess.
|
|
1879
|
+
stderr=subprocess.PIPE,
|
|
1837
1880
|
)
|
|
1838
1881
|
if self.process.stdout is None:
|
|
1839
1882
|
raise RuntimeError("arecord did not provide a PCM stream")
|
|
@@ -1846,7 +1889,10 @@ class AlsaAudioCapture(AudioCapture):
|
|
|
1846
1889
|
while len(self.buffer) < bytes_needed:
|
|
1847
1890
|
chunk = self.process.stdout.read(bytes_needed - len(self.buffer))
|
|
1848
1891
|
if not chunk:
|
|
1849
|
-
|
|
1892
|
+
detail = ""
|
|
1893
|
+
if self.process.stderr is not None:
|
|
1894
|
+
detail = self.process.stderr.read().decode("utf-8", errors="replace").strip()
|
|
1895
|
+
raise OSError(f"ALSA capture stopped on {self.alsa_device}: {detail or 'no PCM data'}")
|
|
1850
1896
|
self.buffer.extend(chunk)
|
|
1851
1897
|
data = bytes(self.buffer[:bytes_needed])
|
|
1852
1898
|
del self.buffer[:bytes_needed]
|
|
@@ -1864,6 +1910,7 @@ class AlsaAudioCapture(AudioCapture):
|
|
|
1864
1910
|
self.process.wait(timeout=2)
|
|
1865
1911
|
except Exception:
|
|
1866
1912
|
self.process.kill()
|
|
1913
|
+
self.process.wait(timeout=1)
|
|
1867
1914
|
|
|
1868
1915
|
|
|
1869
1916
|
class PyAudioCapture(AudioCapture):
|