Update web/templates/entry_view.html

This commit is contained in:
2025-08-21 02:42:57 +00:00
parent 6408d0a589
commit 3ad80494a0
+82 -51
View File
@@ -285,47 +285,38 @@
const ttsBtn = document.getElementById('ttsBtn'); const ttsBtn = document.getElementById('ttsBtn');
if (!ttsBtn) return; if (!ttsBtn) return;
const TTS_URL = "{{ tts_url|default:'' }}"; // staff-only when present const TTS_URL = "{{ tts_url|default:'' }}"; // empty for non-staff → browser TTS
let ttsAudio = null; // holds current playback (Audio or shim)
function stopPlayback(){ // -------- persistent audio + helpers --------
try { let audioEl = null;
if (window.speechSynthesis) speechSynthesis.cancel(); let currentURL = null; // objectURL for OpenAI response
} catch(e){} let playing = false;
try { let fetchCtrl = null;
if (ttsAudio && typeof ttsAudio.pause === 'function') {
ttsAudio.pause(); function ensureAudio(){
ttsAudio.currentTime = 0; if (!audioEl) {
audioEl = new Audio();
audioEl.setAttribute('playsinline',''); // iOS
audioEl.preload = 'auto';
audioEl.addEventListener('ended', () => { playing = false; cleanupURL(); });
audioEl.addEventListener('pause', () => { /* keep playing=false only if really paused */ });
} }
if (ttsAudio && typeof ttsAudio.stop === 'function') { return audioEl;
ttsAudio.stop();
}
} catch(e){}
ttsAudio = null;
} }
async function playOpenAITTS() { function cleanupURL(){
const r = await fetch(TTS_URL, { credentials: 'same-origin', cache: 'no-store' }); if (currentURL) { URL.revokeObjectURL(currentURL); currentURL = null; }
if (!r.ok) {
const msg = await r.text().catch(()=> String(r.status));
throw new Error(`HTTP ${r.status}: ${msg.slice(0,200)}`);
}
const ct = (r.headers.get('content-type') || '').toLowerCase();
if (!ct.startsWith('audio/')) {
const preview = await r.text().catch(()=> '(non-audio response)');
throw new Error(`Unexpected content-type "${ct}". Preview: ${preview.slice(0,200)}`);
}
const blob = await r.blob();
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
audio.preload = 'auto';
audio.setAttribute('playsinline','');
audio.onended = () => { URL.revokeObjectURL(url); ttsAudio = null; };
await audio.play();
ttsAudio = audio;
} }
function stopAll(){
try { if (fetchCtrl) fetchCtrl.abort(); } catch(_){}
fetchCtrl = null;
if (audioEl) { audioEl.pause(); audioEl.currentTime = 0; }
cleanupURL();
playing = false;
}
// -------- text builders --------
function buildCombinedText(){ function buildCombinedText(){
const ill = (document.getElementById('illustration-text')?.innerText || '').trim(); const ill = (document.getElementById('illustration-text')?.innerText || '').trim();
const app = (document.getElementById('application-text')?.innerText || '').trim(); const app = (document.getElementById('application-text')?.innerText || '').trim();
@@ -338,43 +329,83 @@
if (!('speechSynthesis' in window) || !('SpeechSynthesisUtterance' in window)) { if (!('speechSynthesis' in window) || !('SpeechSynthesisUtterance' in window)) {
throw new Error('Browser TTS not supported.'); throw new Error('Browser TTS not supported.');
} }
speechSynthesis.cancel(); window.speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text); const u = new SpeechSynthesisUtterance(text);
u.rate = 1.0; u.pitch = 1.0; u.volume = 1.0; u.rate = 1.0; u.pitch = 1.0; u.volume = 1.0;
u.onend = () => { ttsAudio = null; };
speechSynthesis.speak(u); speechSynthesis.speak(u);
playing = true;
// Create a tiny shim so our toggle can "stop" it
ttsAudio = {
pause: () => speechSynthesis.cancel(),
currentTime: 0
};
} }
ttsBtn.addEventListener('click', async () => { async function playOpenAITTS(){
stopAll();
fetchCtrl = new AbortController();
const r = await fetch(TTS_URL, { credentials:'same-origin', cache:'no-store', signal: fetchCtrl.signal });
if (!r.ok) {
const preview = await r.text().catch(()=> String(r.status));
throw new Error(`HTTP ${r.status}: ${preview.slice(0,200)}`);
}
const ct = (r.headers.get('content-type') || '').toLowerCase();
if (!ct.startsWith('audio/')) {
const preview = await r.text().catch(()=> '(non-audio response)');
throw new Error(`Unexpected content-type "${ct}". Preview: ${preview.slice(0,200)}`);
}
const blob = await r.blob();
currentURL = URL.createObjectURL(blob);
const a = ensureAudio();
a.src = currentURL;
a.currentTime = 0;
a.muted = false;
try { try {
// Toggle: if already playing, stop instead await a.play(); // <- may throw NotAllowedError on some iOS/Safari cases
if (ttsAudio) { playing = true;
stopPlayback(); } catch (err) {
showToast("Playback stopped"); // Autoplay/permission style block (Safari/iOS or Chrome heuristics)
if (err && (err.name === 'NotAllowedError' ||
/not allowed|denied permission/i.test(err.message))) {
showToast("Audio was blocked. Make sure the phone isn't on Silent and tap the speaker again.");
} else {
throw err;
}
}
}
// -------- button: toggle behavior --------
ttsBtn.addEventListener('click', async () => {
// Toggle OFF if something is already playing (either engine)
if (playing) {
try {
stopAll();
if ('speechSynthesis' in window) window.speechSynthesis.cancel();
} finally {
playing = false;
showToast("Stopped.");
}
return; return;
} }
try {
ttsBtn.disabled = true; ttsBtn.disabled = true;
if (TTS_URL) { if (TTS_URL) {
await playOpenAITTS(); await playOpenAITTS();
showToast("Using OpenAI TTS"); if (playing) showToast("Playing with OpenAI TTS");
} else { } else {
speakBrowserTTS(); speakBrowserTTS();
showToast("Using Browser TTS"); showToast("Playing with Browser TTS");
} }
} catch (err) { } catch (err) {
alert('TTS error: ' + (err && err.message ? err.message : String(err))); showToast("TTS error: " + (err?.message || String(err)));
} finally { } finally {
ttsBtn.disabled = false; ttsBtn.disabled = false;
} }
}); });
// Safety: stop audio when navigating away
window.addEventListener('pagehide', stopAll);
})(); })();
</script> </script>