infinicode 2.8.51 → 2.8.53

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.
@@ -5448,14 +5448,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
5448
5448
  // LiveKit is the authoritative camera feed. The standalone OpenCV MJPEG
5449
5449
  // server is opt-in because running it beside preview_agent would open the
5450
5450
  // same Windows camera twice and make both feeds unreliable.
5451
- function tryVisionOverlay(n){
5452
- var img=$('pk-vision-img'); if(!img) return;
5453
- if(String(window.localStorage.getItem('robopark.visionOverlay')||'').toLowerCase()!=='true'){
5454
- visionOn=false; img.src=''; img.style.display='none'; return;
5455
- }
5456
- var row=schedRow(n), ip=row&&row.lan_ip;
5457
- if(!ip){ visionOn=false; img.src=''; img.style.display='none'; return; }
5458
- var port=(row&&row.vision_port)||5000;
5451
+ function tryVisionOverlay(n, force){
5452
+ var img=$('pk-vision-img'); if(!img) return;
5453
+ if(!force && String(window.localStorage.getItem('robopark.visionOverlay')||'').toLowerCase()!=='true'){
5454
+ visionOn=false; img.src=''; img.style.display='none'; return;
5455
+ }
5456
+ var row=schedRow(n)||{}, device=schedulerDevice(n)||{};
5457
+ var preferTail=/^100\./.test(location.hostname);
5458
+ var ip=preferTail ? (device.tailscale_ip||row.tailscale_ip||device.lan_ip||row.lan_ip) : (device.lan_ip||row.lan_ip||device.tailscale_ip||row.tailscale_ip);
5459
+ if(!ip){ visionOn=false; img.src=''; img.style.display='none'; return; }
5460
+ var port=row.vision_port||5000;
5459
5461
  visionOn=true;
5460
5462
  img.onerror=function(){ visionOn=false; img.style.display='none'; img.src=''; };
5461
5463
  img.onload=function(){ img.style.display='block'; };
@@ -5485,7 +5487,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
5485
5487
  var device=schedulerDevice(n)||{}, inv=device.device_inventory||{};
5486
5488
  if(hasRealInventory(inv)) deviceSelectorsReady=true;
5487
5489
  var search=$('pk-device-search'), query=(search&&search.value||'').trim().toLowerCase();
5488
- var video=deviceList(inv,'video',['auto','none','picamera','/dev/video0','/dev/video1','/dev/video2']);
5490
+ // Do not invent Picamera/V4L2 choices when the robot has not reported hardware.
5491
+ var video=deviceList(inv,'video',['auto','none']);
5489
5492
  var input=deviceList(inv,'audio_input',['default','none']);
5490
5493
  var output=deviceList(inv,'audio_output',['default','none']);
5491
5494
  function fill(id, list, selected){
@@ -5501,7 +5504,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
5501
5504
  fill('pk-audio-output', output, device.audio_output_device||'default');
5502
5505
  var greetings=$('pk-greeting-phrases');
5503
5506
  if(greetings && document.activeElement!==greetings) greetings.value=(device.greeting_phrases||[]).join(String.fromCharCode(10));
5504
- var note=$('pk-device-note'); if(note) note.textContent=(device.id?'inventory from '+(device.name||device.id):'waiting for robot heartbeat inventory');
5507
+ var note=$('pk-device-note');
5508
+ if(note){
5509
+ var source=inv.source||'robot heartbeat', counts=(inv.video||[]).length+'/'+(inv.audio_input||[]).length+'/'+(inv.audio_output||[]).length;
5510
+ note.textContent=hasRealInventory(inv) ? ('inventory from '+source+' · camera/mic/speaker '+counts) : (device.id ? 'no real hardware inventory reported yet · restart vision-agent + preview-agent on this robot' : 'waiting for robot heartbeat inventory');
5511
+ }
5505
5512
  }
5506
5513
  function saveDeviceSelectors(n){
5507
5514
  var device=schedulerDevice(n), id=device&&device.id; if(!id){ showOut('device inventory is not available yet'); return; }
@@ -5908,7 +5915,13 @@ button.act:disabled{opacity:0.5;cursor:default;}
5908
5915
  _bindShellSection(n);
5909
5916
  _spkTestRenderHistory(schedId(n));
5910
5917
  var runSpkTest = $('pk-spk-runtest'); if(runSpkTest) runSpkTest.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } runSpeakerTest(n); });
