Update web/templates/entry_view.html

This commit is contained in:
Joshua Laymon 2025-08-21 00:14:53 +00:00
parent 266eec88e7
commit a42a95368c

View File

@ -278,4 +278,129 @@
{% endif %} {% endif %}
})(); })();
</script> </script>
<script>
(function(){
const btn = document.getElementById('ttsBtn');
if (!btn) return;
// Graceful feature check
const synth = window.speechSynthesis;
if (!('speechSynthesis' in window)) {
btn.disabled = true;
btn.title = 'Text-to-Speech not supported in this browser';
return;
}
// Build the combined text (Illustration + two spaces + Application).
function buildParagraph() {
const ill = (btn.dataset.illustration || '').trim();
const app = (btn.dataset.application || '').trim();
// Ensure Illustration ends with punctuation if it has text
let illFixed = ill;
if (illFixed && !/[.!?…]$/.test(illFixed)) {
illFixed += '.';
}
// Two spaces between illustration and application (if both exist)
const spacer = (illFixed && app) ? ' ' : '';
return (illFixed + spacer + app).replace(/\s+/g, ' ').trim();
}
// Try to pick a natural-sounding voice if available
let voices = [];
function loadVoices() {
voices = synth.getVoices();
}
loadVoices();
if (typeof speechSynthesis.onvoiceschanged !== 'undefined') {
speechSynthesis.onvoiceschanged = loadVoices;
}
function chooseVoice() {
if (!voices || !voices.length) return null;
// Prefer a Google or high-quality en voice if present
const prefs = [
/Google\s.+English/i,
/en-US/i,
/English/i
];
for (const rx of prefs) {
const v = voices.find(v => rx.test(v.name) || rx.test(v.lang));
if (v) return v;
}
return voices[0] || null;
}
let isSpeaking = false;
let currentUtterances = [];
function chunkBySentences(text, maxLen=300) {
// Split by sentence-ish boundaries, merge small pieces to respect character limits
const raw = text.split(/(?<=[.!?…])\s+/);
const chunks = [];
let buf = '';
raw.forEach(part => {
if ((buf + ' ' + part).trim().length <= maxLen) {
buf = (buf ? (buf + ' ' + part) : part);
} else {
if (buf) chunks.push(buf.trim());
buf = part;
}
});
if (buf) chunks.push(buf.trim());
return chunks.length ? chunks : [text];
}
function speak(text) {
stop(); // ensure we start fresh
const voice = chooseVoice();
const chunks = chunkBySentences(text);
chunks.forEach((chunk, i) => {
const u = new SpeechSynthesisUtterance(chunk);
if (voice) u.voice = voice;
u.rate = 1.0; // adjust to taste (0.5 - 2)
u.pitch = 1.0; // 0 - 2
u.onstart = () => { isSpeaking = true; btn.classList.add('speaking'); };
u.onend = () => {
// When the last utterance ends, clear state
if (i === chunks.length - 1) {
isSpeaking = false;
btn.classList.remove('speaking');
}
};
u.onerror = () => {
isSpeaking = false;
btn.classList.remove('speaking');
};
currentUtterances.push(u);
});
// Queue them in order
currentUtterances.forEach(u => synth.speak(u));
}
function stop() {
if (synth.speaking || synth.pending) synth.cancel();
currentUtterances = [];
isSpeaking = false;
btn.classList.remove('speaking');
}
// Click to toggle: play if idle; stop if already speaking
btn.addEventListener('click', function(){
if (isSpeaking || synth.speaking || synth.pending) {
stop();
return;
}
const text = buildParagraph();
if (text) speak(text);
});
// Optional: stop if navigating away
window.addEventListener('beforeunload', stop);
})();
</script>
{% endblock %} {% endblock %}