Update web/templates/entry_view.html
This commit is contained in:
parent
73ce315fba
commit
0a833e07f3
@ -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);
|
||||
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
|
||||
// ✅ Use your actual IDs
|
||||
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.";
|
||||
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(){
|
||||
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.1–10 (1.0 = normal)
|
||||
utter.pitch = 1.0; // 0–2
|
||||
utter.volume = 1.0; // 0–1
|
||||
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();
|
||||
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');
|
||||
// ✅ 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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user