// === SETTINGS MANAGER FOR TTS AND DEBUG === const SettingsManager = { // TTS Settings ttsSettings: { rate: 0.8, volume: 1.0, selectedVoice: null }, availableVoices: [], debugMessages: [], init() { this.loadSettings(); this.initTTSSettings(); this.setupEventListeners(); this.checkBrowserSupport(); this.loadVoices(); this.updateBrowserInfo(); }, // === SETTINGS PERSISTENCE === loadSettings() { const saved = localStorage.getItem('tts-settings'); if (saved) { try { this.ttsSettings = { ...this.ttsSettings, ...JSON.parse(saved) }; } catch (e) { console.warn('Failed to load TTS settings:', e); } } }, saveSettings() { localStorage.setItem('tts-settings', JSON.stringify(this.ttsSettings)); }, // === TTS SETTINGS INITIALIZATION === initTTSSettings() { const rateSlider = document.getElementById('tts-rate'); const volumeSlider = document.getElementById('tts-volume'); const voiceSelect = document.getElementById('tts-voice'); if (rateSlider) { rateSlider.value = this.ttsSettings.rate; document.getElementById('tts-rate-value').textContent = this.ttsSettings.rate; } if (volumeSlider) { volumeSlider.value = this.ttsSettings.volume; document.getElementById('tts-volume-value').textContent = this.ttsSettings.volume; } }, setupEventListeners() { // Rate slider const rateSlider = document.getElementById('tts-rate'); if (rateSlider) { rateSlider.addEventListener('input', (e) => { this.ttsSettings.rate = parseFloat(e.target.value); document.getElementById('tts-rate-value').textContent = this.ttsSettings.rate; this.saveSettings(); }); } // Volume slider const volumeSlider = document.getElementById('tts-volume'); if (volumeSlider) { volumeSlider.addEventListener('input', (e) => { this.ttsSettings.volume = parseFloat(e.target.value); document.getElementById('tts-volume-value').textContent = this.ttsSettings.volume; this.saveSettings(); }); } // Voice selection const voiceSelect = document.getElementById('tts-voice'); if (voiceSelect) { voiceSelect.addEventListener('change', (e) => { this.ttsSettings.selectedVoice = e.target.value; this.saveSettings(); }); } }, // === BROWSER SUPPORT CHECK === checkBrowserSupport() { const checks = [ { name: 'speechSynthesis', available: 'speechSynthesis' in window }, { name: 'SpeechSynthesisUtterance', available: 'SpeechSynthesisUtterance' in window }, { name: 'getVoices', available: speechSynthesis && typeof speechSynthesis.getVoices === 'function' }, { name: 'speak', available: speechSynthesis && typeof speechSynthesis.speak === 'function' } ]; const support = checks.every(check => check.available); const supportElement = document.getElementById('browser-support'); if (supportElement) { supportElement.textContent = support ? '✅ Full Support' : '❌ Limited Support'; supportElement.style.color = support ? '#22C55E' : '#EF4444'; } return support; }, // === VOICE MANAGEMENT === loadVoices() { const loadVoicesImpl = () => { this.availableVoices = speechSynthesis.getVoices(); this.updateVoiceInfo(); this.populateVoiceSelect(); this.displayVoiceList(); }; // Try immediately loadVoicesImpl(); // Also try after a delay (some browsers load voices asynchronously) setTimeout(loadVoicesImpl, 100); // Listen for voice changes if (speechSynthesis.onvoiceschanged !== undefined) { speechSynthesis.onvoiceschanged = loadVoicesImpl; } }, updateVoiceInfo() { const voiceCountElement = document.getElementById('voice-count'); const englishVoiceCountElement = document.getElementById('english-voice-count'); if (voiceCountElement) { voiceCountElement.textContent = this.availableVoices.length; } const englishVoices = this.availableVoices.filter(voice => voice.lang.startsWith('en')); if (englishVoiceCountElement) { englishVoiceCountElement.textContent = englishVoices.length; } }, populateVoiceSelect() { const voiceSelect = document.getElementById('tts-voice'); if (!voiceSelect) return; // Clear existing options except the first one voiceSelect.innerHTML = ''; const englishVoices = this.availableVoices.filter(voice => voice.lang.startsWith('en')); englishVoices.forEach(voice => { const option = document.createElement('option'); option.value = voice.name; option.textContent = `${voice.name} (${voice.lang})`; if (voice.name === this.ttsSettings.selectedVoice) { option.selected = true; } voiceSelect.appendChild(option); }); }, displayVoiceList() { const voiceListElement = document.getElementById('voice-list'); if (!voiceListElement) return; if (this.availableVoices.length === 0) { voiceListElement.innerHTML = '
No voices available
'; return; } voiceListElement.innerHTML = ''; this.availableVoices.forEach(voice => { const voiceItem = document.createElement('div'); voiceItem.className = 'voice-item'; voiceItem.innerHTML = `
${voice.name}
${voice.lang}
${voice.localService ? 'Local' : 'Remote'}
`; voiceItem.addEventListener('click', () => { // Test this voice this.testVoice(voice); // Update selection visually document.querySelectorAll('.voice-item').forEach(item => item.classList.remove('selected')); voiceItem.classList.add('selected'); }); voiceListElement.appendChild(voiceItem); }); }, // === TTS TESTING FUNCTIONS === testVoice(voice) { try { const utterance = new SpeechSynthesisUtterance('Hello, this is a voice test'); utterance.voice = voice; utterance.rate = this.ttsSettings.rate; utterance.volume = this.ttsSettings.volume; utterance.onstart = () => { this.addDebugMessage(`Testing voice: ${voice.name}`, 'info'); }; utterance.onerror = (event) => { this.addDebugMessage(`Voice test error: ${event.error}`, 'error'); }; speechSynthesis.speak(utterance); } catch (error) { this.addDebugMessage(`Voice test failed: ${error.message}`, 'error'); } }, // === DEBUG FUNCTIONS === addDebugMessage(message, type = 'info') { const timestamp = new Date().toLocaleTimeString(); const logEntry = `[${timestamp}] ${message}`; this.debugMessages.push({ message: logEntry, type }); this.updateDebugDisplay(); // Also log to console console.log(`[Settings] ${logEntry}`); }, updateDebugDisplay() { const debugLogElement = document.getElementById('debug-log'); if (!debugLogElement) return; const lastEntries = this.debugMessages.slice(-50); // Keep last 50 entries debugLogElement.innerHTML = lastEntries .map(entry => `${entry.message}`) .join('\n'); // Scroll to bottom debugLogElement.scrollTop = debugLogElement.scrollHeight; }, clearDebugLog() { this.debugMessages = []; this.updateDebugDisplay(); }, // === LANGUAGE DETECTION === detectContentLanguage() { // Try to get language from current content in Word Discovery or other games if (window.currentGameContent && window.currentGameContent.language) { this.addDebugMessage(`Auto-detected language: ${window.currentGameContent.language}`, 'info'); return window.currentGameContent.language; } // Fallback: try to detect from navigation context if (window.AppNavigation && window.AppNavigation.scannedContent) { const currentContentKey = window.AppNavigation.selectedContent; if (currentContentKey) { const contentInfo = window.AppNavigation.scannedContent.found.find( content => content.id === currentContentKey ); if (contentInfo && contentInfo.language) { this.addDebugMessage(`Language from navigation: ${contentInfo.language}`, 'info'); return contentInfo.language; } } } this.addDebugMessage('No content language detected, using en-US default', 'warning'); return 'en-US'; }, // === BROWSER INFO === updateBrowserInfo() { const userAgentElement = document.getElementById('user-agent'); const platformElement = document.getElementById('platform'); const languageElement = document.getElementById('browser-language'); if (userAgentElement) { userAgentElement.textContent = navigator.userAgent; } if (platformElement) { platformElement.textContent = navigator.platform; } if (languageElement) { languageElement.textContent = navigator.language; } }, // === PUBLIC TTS API === speak(text, options = {}) { return new Promise((resolve, reject) => { try { if (!('speechSynthesis' in window)) { reject(new Error('Speech synthesis not supported')); return; } const utterance = new SpeechSynthesisUtterance(text); // Apply settings utterance.rate = options.rate || this.ttsSettings.rate; utterance.volume = options.volume || this.ttsSettings.volume; // Auto-detect language from content if available const autoLang = this.detectContentLanguage() || 'en-US'; utterance.lang = options.lang || autoLang; // Apply selected voice if available if (this.ttsSettings.selectedVoice) { const selectedVoice = this.availableVoices.find( voice => voice.name === this.ttsSettings.selectedVoice ); if (selectedVoice) { utterance.voice = selectedVoice; } } utterance.onend = () => { this.addDebugMessage(`Spoke: "${text}"`, 'success'); resolve(); }; utterance.onerror = (event) => { this.addDebugMessage(`Speech error: ${event.error}`, 'error'); reject(new Error(event.error)); }; speechSynthesis.speak(utterance); } catch (error) { this.addDebugMessage(`Speech failed: ${error.message}`, 'error'); reject(error); } }); } }; // === GLOBAL TTS TEST FUNCTIONS === function testBasicTTS() { SettingsManager.addDebugMessage('Testing basic TTS...', 'info'); SettingsManager.speak('Hello world, this is a basic test') .then(() => SettingsManager.addDebugMessage('✅ Basic TTS test completed', 'success')) .catch(error => SettingsManager.addDebugMessage(`❌ Basic TTS test failed: ${error.message}`, 'error')); } function testWithCallbacks() { SettingsManager.addDebugMessage('Testing TTS with detailed callbacks...', 'info'); SettingsManager.speak('Apple, cat, house, car') .then(() => SettingsManager.addDebugMessage('✅ Callback TTS test completed', 'success')) .catch(error => SettingsManager.addDebugMessage(`❌ Callback TTS test failed: ${error.message}`, 'error')); } function testGameWords() { SettingsManager.addDebugMessage('Testing game vocabulary words...', 'info'); const words = ['apple', 'cat', 'house', 'car', 'tree', 'book', 'sun', 'dog']; let index = 0; function speakNext() { if (index >= words.length) { SettingsManager.addDebugMessage('✅ Game words test completed', 'success'); return; } const word = words[index++]; SettingsManager.speak(word) .then(() => { SettingsManager.addDebugMessage(`✅ Spoke: ${word}`, 'info'); setTimeout(speakNext, 500); }) .catch(error => { SettingsManager.addDebugMessage(`❌ Failed to speak ${word}: ${error.message}`, 'error'); setTimeout(speakNext, 500); }); } speakNext(); } function refreshVoices() { SettingsManager.addDebugMessage('Refreshing voice list...', 'info'); SettingsManager.loadVoices(); SettingsManager.addDebugMessage('✅ Voice list refreshed', 'success'); } function clearDebugLog() { SettingsManager.clearDebugLog(); } // Initialize when page loads if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => SettingsManager.init()); } else { SettingsManager.init(); } // Export for use in games window.SettingsManager = SettingsManager;