Update web/templates/entry_view.html

This commit is contained in:
Joshua Laymon 2025-08-21 00:25:37 +00:00
parent 42453c983a
commit 73ce315fba

View File

@ -278,4 +278,138 @@
{% endif %}
})();
</script>
<script>
(function(){
// --- tiny utility to get/keep a chosen voice ---
const LS_KEY = "tts.voiceName";
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
];
function pickBestVoice(targetLang = "en") {
const voices = listVoices();
if (!voices.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 (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;
}
// Fallback: first voice in the requested language, else first voice
return voices.find(v => v.lang.toLowerCase().startsWith(targetLang.toLowerCase())) || voices[0];
}
// 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.";
}
// Speak using Web Speech API
function speakCombined() {
const text = buildCombinedText();
if (!window.speechSynthesis || !window.SpeechSynthesisUtterance) {
alert("Text-to-Speech not supported in this browser.");
return;
}
// Cancel anything already speaking
window.speechSynthesis.cancel();
const start = () => {
const utter = 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);
};
// Voices may load asynchronously on some browsers
if (!listVoices().length) {
speechSynthesis.onvoiceschanged = () => { start(); };
// Trigger load
speechSynthesis.getVoices();
} 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) {
speakBtn.addEventListener('click', speakCombined);
// Optional: place the voice picker next to the speak button
attachVoicePicker(speakBtn.parentElement);
}
})();
</script>
{% endblock %}