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
|
@@ -1083,7 +1083,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1083
1083
|
|
|
1084
1084
|
<!-- ═══ Setup ═══ -->
|
|
1085
1085
|
<div class="rp-sub" id="rp-sub-setup">
|
|
1086
|
-
|
|
1086
|
+
<div class="rp-form" id="rp-provider-secrets">
|
|
1087
|
+
<h3>Voice provider secret</h3>
|
|
1088
|
+
<div class="rp-quickrow">
|
|
1089
|
+
<div class="rp-field" style="flex:1"><label>ElevenLabs API key</label><input type="password" id="rp-elevenlabs-key" autocomplete="new-password" placeholder="stored only on the scheduler"></div>
|
|
1090
|
+
<button class="rp-btn primary" id="rp-elevenlabs-save" style="height:fit-content">Save secret</button>
|
|
1091
|
+
</div>
|
|
1092
|
+
<div class="rp-autonote" id="rp-elevenlabs-status">not configured · cached voice speaker tests require this key</div>
|
|
1093
|
+
</div>
|
|
1094
|
+
|
|
1095
|
+
<!-- Add LiveKit server -->
|
|
1087
1096
|
<div class="rp-form" id="rp-addserver">
|
|
1088
1097
|
<h3>Add LiveKit server</h3>
|
|
1089
1098
|
<div class="rp-quickrow">
|
|
@@ -1893,11 +1902,25 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1893
1902
|
.then(function(){ pmBusy=false; applyReadOnly(); });
|
|
1894
1903
|
});
|
|
1895
1904
|
})();
|
|
1896
|
-
function pollSettings(){
|
|
1897
|
-
fetch('/robopark/api/settings'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(s){
|
|
1898
|
-
if(s && typeof s.production_mode!=='undefined') setPm(!!s.production_mode);
|
|
1899
|
-
|
|
1900
|
-
|
|
1905
|
+
function pollSettings(){
|
|
1906
|
+
fetch('/robopark/api/settings'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(s){
|
|
1907
|
+
if(s && typeof s.production_mode!=='undefined') setPm(!!s.production_mode);
|
|
1908
|
+
var providerStatus=$('rp-elevenlabs-status');
|
|
1909
|
+
if(providerStatus) providerStatus.textContent=s&&s.elevenlabs_configured
|
|
1910
|
+
? 'configured · source: '+(s.elevenlabs_source==='environment'?'scheduler environment':'persistent scheduler store')
|
|
1911
|
+
: 'not configured · cached voice speaker tests require this key';
|
|
1912
|
+
}).catch(function(){});
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
var elevenlabsSave=$('rp-elevenlabs-save');
|
|
1916
|
+
if(elevenlabsSave) elevenlabsSave.addEventListener('click',function(){
|
|
1917
|
+
var input=$('rp-elevenlabs-key'), key=input?input.value.trim():'';
|
|
1918
|
+
if(key.length<8){ showOut('Enter a valid ElevenLabs API key'); return; }
|
|
1919
|
+
elevenlabsSave.disabled=true;
|
|
1920
|
+
rpAction('POST','/robopark/api/settings/provider-secret',{provider:'elevenlabs',api_key:key}).then(function(){
|
|
1921
|
+
input.value=''; showOut('ElevenLabs secret saved on scheduler'); pollSettings();
|
|
1922
|
+
}).catch(function(e){ showOut('ElevenLabs secret: '+e.message); }).then(function(){elevenlabsSave.disabled=false;});
|
|
1923
|
+
});
|
|
1901
1924
|
|
|
1902
1925
|
function robotName(x){ return x.robot_id||x.id||x.name||'robot'; }
|
|
1903
1926
|
function endSession(id){
|
|
@@ -6190,6 +6213,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6190
6213
|
if(!directRef){ visionOn=false; directImg.src=''; directImg.style.display='none'; return; }
|
|
6191
6214
|
var directCamNote=$('pk-camnote'), directLiveNote=$('pk-golivenote'), directRes=$('pk-camres');
|
|
6192
6215
|
visionOn=true;
|
|
6216
|
+
directImg.style.display='block';
|
|
6217
|
+
if(directCamNote)directCamNote.textContent='opening RoboVision camera relay...';
|
|
6218
|
+
if(directRes)directRes.textContent='CONNECTING';
|
|
6193
6219
|
directImg.onerror=function(){ visionOn=false; directImg.style.display='none'; directImg.src=''; if(directCamNote) directCamNote.textContent='RoboVision feed unavailable on robot'; if(directLiveNote) directLiveNote.textContent='camera unavailable'; if(directRes) directRes.textContent='OFFLINE'; };
|
|
6194
6220
|
directImg.onload=function(){ directImg.style.display='block'; if(directCamNote) directCamNote.textContent='live camera via RoboVision mesh relay'; if(directLiveNote) directLiveNote.textContent='live'; if(directRes) directRes.textContent='LIVE'; };
|
|
6195
6221
|
directImg.src='/fed/media/robots/'+encodeURIComponent(directRef)+'/video_feed'+QS+(QS?'&':'?')+'_='+Date.now();
|
|
@@ -6244,13 +6270,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6244
6270
|
var video=deviceList(inv,'video',['auto','none']);
|
|
6245
6271
|
var input=deviceList(inv,'audio_input',['default','none']);
|
|
6246
6272
|
var output=deviceList(inv,'audio_output',['default','none']);
|
|
6247
|
-
function fill(id, list, selected){
|
|
6248
|
-
var el=$(id); if(!el) return;
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
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); }
|
|
6254
6284
|
}
|
|
6255
6285
|
fill('pk-video-device', video, device.video_device||'auto');
|
|
6256
6286
|
fill('pk-audio-input', input, device.audio_device||'default');
|
|
@@ -6270,13 +6300,27 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6270
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');
|
|
6271
6301
|
}
|
|
6272
6302
|
}
|
|
6273
|
-
function saveDeviceSelectors(n){
|
|
6274
|
-
var device=schedulerDevice(n), id=device&&device.id; if(!id){ showOut('device inventory is not available yet'); return; }
|
|
6275
|
-
var body={video_device:$('pk-video-device').value, audio_device:$('pk-audio-input').value, audio_output_device:$('pk-audio-output').value,
|
|
6276
|
-
greeting_phrases:($('pk-greeting-phrases').value||'').split(String.fromCharCode(10)).map(function(x){return x.trim();}).filter(Boolean)};
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
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;});
|
|
6280
6324
|
}
|
|
6281
6325
|
function fmtUptime(secs){
|
|
6282
6326
|
if(secs==null) return '';
|
|
@@ -6679,7 +6723,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6679
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>';
|
|
6680
6724
|
var deviceSearch=$('pk-device-search'); if(deviceSearch) deviceSearch.addEventListener('input', function(){ refreshDeviceSelectors(n); });
|
|
6681
6725
|
var deviceSave=$('pk-device-save'); if(deviceSave) deviceSave.addEventListener('click', function(){ saveDeviceSelectors(n); });
|
|
6682
|
-
|
|
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
|
+
});
|
|
6683
6737
|
var micListen=$('pk-mic-listen');if(micListen)micListen.addEventListener('click',function(){setMicMonitorAudible(!micMonitorAudible);});
|
|
6684
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;});});
|
|
6685
6739
|
startCam(n); startMeters(n); loadEffectiveConfig(schedId(n)); refreshSupervisorPanel(n);
|
|
@@ -6879,7 +6933,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6879
6933
|
.catch(function(){ idleCam('unreachable'); });
|
|
6880
6934
|
}
|
|
6881
6935
|
function idleCam(reason){
|
|
6882
|
-
var cc=$('pk-camcv'), msg={connecting:'connecting to
|
|
6936
|
+
var cc=$('pk-camcv'), msg={connecting:'connecting to RoboVision camera relay…', idle:'camera relay standing by', production_mode_off:'production mode off', 'no-sdk':'voice session active · loading microphone…', unreachable:'camera relay unreachable'}[reason]||'no live feed';
|
|
6883
6937
|
if(cc&&cc.getContext){ var cx=cc.getContext('2d'); cx.fillStyle='#0b0d12'; cx.fillRect(0,0,cc.width,cc.height); cx.strokeStyle='rgba(255,255,255,0.05)'; for(var y=0;y<cc.height;y+=4){cx.beginPath();cx.moveTo(0,y);cx.lineTo(cc.width,y);cx.stroke();} cx.fillStyle='#5a6a80'; cx.font='11px monospace'; cx.textAlign='center'; cx.fillText(msg, cc.width/2, cc.height/2); }
|
|
6884
6938
|
var res=$('pk-camres'); if(res) res.textContent = reason==='idle'?'idle':'—';
|
|
6885
6939
|
var note=$('pk-camnote'); if(note) note.textContent = reason==='idle'?'the feed goes live automatically when the robot opens a session':msg;
|
|
@@ -6985,7 +7039,17 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
6985
7039
|
(function loop(){ if(!$('pk-micbars')||drawerId==null) return;
|
|
6986
7040
|
// MIC — real RMS from the subscribed LiveKit mic track (else no signal).
|
|
6987
7041
|
var lvl=0;
|
|
6988
|
-
if(micAnalyser){
|
|
7042
|
+
if(micAnalyser){
|
|
7043
|
+
micAnalyser.getByteTimeDomainData(buf);
|
|
7044
|
+
var sum=0;
|
|
7045
|
+
for(var q=0;q<buf.length;q++){ var v=(buf[q]-128)/128; sum+=v*v; }
|
|
7046
|
+
var rms=Math.sqrt(sum/buf.length);
|
|
7047
|
+
// USB microphones commonly sit around -55 dBFS at room level and
|
|
7048
|
+
// -20 dBFS when spoken into. Map that useful range to 0..100 rather
|
|
7049
|
+
// than displaying normalized RMS directly (which looked stuck at 1%).
|
|
7050
|
+
var db=rms>0 ? 20*Math.log10(rms) : -90;
|
|
7051
|
+
lvl=Math.max(0,Math.min(1,(db+60)/45));
|
|
7052
|
+
}
|
|
6989
7053
|
for(var i=0;i<mi.length;i++){ var on=i<lvl*mi.length; mi[i].style.height=(3+(on?lvl*13:0))+'px'; mi[i].style.background=on?(i<mi.length*0.7?'var(--ok)':i<mi.length*0.9?'var(--warn)':'var(--bad)'):'var(--glass-border)'; }
|
|
6990
7054
|
set('pk-micval', micAnalyser?Math.round(lvl*100)+'%':'—');
|
|
6991
7055
|
set('pk-micstate', micAnalyser?('selected '+selectedRobotMic()+' · '+(lvl>0.12?'live, hearing audio':'live, quiet')):('selected '+selectedRobotMic()+' · '+micMonitorState));
|
|
@@ -7099,8 +7163,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7099
7163
|
+ '<div><span class="l">duration</span><span class="v">'+esc(result.duration+' s')+'</span></div>'
|
|
7100
7164
|
+ '<div><span class="l">latency</span><span class="v">'+esc(result.duration_ms+' ms')+'</span></div>'
|
|
7101
7165
|
+ '<div><span class="l">output</span><span class="v">'+esc(result.output_device)+'</span></div>'
|
|
7102
|
-
+ '<div><span class="l">input</span><span class="v">'+esc(result.input_device)+'</span></div>'
|
|
7103
|
-
+ '</div>'
|
|
7166
|
+
+ '<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>'
|
|
7168
|
+
+ '</div>';
|
|
7104
7169
|
var diag = result.diagnostic ? '<div class="pk-spk-diag">'+esc(result.diagnostic)+'</div>' : '';
|
|
7105
7170
|
host.innerHTML = '<div class="pk-spk-result '+(result.pass?'ok':'bad')+'">'
|
|
7106
7171
|
+ banner + meta + diag + '</div>';
|
|
@@ -7116,12 +7181,13 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
7116
7181
|
var phrase=String((($('pk-spk-phrase')||{}).value)||'Hello, I am ready to talk.').trim();
|
|
7117
7182
|
var output=(($('pk-audio-output')||{}).value)||'default';
|
|
7118
7183
|
var input=(($('pk-audio-input')||{}).value)||'default';
|
|
7184
|
+
var outputName=selectedRobotOutput(),inputName=selectedRobotMic();
|
|
7119
7185
|
// Animate the meter with a synthetic "during-test" peak so the
|
|
7120
7186
|
// operator sees something happen immediately while waiting for
|
|
7121
7187
|
// the real recording.
|
|
7122
7188
|
_spkTestSetLive(robotId, {live:true, rms: 0.45, pass: undefined, waveform: null});
|
|
7123
7189
|
rpAction('POST','/robopark/api/robots/'+encodeURIComponent(robotId)+'/shell/speaker-test',
|
|
7124
|
-
{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})
|
|
7125
7191
|
.then(function(q){
|
|
7126
7192
|
// long-poll up to 8s, then fall back to 12s of result polling
|
|
7127
7193
|
if(status) status.textContent = mode==='tts'?'playing cached voice on '+selectedRobotOutput()+'…':'playing tone + recording…';
|
package/package.json
CHANGED
|
@@ -69,7 +69,14 @@ ELEVENLABS_API_KEY = (
|
|
|
69
69
|
or os.getenv("ELEVENLABS_KEY", "").strip()
|
|
70
70
|
or os.getenv("ELEVEN_API_KEY", "").strip()
|
|
71
71
|
)
|
|
72
|
-
ELEVENLABS_BASE_URL = os.getenv("ELEVENLABS_BASE_URL", "https://api.elevenlabs.io").rstrip("/")
|
|
72
|
+
ELEVENLABS_BASE_URL = os.getenv("ELEVENLABS_BASE_URL", "https://api.elevenlabs.io").rstrip("/")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def _get_elevenlabs_api_key() -> str:
|
|
76
|
+
"""Resolve the provider key without ever returning it through public APIs."""
|
|
77
|
+
if ELEVENLABS_API_KEY:
|
|
78
|
+
return ELEVENLABS_API_KEY
|
|
79
|
+
return (await get_setting("elevenlabs_api_key") or "").strip()
|
|
73
80
|
|
|
74
81
|
# Optional base URL of a local Kokoro TTS server. When set, /api/voices/kokoro
|
|
75
82
|
# will proxy its /v1/audio/voices endpoint to return the dynamic list of voices
|
|
@@ -1991,12 +1998,29 @@ async def get_settings():
|
|
|
1991
1998
|
"""Public settings with secret values reduced to status only."""
|
|
1992
1999
|
pm = await get_production_mode()
|
|
1993
2000
|
has_default_token = bool(await get_setting("default_enrollment_token"))
|
|
2001
|
+
elevenlabs_key = await _get_elevenlabs_api_key()
|
|
1994
2002
|
return {
|
|
1995
2003
|
"production_mode": pm,
|
|
1996
2004
|
"default_enrollment_token_set": has_default_token,
|
|
1997
2005
|
"agent_token_configured": bool(ROBOPARK_AGENT_TOKEN),
|
|
1998
2006
|
"agent_token_source": "environment" if os.getenv("ROBOPARK_AGENT_TOKEN", "").strip() else "scheduler_secret_file",
|
|
2007
|
+
"elevenlabs_configured": bool(elevenlabs_key),
|
|
2008
|
+
"elevenlabs_source": "environment" if ELEVENLABS_API_KEY else ("scheduler_store" if elevenlabs_key else None),
|
|
1999
2009
|
}
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
@app.post("/api/settings/provider-secret")
|
|
2013
|
+
async def set_provider_secret(payload: dict):
|
|
2014
|
+
"""Persist a provider credential while only exposing configured status."""
|
|
2015
|
+
provider = str(payload.get("provider") or "").strip().lower()
|
|
2016
|
+
api_key = str(payload.get("api_key") or "").strip()
|
|
2017
|
+
if provider != "elevenlabs":
|
|
2018
|
+
raise HTTPException(400, "unsupported provider")
|
|
2019
|
+
if len(api_key) < 8:
|
|
2020
|
+
raise HTTPException(400, "ElevenLabs API key must be at least 8 characters")
|
|
2021
|
+
await set_setting("elevenlabs_api_key", api_key)
|
|
2022
|
+
await _log_history("settings", "provider_secret", provider, "updated", "operator")
|
|
2023
|
+
return {"ok": True, "provider": provider, "configured": True, "source": "scheduler_store"}
|
|
2000
2024
|
|
|
2001
2025
|
@app.put("/api/settings")
|
|
2002
2026
|
async def update_settings(payload: SettingsPayload):
|
|
@@ -3448,8 +3472,10 @@ class SpeakerTestRequest(BaseModel):
|
|
|
3448
3472
|
duration: Optional[float] = None
|
|
3449
3473
|
amplitude: Optional[float] = None
|
|
3450
3474
|
threshold_db: Optional[float] = None
|
|
3451
|
-
output: Optional[str] = None
|
|
3452
|
-
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
|
|
3453
3479
|
sample_rate: Optional[int] = None
|
|
3454
3480
|
|
|
3455
3481
|
|
|
@@ -3462,8 +3488,9 @@ async def _cached_speaker_tts(robot_id: str, text: str) -> dict:
|
|
|
3462
3488
|
voice_id = stack.tts_voice if stack else "21m00Tcm4TlvDq8ikWAM"
|
|
3463
3489
|
if provider != "elevenlabs":
|
|
3464
3490
|
raise HTTPException(400, f"Cached voice test currently requires ElevenLabs; selected stack uses {provider}")
|
|
3465
|
-
|
|
3466
|
-
|
|
3491
|
+
elevenlabs_key = await _get_elevenlabs_api_key()
|
|
3492
|
+
if not elevenlabs_key:
|
|
3493
|
+
raise HTTPException(503, "ElevenLabs is not configured; open Control Center > Setup > Voice provider secret")
|
|
3467
3494
|
if not voice_id:
|
|
3468
3495
|
raise HTTPException(400, "Selected voice stack has no ElevenLabs voice id")
|
|
3469
3496
|
cache_dir = os.path.join(os.path.dirname(DB_PATH), "tts-test-cache")
|
|
@@ -3479,7 +3506,7 @@ async def _cached_speaker_tts(robot_id: str, text: str) -> dict:
|
|
|
3479
3506
|
response = await client.post(
|
|
3480
3507
|
f"{ELEVENLABS_BASE_URL}/v1/text-to-speech/{voice_id}",
|
|
3481
3508
|
params={"output_format": "pcm_24000"},
|
|
3482
|
-
headers={"xi-api-key":
|
|
3509
|
+
headers={"xi-api-key": elevenlabs_key, "Accept": "audio/pcm"},
|
|
3483
3510
|
json={"text": phrase, "model_id": "eleven_flash_v2_5"},
|
|
3484
3511
|
)
|
|
3485
3512
|
if response.status_code != 200:
|
|
@@ -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
|