infinicode 2.8.77 → 2.8.79
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/command-system.js +19 -3
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +45 -16
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +4 -2
- package/packages/robopark/scheduler/preview_agent.py +24 -8
- package/packages/robopark/scheduler/robot_supervisor.py +12 -10
|
@@ -6270,13 +6270,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6270
6270
|
var video=deviceList(inv,'video',['auto','none']);
|
|
6271
6271
|
var input=deviceList(inv,'audio_input',['default','none']);
|
|
6272
6272
|
var output=deviceList(inv,'audio_output',['default','none']);
|
|
6273
|
-
function fill(id, list, selected){
|
|
6274
|
-
var el=$(id); if(!el) return;
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6273
|
+
function fill(id, list, selected){
|
|
6274
|
+
var el=$(id); if(!el) return;
|
|
6275
|
+
// Heartbeat polling rebuilds this panel frequently. Preserve an
|
|
6276
|
+
// operator's unsaved choice instead of snapping back to the last
|
|
6277
|
+
// scheduler value while they move between camera/mic/speaker fields.
|
|
6278
|
+
var effectiveSelected=el.dataset.dirty==='true'?el.value:selected;
|
|
6279
|
+
el.innerHTML='';
|
|
6280
|
+
list.filter(function(x){ return !query || String(x.name||x.id).toLowerCase().indexOf(query)>-1 || String(x.id).toLowerCase().indexOf(query)>-1; }).forEach(function(x){
|
|
6281
|
+
var o=document.createElement('option'); o.value=String(x.id); o.textContent=String(x.name||x.id); if(String(x.id)===String(effectiveSelected||'')) o.selected=true; el.appendChild(o);
|
|
6282
|
+
});
|
|
6283
|
+
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); }
|
|
6280
6284
|
}
|
|
6281
6285
|
fill('pk-video-device', video, device.video_device||'auto');
|
|
6282
6286
|
fill('pk-audio-input', input, device.audio_device||'default');
|
|
@@ -6296,13 +6300,27 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6296
6300
|
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');
|
|
6297
6301
|
}
|
|
6298
6302
|
}
|
|
6299
|
-
function saveDeviceSelectors(n){
|
|
6300
|
-
var device=schedulerDevice(n), id=device&&device.id; if(!id){ showOut('device inventory is not available yet'); return; }
|
|
6301
|
-
var body={video_device:$('pk-video-device').value, audio_device:$('pk-audio-input').value, audio_output_device:$('pk-audio-output').value,
|
|
6302
|
-
greeting_phrases:($('pk-greeting-phrases').value||'').split(String.fromCharCode(10)).map(function(x){return x.trim();}).filter(Boolean)};
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6303
|
+
function saveDeviceSelectors(n){
|
|
6304
|
+
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
|
+
greeting_phrases:($('pk-greeting-phrases').value||'').split(String.fromCharCode(10)).map(function(x){return x.trim();}).filter(Boolean)};
|
|
6307
|
+
var button=$('pk-device-save'),note=$('pk-device-note');
|
|
6308
|
+
if(button){button.disabled=true;button.textContent='Saving...';}
|
|
6309
|
+
if(note)note.textContent='saving device selection to scheduler...';
|
|
6310
|
+
rpAction('PATCH','/robopark/api/devices/'+encodeURIComponent(id),body).then(function(saved){
|
|
6311
|
+
// Keep the drawer's local scheduler record current immediately. The
|
|
6312
|
+
// next /devices poll will confirm the same values from persistent DB.
|
|
6313
|
+
Object.assign(device,saved||body);
|
|
6314
|
+
['pk-video-device','pk-audio-input','pk-audio-output'].forEach(function(field){var el=$(field);if(el)delete el.dataset.dirty;});
|
|
6315
|
+
refreshDeviceSelectors(n);
|
|
6316
|
+
if(note)note.textContent='saved · robot will apply selection on its next config heartbeat';
|
|
6317
|
+
if(button){button.textContent='Saved';setTimeout(function(){if($('pk-device-save')===button)button.textContent='Save device selection + greetings';},1400);}
|
|
6318
|
+
showOut('camera, microphone, and speaker selection saved for '+(device.name||id));
|
|
6319
|
+
}).catch(function(e){
|
|
6320
|
+
if(note)note.textContent='save failed · '+e.message;
|
|
6321
|
+
if(button)button.textContent='Retry save';
|
|
6322
|
+
showOut('device selection: '+e.message);
|
|
6323
|
+
}).then(function(){if(button)button.disabled=false;});
|
|
6306
6324
|
}
|
|
6307
6325
|
function fmtUptime(secs){
|
|
6308
6326
|
if(secs==null) return '';
|
|
@@ -6705,7 +6723,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6705
6723
|
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>';
|
|
6706
6724
|
var deviceSearch=$('pk-device-search'); if(deviceSearch) deviceSearch.addEventListener('input', function(){ refreshDeviceSelectors(n); });
|
|
6707
6725
|
var deviceSave=$('pk-device-save'); if(deviceSave) deviceSave.addEventListener('click', function(){ saveDeviceSelectors(n); });
|
|
6708
|
-
|
|
6726
|
+
['pk-video-device','pk-audio-input','pk-audio-output'].forEach(function(field){
|
|
6727
|
+
var select=$(field);if(!select)return;
|
|
6728
|
+
select.addEventListener('change',function(){
|
|
6729
|
+
select.dataset.dirty='true';
|
|
6730
|
+
var note=$('pk-device-note');if(note)note.textContent='unsaved selection · click Save device selection + greetings';
|
|
6731
|
+
if(field==='pk-audio-output'){
|
|
6732
|
+
var selectedLabel=$('pk-spk-selected'),option=select.options&&select.selectedIndex>=0?select.options[select.selectedIndex]:null;
|
|
6733
|
+
if(selectedLabel)selectedLabel.textContent=option?String(option.textContent||option.value):String(select.value||'default');
|
|
6734
|
+
}
|
|
6735
|
+
});
|
|
6736
|
+
});
|
|
6709
6737
|
var micListen=$('pk-mic-listen');if(micListen)micListen.addEventListener('click',function(){setMicMonitorAudible(!micMonitorAudible);});
|
|
6710
6738
|
var micVolume=$('pk-mic-volume');if(micVolume)micVolume.addEventListener('input',function(){micMonitorVolume=Math.max(0,Math.min(1,Number(micVolume.value||80)/100));lkAudioEls.forEach(function(el){el.volume=micMonitorVolume;});});
|
|
6711
6739
|
startCam(n); startMeters(n); loadEffectiveConfig(schedId(n)); refreshSupervisorPanel(n);
|
|
@@ -7153,12 +7181,13 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7153
7181
|
var phrase=String((($('pk-spk-phrase')||{}).value)||'Hello, I am ready to talk.').trim();
|
|
7154
7182
|
var output=(($('pk-audio-output')||{}).value)||'default';
|
|
7155
7183
|
var input=(($('pk-audio-input')||{}).value)||'default';
|
|
7184
|
+
var outputName=selectedRobotOutput(),inputName=selectedRobotMic();
|
|
7156
7185
|
// Animate the meter with a synthetic "during-test" peak so the
|
|
7157
7186
|
// operator sees something happen immediately while waiting for
|
|
7158
7187
|
// the real recording.
|
|
7159
7188
|
_spkTestSetLive(robotId, {live:true, rms: 0.45, pass: undefined, waveform: null});
|
|
7160
7189
|
rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/speaker-test',
|
|
7161
|
-
{mode:mode, text:phrase, frequency:freq, duration:dur, amplitude:0.6, threshold_db:-30, output:output, input:input})
|
|
7190
|
+
{mode:mode, text:phrase, frequency:freq, duration:dur, amplitude:0.6, threshold_db:-30, output:output, input:input, output_name:outputName, input_name:inputName})
|
|
7162
7191
|
.then(function(q){
|
|
7163
7192
|
// long-poll up to 8s, then fall back to 12s of result polling
|
|
7164
7193
|
if(status) status.textContent = mode==='tts'?'playing cached voice on '+selectedRobotOutput()+'…':'playing tone + recording…';
|
package/package.json
CHANGED
|
@@ -3472,8 +3472,10 @@ class SpeakerTestRequest(BaseModel):
|
|
|
3472
3472
|
duration: Optional[float] = None
|
|
3473
3473
|
amplitude: Optional[float] = None
|
|
3474
3474
|
threshold_db: Optional[float] = None
|
|
3475
|
-
output: Optional[str] = None
|
|
3476
|
-
input: Optional[str] = None
|
|
3475
|
+
output: Optional[str] = None
|
|
3476
|
+
input: Optional[str] = None
|
|
3477
|
+
output_name: Optional[str] = None
|
|
3478
|
+
input_name: Optional[str] = None
|
|
3477
3479
|
sample_rate: Optional[int] = None
|
|
3478
3480
|
|
|
3479
3481
|
|
|
@@ -846,7 +846,18 @@ class PreviewAgent:
|
|
|
846
846
|
return
|
|
847
847
|
self._speaker_test_in_flight = True
|
|
848
848
|
from robot_supervisor import _speaker_roundtrip_test
|
|
849
|
-
|
|
849
|
+
# LiveKit owns the USB microphone continuously. Release that owner
|
|
850
|
+
# for an explicit hardware test, then restore the same session.
|
|
851
|
+
restore_state = self._current_state if self._publisher else None
|
|
852
|
+
if restore_state:
|
|
853
|
+
await self._stop_publisher()
|
|
854
|
+
await asyncio.sleep(0.35)
|
|
855
|
+
try:
|
|
856
|
+
result = await asyncio.to_thread(_speaker_roundtrip_test, request.get("params") or {})
|
|
857
|
+
result["media_owner_paused"] = bool(restore_state)
|
|
858
|
+
finally:
|
|
859
|
+
if restore_state and restore_state.active:
|
|
860
|
+
await self._start_publisher(restore_state)
|
|
850
861
|
result_response = await self._session.post(
|
|
851
862
|
f"{self.scheduler_url.rstrip('/')}/api/devices/{self.device_id}/supervisor-output",
|
|
852
863
|
json={"kind": "speaker_test", "service": None, "payload": result,
|
|
@@ -1092,8 +1103,9 @@ class LiveKitPublisher:
|
|
|
1092
1103
|
self.audio_source: Optional[rtc.AudioSource] = None
|
|
1093
1104
|
self.audio_track: Optional[rtc.LocalAudioTrack] = None
|
|
1094
1105
|
self._stop_event = asyncio.Event()
|
|
1095
|
-
self._tasks: list[asyncio.Task] = []
|
|
1096
|
-
self._capture: Optional["VideoCapture"] = None
|
|
1106
|
+
self._tasks: list[asyncio.Task] = []
|
|
1107
|
+
self._capture: Optional["VideoCapture"] = None
|
|
1108
|
+
self._mic_capture: Optional["AudioCapture"] = None
|
|
1097
1109
|
# Motion sampling state belongs to the publisher instance. Keeping it
|
|
1098
1110
|
# initialized here prevents shutdown/reopen paths from raising while
|
|
1099
1111
|
# the camera is being handed between preview and voice sessions.
|
|
@@ -1373,7 +1385,7 @@ class LiveKitPublisher:
|
|
|
1373
1385
|
out.close()
|
|
1374
1386
|
pa.terminate()
|
|
1375
1387
|
|
|
1376
|
-
async def stop(self) -> None:
|
|
1388
|
+
async def stop(self) -> None:
|
|
1377
1389
|
self._stop_event.set()
|
|
1378
1390
|
for t in self._tasks:
|
|
1379
1391
|
t.cancel()
|
|
@@ -1381,7 +1393,10 @@ class LiveKitPublisher:
|
|
|
1381
1393
|
await t
|
|
1382
1394
|
except asyncio.CancelledError:
|
|
1383
1395
|
pass
|
|
1384
|
-
self._tasks.clear()
|
|
1396
|
+
self._tasks.clear()
|
|
1397
|
+
if self._mic_capture:
|
|
1398
|
+
await asyncio.to_thread(self._mic_capture.stop)
|
|
1399
|
+
self._mic_capture = None
|
|
1385
1400
|
if self._capture:
|
|
1386
1401
|
self._capture.stop()
|
|
1387
1402
|
self._capture = None
|
|
@@ -1442,10 +1457,11 @@ class LiveKitPublisher:
|
|
|
1442
1457
|
|
|
1443
1458
|
async def _audio_loop(self) -> None:
|
|
1444
1459
|
assert self.audio_source is not None
|
|
1445
|
-
mic = create_audio_capture(self.agent.audio_device)
|
|
1446
|
-
if mic is None:
|
|
1460
|
+
mic = create_audio_capture(self.agent.audio_device)
|
|
1461
|
+
if mic is None:
|
|
1447
1462
|
logger.warning("audio_loop: create_audio_capture returned None, mic will not publish")
|
|
1448
|
-
return
|
|
1463
|
+
return
|
|
1464
|
+
self._mic_capture = mic
|
|
1449
1465
|
logger.info(f"audio_loop: mic capture started ({type(mic).__name__})")
|
|
1450
1466
|
# Read in bigger batches (100ms) instead of one 20ms frame per
|
|
1451
1467
|
# asyncio.to_thread() dispatch. Each dispatch/poll cycle has a fixed
|
|
@@ -468,14 +468,14 @@ def _resolve_audio_device(pa, selected: str, kind: str) -> Optional[int]:
|
|
|
468
468
|
result is recorded so the operator can see it."""
|
|
469
469
|
if selected is None:
|
|
470
470
|
selected = "default"
|
|
471
|
-
s = str(selected).strip()
|
|
472
|
-
if s == "" or s.lower() == "default":
|
|
473
|
-
try:
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
idx =
|
|
477
|
-
if idx is not None and idx >= 0:
|
|
478
|
-
return idx
|
|
471
|
+
s = str(selected).strip()
|
|
472
|
+
if s == "" or s.lower() == "default":
|
|
473
|
+
try:
|
|
474
|
+
info = (pa.get_default_input_device_info() if kind == "input"
|
|
475
|
+
else pa.get_default_output_device_info())
|
|
476
|
+
idx = info.get("index")
|
|
477
|
+
if idx is not None and idx >= 0:
|
|
478
|
+
return int(idx)
|
|
479
479
|
except Exception:
|
|
480
480
|
pass
|
|
481
481
|
return None
|
|
@@ -592,8 +592,10 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
592
592
|
dur = float(params.get("duration", SPEAKER_TEST_DURATION_S))
|
|
593
593
|
amp = float(params.get("amplitude", SPEAKER_TEST_AMPLITUDE))
|
|
594
594
|
threshold_db = float(params.get("threshold_db", SPEAKER_TEST_PEAK_DB_THRESHOLD))
|
|
595
|
-
|
|
596
|
-
|
|
595
|
+
# RoboVision inventory indices belong to sounddevice and may not match
|
|
596
|
+
# PyAudio's index space. Prefer the reported hardware name for lookup.
|
|
597
|
+
out_name = params.get("output_name") or params.get("output", os.environ.get("ROBOPARK_AUDIO_OUTPUT") or SPEAKER_TEST_OUTPUT_DEVICE)
|
|
598
|
+
in_name = params.get("input_name") or params.get("input", os.environ.get("ROBOPARK_AUDIO_INPUT") or SPEAKER_TEST_INPUT_DEVICE)
|
|
597
599
|
source_rate = int(params.get("sample_rate", SPEAKER_TEST_SAMPLE_RATE))
|
|
598
600
|
if mode not in ("tone", "tts"):
|
|
599
601
|
return {"ok": False, "error": f"unsupported speaker test mode: {mode}"}
|