5911
- var gl=$('pk-golive'); if(gl) gl.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } goLive(n); });
5918
+ var gl=$('pk-golive');
5919
+ if(gl){
5920
+ gl.addEventListener('click', function(){ if(rpReadOnly){ showOut('read-only node'); return; } goLive(n); });
5921
+ var vl=document.createElement('button'); vl.className='rp-btn'; vl.id='pk-vision-live'; vl.textContent='Open RoboVision feed';
5922
+ vl.addEventListener('click', function(){ tryVisionOverlay(n, true); if(!visionOn) showOut('RoboVision feed unavailable: robot has no reachable LAN/Tailscale address'); });
5923
+ gl.after(vl);
5924
+ }
5912
5925
  var pm=$('pk-prodmode'); if(pm) pm.addEventListener('change', function(){ if(rpReadOnly){ pm.checked=!pm.checked; showOut('read-only node'); return; } setProductionMode(n, pm.checked); });
5913
5926
  var svcCopy=$('pk-svc-clicmd-copy'); if(svcCopy) svcCopy.addEventListener('click', function(){ var i=$('pk-svc-clicmd'); i.select(); try{document.execCommand('copy');}catch(e){} showOut('command copied — run it on the robot itself'); });
5914
5927
  var svcList=$('pk-svc-list'); if(svcList) svcList.addEventListener('click', function(ev){
@@ -2,6 +2,7 @@
2
2
  export declare const VISION_REQUIRED_MODULES: string[];
3
3
  export declare function findSchedulerPath(): Promise<string | null>;
4
4
  export declare function findVisionPath(): Promise<string | null>;
5
+ export declare function findVisionAudioPath(): Promise<string | null>;
5
6
  export declare function findPython(): string;
6
7
  /**
7
8
  * Ensure a robot-side script's Python deps are importable, installing them
@@ -39,6 +39,9 @@ export async function findSchedulerPath() {
39
39
  export async function findVisionPath() {
40
40
  return findPackageScript('vision/app_pi_clean.py');
41
41
  }
42
+ export async function findVisionAudioPath() {
43
+ return findPackageScript('vision/audio_server_pi.py');
44
+ }
42
45
  export function findPython() {
43
46
  const names = process.platform === 'win32'
44
47
  ? ['python', 'python3', 'py']
@@ -23,6 +23,7 @@ const PATTERNS = [
23
23
  'scheduler.{0,3}main\\.py',
24
24
  'preview_agent.py',
25
25
  'app_pi_clean.py',
26
+ 'audio_server_pi.py',
26
27
  ];
27
28
  export async function stopAll() {
28
29
  const platform = process.platform;
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { spawn } from 'node:child_process';
9
9
  import chalk from 'chalk';
10
- import { findPython, findVisionPath, prepareRobotPython, VISION_REQUIRED_MODULES } from './python-env.js';
10
+ import { findPython, findVisionPath, findVisionAudioPath, prepareRobotPython, VISION_REQUIRED_MODULES } from './python-env.js';
11
11
  const DEFAULT_VISION_PORT = 5000;
12
12
  const DEFAULT_MOTION_WEBHOOK_PORT = 5057; // must match preview-agent's --vision-webhook-port default
13
13
  function isPiArm() {
@@ -24,7 +24,8 @@ export async function roboparkVisionAgent(opts) {
24
24
  // requirements_pi_unified.txt) — pip-installing it here would fight the
25
25
  // ARM-optimized system build. Everywhere else: plain pip install works.
26
26
  const reqFile = isPiArm() ? 'requirements_pi_unified.txt' : 'requirements-demo.txt';
27
- const python = prepareRobotPython(basePython, script, VISION_REQUIRED_MODULES, reqFile);
27
+ const requiredModules = isPiArm() ? [...VISION_REQUIRED_MODULES, 'fastapi', 'uvicorn', 'sounddevice', 'soundfile', 'groq'] : VISION_REQUIRED_MODULES;
28
+ const python = prepareRobotPython(basePython, script, requiredModules, reqFile);
28
29
  if (!python)
29
30
  process.exit(1);
30
31
  const port = opts.port ?? String(DEFAULT_VISION_PORT);
@@ -33,19 +34,37 @@ export async function roboparkVisionAgent(opts) {
33
34
  const args = [script, '--port', port, '--motion-webhook-url', webhookUrl];
34
35
  if (motionActive)
35
36
  args.push('--motion-active');
37
+ // The original RoboVision audio selector lives in audio_server_pi.py.
38
+ // Start it beside the camera service on real Pi robots so /devices and
39
+ // /set-device remain the authoritative source for Park dropdowns.
40
+ let audioScript = null;
41
+ if (isPiArm())
42
+ audioScript = await findVisionAudioPath();
36
43
  console.log(chalk.bold('\n robopark vision-agent'));
37
44
  console.log(chalk.dim(' ' + '─'.repeat(52)));
38
45
  console.log(` port: ${chalk.cyan(port)}`);
39
46
  console.log(` webhook: ${chalk.cyan(webhookUrl)}`);
40
47
  console.log(` motion: ${motionActive ? chalk.green('armed') : chalk.dim('off')}`);
48
+ if (audioScript)
49
+ console.log(` audio: ${chalk.green('RoboVision audio server :8000')}`);
41
50
  console.log();
42
51
  if (opts.foreground) {
52
+ if (audioScript) {
53
+ const audio = spawn(python, [audioScript], { stdio: 'inherit', env: { ...process.env, ROBOPARK_AUDIO_SERVER: '1' } });
54
+ audio.unref();
55
+ }
43
56
  const proc = spawn(python, args, { stdio: 'inherit' });
44
57
  await new Promise((resolve) => {
45
58
  proc.on('close', () => resolve());
46
59
  });
47
60
  return;
48
61
  }
62
+ if (audioScript) {
63
+ const audio = spawn(python, [audioScript], {
64
+ stdio: 'ignore', detached: true, env: { ...process.env, ROBOPARK_AUDIO_SERVER: '1' },
65
+ });
66
+ audio.unref();
67
+ }
49
68
  const proc = spawn(python, args, {
50
69
  stdio: 'ignore',
51
70
  detached: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.51",
3
+ "version": "2.8.53",
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",
@@ -19,6 +19,7 @@ current_camera_index = 0
19
19
  camera = None
20
20
  audio_input_device = "default"
21
21
  audio_output_device = "default"
22
+ ROBOVISION_AUDIO_URL = os.getenv("ROBOVISION_AUDIO_URL", "http://127.0.0.1:8000")
22
23
 
23
24
  def _open_camera(index):
24
25
  # On Windows, cv2.VideoCapture(index) with no explicit backend can
@@ -344,22 +345,30 @@ def switch_camera():
344
345
 
345
346
 
346
347
  def _audio_inventory():
347
- inputs = [{"id": "default", "name": "System default input"}]
348
- outputs = [{"id": "default", "name": "System default output"}]
348
+ """Adapt RoboVision's existing audio_server_pi /devices response."""
349
349
  try:
350
- import pyaudio
351
- pa = pyaudio.PyAudio()
352
- for index in range(pa.get_device_count()):
353
- info = pa.get_device_info_by_index(index)
354
- item = {"id": str(index), "name": str(info.get("name", f"Audio device {index}")), "host_api": str(info.get("hostApi", ""))}
355
- if info.get("maxInputChannels", 0) > 0:
350
+ response = requests.get(f"{ROBOVISION_AUDIO_URL.rstrip('/')}/devices", timeout=0.8)
351
+ response.raise_for_status()
352
+ payload = response.json()
353
+ inputs, outputs = [], []
354
+ for device in payload.get("devices", []):
355
+ item = {
356
+ "id": str(device["index"]),
357
+ "name": str(device.get("name", f"Audio device {device['index']}")),
358
+ "backend": "robovision_audio",
359
+ "sample_rate": device.get("default_samplerate"),
360
+ }
361
+ if device.get("max_input_channels", 0) > 0:
356
362
  inputs.append(item.copy())
357
- if info.get("maxOutputChannels", 0) > 0:
363
+ if device.get("max_output_channels", 0) > 0:
358
364
  outputs.append(item.copy())
359
- pa.terminate()
360
- except Exception:
361
- pass
362
- return inputs, outputs
365
+ return inputs, outputs, {
366
+ "input": payload.get("bluetooth_input"),
367
+ "output": payload.get("bluetooth_output"),
368
+ "online": True,
369
+ }
370
+ except Exception as exc:
371
+ return [], [], {"online": False, "error": str(exc)}
363
372
 
364
373
 
365
374
  @app.route('/api/media/inventory', methods=['GET'])
@@ -370,12 +379,17 @@ def media_inventory():
370
379
  # still a valid selectable camera and is safer than hiding it.
371
380
  cameras = [{"id": path, "index": path, "name": f"Camera {path}", "backend": "v4l2"}
372
381
  for path in sorted(glob.glob('/dev/video*'))]
373
- inputs, outputs = _audio_inventory()
382
+ inputs, outputs, audio_state = _audio_inventory()
374
383
  return jsonify({
375
384
  "video": [{"id": "auto", "name": "Auto detect"}, {"id": "none", "name": "Disable camera"}] + cameras,
376
385
  "audio_input": inputs,
377
386
  "audio_output": outputs,
378
- "selected": {"video_device": str(current_camera_index), "audio_device": audio_input_device, "audio_output_device": audio_output_device},
387
+ "selected": {
388
+ "video_device": str(current_camera_index),
389
+ "audio_device": str(audio_state.get("input") if audio_state.get("input") is not None else audio_input_device),
390
+ "audio_output_device": str(audio_state.get("output") if audio_state.get("output") is not None else audio_output_device),
391
+ },
392
+ "audio_server": audio_state,
379
393
  "source": "robovision_pi",
380
394
  })
381
395
 
@@ -397,6 +411,18 @@ def media_config():
397
411
  audio_input_device = str(data["audio_device"])
398
412
  if "audio_output_device" in data:
399
413
  audio_output_device = str(data["audio_output_device"])
414
+ if "audio_device" in data or "audio_output_device" in data:
415
+ # audio_server_pi.py is RoboVision's authoritative selector.
416
+ # It accepts its original sounddevice indices via input/output.
417
+ payload = {}
418
+ if "audio_device" in data:
419
+ payload["input"] = int(audio_input_device) if audio_input_device.isdigit() else audio_input_device
420
+ if "audio_output_device" in data:
421
+ payload["output"] = int(audio_output_device) if audio_output_device.isdigit() else audio_output_device
422
+ try:
423
+ requests.post(f"{ROBOVISION_AUDIO_URL.rstrip('/')}/set-device", json=payload, timeout=0.8).raise_for_status()
424
+ except Exception:
425
+ pass
400
426
  return jsonify({"video_device": str(current_camera_index), "audio_device": audio_input_device, "audio_output_device": audio_output_device, "source": "robovision_pi"})
401
427
 
402
428
  @app.route('/api/motion/status', methods=['GET'])
@@ -67,7 +67,8 @@ pydantic==2.5.0
67
67
  # NETWORKING & HTTP
68
68
  # ============================================================================
69
69
  # HTTP requests
70
- requests==2.31.0
70
+ requests==2.31.0
71
+ groq>=0.11
71
72
 
72
73
  # CORS support
73
74
  python-multipart==0.0.6