Update web/templates/entry_view.html
This commit is contained in:
parent
a42a95368c
commit
42453c983a
@ -278,129 +278,4 @@
|
|||||||
{% 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