diff --git a/public/js/app.js b/public/js/app.js index 24f1f4b..b2db828 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -142,17 +142,41 @@ function speakText(elementId) { if (!el) return; var text = (el.innerText || el.textContent).trim(); if (!text) { showToast('Nothing to read', 'error'); return; } - if (!('speechSynthesis' in window)) { showToast('TTS not supported', 'error'); return; } - window.speechSynthesis.cancel(); - var utter = new SpeechSynthesisUtterance(text); - utter.rate = 0.9; - utter.onend = function() { stopReading(); }; - utter.onerror = function() { stopReading(); }; + currentlyReadingId = elementId; var btn = findReadButton(elementId); if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = ' Stop'; } - window.speechSynthesis.speak(utter); - showToast('Reading — click Stop to end', 'info'); + + // Try ElevenLabs backend first + var token = window.AUTH_TOKEN || localStorage.getItem('ped_scribe_token') || ''; + fetch('/api/text-to-speech', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, + body: JSON.stringify({ text: text }) + }) + .then(function(r) { + if (!r.ok) throw new Error('not configured'); + return r.blob(); + }) + .then(function(blob) { + var url = URL.createObjectURL(blob); + currentAudio = new Audio(url); + currentAudio.onended = function() { URL.revokeObjectURL(url); stopReading(); }; + currentAudio.onerror = function() { URL.revokeObjectURL(url); stopReading(); }; + currentAudio.play(); + showToast('Reading — click Stop to end', 'info'); + }) + .catch(function() { + // Fallback to native speech synthesis + if (!('speechSynthesis' in window)) { stopReading(); showToast('TTS not supported', 'error'); return; } + window.speechSynthesis.cancel(); + var utter = new SpeechSynthesisUtterance(text); + utter.rate = 0.9; + utter.onend = function() { stopReading(); }; + utter.onerror = function() { stopReading(); }; + window.speechSynthesis.speak(utter); + showToast('Reading — click Stop to end', 'info'); + }); } function stopReading() { diff --git a/src/routes/tts.js b/src/routes/tts.js index 4249e81..20cf60b 100644 --- a/src/routes/tts.js +++ b/src/routes/tts.js @@ -8,9 +8,9 @@ router.post('/text-to-speech', authMiddleware, async (req, res) => { if (!process.env.ELEVENLABS_API_KEY) return res.status(400).json({ error: 'Not configured' }); const response = await axios({ method: 'POST', - url: 'https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM', + url: 'https://api.elevenlabs.io/v1/text-to-speech/pNInz6obpgDQGcFmaJgB', headers: { 'xi-api-key': process.env.ELEVENLABS_API_KEY, 'Content-Type': 'application/json' }, - data: { text: req.body.text.substring(0, 5000), model_id: 'eleven_monolingual_v1', voice_settings: { stability: 0.5, similarity_boost: 0.75 } }, + data: { text: req.body.text.substring(0, 5000), model_id: 'eleven_turbo_v2_5', voice_settings: { stability: 0.5, similarity_boost: 0.75 } }, responseType: 'arraybuffer' }); res.set('Content-Type', 'audio/mpeg');