Update web/templates/entry_view.html
This commit is contained in:
parent
73ce315fba
commit
0a833e07f3
@ -281,134 +281,78 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
// --- tiny utility to get/keep a chosen voice ---
|
|
||||||
const LS_KEY = "tts.voiceName";
|
const LS_KEY = "tts.voiceName";
|
||||||
|
|
||||||
function listVoices() {
|
function listVoices(){ return speechSynthesis.getVoices(); }
|
||||||
return speechSynthesis.getVoices();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer nicer voices if present
|
|
||||||
const PREFERRED_ORDER = [
|
const PREFERRED_ORDER = [
|
||||||
/Google .*English/i, // Chrome "Google US/UK English"
|
/Google .*English/i,
|
||||||
/Microsoft .*Natural/i, // Edge natural neural voices
|
/Microsoft .*Natural/i,
|
||||||
/Siri/i, // Safari "Siri" voices
|
/Siri/i,
|
||||||
/Alex/i, // macOS Alex
|
/Alex/i,
|
||||||
/English/i // any English as a last resort
|
/English/i
|
||||||
];
|
];
|
||||||
|
|
||||||
function pickBestVoice(targetLang = "en") {
|
function pickBestVoice(lang="en"){
|
||||||
const voices = listVoices();
|
const vs = listVoices();
|
||||||
if (!voices.length) return null;
|
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);
|
const saved = localStorage.getItem(LS_KEY);
|
||||||
if (saved){
|
if (saved){
|
||||||
const found = voices.find(v => v.name === saved);
|
const found = vs.find(v => v.name === saved);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
}
|
}
|
||||||
|
for (const rx of PREFERRED_ORDER){
|
||||||
// Try to match by preferred patterns (and language)
|
const m = vs.find(v => rx.test(v.name) && v.lang.toLowerCase().startsWith(lang));
|
||||||
for (const pattern of PREFERRED_ORDER) {
|
if (m) return m;
|
||||||
const match = voices.find(v =>
|
}
|
||||||
pattern.test(v.name) && v.lang.toLowerCase().startsWith(targetLang.toLowerCase())
|
return vs.find(v => v.lang.toLowerCase().startsWith(lang)) || vs[0];
|
||||||
);
|
|
||||||
if (match) return match;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: first voice in the requested language, else first voice
|
function ensureSentenceEnd(s){
|
||||||
return voices.find(v => v.lang.toLowerCase().startsWith(targetLang.toLowerCase())) || voices[0];
|
if (!s) return "";
|
||||||
|
return /[.!?…]$/.test(s) ? s : (s + ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build combined text from your page's Illustration + Application fields
|
// ✅ Use your actual IDs
|
||||||
function buildCombinedText(){
|
function buildCombinedText(){
|
||||||
// Adjust selectors to your actual DOM:
|
const ill = (document.getElementById("illustration-text")?.innerText || "").trim();
|
||||||
const illEl = document.querySelector('[data-field="illustration"]') || document.querySelector('.lead-text[data-section="illustration"]');
|
const app = (document.getElementById("application-text")?.innerText || "").trim();
|
||||||
const appEl = document.querySelector('[data-field="application"]') || document.querySelector('.lead-text[data-section="application"]');
|
const illP = ensureSentenceEnd(ill);
|
||||||
|
return [illP, app].filter(Boolean).join(" ") || "No text available for this illustration.";
|
||||||
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(){
|
function speakCombined(){
|
||||||
const text = buildCombinedText();
|
const text = buildCombinedText();
|
||||||
if (!window.speechSynthesis || !window.SpeechSynthesisUtterance) {
|
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.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
speechSynthesis.cancel();
|
||||||
// Cancel anything already speaking
|
|
||||||
window.speechSynthesis.cancel();
|
|
||||||
|
|
||||||
const start = () => {
|
const start = () => {
|
||||||
const utter = new SpeechSynthesisUtterance(text);
|
const u = new SpeechSynthesisUtterance(text);
|
||||||
const v = pickBestVoice("en");
|
const v = pickBestVoice("en");
|
||||||
if (v) utter.voice = v;
|
if (v) u.voice = v;
|
||||||
utter.rate = 1.0; // 0.1–10 (1.0 = normal)
|
u.rate = 1.0; u.pitch = 1.0; u.volume = 1.0;
|
||||||
utter.pitch = 1.0; // 0–2
|
speechSynthesis.speak(u);
|
||||||
utter.volume = 1.0; // 0–1
|
|
||||||
speechSynthesis.speak(utter);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Voices may load asynchronously on some browsers
|
|
||||||
if (!listVoices().length){
|
if (!listVoices().length){
|
||||||
speechSynthesis.onvoiceschanged = () => { start(); };
|
speechSynthesis.onvoiceschanged = () => { speechSynthesis.onvoiceschanged = null; start(); };
|
||||||
// Trigger load
|
speechSynthesis.getVoices(); // nudge
|
||||||
speechSynthesis.getVoices();
|
setTimeout(() => { if (!speechSynthesis.speaking) start(); }, 600);
|
||||||
} else {
|
} else {
|
||||||
start();
|
start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OPTIONAL: a tiny voice picker you can reveal if you want
|
// ✅ Listen for YOUR button id
|
||||||
function attachVoicePicker(buttonContainerSelector){
|
const speakBtn = document.querySelector('#ttsBtn, #btn-speak, .btn-speak');
|
||||||
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){
|
if (speakBtn){
|
||||||
speakBtn.addEventListener('click', speakCombined);
|
speakBtn.addEventListener('click', speakCombined);
|
||||||
// Optional: place the voice picker next to the speak button
|
} else {
|
||||||
attachVoicePicker(speakBtn.parentElement);
|
console.warn('TTS: speak button not found (looked for #ttsBtn, #btn-speak, .btn-speak).');
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user