Update web/templates/entry_view.html

This commit is contained in:
Joshua Laymon 2025-08-21 00:37:38 +00:00
parent 0a833e07f3
commit 3975afed29

View File

@ -279,17 +279,36 @@
})(); })();
</script> </script>
<!-- TTS toast (one-time, tiny helper UI) -->
<div id="tts-toast"
style="position:fixed;bottom:84px;left:50%;transform:translateX(-50%);
background:#111827;color:#fff;padding:10px 14px;border-radius:8px;
font-size:14px;display:none;z-index:9999;box-shadow:0 6px 18px rgba(0,0,0,.25);">
</div>
<script> <script>
(function(){ (function(){
const LS_KEY = "tts.voiceName"; const LS_KEY = "tts.voiceName";
const speakBtn = document.querySelector('#ttsBtn, #btn-speak, .btn-speak');
function listVoices(){ return speechSynthesis.getVoices(); } // --- Toast ---
function showTTSToast(msg, ms=2200){
const el = document.getElementById('tts-toast');
if (!el) return;
el.textContent = msg;
el.style.display = 'block';
clearTimeout(showTTSToast._t);
showTTSToast._t = setTimeout(()=>{ el.style.display = 'none'; }, ms);
}
// --- Voice helpers ---
function listVoices(){ return window.speechSynthesis ? speechSynthesis.getVoices() : []; }
const PREFERRED_ORDER = [ const PREFERRED_ORDER = [
/Google .*English/i, /Google .*English/i, // Chrome voices
/Microsoft .*Natural/i, /Microsoft .*Natural/i, // Edge neural voices
/Siri/i, /Siri/i, // Safari voices
/Alex/i, /Alex/i, // macOS Alex
/English/i /English/i
]; ];
@ -303,18 +322,27 @@
if (found) return found; if (found) return found;
} }
for (const rx of PREFERRED_ORDER){ for (const rx of PREFERRED_ORDER){
const m = vs.find(v => rx.test(v.name) && v.lang.toLowerCase().startsWith(lang)); const m = vs.find(v => rx.test(v.name) && v.lang?.toLowerCase().startsWith(lang));
if (m) return m; if (m) return m;
} }
return vs.find(v => v.lang.toLowerCase().startsWith(lang)) || vs[0]; return vs.find(v => v.lang?.toLowerCase().startsWith(lang)) || vs[0];
} }
function voiceServiceLabel(v){
if (!v) return "Browser TTS";
const n = (v.name || "").toLowerCase();
if (/google/.test(n)) return "Google (Chrome) voice: " + v.name;
if (/microsoft|aria|guy|jessa|zira|david|mark|neural/.test(n)) return "Microsoft (Edge) voice: " + v.name;
if (/siri/.test(n)) return "Apple Siri voice: " + v.name;
if (/alex/.test(n)) return "Apple Alex voice";
return "Browser TTS voice: " + v.name + (v.lang ? ` [${v.lang}]` : "");
}
// --- Build the text (ensure punctuation + two spaces) ---
function ensureSentenceEnd(s){ function ensureSentenceEnd(s){
if (!s) return ""; if (!s) return "";
return /[.!?…]$/.test(s) ? s : (s + "."); return /[.!?…]$/.test(s) ? s : (s + ".");
} }
// ✅ Use your actual IDs
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();
@ -322,7 +350,11 @@
return [illP, app].filter(Boolean).join(" ") || "No text available for this illustration."; return [illP, app].filter(Boolean).join(" ") || "No text available for this illustration.";
} }
function speakCombined(){ function isSpeaking(){
return !!(window.speechSynthesis && (speechSynthesis.speaking || speechSynthesis.pending));
}
function startSpeaking(){
const text = buildCombinedText(); const text = buildCombinedText();
if (!("speechSynthesis" in window) || !("SpeechSynthesisUtterance" in window)){ if (!("speechSynthesis" in window) || !("SpeechSynthesisUtterance" in window)){
alert("Text-to-Speech not supported in this browser."); alert("Text-to-Speech not supported in this browser.");
@ -330,27 +362,53 @@
} }
speechSynthesis.cancel(); speechSynthesis.cancel();
const start = () => { const begin = () => {
const u = new SpeechSynthesisUtterance(text); const u = new SpeechSynthesisUtterance(text);
const v = pickBestVoice("en"); const v = pickBestVoice("en");
if (v) u.voice = v; if (v) u.voice = v;
u.rate = 1.0; u.pitch = 1.0; u.volume = 1.0; u.rate = 1.0;
u.pitch = 1.0;
u.volume = 1.0;
// UI feedback
speakBtn?.setAttribute('aria-pressed', 'true');
speakBtn?.classList.add('active');
u.onend = u.onerror = () => {
speakBtn?.setAttribute('aria-pressed', 'false');
speakBtn?.classList.remove('active');
};
speechSynthesis.speak(u); speechSynthesis.speak(u);
showTTSToast(voiceServiceLabel(v));
}; };
if (!listVoices().length){ if (!listVoices().length){
speechSynthesis.onvoiceschanged = () => { speechSynthesis.onvoiceschanged = null; start(); }; speechSynthesis.onvoiceschanged = () => { speechSynthesis.onvoiceschanged = null; begin(); };
speechSynthesis.getVoices(); // nudge speechSynthesis.getVoices(); // nudge
setTimeout(() => { if (!speechSynthesis.speaking) start(); }, 600); setTimeout(() => { if (!speechSynthesis.speaking) begin(); }, 600);
} else { } else {
start(); begin();
} }
} }
// ✅ Listen for YOUR button id function stopSpeaking(){
const speakBtn = document.querySelector('#ttsBtn, #btn-speak, .btn-speak'); if (!window.speechSynthesis) return;
speechSynthesis.cancel();
speakBtn?.setAttribute('aria-pressed', 'false');
speakBtn?.classList.remove('active');
showTTSToast("Playback stopped");
}
if (speakBtn){ if (speakBtn){
speakBtn.addEventListener('click', speakCombined); // Toggle behavior
speakBtn.addEventListener('click', () => {
if (isSpeaking()){
stopSpeaking();
} else {
startSpeaking();
}
});
} else { } else {
console.warn('TTS: speak button not found (looked for #ttsBtn, #btn-speak, .btn-speak).'); console.warn('TTS: speak button not found (looked for #ttsBtn, #btn-speak, .btn-speak).');
} }