infinicode 2.8.79 → 2.8.81
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 +23 -15
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +26 -1
- package/packages/robopark/scheduler/preview_agent.py +15 -1
- package/packages/robopark/scheduler/robot_supervisor.py +50 -23
|
@@ -6819,6 +6819,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6819
6819
|
if(!result.queued) throw new Error(result.reason||'trigger was not queued');
|
|
6820
6820
|
showOut('full production test queued for '+(n.label||n.name));
|
|
6821
6821
|
pollPipeline(n);
|
|
6822
|
+
// The stream endpoint is initially idle and becomes active only after
|
|
6823
|
+
// the robot consumes the trigger. Retry through that transition.
|
|
6824
|
+
[400,1200,2500,4500].forEach(function(delay){setTimeout(function(){if(drawerId)startMicMonitor(n);},delay);});
|
|
6822
6825
|
}).catch(function(e){ showOut('production test: '+e.message); pollPipeline(n); }).finally(function(){ if(start){ start.disabled=false; start.textContent='Start full test'; } });
|
|
6823
6826
|
}
|
|
6824
6827
|
function startPipelineTest(n){
|
|
@@ -6969,19 +6972,25 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6969
6972
|
var note=$('pk-camnote'), res=$('pk-camres');
|
|
6970
6973
|
try{
|
|
6971
6974
|
var LK=window.LivekitClient; lkRoom=new LK.Room();
|
|
6972
|
-
lkRoom.on(LK.RoomEvent.TrackSubscribed, function(track){
|
|
6975
|
+
lkRoom.on(LK.RoomEvent.TrackSubscribed, function(track,publication,participant){
|
|
6973
6976
|
if(track.kind==='video'){
|
|
6974
6977
|
if(audioOnly)return;
|
|
6975
6978
|
var el=track.attach(); el.setAttribute('playsinline',''); el.muted=true; el.autoplay=true;
|
|
6976
6979
|
var cv=$('pk-camcv'); if(cv&&cv.parentNode) cv.parentNode.replaceChild(el, cv);
|
|
6977
6980
|
if(res) res.textContent='LIVE'; if(note) note.textContent='live camera via LiveKit · '+esc(info.room);
|
|
6978
6981
|
} else if(track.kind==='audio'){
|
|
6979
|
-
//
|
|
6980
|
-
//
|
|
6981
|
-
//
|
|
6982
|
-
|
|
6982
|
+
// Track names are backend-dependent ("mic", device labels, or
|
|
6983
|
+
// empty). Identify the robot by participant instead; reject the
|
|
6984
|
+
// voice worker/viewer so this remains the physical robot mic.
|
|
6985
|
+
var identity=String(participant&&participant.identity||'');
|
|
6986
|
+
var expected=String(schedId(n)||'').toLowerCase();
|
|
6987
|
+
var nodeName=String(n.name||'').toLowerCase();
|
|
6988
|
+
var lowerIdentity=identity.toLowerCase();
|
|
6989
|
+
var isWorker=/(agent|operator|viewer)/.test(lowerIdentity);
|
|
6990
|
+
var isRobot=!isWorker&&(lowerIdentity===expected||lowerIdentity===nodeName||lowerIdentity.indexOf(expected)>-1||lowerIdentity.indexOf(nodeName)>-1);
|
|
6991
|
+
if(!isRobot){if(window.console)console.debug('RoboPark ignored non-robot audio track',identity,track.name,publication&&publication.source);return;}
|
|
6983
6992
|
hookMicAnalyser(track);
|
|
6984
|
-
micMonitorState='live
|
|
6993
|
+
micMonitorState='live robot mic from '+identity;
|
|
6985
6994
|
var micState=$('pk-micstate');if(micState)micState.textContent='selected '+selectedRobotMic()+' · live track published';
|
|
6986
6995
|
// Muted by default: this is the camera-preview panel, running on
|
|
6987
6996
|
// the SAME physical device as the robot in local testing. Auto-
|
|
@@ -6991,9 +7000,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6991
7000
|
// that drowns out the actual conversation. The VU meter above
|
|
6992
7001
|
// (hookMicAnalyser) taps the raw track directly and works fine
|
|
6993
7002
|
// muted.
|
|
6994
|
-
var audio=track.attach(); audio.autoplay=true; audio.controls=false; audio.muted=!micMonitorAudible; audio.volume=micMonitorVolume;audio.style.display='none'; document.body.appendChild(audio); lkAudioEls.push(audio);
|
|
7003
|
+
var audio=track.attach(); audio.autoplay=true; audio.playsInline=true;audio.setAttribute('playsinline','');audio.controls=false; audio.muted=!micMonitorAudible; audio.volume=micMonitorVolume;audio.style.display='none'; document.body.appendChild(audio); lkAudioEls.push(audio);
|
|
6995
7004
|
var listenNote=$('pk-mic-listen-note');if(listenNote&&micMonitorAudible)listenNote.textContent='Robot mic is playing in this browser. Use headphones.';
|
|
6996
|
-
audio.play().catch(function(){});
|
|
7005
|
+
audio.play().catch(function(e){var listenNote=$('pk-mic-listen-note');if(listenNote)listenNote.textContent='Browser blocked robot audio: '+e.message+'. Click Listen in browser.';});
|
|
6997
7006
|
}
|
|
6998
7007
|
});
|
|
6999
7008
|
lkRoom.on(LK.RoomEvent.Disconnected, function(){lkRoom=null;micAnalyser=null;micMonitorConnecting=false;micMonitorState='session ended';var micState=$('pk-micstate');if(micState)micState.textContent='selected '+selectedRobotMic()+' · session ended';if(!audioOnly&¬e)note.textContent='session ended';if(!audioOnly&&res)res.textContent='—';});
|
|
@@ -7155,16 +7164,14 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7155
7164
|
var banner = result.pass
|
|
7156
7165
|
? '<div class="pk-spk-banner ok">✓ '+(isTts?'cached voice playback OK':'speaker round-trip OK')+'</div>'
|
|
7157
7166
|
: '<div class="pk-spk-banner bad">✗ no signal detected</div>';
|
|
7158
|
-
var meta = '<div class="pk-spk-meta">'
|
|
7159
|
-
+ '<div><span class="l">peak</span><span class="v">'+esc(result.recorded_peak_db+' dB')+'</span></div>'
|
|
7160
|
-
+ '<div><span class="l">RMS</span><span class="v">'+esc(result.recorded_rms_db+' dB')+'</span></div>'
|
|
7161
|
-
+ '<div><span class="l">threshold</span><span class="v">'+esc(result.threshold_db+' dB')+'</span></div>'
|
|
7167
|
+
var meta = '<div class="pk-spk-meta">'
|
|
7168
|
+
+ (result.playback_only?'':'<div><span class="l">peak</span><span class="v">'+esc(result.recorded_peak_db+' dB')+'</span></div><div><span class="l">RMS</span><span class="v">'+esc(result.recorded_rms_db+' dB')+'</span></div><div><span class="l">threshold</span><span class="v">'+esc(result.threshold_db+' dB')+'</span></div>')
|
|
7162
7169
|
+ (isTts?'<div><span class="l">voice</span><span class="v">'+esc((result.tts_provider||'tts')+' / '+(result.tts_voice||'default'))+'</span></div><div><span class="l">cache</span><span class="v">'+(result.cache_hit?'hit':'generated')+'</span></div>':'<div><span class="l">frequency</span><span class="v">'+esc(Math.round(result.frequency)+' Hz')+'</span></div>')
|
|
7163
7170
|
+ '<div><span class="l">duration</span><span class="v">'+esc(result.duration+' s')+'</span></div>'
|
|
7164
7171
|
+ '<div><span class="l">latency</span><span class="v">'+esc(result.duration_ms+' ms')+'</span></div>'
|
|
7165
7172
|
+ '<div><span class="l">output</span><span class="v">'+esc(result.output_device)+'</span></div>'
|
|
7166
7173
|
+ '<div><span class="l">input</span><span class="v">'+esc(result.input_device)+'</span></div>'
|
|
7167
|
-
+ '<div><span class="l">format</span><span class="v">'+esc((result.output_sample_rate||result.sample_rate)+' Hz / '+(result.output_channels||1)+'ch out · '+(result.input_sample_rate||result.sample_rate)+' Hz / '+(result.input_channels||1)+'ch in')+'</span></div>'
|
|
7174
|
+
+ '<div><span class="l">format</span><span class="v">'+esc((result.output_sample_rate||result.sample_rate)+' Hz / '+(result.output_channels||1)+'ch out'+(result.playback_only?'':' · '+(result.input_sample_rate||result.sample_rate)+' Hz / '+(result.input_channels||1)+'ch in'))+'</span></div>'
|
|
7168
7175
|
+ '</div>';
|
|
7169
7176
|
var diag = result.diagnostic ? '<div class="pk-spk-diag">'+esc(result.diagnostic)+'</div>' : '';
|
|
7170
7177
|
host.innerHTML = '<div class="pk-spk-result '+(result.pass?'ok':'bad')+'">'
|
|
@@ -7187,7 +7194,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7187
7194
|
// the real recording.
|
|
7188
7195
|
_spkTestSetLive(robotId, {live:true, rms: 0.45, pass: undefined, waveform: null});
|
|
7189
7196
|
rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/speaker-test',
|
|
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})
|
|
7197
|
+
{mode:mode, playback_only:mode==='tts', text:phrase, frequency:freq, duration:dur, amplitude:0.6, threshold_db:-30, output:output, input:input, output_name:outputName, input_name:inputName})
|
|
7191
7198
|
.then(function(q){
|
|
7192
7199
|
// long-poll up to 8s, then fall back to 12s of result polling
|
|
7193
7200
|
if(status) status.textContent = mode==='tts'?'playing cached voice on '+selectedRobotOutput()+'…':'playing tone + recording…';
|
|
@@ -7233,7 +7240,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7233
7240
|
var status = $('pk-spk-test-stateline');
|
|
7234
7241
|
if(status){
|
|
7235
7242
|
if(!result || !result.ok){ status.textContent = '✗ test failed'; }
|
|
7236
|
-
else if(result.
|
|
7243
|
+
else if(result.playback_only&&result.played){ status.textContent = '✓ speaker playback completed'; }
|
|
7244
|
+
else if(result.pass){ status.textContent = '✓ pass · '+result.recorded_peak_db+' dB peak'; }
|
|
7237
7245
|
else { status.textContent = '✗ no signal — '+result.recorded_peak_db+' dB peak'; }
|
|
7238
7246
|
}
|
|
7239
7247
|
var btn = $('pk-spk-runtest'); if(btn){ btn.disabled = false; btn.textContent = (($('pk-spk-mode')||{}).value==='tone'?'Run tone test':'Play cached voice'); }
|
package/package.json
CHANGED
|
@@ -3477,6 +3477,7 @@ class SpeakerTestRequest(BaseModel):
|
|
|
3477
3477
|
output_name: Optional[str] = None
|
|
3478
3478
|
input_name: Optional[str] = None
|
|
3479
3479
|
sample_rate: Optional[int] = None
|
|
3480
|
+
playback_only: Optional[bool] = None
|
|
3480
3481
|
|
|
3481
3482
|
|
|
3482
3483
|
async def _cached_speaker_tts(robot_id: str, text: str) -> dict:
|
|
@@ -3533,7 +3534,8 @@ async def robot_speaker_test(robot_id: str, payload: SpeakerTestRequest):
|
|
|
3533
3534
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
3534
3535
|
db.row_factory = aiosqlite.Row
|
|
3535
3536
|
async with db.execute(
|
|
3536
|
-
"SELECT id
|
|
3537
|
+
"SELECT id, audio_device, audio_output_device, device_inventory FROM devices "
|
|
3538
|
+
"WHERE id = ? OR lower(name) = lower(?) "
|
|
3537
3539
|
"ORDER BY CASE WHEN id = ? THEN 0 ELSE 1 END LIMIT 1",
|
|
3538
3540
|
(robot_id, robot_id, robot_id),
|
|
3539
3541
|
) as cursor:
|
|
@@ -3542,7 +3544,30 @@ async def robot_speaker_test(robot_id: str, payload: SpeakerTestRequest):
|
|
|
3542
3544
|
raise HTTPException(404, f"Robot device not found: {robot_id}")
|
|
3543
3545
|
device_id = device["id"]
|
|
3544
3546
|
params = payload.model_dump(exclude_none=True)
|
|
3547
|
+
# Scheduler persistence is authoritative. Dashboard state can be stale
|
|
3548
|
+
# while heartbeat polling rebuilds the drawer, so never let it silently
|
|
3549
|
+
# route a test to a different device than the one the operator saved.
|
|
3550
|
+
if device["audio_device"] not in (None, ""):
|
|
3551
|
+
params["input"] = str(device["audio_device"])
|
|
3552
|
+
if device["audio_output_device"] not in (None, ""):
|
|
3553
|
+
params["output"] = str(device["audio_output_device"])
|
|
3554
|
+
try:
|
|
3555
|
+
inventory = json.loads(device["device_inventory"] or "{}")
|
|
3556
|
+
except (TypeError, ValueError):
|
|
3557
|
+
inventory = {}
|
|
3558
|
+
for field, inventory_key, name_key in (
|
|
3559
|
+
("input", "audio_input", "input_name"),
|
|
3560
|
+
("output", "audio_output", "output_name"),
|
|
3561
|
+
):
|
|
3562
|
+
selected = str(params.get(field, ""))
|
|
3563
|
+
for item in inventory.get(inventory_key, []) or []:
|
|
3564
|
+
if str(item.get("id", "")) == selected:
|
|
3565
|
+
params[name_key] = str(item.get("name") or selected)
|
|
3566
|
+
break
|
|
3545
3567
|
if payload.mode == "tts":
|
|
3568
|
+
# Cached voice is a speaker test. Do not also seize the microphone,
|
|
3569
|
+
# which may be owned by the live preview/conversation publisher.
|
|
3570
|
+
params.setdefault("playback_only", True)
|
|
3546
3571
|
params.update(await _cached_speaker_tts(robot_id, payload.text or ""))
|
|
3547
3572
|
elif payload.mode != "tone":
|
|
3548
3573
|
raise HTTPException(422, "mode must be tone or tts")
|
|
@@ -392,6 +392,10 @@ class PreviewAgent:
|
|
|
392
392
|
self.enrollment_token: Optional[str] = cfg.get("enrollment_token")
|
|
393
393
|
self.video_device = cfg.get("video_device", os.getenv("VIDEO_DEVICE", "auto"))
|
|
394
394
|
self.audio_device = cfg.get("audio_device", os.getenv("AUDIO_DEVICE", "default"))
|
|
395
|
+
# Scheduler inventory IDs come from RoboVision/sounddevice. They are
|
|
396
|
+
# not guaranteed to equal PyAudio's device indexes, so capture must
|
|
397
|
+
# use the resolved hardware name reported in that same inventory.
|
|
398
|
+
self.audio_capture_device = self.audio_device
|
|
395
399
|
self.audio_output_device = cfg.get("audio_output_device", os.getenv("AUDIO_OUTPUT_DEVICE", "default"))
|
|
396
400
|
self.robovision_url = cfg.get("robovision_url", os.getenv("ROBOVISION_URL", "http://127.0.0.1:5000"))
|
|
397
401
|
self.use_robovision_camera = bool(cfg.get("use_robovision_camera", False))
|
|
@@ -672,6 +676,16 @@ class PreviewAgent:
|
|
|
672
676
|
if data.get("audio_device") is not None:
|
|
673
677
|
changed = changed or self.audio_device != data["audio_device"]
|
|
674
678
|
self.audio_device = data["audio_device"]
|
|
679
|
+
inventory = data.get("device_inventory") or {}
|
|
680
|
+
selected = str(self.audio_device)
|
|
681
|
+
self.audio_capture_device = next(
|
|
682
|
+
(
|
|
683
|
+
str(item.get("name"))
|
|
684
|
+
for item in inventory.get("audio_input", [])
|
|
685
|
+
if str(item.get("id")) == selected and item.get("name")
|
|
686
|
+
),
|
|
687
|
+
self.audio_device,
|
|
688
|
+
)
|
|
675
689
|
if data.get("audio_output_device") is not None:
|
|
676
690
|
changed = changed or self.audio_output_device != data["audio_output_device"]
|
|
677
691
|
self.audio_output_device = data["audio_output_device"]
|
|
@@ -1457,7 +1471,7 @@ class LiveKitPublisher:
|
|
|
1457
1471
|
|
|
1458
1472
|
async def _audio_loop(self) -> None:
|
|
1459
1473
|
assert self.audio_source is not None
|
|
1460
|
-
mic = create_audio_capture(self.agent.
|
|
1474
|
+
mic = create_audio_capture(self.agent.audio_capture_device)
|
|
1461
1475
|
if mic is None:
|
|
1462
1476
|
logger.warning("audio_loop: create_audio_capture returned None, mic will not publish")
|
|
1463
1477
|
return
|
|
@@ -588,6 +588,7 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
588
588
|
except ImportError as e:
|
|
589
589
|
return {"ok": False, "error": f"pyaudio not installed on this robot: {e}"}
|
|
590
590
|
mode = str(params.get("mode", "tone")).lower()
|
|
591
|
+
playback_only = bool(params.get("playback_only", mode == "tts"))
|
|
591
592
|
freq = float(params.get("frequency", SPEAKER_TEST_FREQUENCY_HZ))
|
|
592
593
|
dur = float(params.get("duration", SPEAKER_TEST_DURATION_S))
|
|
593
594
|
amp = float(params.get("amplitude", SPEAKER_TEST_AMPLITUDE))
|
|
@@ -622,13 +623,13 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
622
623
|
|
|
623
624
|
pa = pyaudio.PyAudio()
|
|
624
625
|
out_idx = _resolve_audio_device(pa, out_name, "output")
|
|
625
|
-
in_idx = _resolve_audio_device(pa, in_name, "input")
|
|
626
|
+
in_idx = None if playback_only else _resolve_audio_device(pa, in_name, "input")
|
|
626
627
|
def _dev_name(idx):
|
|
627
628
|
if idx is None: return "(default)"
|
|
628
629
|
try: return pa.get_device_info_by_index(idx).get("name", str(idx))
|
|
629
630
|
except Exception: return str(idx)
|
|
630
631
|
out_label = _dev_name(out_idx)
|
|
631
|
-
in_label = _dev_name(in_idx)
|
|
632
|
+
in_label = "not opened (speaker-only test)" if playback_only else _dev_name(in_idx)
|
|
632
633
|
|
|
633
634
|
recorded_peak = 0
|
|
634
635
|
recorded_rms = 0.0
|
|
@@ -644,11 +645,12 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
644
645
|
out_stream, output_rate, output_channels, out_info = _open_pcm_stream(
|
|
645
646
|
pa, pyaudio, "output", out_idx, source_rate
|
|
646
647
|
)
|
|
647
|
-
in_stream, input_rate, input_channels, in_info = _open_pcm_stream(
|
|
648
|
-
pa, pyaudio, "input", in_idx, source_rate
|
|
649
|
-
)
|
|
650
648
|
out_label = str(out_info.get("name", out_label))
|
|
651
|
-
|
|
649
|
+
if not playback_only:
|
|
650
|
+
in_stream, input_rate, input_channels, in_info = _open_pcm_stream(
|
|
651
|
+
pa, pyaudio, "input", in_idx, source_rate
|
|
652
|
+
)
|
|
653
|
+
in_label = str(in_info.get("name", in_label))
|
|
652
654
|
|
|
653
655
|
if mono_pcm is not None:
|
|
654
656
|
output_mono = _resample_pcm16_mono(mono_pcm, source_rate, output_rate)
|
|
@@ -671,7 +673,8 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
671
673
|
raw = output_mono
|
|
672
674
|
|
|
673
675
|
chunk = 1024
|
|
674
|
-
in_stream
|
|
676
|
+
if in_stream is not None:
|
|
677
|
+
in_stream.start_stream()
|
|
675
678
|
chunk_bytes = chunk * 2 * output_channels
|
|
676
679
|
pos = 0
|
|
677
680
|
peak_acc = 0
|
|
@@ -682,8 +685,10 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
682
685
|
end = min(pos + chunk_bytes, len(raw))
|
|
683
686
|
out_stream.write(raw[pos:end])
|
|
684
687
|
pos = end
|
|
685
|
-
try:
|
|
686
|
-
|
|
688
|
+
try:
|
|
689
|
+
if in_stream is None:
|
|
690
|
+
continue
|
|
691
|
+
avail = in_stream.get_read_available()
|
|
687
692
|
if avail and avail > 0:
|
|
688
693
|
data = in_stream.read(avail, exception_on_overflow=False)
|
|
689
694
|
for s in range(0, len(data) - 1, 2):
|
|
@@ -695,18 +700,20 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
695
700
|
except Exception:
|
|
696
701
|
pass
|
|
697
702
|
out_stream.stop_stream(); out_stream.close(); out_stream = None
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
703
|
+
if in_stream is not None:
|
|
704
|
+
try:
|
|
705
|
+
tail_frames = min(int(input_rate * 0.35), max(0, in_stream.get_read_available()))
|
|
706
|
+
tail = in_stream.read(tail_frames, exception_on_overflow=False) if tail_frames else b""
|
|
707
|
+
for s in range(0, len(tail) - 1, 2):
|
|
708
|
+
v = int.from_bytes(tail[s:s+2], "little", signed=True)
|
|
709
|
+
a = abs(v)
|
|
710
|
+
if a > peak_acc: peak_acc = a
|
|
711
|
+
rms_acc += v * v
|
|
712
|
+
n_samp += 1
|
|
713
|
+
except Exception:
|
|
714
|
+
pass
|
|
715
|
+
if in_stream is not None:
|
|
716
|
+
in_stream.stop_stream(); in_stream.close(); in_stream = None
|
|
710
717
|
recorded_peak = peak_acc
|
|
711
718
|
recorded_rms = (rms_acc / max(1, n_samp)) ** 0.5
|
|
712
719
|
except Exception as e:
|
|
@@ -721,10 +728,30 @@ def _speaker_roundtrip_test(params: dict) -> dict:
|
|
|
721
728
|
try: pa.terminate()
|
|
722
729
|
except Exception: pass
|
|
723
730
|
duration_ms = int((time.monotonic() - t0) * 1000)
|
|
724
|
-
if err:
|
|
731
|
+
if err:
|
|
725
732
|
return {"ok": False, "error": err, "duration_ms": duration_ms,
|
|
726
733
|
"output_device": out_label, "input_device": in_label,
|
|
727
|
-
"frequency": freq, "duration": dur}
|
|
734
|
+
"frequency": freq, "duration": dur}
|
|
735
|
+
if playback_only:
|
|
736
|
+
return {
|
|
737
|
+
"ok": True,
|
|
738
|
+
"pass": True,
|
|
739
|
+
"played": True,
|
|
740
|
+
"playback_only": True,
|
|
741
|
+
"mode": mode,
|
|
742
|
+
"text": params.get("text"),
|
|
743
|
+
"cache_hit": bool(params.get("cache_hit")),
|
|
744
|
+
"tts_provider": params.get("tts_provider"),
|
|
745
|
+
"tts_voice": params.get("tts_voice"),
|
|
746
|
+
"duration": dur,
|
|
747
|
+
"duration_ms": duration_ms,
|
|
748
|
+
"sample_rate": source_rate,
|
|
749
|
+
"output_sample_rate": output_rate,
|
|
750
|
+
"output_channels": output_channels,
|
|
751
|
+
"output_device": out_label,
|
|
752
|
+
"input_device": in_label,
|
|
753
|
+
"diagnostic": "speaker playback completed; microphone was intentionally not opened",
|
|
754
|
+
}
|
|
728
755
|
if recorded_peak > 0:
|
|
729
756
|
peak_db = 20.0 * math.log10(recorded_peak / 32767.0)
|
|
730
757
|
else:
|