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 search=$('pk-device-search'), query=(search&&search.value||'').trim().toLowerCase();
6269
- // Do not invent Picamera/V4L2 choices when the robot has not reported hardware.
6270
- var video=deviceList(inv,'video',['auto','none']);
6271
- var input=deviceList(inv,'audio_input',['default','none']);
6272
- var output=deviceList(inv,'audio_output',['default','none']);
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, device.video_device||'auto');
6286
- fill('pk-audio-input', input, device.audio_device||'default');
6287
- fill('pk-audio-output', output, device.audio_output_device||'default');
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:$('pk-video-device').value, audio_device:$('pk-audio-input').value, audio_output_device:$('pk-audio-output').value,
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('Camera + audio devices','<div class="rp-field"><label>Search devices</label><input id="pk-device-search" placeholder="camera, USB, microphone, HDMI..." autocomplete="off"></div><div class="rp-field"><label>Camera input</label><select id="pk-video-device"></select></div><div class="rp-field"><label>Microphone input</label><select id="pk-audio-input"></select></div><div class="rp-field"><label>Speaker / output inventory</label><select id="pk-audio-output"></select></div><button class="rp-btn primary" id="pk-device-test-speaker" style="margin-top:0.35rem">Test selected 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 device selection + greetings</button><div class="pk-note" id="pk-device-note">waiting for robot heartbeat inventory</div>');
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 ?? 'auto', '--audio-device', opts.audioDevice ?? 'default',
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);
@@ -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 ?? 'auto',
208
- '--audio-device', opts.audioDevice ?? 'default',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.80",
3
+ "version": "2.8.82",
4
4
  "description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/kernel/index.js",
@@ -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.audio_device)
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 PyAudioCapture(AudioCapture):
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
- try:
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: