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.
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +24 -11
- package/dist/robopark/python-env.d.ts +1 -0
- package/dist/robopark/python-env.js +3 -0
- package/dist/robopark/stop-all.js +1 -0
- package/dist/robopark/vision-agent-launcher.js +21 -2
- package/package.json +1 -1
- package/packages/robopark/vision/app_pi_clean.py +41 -15
- package/packages/robopark/vision/requirements_pi_unified.txt +2 -1
|
@@ -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),
|
|
5457
|
-
|
|
5458
|
-
var
|
|
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
|
-
|
|
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');
|
|
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');
|
|
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']
|
|
@@ -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
|
|
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
|
@@ -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
|
-
|
|
348
|
-
outputs = [{"id": "default", "name": "System default output"}]
|
|
348
|
+
"""Adapt RoboVision's existing audio_server_pi /devices response."""
|
|
349
349
|
try:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
363
|
+
if device.get("max_output_channels", 0) > 0:
|
|
358
364
|
outputs.append(item.copy())
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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": {
|
|
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'])
|