mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
feat: ElevenLabs TTS George voice + fix HA toggle optimistic update
This commit is contained in:
@@ -1322,10 +1322,23 @@ function renderHATable(entities) {
|
||||
}
|
||||
|
||||
function haToggle(entityId, currentState, el) {
|
||||
const ON_STATES = ['on','home','open','playing','mowing','armed_home','armed_away','armed_night','active'];
|
||||
const wasOn = ON_STATES.includes(currentState);
|
||||
el.style.opacity = '0.5';
|
||||
apiPost('ha_toggle', {entity_id: entityId, state: currentState}, () => {
|
||||
apiPost('ha_toggle', {entity_id: entityId, state: currentState}, (res) => {
|
||||
el.style.opacity = '1';
|
||||
loadHA();
|
||||
if (res.ok) {
|
||||
// Optimistic update — flip state in cache so re-render shows new state immediately
|
||||
const ent = _haEntities.find(e => e.entity_id === entityId);
|
||||
if (ent) {
|
||||
ent.state = wasOn ? 'off' : 'on';
|
||||
filterHATable();
|
||||
}
|
||||
// Also sync from HA after 3s (actual state confirmation)
|
||||
setTimeout(loadHA, 3000);
|
||||
} else {
|
||||
toast('Toggle failed (code ' + (res.code||'?') + ')', 'err');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@ switch ($endpoint) {
|
||||
case 'ha':
|
||||
require __DIR__ . '/../api/endpoints/ha.php';
|
||||
break;
|
||||
case 'tts':
|
||||
require __DIR__ . '/../api/endpoints/tts.php';
|
||||
break;
|
||||
case 'do':
|
||||
require __DIR__ . '/../api/endpoints/do_server.php';
|
||||
break;
|
||||
|
||||
+32
-12
@@ -1781,22 +1781,42 @@ function loadVoices() {
|
||||
synth.onvoiceschanged = set;
|
||||
}
|
||||
|
||||
function speak(text) {
|
||||
let _ttsAudio = null;
|
||||
|
||||
async function speak(text) {
|
||||
if (!text) return;
|
||||
if (_ttsAudio) { _ttsAudio.pause(); _ttsAudio = null; }
|
||||
synth?.cancel();
|
||||
const reactor = document.getElementById('arcReactor');
|
||||
reactor?.classList.add('speaking');
|
||||
try {
|
||||
const res = await fetch('/api/tts', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json','X-Session-Token': sessionToken},
|
||||
body: JSON.stringify({text: text.substring(0, 400)}),
|
||||
});
|
||||
if (!res.ok) throw new Error('tts');
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
_ttsAudio = new Audio(url);
|
||||
_ttsAudio.onended = () => { URL.revokeObjectURL(url); _ttsAudio = null; reactor?.classList.remove('speaking'); };
|
||||
_ttsAudio.onerror = () => { reactor?.classList.remove('speaking'); _ttsAudio = null; };
|
||||
await _ttsAudio.play();
|
||||
} catch(e) {
|
||||
reactor?.classList.remove('speaking');
|
||||
_speakFallback(text);
|
||||
}
|
||||
}
|
||||
|
||||
function _speakFallback(text) {
|
||||
if (!synth || !text) return;
|
||||
synth.cancel();
|
||||
const utter = new SpeechSynthesisUtterance(text);
|
||||
if (selectedVoice) utter.voice = selectedVoice;
|
||||
utter.rate = 0.92;
|
||||
utter.pitch = 0.85;
|
||||
utter.volume = 1;
|
||||
|
||||
utter.onstart = () => {
|
||||
document.getElementById('arcReactor').classList.add('speaking');
|
||||
};
|
||||
utter.onend = () => {
|
||||
document.getElementById('arcReactor').classList.remove('speaking');
|
||||
};
|
||||
|
||||
utter.rate = 0.92; utter.pitch = 0.85; utter.volume = 1;
|
||||
const reactor = document.getElementById('arcReactor');
|
||||
utter.onstart = () => reactor?.classList.add('speaking');
|
||||
utter.onend = () => reactor?.classList.remove('speaking');
|
||||
synth.speak(utter);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user