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.
@@ -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
- el.innerHTML='';
6276
- 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){
6277
- var o=document.createElement('option'); o.value=String(x.id); o.textContent=String(x.name||x.id); if(String(x.id)===String(selected||'')) o.selected=true; el.appendChild(o);
6278
- });
6279
- if(!el.value && selected){ var o=document.createElement('option'); o.value=String(selected); o.textContent=String(selected)+' (saved)'; o.selected=true; el.appendChild(o); }
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
- rpAction('PATCH','/robopark/api/devices/'+encodeURIComponent(id),body).then(function(){
6304
- showOut('camera and microphone selection saved for '+(device.name||id));
6305
- }).catch(function(e){ showOut('device selection: '+e.message); });
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
- var outputSelect=$('pk-audio-output'); if(outputSelect) outputSelect.addEventListener('change', function(){ refreshDeviceSelectors(n); });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.77",
3
+ "version": "2.8.79",
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",
@@ -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
- result = await asyncio.to_thread(_speaker_roundtrip_test, request.get("params") or {})
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
- wasapi = pa.get_host_api_info_by_type(pyaudio.paWASAPI)
475
- key = "defaultInputDevice" if kind == "input" else "defaultOutputDevice"
476
- idx = wasapi.get(key)
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
- out_name = params.get("output", os.environ.get("ROBOPARK_AUDIO_OUTPUT") or SPEAKER_TEST_OUTPUT_DEVICE)
596
- in_name = params.get("input", os.environ.get("ROBOPARK_AUDIO_INPUT") or SPEAKER_TEST_INPUT_DEVICE)
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}"}