/** * TTS Player Module * Manages TTS functionality and interacts with available TTS handlers */ import { BaseModule } from './base-module.js'; import { moduleRegistry } from './module-registry.js'; class TTSPlayerModule extends BaseModule { constructor() { super('tts-player', 'TTS Player'); // Module dependencies this.dependencies = ['tts-factory']; // TTS state this.enabled = true; this.currentSpeech = null; this.pendingCallback = null; // Preloading mechanism this.preloadQueue = []; this.preloadedAudio = new Map(); // Cache for preloaded TTS this.isPreloading = false; // Bind methods using parent's bindMethods utility this.bindMethods([ 'speak', 'preloadSpeech', 'processPreloadQueue', 'stop', 'enable', 'isEnabled', 'isSpeaking', 'setVoice', 'setSpeed', 'getVoices', 'toggle' ]); } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { this.reportProgress(20, "Initializing TTS Player"); // Get TTS Factory dependency const ttsFactory = this.getModule('tts-factory'); if (!ttsFactory) { console.error("TTS Player: TTS Factory dependency not found"); this.reportProgress(100, "TTS Player failed - missing dependencies"); return false; } // Check TTS availability from TTS Factory this.enabled = ttsFactory.ttsAvailable && ttsFactory.getPreference('tts', 'enabled', false); // Set up event listeners this.addEventListener(document, 'tts:enabled', (event) => { if (event.detail) { this.enabled = event.detail.enabled; console.log(`TTS Player: TTS ${this.enabled ? 'enabled' : 'disabled'}`); } }); // Listen for TTS availability changes this.addEventListener(document, 'tts:availability', (event) => { if (event.detail) { const available = event.detail.available; console.log(`TTS Player: TTS availability changed to ${available ? 'available' : 'unavailable'}`); // If TTS becomes unavailable, disable it if (!available) { this.enabled = false; // Notify UI that TTS is disabled document.dispatchEvent(new CustomEvent('tts:stateChange', { detail: { enabled: false, available: false } })); } } }); // Listen for TTS toggle events from UI - support both event names this.addEventListener(document, 'tts:toggle', () => { this.toggle(); // Dispatch state change event for UI to update document.dispatchEvent(new CustomEvent('tts:stateChange', { detail: { enabled: this.enabled, available: ttsFactory.ttsAvailable } })); }); // Also listen for ui:tts:toggle events (from the main UI) this.addEventListener(document, 'ui:tts:toggle', (event) => { // If we have explicit enabled value, use it instead of toggling if (event.detail && typeof event.detail.enabled === 'boolean') { this.enabled = event.detail.enabled; } else { this.toggle(); } // Dispatch state change event for UI to update document.dispatchEvent(new CustomEvent('tts:stateChange', { detail: { enabled: this.enabled, available: ttsFactory.ttsAvailable } })); }); // Listen for sentence ready events to preload TTS this.addEventListener(document, 'buffer:sentence', (event) => { if (event.detail && event.detail.sentence && this.enabled) { // Add to preload queue this.preloadSpeech(event.detail.sentence); } }); // Dispatch initial state to UI document.dispatchEvent(new CustomEvent('tts:stateChange', { detail: { enabled: this.enabled, available: ttsFactory.ttsAvailable } })); this.reportProgress(100, "TTS Player ready"); return true; } catch (error) { console.error("Error initializing TTS Player:", error); return false; } } /** * Preload speech for a sentence * @param {string} text - Text to preload */ preloadSpeech(text) { if (!text || !this.enabled) return; // Don't preload if already in cache if (this.preloadedAudio.has(text)) return; // Add to preload queue this.preloadQueue.push(text); console.log(`TTS Player: Added to preload queue: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`); // Start processing the queue if not already processing if (!this.isPreloading) { this.processPreloadQueue(); } } /** * Process the preload queue */ async processPreloadQueue() { if (this.preloadQueue.length === 0 || this.isPreloading) return; this.isPreloading = true; const text = this.preloadQueue.shift(); try { // Get TTSFactory from module registry const ttsFactory = this.getModule('tts-factory'); if (!ttsFactory) { console.error("TTS Player: TTSFactory module not found in registry"); this.isPreloading = false; return; } // Only preload if we're not currently speaking or the text is different from current speech if (!this.isSpeaking() || (this.currentSpeech && this.currentSpeech !== text)) { console.log(`TTS Player: Preloading speech for: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`); // Use the preload method of the TTS factory const preloadData = await ttsFactory.preloadSpeech(text); if (preloadData) { this.preloadedAudio.set(text, preloadData); console.log(`TTS Player: Successfully preloaded speech for: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`); } else { console.warn(`TTS Player: Failed to preload speech for: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`); } } } catch (error) { console.warn("TTS Player: Error preloading speech:", error); } finally { this.isPreloading = false; // Process next in queue if available if (this.preloadQueue.length > 0) { // Use requestAnimationFrame to prevent blocking requestAnimationFrame(() => this.processPreloadQueue()); } } } /** * Speak a sentence * @param {string} text - Text to speak * @param {Function} callback - Callback for when speech completes * @returns {boolean} - Success status */ speak(text, callback = null) { // Check if TTS is enabled if (!this.enabled) { if (callback) { setTimeout(() => callback({ success: false, reason: 'tts_disabled' }), 0); } return false; } // Get TTSFactory from module registry const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { this.pendingCallback = callback; this.currentSpeech = text; // Check if this text was preloaded const preloadedData = this.preloadedAudio.get(text); if (preloadedData) { console.log("TTS Player: Using preloaded speech"); this.preloadedAudio.delete(text); // Remove from cache after use // Use the preloaded speech data ttsFactory.speakPreloaded(preloadedData, (result) => { // Store the completed result this.currentSpeech = null; // Call the callback if provided if (this.pendingCallback) { this.pendingCallback(result); this.pendingCallback = null; } // Process next in preload queue if any if (this.preloadQueue.length > 0 && !this.isPreloading) { this.processPreloadQueue(); } }); } else { // Start TTS with regular speech if not preloaded ttsFactory.speak(text, (result) => { // Store the completed result this.currentSpeech = null; // Call the callback if provided if (this.pendingCallback) { this.pendingCallback(result); this.pendingCallback = null; } // Process next in preload queue if any if (this.preloadQueue.length > 0 && !this.isPreloading) { this.processPreloadQueue(); } }); } return true; } else { console.error("TTS Player: TTSFactory module not found in registry"); if (callback) { setTimeout(() => callback({ success: false, reason: 'no_tts_factory' }), 0); } return false; } } /** * Stop speaking */ stop() { const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { ttsFactory.stop(); } this.currentSpeech = null; this.pendingCallback = null; } /** * Toggle TTS enabled state */ toggle() { this.enabled = !this.enabled; this.enable(this.enabled); return this.enabled; } /** * Enable or disable TTS * @param {boolean} enabled - Whether TTS should be enabled */ enable(enabled) { this.enabled = enabled; console.log(`TTS Player: ${this.enabled ? 'Enabled' : 'Disabled'}`); // Save preference if persistence manager is available const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('tts', 'enabled', this.enabled); } } /** * Check if TTS is enabled * @returns {boolean} - Whether TTS is enabled */ isEnabled() { return this.enabled; } /** * Check if TTS is currently speaking * @returns {boolean} - Whether TTS is speaking */ isSpeaking() { const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { return ttsFactory.isSpeaking(); } return false; } /** * Set the voice to use * @param {string} voice - Voice identifier */ setVoice(voice) { const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { ttsFactory.configure({ voice }); } } /** * Set the speech rate/speed * @param {number} speed - Speech rate (0.5-2.0) */ setSpeed(speed) { const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { ttsFactory.configure({ speed }); } } /** * Get available voices * @returns {Promise} - Resolves with array of voice objects */ async getVoices() { const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { return ttsFactory.getVoices(); } return []; } } // Create the singleton instance const TTSPlayer = new TTSPlayerModule(); // Register with the module registry moduleRegistry.register(TTSPlayer); // Export the module export { TTSPlayer };