infinicode 2.8.99 → 2.8.100
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.
|
@@ -139,6 +139,12 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
139
139
|
// from these USB product labels on every inventory refresh.
|
|
140
140
|
ROBOPARK_AUDIO_INPUT_MATCH: 'Usb Audio Device: USB Audio',
|
|
141
141
|
ROBOPARK_AUDIO_OUTPUT_MATCH: 'USB Audio Device: -',
|
|
142
|
+
// Launch hardware has no acoustic echo cancellation. Suppress mic PCM
|
|
143
|
+
// only while audible robot speech is playing, then hold briefly for
|
|
144
|
+
// room echo decay so VAD cannot interrupt the robot with its own voice.
|
|
145
|
+
ROBOPARK_HALF_DUPLEX: 'true',
|
|
146
|
+
ROBOPARK_ECHO_TAIL_MS: '700',
|
|
147
|
+
ROBOPARK_ECHO_GATE_PEAK: '96',
|
|
142
148
|
// Inventory changes should reach the Control Center promptly.
|
|
143
149
|
HEARTBEAT_INTERVAL: '5',
|
|
144
150
|
},
|
|
@@ -199,3 +199,16 @@ Required result: `playback_started ok`.
|
|
|
199
199
|
rejects duplicate assignments, limits pulses to 10 seconds, serializes all
|
|
200
200
|
actuation, and de-energizes every registered relay on errors and shutdown.
|
|
201
201
|
- A missing or busy GPIO driver is a failed test, never a simulated success.
|
|
202
|
+
|
|
203
|
+
## Robot Speaker Echo Contract (2.8.100+)
|
|
204
|
+
|
|
205
|
+
- The launch USB speaker/microphone stack has no hardware acoustic echo
|
|
206
|
+
cancellation, so production uses robot-side half-duplex echo suppression.
|
|
207
|
+
- While outbound TTS contains audible PCM, the Pi publishes timing-correct
|
|
208
|
+
silence instead of amplified microphone PCM. The microphone track remains
|
|
209
|
+
connected and resumes after a 700 ms acoustic decay tail.
|
|
210
|
+
- Silent frames on the persistent TTS track do not hold the gate open. Output
|
|
211
|
+
peak detection releases the visitor microphone between responses.
|
|
212
|
+
- `ROBOPARK_HALF_DUPLEX`, `ROBOPARK_ECHO_TAIL_MS`, and
|
|
213
|
+
`ROBOPARK_ECHO_GATE_PEAK` are explicit runtime controls. Disable half-duplex
|
|
214
|
+
only after deploying real acoustic echo cancellation.
|
package/package.json
CHANGED
|
@@ -1284,6 +1284,19 @@ class LiveKitPublisher:
|
|
|
1284
1284
|
self._mic_capture: Optional["AudioCapture"] = None
|
|
1285
1285
|
self._mic_streaming = asyncio.Event()
|
|
1286
1286
|
self._mic_error: Optional[str] = None
|
|
1287
|
+
# The launch hardware has no acoustic echo cancellation. Publishing
|
|
1288
|
+
# the amplified USB microphone while the robot speaker plays TTS makes
|
|
1289
|
+
# the voice worker hear itself and trigger barge-in, truncating or
|
|
1290
|
+
# chopping its own response. Keep the track alive with silence while
|
|
1291
|
+
# playback is active, plus a short room-echo decay tail.
|
|
1292
|
+
self._half_duplex = str(os.getenv("ROBOPARK_HALF_DUPLEX", "true")).lower() in (
|
|
1293
|
+
"1", "true", "yes", "on",
|
|
1294
|
+
)
|
|
1295
|
+
self._speaker_playback_active = threading.Event()
|
|
1296
|
+
self._speaker_gate_until = 0.0
|
|
1297
|
+
self._speaker_echo_tail = max(
|
|
1298
|
+
0.1, min(float(os.getenv("ROBOPARK_ECHO_TAIL_MS", "700")) / 1000.0, 2.0)
|
|
1299
|
+
)
|
|
1287
1300
|
# Motion sampling state belongs to the publisher instance. Keeping it
|
|
1288
1301
|
# initialized here prevents shutdown/reopen paths from raising while
|
|
1289
1302
|
# the camera is being handed between preview and voice sessions.
|
|
@@ -1524,12 +1537,41 @@ class LiveKitPublisher:
|
|
|
1524
1537
|
stream_failed = threading.Event()
|
|
1525
1538
|
playback_reported = threading.Event()
|
|
1526
1539
|
event_loop = asyncio.get_running_loop()
|
|
1540
|
+
echo_gate_peak = max(16, min(int(os.getenv("ROBOPARK_ECHO_GATE_PEAK", "96")), 4096))
|
|
1541
|
+
|
|
1542
|
+
def _outbound_peak(chunk: bytes) -> int:
|
|
1543
|
+
if len(chunk) < 2:
|
|
1544
|
+
return 0
|
|
1545
|
+
try:
|
|
1546
|
+
samples = memoryview(chunk).cast("h")
|
|
1547
|
+
# Stereo duplication means sampling every eighth value is
|
|
1548
|
+
# sufficient and keeps the writer thread lightweight.
|
|
1549
|
+
return max((abs(int(value)) for value in samples[::8]), default=0)
|
|
1550
|
+
except (TypeError, ValueError):
|
|
1551
|
+
return 0
|
|
1552
|
+
|
|
1553
|
+
def _open_echo_gate() -> None:
|
|
1554
|
+
if not self._half_duplex:
|
|
1555
|
+
return
|
|
1556
|
+
self._speaker_playback_active.set()
|
|
1557
|
+
self._speaker_gate_until = time.monotonic() + self._speaker_echo_tail
|
|
1558
|
+
|
|
1559
|
+
def _extend_echo_gate() -> None:
|
|
1560
|
+
if self._half_duplex:
|
|
1561
|
+
self._speaker_gate_until = time.monotonic() + self._speaker_echo_tail
|
|
1527
1562
|
|
|
1528
|
-
def _safe_write(chunk: bytes) -> bool:
|
|
1529
|
-
if stream_failed.is_set():
|
|
1530
|
-
return False
|
|
1563
|
+
def _safe_write(chunk: bytes) -> bool:
|
|
1564
|
+
if stream_failed.is_set():
|
|
1565
|
+
return False
|
|
1531
1566
|
try:
|
|
1567
|
+
audible = _outbound_peak(chunk) >= echo_gate_peak
|
|
1568
|
+
if audible:
|
|
1569
|
+
_open_echo_gate()
|
|
1570
|
+
elif time.monotonic() >= self._speaker_gate_until:
|
|
1571
|
+
self._speaker_playback_active.clear()
|
|
1532
1572
|
out.write(chunk)
|
|
1573
|
+
if audible:
|
|
1574
|
+
_extend_echo_gate()
|
|
1533
1575
|
if not playback_reported.is_set():
|
|
1534
1576
|
playback_reported.set()
|
|
1535
1577
|
asyncio.run_coroutine_threadsafe(
|
|
@@ -1661,6 +1703,9 @@ class LiveKitPublisher:
|
|
|
1661
1703
|
pa.terminate()
|
|
1662
1704
|
if speaker_guard is not None:
|
|
1663
1705
|
speaker_guard.release()
|
|
1706
|
+
if self._half_duplex:
|
|
1707
|
+
self._speaker_gate_until = time.monotonic() + self._speaker_echo_tail
|
|
1708
|
+
self._speaker_playback_active.clear()
|
|
1664
1709
|
|
|
1665
1710
|
async def stop(self) -> None:
|
|
1666
1711
|
self._stop_event.set()
|
|
@@ -1783,8 +1828,9 @@ class LiveKitPublisher:
|
|
|
1783
1828
|
_diag_count = 0
|
|
1784
1829
|
_diag_last_log = time.monotonic()
|
|
1785
1830
|
_diag_read_ms = 0.0
|
|
1786
|
-
_diag_publish_ms = 0.0
|
|
1787
|
-
|
|
1831
|
+
_diag_publish_ms = 0.0
|
|
1832
|
+
_echo_gate_was_active = False
|
|
1833
|
+
batch_interval = BATCH_MS / 1000
|
|
1788
1834
|
while not self._stop_event.is_set():
|
|
1789
1835
|
_iter_start = time.monotonic()
|
|
1790
1836
|
_t0 = time.monotonic()
|
|
@@ -1792,10 +1838,27 @@ class LiveKitPublisher:
|
|
|
1792
1838
|
_t1 = time.monotonic()
|
|
1793
1839
|
if batch is not None:
|
|
1794
1840
|
data = bytes(batch.data)
|
|
1795
|
-
_pub_start = time.monotonic()
|
|
1796
|
-
for off in range(0, len(data) - bytes_per_frame + 1, bytes_per_frame):
|
|
1841
|
+
_pub_start = time.monotonic()
|
|
1842
|
+
for off in range(0, len(data) - bytes_per_frame + 1, bytes_per_frame):
|
|
1797
1843
|
chunk = data[off:off + bytes_per_frame]
|
|
1798
|
-
|
|
1844
|
+
echo_gate_active = self._half_duplex and (
|
|
1845
|
+
self._speaker_playback_active.is_set()
|
|
1846
|
+
or time.monotonic() < self._speaker_gate_until
|
|
1847
|
+
)
|
|
1848
|
+
if echo_gate_active:
|
|
1849
|
+
# Preserve 20 ms frame cadence; only suppress content.
|
|
1850
|
+
# Stopping publication would create gaps and destabilize
|
|
1851
|
+
# VAD/endpointing when listening resumes.
|
|
1852
|
+
chunk = b"\x00" * len(chunk)
|
|
1853
|
+
p = 0
|
|
1854
|
+
else:
|
|
1855
|
+
chunk, p = _pcm16_scale_and_peak(chunk, mic_gain)
|
|
1856
|
+
if echo_gate_active != _echo_gate_was_active:
|
|
1857
|
+
logger.info(
|
|
1858
|
+
"audio_loop: speaker echo gate %s",
|
|
1859
|
+
"active" if echo_gate_active else "released",
|
|
1860
|
+
)
|
|
1861
|
+
_echo_gate_was_active = echo_gate_active
|
|
1799
1862
|
frame = self.rtc.AudioFrame(
|
|
1800
1863
|
data=chunk, sample_rate=48000, num_channels=1, samples_per_channel=samples_per_frame,
|
|
1801
1864
|
)
|