Update web/templates/entry_view.html
This commit is contained in:
parent
07710ec830
commit
2c694f1c6d
@ -279,139 +279,101 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- TTS toast (one-time, tiny helper UI) -->
|
||||
<div id="tts-toast"
|
||||
style="position:fixed;bottom:84px;left:50%;transform:translateX(-50%);
|
||||
background:#111827;color:#fff;padding:10px 14px;border-radius:8px;
|
||||
font-size:14px;display:none;z-index:9999;box-shadow:0 6px 18px rgba(0,0,0,.25);">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const LS_KEY = "tts.voiceName";
|
||||
const speakBtn = document.querySelector('#ttsBtn, #btn-speak, .btn-speak');
|
||||
const btn = document.getElementById('ttsBtn');
|
||||
if (!btn) return;
|
||||
|
||||
// --- Toast ---
|
||||
function showTTSToast(msg, ms=2200){
|
||||
const el = document.getElementById('tts-toast');
|
||||
if (!el) return;
|
||||
el.textContent = msg;
|
||||
el.style.display = 'block';
|
||||
clearTimeout(showTTSToast._t);
|
||||
showTTSToast._t = setTimeout(()=>{ el.style.display = 'none'; }, ms);
|
||||
const ttsUrl = btn.dataset.ttsUrl || ""; // Present only for staff
|
||||
let audioEl = null;
|
||||
|
||||
// Build combined text (punctuation-safe) for browser TTS fallback
|
||||
function buildCombinedText() {
|
||||
const ill = (document.getElementById('illustration-text')?.innerText || '').trim();
|
||||
const app = (document.getElementById('application-text')?.innerText || '').trim();
|
||||
const punctIll = ill && /[.!?…—]$/.test(ill) ? ill : (ill ? ill + '.' : '');
|
||||
return [punctIll, app].filter(Boolean).join(' ') || 'No text available for this illustration.';
|
||||
}
|
||||
|
||||
// --- Voice helpers ---
|
||||
function listVoices(){ return window.speechSynthesis ? speechSynthesis.getVoices() : []; }
|
||||
|
||||
const PREFERRED_ORDER = [
|
||||
/Google .*English/i, // Chrome voices
|
||||
/Microsoft .*Natural/i, // Edge neural voices
|
||||
/Siri/i, // Safari voices
|
||||
/Alex/i, // macOS Alex
|
||||
/English/i
|
||||
];
|
||||
|
||||
function pickBestVoice(lang="en"){
|
||||
const vs = listVoices();
|
||||
if (!vs.length) return null;
|
||||
|
||||
const saved = localStorage.getItem(LS_KEY);
|
||||
if (saved){
|
||||
const found = vs.find(v => v.name === saved);
|
||||
if (found) return found;
|
||||
// --- Staff path: stream OpenAI TTS from server and toggle on click ---
|
||||
if (ttsUrl) {
|
||||
btn.addEventListener('click', () => {
|
||||
// Stop if already playing
|
||||
if (audioEl && !audioEl.paused) {
|
||||
audioEl.pause();
|
||||
audioEl.currentTime = 0;
|
||||
return;
|
||||
}
|
||||
if (!audioEl) {
|
||||
audioEl = new Audio();
|
||||
audioEl.addEventListener('ended', () => {
|
||||
btn.classList.remove('playing');
|
||||
});
|
||||
}
|
||||
// Cache-buster so we always pick up newly cached files
|
||||
audioEl.src = ttsUrl + (ttsUrl.includes('?') ? '&' : '?') + 't=' + Date.now();
|
||||
audioEl.play().then(() => {
|
||||
btn.classList.add('playing');
|
||||
}).catch(err => {
|
||||
alert('TTS error: ' + err);
|
||||
});
|
||||
});
|
||||
// Optional little notice the first time
|
||||
if (!sessionStorage.getItem('tts_notice_shown')) {
|
||||
sessionStorage.setItem('tts_notice_shown', '1');
|
||||
// toast-style notice:
|
||||
const notice = document.createElement('div');
|
||||
notice.textContent = 'Using OpenAI TTS (gpt-4o-mini-tts, alloy)';
|
||||
Object.assign(notice.style, {
|
||||
position:'fixed', bottom:'18px', left:'50%', transform:'translateX(-50%)',
|
||||
background:'#333', color:'#fff', padding:'8px 12px', borderRadius:'6px',
|
||||
fontSize:'13px', zIndex:'99999'
|
||||
});
|
||||
document.body.appendChild(notice);
|
||||
setTimeout(()=>notice.remove(), 2500);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// --- Non-staff path: keep your browser TTS (Web Speech API) ---
|
||||
function pickVoice() {
|
||||
const voices = speechSynthesis.getVoices();
|
||||
// Preference order (same as you had)
|
||||
const prefs = [/Google .*English/i, /Microsoft .*Natural/i, /Siri/i, /Alex/i, /English/i];
|
||||
for (const p of prefs) {
|
||||
const v = voices.find(v => p.test(v.name) && v.lang.toLowerCase().startsWith('en'));
|
||||
if (v) return v;
|
||||
}
|
||||
return vs.find(v => v.lang?.toLowerCase().startsWith(lang)) || vs[0];
|
||||
return voices.find(v => v.lang.toLowerCase().startsWith('en')) || voices[0] || null;
|
||||
}
|
||||
|
||||
function voiceServiceLabel(v){
|
||||
if (!v) return "Browser TTS";
|
||||
const n = (v.name || "").toLowerCase();
|
||||
if (/google/.test(n)) return "Google (Chrome) voice: " + v.name;
|
||||
if (/microsoft|aria|guy|jessa|zira|david|mark|neural/.test(n)) return "Microsoft (Edge) voice: " + v.name;
|
||||
if (/siri/.test(n)) return "Apple Siri voice: " + v.name;
|
||||
if (/alex/.test(n)) return "Apple Alex voice";
|
||||
return "Browser TTS voice: " + v.name + (v.lang ? ` [${v.lang}]` : "");
|
||||
}
|
||||
|
||||
// --- Build the text (ensure punctuation + two spaces) ---
|
||||
function ensureSentenceEnd(s){
|
||||
if (!s) return "";
|
||||
return /[.!?…]$/.test(s) ? s : (s + ".");
|
||||
}
|
||||
function buildCombinedText(){
|
||||
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.";
|
||||
}
|
||||
|
||||
function isSpeaking(){
|
||||
return !!(window.speechSynthesis && (speechSynthesis.speaking || speechSynthesis.pending));
|
||||
}
|
||||
|
||||
function startSpeaking(){
|
||||
function speakBrowserTTS() {
|
||||
const text = buildCombinedText();
|
||||
if (!("speechSynthesis" in window) || !("SpeechSynthesisUtterance" in window)){
|
||||
alert("Text-to-Speech not supported in this browser.");
|
||||
if (!window.speechSynthesis || !window.SpeechSynthesisUtterance) {
|
||||
alert('Text-to-Speech not supported in this browser.');
|
||||
return;
|
||||
}
|
||||
speechSynthesis.cancel();
|
||||
|
||||
const begin = () => {
|
||||
// If currently speaking—toggle off
|
||||
if (speechSynthesis.speaking || speechSynthesis.pending) {
|
||||
speechSynthesis.cancel();
|
||||
return;
|
||||
}
|
||||
const start = () => {
|
||||
const u = new SpeechSynthesisUtterance(text);
|
||||
const v = pickBestVoice("en");
|
||||
const v = pickVoice();
|
||||
if (v) u.voice = v;
|
||||
u.rate = 1.0;
|
||||
u.pitch = 1.0;
|
||||
u.volume = 1.0;
|
||||
|
||||
// UI feedback
|
||||
speakBtn?.setAttribute('aria-pressed', 'true');
|
||||
speakBtn?.classList.add('active');
|
||||
|
||||
u.onend = u.onerror = () => {
|
||||
speakBtn?.setAttribute('aria-pressed', 'false');
|
||||
speakBtn?.classList.remove('active');
|
||||
};
|
||||
|
||||
u.rate = 1.0; u.pitch = 1.0; u.volume = 1.0;
|
||||
speechSynthesis.speak(u);
|
||||
showTTSToast(voiceServiceLabel(v));
|
||||
};
|
||||
|
||||
if (!listVoices().length){
|
||||
speechSynthesis.onvoiceschanged = () => { speechSynthesis.onvoiceschanged = null; begin(); };
|
||||
speechSynthesis.getVoices(); // nudge
|
||||
setTimeout(() => { if (!speechSynthesis.speaking) begin(); }, 600);
|
||||
if (!speechSynthesis.getVoices().length) {
|
||||
speechSynthesis.onvoiceschanged = () => start();
|
||||
speechSynthesis.getVoices();
|
||||
} else {
|
||||
begin();
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
function stopSpeaking(){
|
||||
if (!window.speechSynthesis) return;
|
||||
speechSynthesis.cancel();
|
||||
speakBtn?.setAttribute('aria-pressed', 'false');
|
||||
speakBtn?.classList.remove('active');
|
||||
showTTSToast("Playback stopped");
|
||||
}
|
||||
|
||||
if (speakBtn){
|
||||
// Toggle behavior
|
||||
speakBtn.addEventListener('click', () => {
|
||||
if (isSpeaking()){
|
||||
stopSpeaking();
|
||||
} else {
|
||||
startSpeaking();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('TTS: speak button not found (looked for #ttsBtn, #btn-speak, .btn-speak).');
|
||||
}
|
||||
btn.addEventListener('click', speakBrowserTTS);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue
Block a user