Update web/templates/entry_view.html
This commit is contained in:
parent
266eec88e7
commit
a42a95368c
@ -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 %}
|
||||||
Loading…
Reference in New Issue
Block a user