/** * TTS Factory Module * Creates and manages TTS handler instances */ import { BaseModule } from './base-module.js'; import { moduleRegistry } from './module-registry.js'; import { BrowserTTSHandler } from './browser-tts-handler.js'; import { ApiTTSHandler } from './api-tts-handler.js'; import { KokoroHandler } from './kokoro-handler.js'; class TTSFactoryModule extends BaseModule { /** * Create a new TTS factory */ constructor() { super('tts-factory', 'TTS Factory'); // Available TTS handlers this.handlers = {}; // Current active handler this.activeHandler = null; // Handler initialization status this.initStatus = { browser: false, api: false, kokoro: false }; // TTS availability flag this.ttsAvailable = false; // Bind methods this.bindMethods([ 'registerHandler', 'initializeHandler', 'getHandler', 'setActiveHandler', 'getActiveHandler', 'getAvailableHandlers', 'speak', 'stop', 'pause', 'resume', 'getVoices', 'getPreference' ]); // Add dependencies this.dependencies = ['persistence-manager', 'localization']; } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { this.reportProgress(10, "Initializing TTS factory"); // Get dependencies const persistenceManager = this.getModule('persistence-manager'); const localization = this.getModule('localization'); if (!persistenceManager || !localization) { console.error("TTS Factory: Required dependencies not found"); this.reportProgress(100, "TTS factory failed - missing dependencies"); return false; } // Register available handlers this.registerHandler('browser', new BrowserTTSHandler()); this.registerHandler('api', new ApiTTSHandler()); this.registerHandler('kokoro', new KokoroHandler()); this.reportProgress(30, "Registered TTS handlers"); // Get user preferences const ttsEnabled = this.getPreference('tts', 'enabled', false); const preferredProvider = this.getPreference('tts', 'provider', 'browser'); // Initialize handlers based on preferences let initSuccess = false; if (ttsEnabled) { // Try to initialize preferred handler first this.reportProgress(50, `Initializing preferred TTS handler: ${preferredProvider}`); initSuccess = await this.initializeHandler(preferredProvider); if (initSuccess) { this.setActiveHandler(preferredProvider); } else { // If preferred handler failed, try alternatives based on priority: Kokoro -> Browser -> None console.warn(`Failed to initialize preferred TTS handler: ${preferredProvider}, trying alternatives`); // Try Kokoro TTS as fallback if not already tried if (preferredProvider !== 'kokoro') { this.reportProgress(60, "Trying Kokoro TTS as fallback"); initSuccess = await this.initializeHandler('kokoro'); if (initSuccess) { this.setActiveHandler('kokoro'); // Update preference to Kokoro since it worked this.getModule('persistence-manager').updatePreference('tts', 'provider', 'kokoro'); } } // If Kokoro TTS failed, try Browser TTS if (!initSuccess && preferredProvider !== 'browser') { this.reportProgress(70, "Trying Browser TTS as fallback"); initSuccess = await this.initializeHandler('browser'); if (initSuccess) { this.setActiveHandler('browser'); // Update preference to browser since it worked this.getModule('persistence-manager').updatePreference('tts', 'provider', 'browser'); } } // Note: API TTS is not used as a fallback as it requires manual configuration } } else { // Even if TTS is disabled, initialize handlers in the background // so they're ready if the user enables TTS later this.reportProgress(50, "TTS disabled, initializing handlers in background"); // Initialize Kokoro and Browser handlers in parallel (not API as it requires configuration) const initPromises = [ this.initializeHandler('kokoro'), this.initializeHandler('browser') ]; // Wait for all handlers to initialize await Promise.allSettled(initPromises); // Check if any handler initialized successfully initSuccess = this.initStatus.kokoro || this.initStatus.browser; } // Set TTS availability flag and dispatch event this.ttsAvailable = initSuccess; // Dispatch event to notify UI about TTS availability document.dispatchEvent(new CustomEvent('tts:availability', { detail: { available: this.ttsAvailable } })); this.reportProgress(100, initSuccess ? "TTS factory ready" : "TTS factory ready (no handlers available)"); // Always return true since TTS is optional for the application return true; } catch (error) { console.error("Error initializing TTS factory:", error); this.reportProgress(100, "TTS factory failed"); // Set TTS availability to false and dispatch event this.ttsAvailable = false; document.dispatchEvent(new CustomEvent('tts:availability', { detail: { available: false } })); // Still return true since TTS is optional return true; } } /** * Register a TTS handler * @param {string} id - Handler ID * @param {Object} handler - TTS handler instance */ registerHandler(id, handler) { if (!id || !handler) return; this.handlers[id] = handler; } /** * Initialize a specific TTS handler * @param {string} id - Handler ID * @returns {Promise} - Success status */ async initializeHandler(id) { if (!id || !this.handlers[id]) { console.error(`TTS Factory: Handler '${id}' not found`); return false; } try { this.reportProgress(0, `Initializing ${id} TTS handler`); // Initialize the handler const success = await this.handlers[id].initialize( (progress, message) => { this.reportProgress(progress, message); } ); // Update initialization status this.initStatus[id] = success; if (success) { console.log(`TTS Factory: Successfully initialized ${id} TTS handler`); } else { console.error(`TTS Factory: Failed to initialize ${id} TTS handler`); } return success; } catch (error) { console.error(`TTS Factory: Error initializing ${id} TTS handler:`, error); this.initStatus[id] = false; return false; } } /** * Get a TTS handler by ID * @param {string} id - Handler ID * @returns {Object|null} - TTS handler instance or null if not found */ getHandler(id) { if (!id || !this.handlers[id]) return null; return this.handlers[id]; } /** * Set the active TTS handler * @param {string} id - Handler ID * @returns {boolean} - Success status */ setActiveHandler(id) { if (!id || !this.handlers[id] || !this.initStatus[id]) { console.warn(`Cannot set active handler to ${id}: handler not found or not initialized`); return false; } // Stop current handler if active if (this.activeHandler) { this.handlers[this.activeHandler].stop(); } // Set new active handler this.activeHandler = id; // Update preference this.getModule('persistence-manager').updatePreference('tts', 'provider', id); // Dispatch event this.dispatchEvent('tts-handler-changed', { handler: id }); return true; } /** * Get the active TTS handler * @returns {Object|null} - Active TTS handler instance or null if none active */ getActiveHandler() { if (!this.activeHandler) return null; return this.handlers[this.activeHandler]; } /** * Get all available TTS handlers * @returns {Object} - Map of handler IDs to initialization status */ getAvailableHandlers() { const available = {}; for (const id in this.handlers) { available[id] = this.initStatus[id]; } return available; } /** * Speak text using the active TTS handler * @param {string} text - Text to speak * @param {Object} options - TTS options * @returns {Promise} - Success status */ async speak(text, options = {}) { if (!this.activeHandler) { console.warn("No active TTS handler"); return false; } try { return await this.handlers[this.activeHandler].speak(text, options); } catch (error) { console.error("Error speaking text:", error); return false; } } /** * Stop speaking * @returns {boolean} - Success status */ stop() { if (!this.activeHandler) return false; try { return this.handlers[this.activeHandler].stop(); } catch (error) { console.error("Error stopping TTS:", error); return false; } } /** * Pause speaking * @returns {boolean} - Success status */ pause() { if (!this.activeHandler) return false; try { return this.handlers[this.activeHandler].pause(); } catch (error) { console.error("Error pausing TTS:", error); return false; } } /** * Resume speaking * @returns {boolean} - Success status */ resume() { if (!this.activeHandler) return false; try { return this.handlers[this.activeHandler].resume(); } catch (error) { console.error("Error resuming TTS:", error); return false; } } /** * Get available voices for the active TTS handler * @returns {Array} - Array of voice objects */ getVoices() { if (!this.activeHandler) return []; try { return this.handlers[this.activeHandler].getVoices(); } catch (error) { console.error("Error getting voices:", error); return []; } } /** * Get a preference from the persistence manager * @param {string} category - Preference category * @param {string} key - Preference key * @param {*} defaultValue - Default value if preference doesn't exist * @returns {*} - Preference value */ getPreference(category, key, defaultValue) { const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { return persistenceManager.getPreference(category, key, defaultValue); } return defaultValue; } /** * Clean up when module is disposed */ dispose() { // Stop any active TTS if (this.activeHandler) { this.handlers[this.activeHandler].stop(); } // Dispose all handlers for (const id in this.handlers) { if (this.handlers[id].dispose) { this.handlers[id].dispose(); } } // Clear handlers this.handlers = {}; this.activeHandler = null; } } // Create the singleton instance const TTSFactory = new TTSFactoryModule(); // Register with the module registry moduleRegistry.register(TTSFactory); // Export the module export { TTSFactory };