infinicode 2.8.80 → 2.8.82
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.
|
@@ -6265,11 +6265,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6265
6265
|
function refreshDeviceSelectors(n){
|
|
6266
6266
|
var device=schedulerDevice(n)||{}, inv=device.device_inventory||{};
|
|
6267
6267
|
if(hasRealInventory(inv)) deviceSelectorsReady=true;
|
|
6268
|
-
var
|
|
6269
|
-
//
|
|
6270
|
-
|
|
6271
|
-
var
|
|
6272
|
-
var
|
|
6268
|
+
var query='';
|
|
6269
|
+
// Launch hardware profile shared by every deployed robot. Do not expose
|
|
6270
|
+
// unstable PortAudio indexes, HDMI, or Pi codec devices in production.
|
|
6271
|
+
var video=[{id:'/dev/video0',name:'H65 USB CAMERA (/dev/video0)'}];
|
|
6272
|
+
var input=[{id:'2',name:'USB microphone (hw:3,0)'}];
|
|
6273
|
+
var output=[{id:'1',name:'USB speaker (hw:2,0)'}];
|
|
6273
6274
|
function fill(id, list, selected){
|
|
6274
6275
|
var el=$(id); if(!el) return;
|
|
6275
6276
|
// Heartbeat polling rebuilds this panel frequently. Preserve an
|
|
@@ -6282,9 +6283,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6282
6283
|
});
|
|
6283
6284
|
if(!el.value && effectiveSelected){ var o=document.createElement('option'); o.value=String(effectiveSelected); o.textContent=String(effectiveSelected)+(el.dataset.dirty==='true'?' (selected)':' (saved)'); o.selected=true; el.appendChild(o); }
|
|
6284
6285
|
}
|
|
6285
|
-
fill('pk-video-device', video,
|
|
6286
|
-
fill('pk-audio-input', input,
|
|
6287
|
-
fill('pk-audio-output', output,
|
|
6286
|
+
fill('pk-video-device', video, '/dev/video0');
|
|
6287
|
+
fill('pk-audio-input', input, '2');
|
|
6288
|
+
fill('pk-audio-output', output, '1');
|
|
6288
6289
|
var selectedOutput=$('pk-audio-output'),selectedLabel=$('pk-spk-selected');
|
|
6289
6290
|
if(selectedLabel&&selectedOutput){var selectedOption=selectedOutput.options&&selectedOutput.selectedIndex>=0?selectedOutput.options[selectedOutput.selectedIndex]:null;selectedLabel.textContent=selectedOption?String(selectedOption.textContent||selectedOption.value):String(selectedOutput.value||'default');}
|
|
6290
6291
|
var greetings=$('pk-greeting-phrases');
|
|
@@ -6302,7 +6303,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6302
6303
|
}
|
|
6303
6304
|
function saveDeviceSelectors(n){
|
|
6304
6305
|
var device=schedulerDevice(n), id=device&&device.id; if(!id){ showOut('device inventory is not available yet'); return; }
|
|
6305
|
-
var body={video_device
|
|
6306
|
+
var body={video_device:'/dev/video0', audio_device:'2', audio_output_device:'1',
|
|
6306
6307
|
greeting_phrases:($('pk-greeting-phrases').value||'').split(String.fromCharCode(10)).map(function(x){return x.trim();}).filter(Boolean)};
|
|
6307
6308
|
var button=$('pk-device-save'),note=$('pk-device-note');
|
|
6308
6309
|
if(button){button.disabled=true;button.textContent='Saving...';}
|
|
@@ -6713,7 +6714,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6713
6714
|
+ sect('🩺 robot services','<div id="pk-svc-list" class="pk-svc-list"><div class="pk-note">waiting for supervisor report…</div></div><div class="pk-note" id="pk-svc-age" style="margin-top:0.35rem;"></div><div class="rp-field" style="margin-top:0.55rem;"><label>Quick run (on the robot, from packages/robopark/scheduler)</label><input id="pk-svc-clicmd" readonly value="python robot_supervisor.py"></div><button class="rp-btn" id="pk-svc-clicmd-copy" style="margin-top:0.35rem;">📋 Copy command</button>')
|
|
6714
6715
|
+ buildShellSection(schedId(n))
|
|
6715
6716
|
+ sect('🎭 character / voice','<div class="pk-note" id="pk-eff-note" style="margin-bottom:0.45rem;">loading effective config…</div><div class="rp-field"><label>Character preset</label><select class="rp-char" id="pk-eff-char" data-robot="'+esc(schedId(n))+'">'+effPresetOptions+'</select></div><div class="rp-field"><label>Voice stack</label><select class="rp-voice" id="pk-eff-voice" data-robot="'+esc(schedId(n))+'">'+effVoiceOptions+'</select></div><div class="rp-config-actions" style="margin-top:.45rem"><button class="rp-btn primary" id="pk-eff-save" data-robot="'+esc(schedId(n))+'">Apply to robot</button><button class="rp-btn" id="pk-edit-character">Edit character</button><button class="rp-btn" id="pk-edit-stack">Edit stack</button><button class="rp-btn" id="pk-new-stack">New stack</button></div>');
|
|
6716
|
-
b.innerHTML += sect('
|
|
6717
|
+
b.innerHTML += sect('Production camera + audio','<div class="pk-note" id="pk-device-search" style="margin-bottom:.55rem">Hardware locked for launch. Camera, microphone, and speaker cannot drift between restarts.</div><div class="rp-field"><label>Camera input</label><select id="pk-video-device" disabled></select></div><div class="rp-field"><label>Microphone input</label><select id="pk-audio-input" disabled></select></div><div class="rp-field"><label>Speaker output</label><select id="pk-audio-output" disabled></select></div><button class="rp-btn primary" id="pk-device-test-speaker" style="margin-top:0.35rem">Test USB speaker now</button><div class="rp-field"><label>Preset greeting phrases <span class="pk-note">one per line, cached per voice</span></label><textarea id="pk-greeting-phrases" rows="3" placeholder="Hi, I am Jaguar! / Welcome to RoboPark."></textarea></div><button class="rp-btn" id="pk-device-save" style="margin-top:0.35rem">Save greetings + enforce hardware</button><div class="pk-note" id="pk-device-note">waiting for robot heartbeat inventory</div>');
|
|
6717
6718
|
// Put live hardware selection beside the camera and audio tests,
|
|
6718
6719
|
// ahead of persona and service administration in the Park drawer.
|
|
6719
6720
|
var deviceInput=$('pk-device-search'), deviceSection=deviceInput&&deviceInput.closest('.pk-sect');
|
|
@@ -6721,7 +6722,6 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6721
6722
|
if(deviceSection && speakerSection) speakerSection.after(deviceSection);
|
|
6722
6723
|
refreshDeviceSelectors(n);
|
|
6723
6724
|
b.innerHTML += '<div class="pk-ctl" style="margin-top:0.5rem"><button class="rp-btn danger" id="pk-recover" data-robot="'+esc(n.name)+'">Recover robot</button><span class="pk-note" style="margin:0">clears stale sessions and queued triggers for this robot</span></div>';
|
|
6724
|
-
var deviceSearch=$('pk-device-search'); if(deviceSearch) deviceSearch.addEventListener('input', function(){ refreshDeviceSelectors(n); });
|
|
6725
6725
|
var deviceSave=$('pk-device-save'); if(deviceSave) deviceSave.addEventListener('click', function(){ saveDeviceSelectors(n); });
|
|
6726
6726
|
['pk-video-device','pk-audio-input','pk-audio-output'].forEach(function(field){
|
|
6727
6727
|
var select=$(field);if(!select)return;
|
|
@@ -101,7 +101,8 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
101
101
|
}
|
|
102
102
|
const previewArgs = [cli, 'preview-agent', '--foreground',
|
|
103
103
|
'--scheduler-url', opts.schedulerUrl, '--robot-id', opts.name,
|
|
104
|
-
'--video-device', opts.videoDevice ?? '
|
|
104
|
+
'--video-device', opts.videoDevice ?? '/dev/video0',
|
|
105
|
+
'--audio-device', opts.audioDevice ?? 'Usb Audio Device: USB Audio (hw:3,0)',
|
|
105
106
|
'--save-config'];
|
|
106
107
|
if (opts.enrollmentToken)
|
|
107
108
|
previewArgs.push('--enrollment-token', opts.enrollmentToken);
|
package/dist/robopark/setup.js
CHANGED
|
@@ -204,8 +204,8 @@ async function setupRobot(config, opts, ctx, internal) {
|
|
|
204
204
|
'--token', internal.token,
|
|
205
205
|
'--port', String(internal.port),
|
|
206
206
|
networkFlag,
|
|
207
|
-
'--video-device', opts.videoDevice ?? '
|
|
208
|
-
'--audio-device', opts.audioDevice ?? '
|
|
207
|
+
'--video-device', opts.videoDevice ?? '/dev/video0',
|
|
208
|
+
'--audio-device', opts.audioDevice ?? 'Usb Audio Device: USB Audio (hw:3,0)',
|
|
209
209
|
];
|
|
210
210
|
if (opts.enrollmentToken)
|
|
211
211
|
runtimeArgs.push('--enrollment-token', opts.enrollmentToken);
|
package/package.json
CHANGED
|
@@ -392,6 +392,10 @@ class PreviewAgent:
|
|
|
392
392
|
self.enrollment_token: Optional[str] = cfg.get("enrollment_token")
|
|
393
393
|
self.video_device = cfg.get("video_device", os.getenv("VIDEO_DEVICE", "auto"))
|
|
394
394
|
self.audio_device = cfg.get("audio_device", os.getenv("AUDIO_DEVICE", "default"))
|
|
395
|
+
# Scheduler inventory IDs come from RoboVision/sounddevice. They are
|
|
396
|
+
# not guaranteed to equal PyAudio's device indexes, so capture must
|
|
397
|
+
# use the resolved hardware name reported in that same inventory.
|
|
398
|
+
self.audio_capture_device = self.audio_device
|
|
395
399
|
self.audio_output_device = cfg.get("audio_output_device", os.getenv("AUDIO_OUTPUT_DEVICE", "default"))
|
|
396
400
|
self.robovision_url = cfg.get("robovision_url", os.getenv("ROBOVISION_URL", "http://127.0.0.1:5000"))
|
|
397
401
|
self.use_robovision_camera = bool(cfg.get("use_robovision_camera", False))
|
|
@@ -672,6 +676,16 @@ class PreviewAgent:
|
|
|
672
676
|
if data.get("audio_device") is not None:
|
|
673
677
|
changed = changed or self.audio_device != data["audio_device"]
|
|
674
678
|
self.audio_device = data["audio_device"]
|
|
679
|
+
inventory = data.get("device_inventory") or {}
|
|
680
|
+
selected = str(self.audio_device)
|
|
681
|
+
self.audio_capture_device = next(
|
|
682
|
+
(
|
|
683
|
+
str(item.get("name"))
|
|
684
|
+
for item in inventory.get("audio_input", [])
|
|
685
|
+
if str(item.get("id")) == selected and item.get("name")
|
|
686
|
+
),
|
|
687
|
+
self.audio_device,
|
|
688
|
+
)
|
|
675
689
|
if data.get("audio_output_device") is not None:
|
|
676
690
|
changed = changed or self.audio_output_device != data["audio_output_device"]
|
|
677
691
|
self.audio_output_device = data["audio_output_device"]
|
|
@@ -1457,7 +1471,7 @@ class LiveKitPublisher:
|
|
|
1457
1471
|
|
|
1458
1472
|
async def _audio_loop(self) -> None:
|
|
1459
1473
|
assert self.audio_source is not None
|
|
1460
|
-
mic = create_audio_capture(self.agent.
|
|
1474
|
+
mic = create_audio_capture(self.agent.audio_capture_device)
|
|
1461
1475
|
if mic is None:
|
|
1462
1476
|
logger.warning("audio_loop: create_audio_capture returned None, mic will not publish")
|
|
1463
1477
|
return
|
|
@@ -1681,15 +1695,66 @@ def create_video_capture(device: str, width: int, height: int, fps: int,
|
|
|
1681
1695
|
# Audio capture abstraction: PyAudio or sounddevice -> LiveKit AudioFrame.
|
|
1682
1696
|
# -----------------------------------------------------------------------------
|
|
1683
1697
|
|
|
1684
|
-
class AudioCapture:
|
|
1698
|
+
class AudioCapture:
|
|
1685
1699
|
def read(self, samples_per_frame: int) -> Optional["rtc.AudioFrame"]:
|
|
1686
1700
|
raise NotImplementedError
|
|
1687
1701
|
|
|
1688
|
-
def stop(self) -> None:
|
|
1689
|
-
raise NotImplementedError
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
class
|
|
1702
|
+
def stop(self) -> None:
|
|
1703
|
+
raise NotImplementedError
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
class AlsaAudioCapture(AudioCapture):
|
|
1707
|
+
"""Capture Linux PCM through the same ALSA path used by onsite tests."""
|
|
1708
|
+
|
|
1709
|
+
def __init__(self, device: str):
|
|
1710
|
+
import re
|
|
1711
|
+
import subprocess
|
|
1712
|
+
|
|
1713
|
+
match = re.search(r"\b(hw:\d+,\d+)\b", device)
|
|
1714
|
+
if not match:
|
|
1715
|
+
raise ValueError(f"no ALSA hardware address in {device!r}")
|
|
1716
|
+
alsa_device = f"plug{match.group(1)}"
|
|
1717
|
+
self.buffer = bytearray()
|
|
1718
|
+
self.process = subprocess.Popen(
|
|
1719
|
+
[
|
|
1720
|
+
"arecord", "-q", "-D", alsa_device, "-t", "raw",
|
|
1721
|
+
"-f", "S16_LE", "-r", "48000", "-c", "1",
|
|
1722
|
+
],
|
|
1723
|
+
stdout=subprocess.PIPE,
|
|
1724
|
+
stderr=subprocess.DEVNULL,
|
|
1725
|
+
)
|
|
1726
|
+
if self.process.stdout is None:
|
|
1727
|
+
raise RuntimeError("arecord did not provide a PCM stream")
|
|
1728
|
+
self.alsa_device = alsa_device
|
|
1729
|
+
|
|
1730
|
+
def read(self, samples_per_frame: int):
|
|
1731
|
+
from livekit import rtc
|
|
1732
|
+
|
|
1733
|
+
bytes_needed = samples_per_frame * 2
|
|
1734
|
+
while len(self.buffer) < bytes_needed:
|
|
1735
|
+
chunk = self.process.stdout.read(bytes_needed - len(self.buffer))
|
|
1736
|
+
if not chunk:
|
|
1737
|
+
raise OSError(f"ALSA capture stopped on {self.alsa_device}")
|
|
1738
|
+
self.buffer.extend(chunk)
|
|
1739
|
+
data = bytes(self.buffer[:bytes_needed])
|
|
1740
|
+
del self.buffer[:bytes_needed]
|
|
1741
|
+
return rtc.AudioFrame(
|
|
1742
|
+
data=data,
|
|
1743
|
+
sample_rate=48000,
|
|
1744
|
+
num_channels=1,
|
|
1745
|
+
samples_per_channel=samples_per_frame,
|
|
1746
|
+
)
|
|
1747
|
+
|
|
1748
|
+
def stop(self):
|
|
1749
|
+
if self.process.poll() is None:
|
|
1750
|
+
self.process.terminate()
|
|
1751
|
+
try:
|
|
1752
|
+
self.process.wait(timeout=2)
|
|
1753
|
+
except Exception:
|
|
1754
|
+
self.process.kill()
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
class PyAudioCapture(AudioCapture):
|
|
1693
1758
|
def __init__(self, device: str | int | None):
|
|
1694
1759
|
import pyaudio
|
|
1695
1760
|
self.pa = pyaudio.PyAudio()
|
|
@@ -1768,10 +1833,15 @@ def has_audio() -> bool:
|
|
|
1768
1833
|
return False
|
|
1769
1834
|
|
|
1770
1835
|
|
|
1771
|
-
def create_audio_capture(device: str) -> Optional[AudioCapture]:
|
|
1772
|
-
if device.lower() in ("none", "", "false", "null"):
|
|
1773
|
-
return None
|
|
1774
|
-
|
|
1836
|
+
def create_audio_capture(device: str) -> Optional[AudioCapture]:
|
|
1837
|
+
if device.lower() in ("none", "", "false", "null"):
|
|
1838
|
+
return None
|
|
1839
|
+
if sys.platform.startswith("linux") and "hw:" in device:
|
|
1840
|
+
try:
|
|
1841
|
+
return AlsaAudioCapture(device)
|
|
1842
|
+
except Exception as e:
|
|
1843
|
+
logger.warning(f"ALSA capture unavailable for {device}: {e}")
|
|
1844
|
+
try:
|
|
1775
1845
|
import pyaudio # noqa: F401
|
|
1776
1846
|
return PyAudioCapture(device if device != "default" else None)
|
|
1777
1847
|
except Exception as e:
|