Update web/templates/entry_view.html

This commit is contained in:
Joshua Laymon 2025-08-21 00:32:54 +00:00
parent 73ce315fba
commit 0a833e07f3

View File

@ -281,134 +281,78 @@
<script>
(function(){
// --- tiny utility to get/keep a chosen voice ---
const LS_KEY = "tts.voiceName";
function listVoices() {
return speechSynthesis.getVoices();
}
function listVoices(){ return speechSynthesis.getVoices(); }
// Prefer nicer voices if present
const PREFERRED_ORDER = [
/Google .*English/i, // Chrome "Google US/UK English"
/Microsoft .*Natural/i, // Edge natural neural voices
/Siri/i, // Safari "Siri" voices
/Alex/i, // macOS Alex
/English/i // any English as a last resort
/Google .*English/i,
/Microsoft .*Natural/i,
/Siri/i,
/Alex/i,
/English/i
];
function pickBestVoice(targetLang = "en") {
const voices = listVoices();
if (!voices.length) return null;
function pickBestVoice(lang="en"){
const vs = listVoices();
if (!vs.length) return null;
// If the user previously chose a voice and it's still available, use it
const saved = localStorage.getItem(LS_KEY);
if (saved) {
const found = voices.find(v => v.name === saved);
if (saved){
const found = vs.find(v => v.name === saved);
if (found) return found;
}
// Try to match by preferred patterns (and language)
for (const pattern of PREFERRED_ORDER) {
const match = voices.find(v =>
pattern.test(v.name) && v.lang.toLowerCase().startsWith(targetLang.toLowerCase())
);
if (match) return match;
for (const rx of PREFERRED_ORDER){
const m = vs.find(v => rx.test(v.name) && v.lang.toLowerCase().startsWith(lang));
if (m) return m;
}
return vs.find(v => v.lang.toLowerCase().startsWith(lang)) || vs[0];
}
// Fallback: first voice in the requested language, else first voice
return voices.find(v => v.lang.toLowerCase().startsWith(targetLang.toLowerCase())) || voices[0];
function ensureSentenceEnd(s){
if (!s) return "";
return /[.!?…]$/.test(s) ? s : (s + ".");
}
// Build combined text from your page's Illustration + Application fields
function buildCombinedText() {
// Adjust selectors to your actual DOM:
const illEl = document.querySelector('[data-field="illustration"]') || document.querySelector('.lead-text[data-section="illustration"]');
const appEl = document.querySelector('[data-field="application"]') || document.querySelector('.lead-text[data-section="application"]');
const ill = illEl ? illEl.textContent.trim() : "";
const app = appEl ? appEl.textContent.trim() : "";
// Ensure illustration ends in punctuation
const punctuatedIll = ill && /[.!?…]$/.test(ill) ? ill : (ill ? ill + "." : "");
// Two spaces between parts
const combined = [punctuatedIll, app].filter(Boolean).join(" ");
return combined || "No text available for this illustration.";
// ✅ Use your actual IDs
function buildCombinedText(){
const ill = (document.getElementById("illustration-text")?.innerText || "").trim();
const app = (document.getElementById("application-text")?.innerText || "").trim();
const illP = ensureSentenceEnd(ill);
return [illP, app].filter(Boolean).join(" ") || "No text available for this illustration.";
}
// Speak using Web Speech API
function speakCombined() {
function speakCombined(){
const text = buildCombinedText();
if (!window.speechSynthesis || !window.SpeechSynthesisUtterance) {
if (!("speechSynthesis" in window) || !("SpeechSynthesisUtterance" in window)){
alert("Text-to-Speech not supported in this browser.");
return;
}
// Cancel anything already speaking
window.speechSynthesis.cancel();
speechSynthesis.cancel();
const start = () => {
const utter = new SpeechSynthesisUtterance(text);
const u = new SpeechSynthesisUtterance(text);
const v = pickBestVoice("en");
if (v) utter.voice = v;
utter.rate = 1.0; // 0.110 (1.0 = normal)
utter.pitch = 1.0; // 02
utter.volume = 1.0; // 01
speechSynthesis.speak(utter);
if (v) u.voice = v;
u.rate = 1.0; u.pitch = 1.0; u.volume = 1.0;
speechSynthesis.speak(u);
};
// Voices may load asynchronously on some browsers
if (!listVoices().length) {
speechSynthesis.onvoiceschanged = () => { start(); };
// Trigger load
speechSynthesis.getVoices();
if (!listVoices().length){
speechSynthesis.onvoiceschanged = () => { speechSynthesis.onvoiceschanged = null; start(); };
speechSynthesis.getVoices(); // nudge
setTimeout(() => { if (!speechSynthesis.speaking) start(); }, 600);
} else {
start();
}
}
// OPTIONAL: a tiny voice picker you can reveal if you want
function attachVoicePicker(buttonContainerSelector){
const container = document.querySelector(buttonContainerSelector);
if (!container) return;
const btn = document.createElement('button');
btn.type = "button";
btn.className = "btn btn-secondary small";
btn.textContent = "Voice…";
btn.style.marginLeft = "6px";
btn.addEventListener('click', () => {
const voices = listVoices();
if (!voices.length) {
alert("Voices are still loading. Try again in a moment.");
speechSynthesis.getVoices(); // nudge
return;
}
const current = pickBestVoice("en");
const names = voices.map(v => v.name);
const pick = prompt(
"Available voices:\n" +
names.map(n => (n === (current && current.name) ? "• " + n + " (current)" : "• " + n)).join("\n") +
"\n\nType the exact voice name to use:",
current ? current.name : (names[0] || "")
);
if (pick && names.includes(pick)) {
localStorage.setItem(LS_KEY, pick);
alert("Voice set to: " + pick);
}
});
container.appendChild(btn);
}
// Wire your existing speaker button
// Update the selector if your icon/button has a different id or class
const speakBtn = document.querySelector('#btn-speak, .btn-speak');
if (speakBtn) {
// ✅ Listen for YOUR button id
const speakBtn = document.querySelector('#ttsBtn, #btn-speak, .btn-speak');
if (speakBtn){
speakBtn.addEventListener('click', speakCombined);
// Optional: place the voice picker next to the speak button
attachVoicePicker(speakBtn.parentElement);
} else {
console.warn('TTS: speak button not found (looked for #ttsBtn, #btn-speak, .btn-speak).');
}
})();
</script>