infinicode 2.8.76 → 2.8.78
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 +92 -26
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +33 -6
- package/packages/robopark/scheduler/preview_agent.py +24 -8
- package/packages/robopark/scheduler/robot_supervisor.py +168 -64
|
@@ -458,7 +458,7 @@ SPEAKER_TEST_INPUT_DEVICE = None
|
|
|
458
458
|
SPEAKER_TEST_PEAK_DB_THRESHOLD = -30.0
|
|
459
459
|
|
|
460
460
|
|
|
461
|
-
def _resolve_audio_device(pa, selected: str, kind: str) -> Optional[int]:
|
|
461
|
+
def _resolve_audio_device(pa, selected: str, kind: str) -> Optional[int]:
|
|
462
462
|
"""Resolve a free-form device name/index to a PyAudio device index.
|
|
463
463
|
|
|
464
464
|
kind is "input" or "output". None / "default" picks the WASAPI
|
|
@@ -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
|
|
@@ -491,9 +491,93 @@ def _resolve_audio_device(pa, selected: str, kind: str) -> Optional[int]:
|
|
|
491
491
|
continue
|
|
492
492
|
if needle in str(info.get("name", "")).lower():
|
|
493
493
|
return i
|
|
494
|
-
return None
|
|
495
|
-
|
|
496
|
-
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _audio_rate_candidates(info: dict, requested_rate: int) -> list[int]:
|
|
498
|
+
"""Return practical PCM rates with the device native rate first."""
|
|
499
|
+
rates = []
|
|
500
|
+
for value in (
|
|
501
|
+
info.get("defaultSampleRate"),
|
|
502
|
+
requested_rate,
|
|
503
|
+
48000,
|
|
504
|
+
44100,
|
|
505
|
+
32000,
|
|
506
|
+
24000,
|
|
507
|
+
16000,
|
|
508
|
+
):
|
|
509
|
+
try:
|
|
510
|
+
rate = int(round(float(value)))
|
|
511
|
+
except (TypeError, ValueError):
|
|
512
|
+
continue
|
|
513
|
+
if rate > 0 and rate not in rates:
|
|
514
|
+
rates.append(rate)
|
|
515
|
+
return rates
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _resample_pcm16_mono(raw: bytes, source_rate: int, target_rate: int) -> bytes:
|
|
519
|
+
"""Linearly resample little-endian mono PCM16 without optional DSP deps."""
|
|
520
|
+
if source_rate == target_rate or not raw:
|
|
521
|
+
return raw
|
|
522
|
+
source_count = len(raw) // 2
|
|
523
|
+
target_count = max(1, int(round(source_count * target_rate / source_rate)))
|
|
524
|
+
source = struct.unpack(f"<{source_count}h", raw[:source_count * 2])
|
|
525
|
+
if source_count == 1:
|
|
526
|
+
return struct.pack("<h", source[0]) * target_count
|
|
527
|
+
scale = (source_count - 1) / max(1, target_count - 1)
|
|
528
|
+
result = bytearray(target_count * 2)
|
|
529
|
+
for index in range(target_count):
|
|
530
|
+
position = index * scale
|
|
531
|
+
left = int(position)
|
|
532
|
+
right = min(left + 1, source_count - 1)
|
|
533
|
+
fraction = position - left
|
|
534
|
+
value = int(round(source[left] + (source[right] - source[left]) * fraction))
|
|
535
|
+
struct.pack_into("<h", result, index * 2, value)
|
|
536
|
+
return bytes(result)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _open_pcm_stream(pa, pyaudio_module, kind: str, device_index: Optional[int],
|
|
540
|
+
requested_rate: int):
|
|
541
|
+
"""Open a PCM16 stream using the first format the device accepts."""
|
|
542
|
+
try:
|
|
543
|
+
if device_index is None:
|
|
544
|
+
info = (pa.get_default_output_device_info() if kind == "output"
|
|
545
|
+
else pa.get_default_input_device_info())
|
|
546
|
+
device_index = int(info.get("index"))
|
|
547
|
+
else:
|
|
548
|
+
info = pa.get_device_info_by_index(device_index)
|
|
549
|
+
except Exception as exc:
|
|
550
|
+
raise OSError(f"selected {kind} device is unavailable: {exc}") from exc
|
|
551
|
+
|
|
552
|
+
channel_key = "maxOutputChannels" if kind == "output" else "maxInputChannels"
|
|
553
|
+
max_channels = int(info.get(channel_key, 0) or 0)
|
|
554
|
+
if max_channels < 1:
|
|
555
|
+
raise OSError(f"{info.get('name', device_index)} has no {kind} channels")
|
|
556
|
+
channels = [2, 1] if kind == "output" and max_channels >= 2 else [1]
|
|
557
|
+
errors = []
|
|
558
|
+
for rate in _audio_rate_candidates(info, requested_rate):
|
|
559
|
+
for channel_count in channels:
|
|
560
|
+
kwargs = {
|
|
561
|
+
"format": pyaudio_module.paInt16,
|
|
562
|
+
"channels": channel_count,
|
|
563
|
+
"rate": rate,
|
|
564
|
+
kind: True,
|
|
565
|
+
f"{kind}_device_index": device_index,
|
|
566
|
+
}
|
|
567
|
+
if kind == "input":
|
|
568
|
+
kwargs["frames_per_buffer"] = 1024
|
|
569
|
+
try:
|
|
570
|
+
stream = pa.open(**kwargs)
|
|
571
|
+
return stream, rate, channel_count, info
|
|
572
|
+
except Exception as exc:
|
|
573
|
+
errors.append(f"{rate}Hz/{channel_count}ch: {exc}")
|
|
574
|
+
attempts = "; ".join(errors[-4:])
|
|
575
|
+
raise OSError(
|
|
576
|
+
f"{info.get('name', device_index)} supports no usable PCM16 {kind} format"
|
|
577
|
+
+ (f" ({attempts})" if attempts else "")
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
|
|
497
581
|
def _speaker_roundtrip_test(params: dict) -> dict:
|
|
498
582
|
"""Play a test tone + record the mic simultaneously; return metrics.
|
|
499
583
|
|
|
@@ -508,9 +592,11 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
508
592
|
dur = float(params.get("duration", SPEAKER_TEST_DURATION_S))
|
|
509
593
|
amp = float(params.get("amplitude", SPEAKER_TEST_AMPLITUDE))
|
|
510
594
|
threshold_db = float(params.get("threshold_db", SPEAKER_TEST_PEAK_DB_THRESHOLD))
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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)
|
|
599
|
+
source_rate = int(params.get("sample_rate", SPEAKER_TEST_SAMPLE_RATE))
|
|
514
600
|
if mode not in ("tone", "tts"):
|
|
515
601
|
return {"ok": False, "error": f"unsupported speaker test mode: {mode}"}
|
|
516
602
|
if mode == "tone" and not (50.0 <= freq <= 8000.0):
|
|
@@ -527,8 +613,10 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
527
613
|
return {"ok": False, "error": f"invalid cached TTS audio: {e}"}
|
|
528
614
|
if len(mono_pcm) < 480 or len(mono_pcm) % 2:
|
|
529
615
|
return {"ok": False, "error": "cached TTS audio is empty or malformed"}
|
|
530
|
-
dur = len(mono_pcm) / 2 /
|
|
531
|
-
|
|
616
|
+
dur = len(mono_pcm) / 2 / source_rate
|
|
617
|
+
if dur > 10.0:
|
|
618
|
+
return {"ok": False, "error": "cached TTS audio exceeds the 10 second test limit"}
|
|
619
|
+
n_samples = int(source_rate * dur)
|
|
532
620
|
if n_samples < 480:
|
|
533
621
|
return {"ok": False, "error": "duration too short"}
|
|
534
622
|
|
|
@@ -542,44 +630,49 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
542
630
|
out_label = _dev_name(out_idx)
|
|
543
631
|
in_label = _dev_name(in_idx)
|
|
544
632
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
633
|
+
recorded_peak = 0
|
|
634
|
+
recorded_rms = 0.0
|
|
635
|
+
err = None
|
|
636
|
+
output_rate = None
|
|
637
|
+
input_rate = None
|
|
638
|
+
output_channels = None
|
|
639
|
+
input_channels = None
|
|
640
|
+
t0 = time.monotonic()
|
|
641
|
+
out_stream = None
|
|
642
|
+
in_stream = None
|
|
643
|
+
try:
|
|
644
|
+
out_stream, output_rate, output_channels, out_info = _open_pcm_stream(
|
|
645
|
+
pa, pyaudio, "output", out_idx, source_rate
|
|
646
|
+
)
|
|
647
|
+
in_stream, input_rate, input_channels, in_info = _open_pcm_stream(
|
|
648
|
+
pa, pyaudio, "input", in_idx, source_rate
|
|
649
|
+
)
|
|
650
|
+
out_label = str(out_info.get("name", out_label))
|
|
651
|
+
in_label = str(in_info.get("name", in_label))
|
|
652
|
+
|
|
653
|
+
if mono_pcm is not None:
|
|
654
|
+
output_mono = _resample_pcm16_mono(mono_pcm, source_rate, output_rate)
|
|
655
|
+
else:
|
|
656
|
+
output_samples = int(output_rate * dur)
|
|
657
|
+
frames = bytearray(output_samples * 2)
|
|
658
|
+
peak_value = int(32767 * amp)
|
|
659
|
+
for n in range(output_samples):
|
|
660
|
+
env = min(1.0, n / (output_rate * 0.004),
|
|
661
|
+
(output_samples - n) / (output_rate * 0.008))
|
|
662
|
+
value = int(peak_value * env * math.sin(2 * math.pi * freq * n / output_rate))
|
|
663
|
+
struct.pack_into("<h", frames, n * 2, value)
|
|
664
|
+
output_mono = bytes(frames)
|
|
665
|
+
if output_channels == 2:
|
|
666
|
+
frames = bytearray(len(output_mono) * 2)
|
|
667
|
+
for pos in range(0, len(output_mono), 2):
|
|
668
|
+
frames[pos * 2:pos * 2 + 4] = output_mono[pos:pos + 2] * 2
|
|
669
|
+
raw = bytes(frames)
|
|
670
|
+
else:
|
|
671
|
+
raw = output_mono
|
|
672
|
+
|
|
673
|
+
chunk = 1024
|
|
674
|
+
in_stream.start_stream()
|
|
675
|
+
chunk_bytes = chunk * 2 * output_channels
|
|
583
676
|
pos = 0
|
|
584
677
|
peak_acc = 0
|
|
585
678
|
rms_acc = 0.0
|
|
@@ -601,9 +694,10 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
601
694
|
n_samp += 1
|
|
602
695
|
except Exception:
|
|
603
696
|
pass
|
|
604
|
-
out_stream.stop_stream(); out_stream.close()
|
|
605
|
-
try:
|
|
606
|
-
|
|
697
|
+
out_stream.stop_stream(); out_stream.close(); out_stream = None
|
|
698
|
+
try:
|
|
699
|
+
tail_frames = min(int(input_rate * 0.35), max(0, in_stream.get_read_available()))
|
|
700
|
+
tail = in_stream.read(tail_frames, exception_on_overflow=False) if tail_frames else b""
|
|
607
701
|
for s in range(0, len(tail) - 1, 2):
|
|
608
702
|
v = int.from_bytes(tail[s:s+2], "little", signed=True)
|
|
609
703
|
a = abs(v)
|
|
@@ -612,13 +706,19 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
612
706
|
n_samp += 1
|
|
613
707
|
except Exception:
|
|
614
708
|
pass
|
|
615
|
-
in_stream.stop_stream(); in_stream.close()
|
|
709
|
+
in_stream.stop_stream(); in_stream.close(); in_stream = None
|
|
616
710
|
recorded_peak = peak_acc
|
|
617
711
|
recorded_rms = (rms_acc / max(1, n_samp)) ** 0.5
|
|
618
|
-
except Exception as e:
|
|
619
|
-
err = f"{type(e).__name__}: {e}"
|
|
620
|
-
finally:
|
|
621
|
-
|
|
712
|
+
except Exception as e:
|
|
713
|
+
err = f"{type(e).__name__}: {e}"
|
|
714
|
+
finally:
|
|
715
|
+
for stream in (out_stream, in_stream):
|
|
716
|
+
if stream is not None:
|
|
717
|
+
try: stream.stop_stream()
|
|
718
|
+
except Exception: pass
|
|
719
|
+
try: stream.close()
|
|
720
|
+
except Exception: pass
|
|
721
|
+
try: pa.terminate()
|
|
622
722
|
except Exception: pass
|
|
623
723
|
duration_ms = int((time.monotonic() - t0) * 1000)
|
|
624
724
|
if err:
|
|
@@ -645,7 +745,11 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
645
745
|
"frequency": freq,
|
|
646
746
|
"duration": dur,
|
|
647
747
|
"duration_ms": duration_ms,
|
|
648
|
-
"sample_rate":
|
|
748
|
+
"sample_rate": source_rate,
|
|
749
|
+
"output_sample_rate": output_rate,
|
|
750
|
+
"input_sample_rate": input_rate,
|
|
751
|
+
"output_channels": output_channels,
|
|
752
|
+
"input_channels": input_channels,
|
|
649
753
|
"amplitude": amp,
|
|
650
754
|
"output_device": out_label,
|
|
651
755
|
"input_device": in_label,
|