Update web/templates/entry_view.html
This commit is contained in:
parent
0a833e07f3
commit
3975afed29
@ -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).');
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user