Files

106 lines
3.2 KiB
JavaScript

var speechVoices = [];
var speechVoicesLoaded = false;
var speechTimer = null;
var piperFallbackTimer = null;
function refreshSpeechVoices() {
if (!window.speechSynthesis) return;
var voices = window.speechSynthesis.getVoices();
if (voices && voices.length) {
speechVoices = voices;
speechVoicesLoaded = true;
}
return speechVoices;
}
function pickSpeechVoice(lang) {
refreshSpeechVoices();
var target = lang.toLowerCase();
var exact = speechVoices.find(function(voice) {
return String(voice.lang || '').toLowerCase() === target;
});
if (exact) return exact;
var prefix = target.split('-')[0];
return speechVoices.find(function(voice) {
return String(voice.lang || '').toLowerCase().split('-')[0] === prefix;
}) || null;
}
function buildSpeechUtterance(text, lang) {
var utterance = new SpeechSynthesisUtterance(text);
var langMap = { 'es': 'es-ES', 'en': 'en-US', 'zh': 'zh-CN' };
utterance.lang = langMap[lang] || lang;
var voice = pickSpeechVoice(utterance.lang);
if (voice) utterance.voice = voice;
utterance.rate = 0.9;
utterance.pitch = 1;
return utterance;
}
function doSpeak(text, lang) {
if (!window.speechSynthesis || !window.SpeechSynthesisUtterance || !text) return;
window.speechSynthesis.cancel();
if (speechTimer) clearTimeout(speechTimer);
var utterance = buildSpeechUtterance(text, lang);
speechTimer = setTimeout(function() {
window.speechSynthesis.speak(utterance);
if (window.speechSynthesis.paused) window.speechSynthesis.resume();
}, 60);
}
function speakText(text, lang) {
if (!text) return;
if (piperFallbackTimer) clearTimeout(piperFallbackTimer);
if (!window.speechSynthesis || !window.SpeechSynthesisUtterance) {
playPiperAudio(text, lang);
return;
}
refreshSpeechVoices();
doSpeak(text, lang);
window.speechSynthesis.getVoices();
if (!speechVoicesLoaded) {
if (piperFallbackTimer) clearTimeout(piperFallbackTimer);
piperFallbackTimer = setTimeout(function() {
refreshSpeechVoices();
if (!speechVoicesLoaded) playPiperAudio(text, lang);
}, 900);
}
}
function playPiperAudio(text, lang) {
if (!text) return;
fetch('/api/tts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text, lang: lang || 'es' })
}).then(function(res) {
if (!res.ok) throw new Error('tts failed');
return res.blob();
}).then(function(blob) {
var url = URL.createObjectURL(blob);
var audio = new Audio(url);
audio.onended = function() { URL.revokeObjectURL(url); };
audio.play();
}).catch(function() {});
}
function initSpeech() {
if (!window.speechSynthesis) return;
refreshSpeechVoices();
window.speechSynthesis.onvoiceschanged = function() {
refreshSpeechVoices();
};
}
if (document.readyState === 'complete') initSpeech();
else document.addEventListener('DOMContentLoaded', initSpeech);
function warmSpeechVoices() {
if (!window.speechSynthesis) return;
window.speechSynthesis.getVoices();
refreshSpeechVoices();
}
document.addEventListener('touchstart', warmSpeechVoices, { once: true, passive: true });
document.addEventListener('pointerdown', warmSpeechVoices, { once: true, passive: true });