Spaces:
Running
Running
| // === Daily Satsang Frontend (enhanced) === | |
| let allSatsangs = []; | |
| let player = null; | |
| let currentVideoId = null; | |
| let userEmail = null; | |
| let saveInterval = null; | |
| let initialLoaderShown = false; | |
| // ---- Helpers ---- | |
| const API = "https://bhagvat-mongodb.hf.space"; | |
| const timeKey = (vid) => `satsang_time_${vid}`; | |
| const extractId = (y) => (y || "").split("/").pop(); | |
| // Debounce utility | |
| function debounce(fn, delay=400){ let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a), delay); }; } | |
| // Prefer male-ish Hindi/Indian voice when possible | |
| function pickVoice() { | |
| const voices = speechSynthesis.getVoices(); | |
| // Strong filters first | |
| const byPref = [ | |
| v => /sa-IN|hi-IN/i.test(v.lang) && /(male|deep|bass)/i.test(v.name), | |
| v => /sa-IN|hi-IN/i.test(v.lang) && /(google)/i.test(v.name) && /(male|india)/i.test(v.name), | |
| v => /sa-IN|hi-IN/i.test(v.lang), | |
| v => /en-IN/i.test(v.lang) && /(male|india)/i.test(v.name), | |
| v => /en-IN/i.test(v.lang), | |
| ]; | |
| for (const pick of byPref) { | |
| const found = voices.find(pick); | |
| if (found) return found; | |
| } | |
| return voices[0]; // fallback | |
| } | |
| // ---- Init ---- | |
| window.onload = async () => { | |
| const token = localStorage.getItem('authToken'); | |
| if (token) { | |
| try { userEmail = jwt_decode(token)?.email; } catch {} | |
| } | |
| await fetchAllSatsangs(); | |
| await fetchUserSaved(); // fills favorites/playlists & counts | |
| // Show loader while we try to restore last watched | |
| const il = document.getElementById('initialLoader'); | |
| il.style.display = 'flex'; | |
| initialLoaderShown = true; | |
| await restoreLastOrKeep(); | |
| // If nothing was loaded (user has no last video), hide the loader | |
| if (initialLoaderShown) il.style.display = 'none'; | |
| }; | |
| // Fetch satsangs (all) | |
| async function fetchAllSatsangs(){ | |
| try{ | |
| const res = await fetch(`${API}/satsangs?type=all`); | |
| const data = await res.json(); | |
| allSatsangs = data.satsangs || []; | |
| }catch(e){ console.error("fetchAllSatsangs", e); } | |
| } | |
| // Restore last watched from backend (if user), else keep last from local lastPlayedVideoId | |
| async function restoreLastOrKeep(){ | |
| let lastId = localStorage.getItem("lastPlayedVideoId"); | |
| if (userEmail){ | |
| try{ | |
| const r = await fetch(`${API}/get_user_saved?email=${encodeURIComponent(userEmail)}`); | |
| const saved = await r.json(); | |
| if (saved?.last_video_id) lastId = saved.last_video_id; | |
| }catch(e){ console.warn("get_user_saved last_video_id failed", e); } | |
| } | |
| if (lastId){ | |
| const match = allSatsangs.find(s => (extractId(s.youtube) === lastId) || (s.youtube === lastId)); | |
| if (match){ | |
| await showSatsang(match, {autoPlay:true}); | |
| return; | |
| } | |
| } | |
| // If no match, do nothing (user can hit Play Random) | |
| } | |
| function showRandomLoader(){ const el = document.getElementById('randomLoader'); if (el) el.style.display = 'block'; } | |
| function hideRandomLoader(){ const el = document.getElementById('randomLoader'); if (el) el.style.display = 'none'; } | |
| // Load random with filters + exclude previously played local (played list deprecated in favor of DB history) | |
| async function loadNextSatsang(){ | |
| showRandomLoader(); | |
| try { | |
| const type = document.getElementById('satsangType').value; | |
| const tagSearch = document.getElementById('tagSearch').value.trim().toLowerCase(); | |
| let filtered = allSatsangs.filter(s=>{ | |
| if (!s?.youtube) return false; | |
| const matchesType = (type==='all') || (s.type===type); | |
| const matchesTag = !tagSearch || (Array.isArray(s.tags) && s.tags.some(t=>t.toLowerCase().includes(tagSearch))); | |
| return matchesType && matchesTag; | |
| }); | |
| if (!filtered.length){ alert("No satsang videos found to play."); return; } | |
| const pick = filtered[Math.floor(Math.random()*filtered.length)]; | |
| await showSatsang(pick, {autoPlay:true}); | |
| } finally { | |
| // if player starts, we'll also hide in onPlayerStateChange | |
| setTimeout(()=> hideRandomLoader(), 1200); // safety hide | |
| } | |
| } | |
| // --- Advanced Search: debounce + backend scoring; loads BEST match automatically | |
| const doSearch = debounce(async ()=>{ | |
| const q = document.getElementById('tagSearch').value.trim(); | |
| if (!q) return; | |
| const type = document.getElementById('satsangType').value; | |
| try{ | |
| const res = await fetch(`${API}/satsangs/search?q=${encodeURIComponent(q)}&type=${encodeURIComponent(type)}`); | |
| const data = await res.json(); | |
| const best = data.results?.[0]; | |
| if (best) await showSatsang(best, {autoPlay:true}); | |
| else alert("No matching satsangs found."); | |
| }catch(e){ | |
| console.error("search failed", e); | |
| } | |
| }, 500); | |
| document.addEventListener('input', (e)=>{ | |
| if (e.target && e.target.id === 'tagSearch') doSearch(); | |
| }); | |
| // ---- Player + state/time ---- | |
| function whenYTReady(cb){ | |
| if (window.YT && YT.Player) return cb(); | |
| const prev = window.onYouTubeIframeAPIReady; | |
| window.onYouTubeIframeAPIReady = function(){ prev && prev(); cb(); }; | |
| } | |
| async function showSatsang(data, opts={}){ | |
| document.getElementById("videoTitle").textContent = data.title || "Untitled"; | |
| document.getElementById("videoDescription").textContent = data.description || ""; | |
| document.getElementById("transcriptContent").textContent = | |
| Array.isArray(data.transcript) ? data.transcript.map(t=>t.text).join(" ") : (data.transcript || "No transcript available."); | |
| const videoId = extractId(data.youtube); | |
| currentVideoId = videoId; | |
| localStorage.setItem("lastPlayedVideoId", videoId); | |
| // tell backend "this is my last watched" + add to history_ids (no time) | |
| if (userEmail){ | |
| fetch(`${API}/user/set_last_watched`, { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({email:userEmail, video_id:videoId}) | |
| }).catch(()=>{}); | |
| } | |
| const startSeconds = Math.max(0, (parseFloat(localStorage.getItem(timeKey(videoId))) || 0) - 5); | |
| whenYTReady(()=>{ | |
| const container = document.getElementById('player'); | |
| if (!container){ console.error("Missing #player"); return; } | |
| const params = { height:'315', width:'100%', videoId, playerVars:{autoplay:1, start:startSeconds, rel:0, controls:1}, events:{ onStateChange:onPlayerStateChange } }; | |
| if (!player) player = new YT.Player('player', params); | |
| else { | |
| try { player.loadVideoById({ videoId, startSeconds }); } | |
| catch { player = new YT.Player('player', params); } | |
| } | |
| }); | |
| } | |
| function onPlayerStateChange(e){ | |
| if (!currentVideoId) return; | |
| if (e.data === YT.PlayerState.PLAYING){ | |
| hideRandomLoader(); | |
| const il = document.getElementById('initialLoader'); | |
| if (initialLoaderShown && il) { il.style.display = 'none'; initialLoaderShown = false; } | |
| clearInterval(saveInterval); | |
| saveInterval = setInterval(()=>{ | |
| const t = Math.floor(player?.getCurrentTime?.() || 0); | |
| if (t>0){ | |
| localStorage.setItem(timeKey(currentVideoId), t); | |
| // Optional: still ping backend to record history (id only) | |
| if (userEmail){ | |
| fetch(`${API}/update_progress`, { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({email:userEmail, video_id: currentVideoId}) | |
| }).catch(()=>{}); | |
| } | |
| } | |
| }, 8000); | |
| } | |
| if (e.data === YT.PlayerState.PAUSED || e.data === YT.PlayerState.ENDED){ | |
| clearInterval(saveInterval); saveInterval=null; | |
| const t = Math.floor(player?.getCurrentTime?.() || 0); | |
| if (t>0) localStorage.setItem(timeKey(currentVideoId), t); | |
| } | |
| } | |
| function cancelInitialAndPlayRandom(){ | |
| const il = document.getElementById('initialLoader'); | |
| if (il) il.style.display = 'none'; | |
| initialLoaderShown = false; | |
| loadNextSatsang(); | |
| } | |
| // ---- Favorites & Playlists (instant UI, titles not ids) ---- | |
| async function fetchUserSaved(){ | |
| if (!userEmail) return; | |
| const res = await fetch(`${API}/get_user_saved?email=${encodeURIComponent(userEmail)}`); | |
| const data = await res.json(); | |
| const div = document.getElementById("savedContent"); | |
| div.innerHTML = ""; | |
| const idToTitle = {}; | |
| (allSatsangs || []).forEach(s => { idToTitle[extractId(s.youtube)] = s.title || extractId(s.youtube); }); | |
| if (Array.isArray(data.favorites) && data.favorites.length){ | |
| const html = data.favorites.map(v=>{ | |
| const vid = extractId(v); | |
| const title = idToTitle[vid] || vid; | |
| return `<li><span class="saved-item" onclick="loadFromId('${vid}')">${title}</span></li>`; | |
| }).join(""); | |
| div.innerHTML += `<h4>Favorites</h4><ul class="saved-list">${html}</ul>`; | |
| } | |
| if (data.playlists){ | |
| for (const [name, videos] of Object.entries(data.playlists)){ | |
| const html = (videos||[]).map(v=>{ | |
| const vid = extractId(v); | |
| const title = idToTitle[vid] || vid; | |
| return `<li><span class="saved-item" onclick="loadFromId('${vid}')">${title}</span></li>`; | |
| }).join(""); | |
| div.innerHTML += ` | |
| <details> | |
| <summary>${name}</summary> | |
| <ul class="saved-list">${html || "<li><em>Empty</em></li>"}</ul> | |
| </details>`; | |
| } | |
| } | |
| // Counts: short & long | |
| if (Array.isArray(data.history_ids)){ | |
| let short = 0, long = 0; | |
| const typeMap = {}; | |
| allSatsangs.forEach(s => typeMap[extractId(s.youtube)] = s.type); | |
| data.history_ids.forEach(id=>{ | |
| const t = typeMap[extractId(id)]; | |
| if (t === 'short') short++; | |
| if (t === 'long') long++; | |
| }); | |
| document.getElementById('shortCount').textContent = short; | |
| document.getElementById('longCount').textContent = long; | |
| } | |
| } | |
| function viewSaved(){ | |
| document.querySelector('.saved-block')?.scrollIntoView({behavior:'smooth', block:'start'}); | |
| } | |
| async function markFavorite(){ | |
| if (!currentVideoId || !userEmail){ alert("Login required to mark favorite."); return; } | |
| await fetch(`${API}/mark_favorite`, { method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({ email:userEmail, video_id: currentVideoId }) }); | |
| // Refresh list instantly | |
| await fetchUserSaved(); | |
| alert("Added to Favorites"); | |
| } | |
| async function addToPlaylist(){ | |
| const playlist = document.getElementById('playlistName').value.trim(); | |
| if (!playlist || !currentVideoId || !userEmail){ alert("Login and playlist name required."); return; } | |
| await fetch(`${API}/add_to_playlist`, { method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({ email:userEmail, playlist_name: playlist, video_id: currentVideoId }) }); | |
| await fetchUserSaved(); | |
| alert(`Added to "${playlist}" playlist.`); | |
| } | |
| async function removeFromPlaylist(){ | |
| const playlist = document.getElementById('playlistName').value.trim(); | |
| if (!playlist || !currentVideoId || !userEmail){ alert("Login and playlist name required."); return; } | |
| await fetch(`${API}/remove_from_playlist`, { method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({ email:userEmail, playlist_name: playlist, video_id: currentVideoId }) }); | |
| await fetchUserSaved(); | |
| alert(`Removed from "${playlist}".`); | |
| } | |
| async function renamePlaylistPrompt(){ | |
| if (!userEmail){ alert("Login required."); return; } | |
| const oldName = document.getElementById('playlistName').value.trim(); | |
| if (!oldName){ alert("Type the current playlist name first."); return; } | |
| const newName = prompt("Enter new playlist name:", oldName); | |
| if (!newName || newName.trim() === oldName) return; | |
| const res = await fetch(`${API}/rename_playlist`, { | |
| method:'POST', headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({ email:userEmail, old_name: oldName, new_name: newName.trim() }) | |
| }); | |
| const data = await res.json(); | |
| if (res.ok) { | |
| document.getElementById('playlistName').value = newName.trim(); | |
| await fetchUserSaved(); | |
| alert(`Playlist renamed to "${newName.trim()}"`); | |
| } else { | |
| alert(data.error || "Rename failed"); | |
| } | |
| } | |
| function loadFromId(videoId){ | |
| const match = allSatsangs.find(s => extractId(s.youtube) === videoId || s.youtube === videoId); | |
| if (match) showSatsang(match, {autoPlay:true}); | |
| } | |
| // ---- Lock / Unlock Overlay ---- | |
| function toggleLockScreen(){ | |
| const overlay = document.createElement("div"); | |
| overlay.id = "customLockOverlay"; | |
| Object.assign(overlay.style, { | |
| position:"fixed", top:"0", left:"0", width:"100vw", height:"100vh", | |
| background:"transparent", zIndex:"9999", display:"flex", | |
| justifyContent:"flex-end", alignItems:"flex-start", pointerEvents:"auto" | |
| }); | |
| const btn = document.createElement("button"); | |
| btn.innerHTML = "🔓"; btn.title = "Unlock Screen"; | |
| Object.assign(btn.style, { margin:"10px", padding:"10px", fontSize:"20px", backgroundColor:"white", borderRadius:"8px", boxShadow:"0 0 10px rgba(0,0,0,0.2)" }); | |
| btn.onclick = ()=>document.body.removeChild(overlay); | |
| overlay.appendChild(btn); | |
| document.body.appendChild(overlay); | |
| } | |
| function unlockScreen(e){ e?.stopPropagation?.(); document.getElementById('lockOverlay').style.display='none'; } | |
| /************ TTS: Respect user language, voice, rate, pitch ************/ | |
| /** Try to read TTS prefs from your existing popup/menu/state. | |
| * We support several sources so we don't break your UI: | |
| * - window.ttsPrefs = {lang, voiceName, rate, pitch, engine} | |
| * - localStorage keys: tts.lang, tts.voiceName, tts.rate, tts.pitch, tts.engine | |
| * - DOM (OPTIONAL): #ttsLang, #voiceSelect, #ttsRate, #ttsPitch, #ttsEngine | |
| * engine: 'browser' (default) or 'server' (optional if you also wired server TTS) | |
| */ | |
| function getTTSPrefs() { | |
| const pick = (domId, lsKey, fallback) => { | |
| const domEl = document.getElementById(domId); | |
| if (domEl && domEl.value) return domEl.value; | |
| const ls = localStorage.getItem(lsKey); | |
| if (ls !== null && ls !== undefined && ls !== '') return ls; | |
| return fallback; | |
| }; | |
| const fromWin = (window.ttsPrefs && typeof window.ttsPrefs === 'object') ? window.ttsPrefs : {}; | |
| const prefs = { | |
| lang: fromWin.lang || pick('ttsLang', 'tts.lang', 'hi-IN'), // default Hindi | |
| voiceName: fromWin.voiceName || pick('voiceSelect', 'tts.voiceName', ''), // exact voice name if saved | |
| rate: parseFloat(fromWin.rate ?? pick('ttsRate', 'tts.rate', '0.85')) || 0.85, | |
| pitch: parseFloat(fromWin.pitch ?? pick('ttsPitch', 'tts.pitch', '0.8')) || 0.8, | |
| engine: (fromWin.engine || pick('ttsEngine', 'tts.engine', 'browser')).toLowerCase() | |
| }; | |
| return prefs; | |
| } | |
| /** Promise that resolves when voices are loaded (Chrome quirk). */ | |
| function loadVoicesOnce() { | |
| return new Promise(resolve => { | |
| const have = speechSynthesis.getVoices(); | |
| if (have && have.length) return resolve(have); | |
| const handler = () => { | |
| const vs = speechSynthesis.getVoices(); | |
| if (vs && vs.length) { | |
| speechSynthesis.onvoiceschanged = null; | |
| resolve(vs); | |
| } | |
| }; | |
| speechSynthesis.onvoiceschanged = handler; | |
| // Edge case: poke the engine to trigger load | |
| setTimeout(() => handler(), 50); | |
| }); | |
| } | |
| /** Find a voice by exact name if provided; else pick best match for lang; else fallback. */ | |
| function resolveVoice(voices, { lang, voiceName }) { | |
| // exact voice name match first | |
| if (voiceName) { | |
| const byName = voices.find(v => v.name === voiceName); | |
| if (byName) return byName; | |
| } | |
| // next: best match inside selected language | |
| const sameLang = voices.filter(v => (v.lang || '').toLowerCase() === (lang || '').toLowerCase()); | |
| if (sameLang.length) { | |
| // try to prefer deeper/male-ish names if exist | |
| const preferred = sameLang.find(v => /(male|deep|bass|baritone)/i.test(v.name)) || sameLang[0]; | |
| return preferred; | |
| } | |
| // next: language prefix match (e.g., hi vs hi-IN) | |
| const langPrefix = (lang || '').split('-')[0]; | |
| if (langPrefix) { | |
| const pref = voices.filter(v => (v.lang || '').toLowerCase().startsWith(langPrefix.toLowerCase())); | |
| if (pref.length) return pref[0]; | |
| } | |
| // final fallback | |
| return voices[0]; | |
| } | |
| /** Split transcript into natural chunks: danda, punctuation, comma, pipe. */ | |
| function buildTranscriptChunks(text) { | |
| const parts = text.split(/[।॥.!?\u0964\u0965,|]\s*/).filter(Boolean); | |
| const chunks = []; | |
| let buf = ''; | |
| for (const p of parts) { | |
| const next = buf ? (buf + ' ' + p) : p; | |
| if (next.length > 200) { // keep chunks ~180-220 chars | |
| if (buf) chunks.push(buf.trim()); | |
| buf = p; | |
| } else { | |
| buf = next; | |
| } | |
| } | |
| if (buf) chunks.push(buf.trim()); | |
| return chunks; | |
| } | |
| /** Speak an array of chunks with the browser engine. */ | |
| async function speakChunksBrowser(chunks, prefs) { | |
| const voices = await loadVoicesOnce(); | |
| const v = resolveVoice(voices, prefs); | |
| // clear any residual queue | |
| try { speechSynthesis.cancel(); } catch {} | |
| ttsPaused = false; | |
| ttsUtterances = chunks.map(txt => { | |
| const u = new SpeechSynthesisUtterance(txt); | |
| u.voice = v; | |
| u.lang = v?.lang || prefs.lang || 'hi-IN'; | |
| u.rate = prefs.rate; // slower | |
| u.pitch = prefs.pitch; // deeper | |
| u.volume = 1.0; | |
| return u; | |
| }); | |
| await new Promise(resolve => { | |
| const speakNext = () => { | |
| if (!ttsUtterances.length) return resolve(); | |
| const u = ttsUtterances.shift(); | |
| u.onend = () => { if (!ttsPaused) setTimeout(speakNext, 150); }; | |
| u.onerror = () => { if (!ttsPaused) setTimeout(speakNext, 150); }; | |
| try { speechSynthesis.speak(u); } catch { setTimeout(speakNext, 150); } | |
| }; | |
| speakNext(); | |
| }); | |
| } | |
| /** OPTIONAL: server TTS if you support it (engine === 'server'). | |
| * Keep this if you already wired a server TTS route; otherwise ignore. | |
| * Expects POST /tts_hi_male (or your multi-lang route) {text, lang, rate, pitch, voiceName?} | |
| */ | |
| async function speakChunksServer(chunks, prefs) { | |
| // Map browser slider to SSML style values roughly | |
| const ssmlRate = prefs.rate <= 0.9 ? '-15%' : '-5%'; | |
| const ssmlPitch = prefs.pitch <= 0.85 ? '-4st' : '-2st'; | |
| for (const chunk of chunks) { | |
| const res = await fetch(`${API.replace('/satsangs','')}/tts_hi_male`, { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ | |
| text: chunk, | |
| lang: prefs.lang, // if your backend supports switching voices per lang | |
| rate: ssmlRate, | |
| pitch: ssmlPitch, | |
| voiceName: prefs.voiceName || '' // optional: exact server voice | |
| }) | |
| }); | |
| if (!res.ok) throw new Error('Server TTS failed'); | |
| const blob = await res.blob(); | |
| await new Promise(resolve => { | |
| const url = URL.createObjectURL(blob); | |
| const audio = new Audio(url); | |
| audio.onended = () => { URL.revokeObjectURL(url); setTimeout(resolve, 120); }; | |
| audio.onerror = () => { URL.revokeObjectURL(url); setTimeout(resolve, 120); }; | |
| audio.play(); | |
| }); | |
| } | |
| } | |
| /** Public controls */ | |
| function pauseTranscriptTTS(){ try{ speechSynthesis.pause(); ttsPaused = true; }catch{} } | |
| function resumeTranscriptTTS(){ try{ speechSynthesis.resume(); ttsPaused = false; }catch{} } | |
| function stopTranscriptTTS(silent=false){ | |
| try{ speechSynthesis.cancel(); }catch{} | |
| ttsPaused = false; ttsUtterances = []; | |
| if (!silent) {/* noop */} | |
| } | |
| /** Main entry: follows user’s selected language, voice, rate, pitch, engine. */ | |
| async function playTranscriptTTS() { | |
| const textEl = document.getElementById('transcriptContent'); | |
| if (!textEl) return alert('Transcript element not found.'); | |
| const text = textEl.textContent.trim(); | |
| if (!text) return alert('No transcript to play.'); | |
| stopTranscriptTTS(true); | |
| const prefs = getTTSPrefs(); // <- read from your popup/menu/localStorage | |
| const chunks = buildTranscriptChunks(text); | |
| try { | |
| if (prefs.engine === 'server') { | |
| // if you have server engine wired per language/voice | |
| await speakChunksServer(chunks, prefs); | |
| } else { | |
| await speakChunksBrowser(chunks, prefs); | |
| } | |
| } catch (e) { | |
| console.warn('TTS failed, trying browser fallback…', e); | |
| try { await speakChunksBrowser(chunks, prefs); } catch {} | |
| } | |
| } | |
| // // ---- Transcript TTS (male-ish, slow, resonant) ---- | |
| // let ttsUtterances = []; | |
| // let ttsPaused = false; | |
| // function buildTranscriptChunks(text){ | |
| // // Split on Sanskrit danda, punctuation, comma, and pipe with spaces optionally after | |
| // const parts = text.split(/[।॥.!?,|]\s*/).filter(Boolean); | |
| // // Merge into ~180-220 char chunks for natural pacing | |
| // const chunks = []; | |
| // let buffer = ""; | |
| // for (const p of parts){ | |
| // if ((buffer + " " + p).length > 200){ | |
| // if (buffer) chunks.push(buffer.trim()); | |
| // buffer = p; | |
| // } else buffer = (buffer ? buffer + " " : "") + p; | |
| // } | |
| // if (buffer) chunks.push(buffer.trim()); | |
| // return chunks; | |
| // } | |
| // function playTranscriptTTS(){ | |
| // const text = document.getElementById('transcriptContent').textContent.trim(); | |
| // if (!text){ alert("No transcript to play."); return; } | |
| // stopTranscriptTTS(true); // clear any existing | |
| // const rate = parseFloat(document.getElementById('ttsRate').value || "0.85"); | |
| // const pitch = parseFloat(document.getElementById('ttsPitch').value || "0.8"); | |
| // const voice = pickVoice(); | |
| // const chunks = buildTranscriptChunks(text); | |
| // ttsUtterances = chunks.map(chunk=>{ | |
| // const u = new SpeechSynthesisUtterance(chunk); | |
| // u.voice = voice; | |
| // u.lang = (voice?.lang || "hi-IN"); | |
| // u.rate = rate; // slower | |
| // u.pitch = pitch; // slightly deeper | |
| // u.volume = 1.0; // loud | |
| // // add small pause between chunks | |
| // u.onend = ()=>{ if (ttsUtterances.length) setTimeout(()=>{}, 250); }; | |
| // return u; | |
| // }); | |
| // // Some browsers need async voice load | |
| // const startSpeaking = () => { | |
| // // Chain play | |
| // function speakNext(){ | |
| // if (!ttsUtterances.length) return; | |
| // const u = ttsUtterances.shift(); | |
| // u.onend = ()=>{ if (!ttsPaused) speakNext(); }; | |
| // try { speechSynthesis.speak(u); } catch {} | |
| // } | |
| // speakNext(); | |
| // }; | |
| // if (speechSynthesis.getVoices().length === 0) { | |
| // speechSynthesis.onvoiceschanged = () => startSpeaking(); | |
| // } else { | |
| // startSpeaking(); | |
| // } | |
| // } | |
| // function pauseTranscriptTTS(){ try{ speechSynthesis.pause(); ttsPaused = true; }catch{} } | |
| // function resumeTranscriptTTS(){ try{ speechSynthesis.resume(); ttsPaused = false; }catch{} } | |
| // function stopTranscriptTTS(silent=false){ | |
| // try{ speechSynthesis.cancel(); }catch{} | |
| // ttsPaused = false; ttsUtterances = []; | |
| // if (!silent) { /* noop */ } | |
| // } | |
| // Logout helper (if you ever need) | |
| function logout(){ localStorage.removeItem("authToken"); window.location.href = "login.html"; } | |