/** * TTS Factory for AI Interactive Fiction * Attempts to use Kokoro TTS first, then falls back to browser TTS if needed */ class TTSFactory { constructor() { this.activeTTSHandler = null; this.kokoroHandler = null; this.browserTTSHandler = null; this.initializationAttempted = false; this.usingKokoro = false; this.initializationPromise = null; // Promise for the factory initialization // Initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.initialize()); } else { // Use requestAnimationFrame to ensure scripts are parsed requestAnimationFrame(() => this.initialize()); } } /** * Initialize available TTS handlers */ async initialize() { // Prevent multiple initializations if (this.initializationAttempted) return this.initializationPromise; this.initializationAttempted = true; console.log('Initializing TTS Factory...'); this.initializationPromise = new Promise(async (resolve) => { let kokoroInitialized = false; // Try to initialize Kokoro first (preferred option) try { // Check if KokoroHandler class is defined (loaded via script tag) if (typeof KokoroHandler !== 'undefined') { console.log('Attempting to initialize Kokoro TTS...'); this.kokoroHandler = new KokoroHandler(); // --- Increase Timeout for Kokoro Initialization --- // Wait for KokoroHandler's internal initialization promise // Use Promise.race to add a longer timeout specifically for Kokoro init const kokoroTimeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Kokoro initialization timed out in factory')), 60000) // 60 seconds timeout ); try { kokoroInitialized = await Promise.race([ this.kokoroHandler.initializationPromise, kokoroTimeoutPromise ]); } catch (timeoutError) { console.error(timeoutError.message); // Log the timeout error kokoroInitialized = false; } // --- End Increase Timeout --- if (kokoroInitialized) { console.log('Kokoro Handler reported successful initialization.'); } else { console.warn('Kokoro Handler reported failed or timed out initialization.'); } } else { console.warn('KokoroHandler class not found when factory initialized.'); } } catch (error) { console.error('Error creating or initializing Kokoro Handler:', error); kokoroInitialized = false; // Ensure it's marked as failed } // Initialize browser TTS as fallback (can happen in parallel) try { if (typeof TTSHandler !== 'undefined') { console.log('Initializing browser TTS as fallback...'); this.browserTTSHandler = new TTSHandler(); } else { console.warn('TTSHandler class not found when factory initialized.'); } } catch (error) { console.error('Error initializing browser TTS:', error); } // Decide which handler to use based on Kokoro's success this.selectActiveHandler(kokoroInitialized); resolve(); // Resolve the factory's promise }); return this.initializationPromise; } // Removed waitForKokoroInitialization as KokoroHandler now manages its own promise /** * Select which TTS handler to use * @param {boolean} kokoroInitialized - Whether Kokoro initialization succeeded */ selectActiveHandler(kokoroInitialized) { // First choice: Kokoro if it's available and initialized successfully if (kokoroInitialized && this.kokoroHandler && this.kokoroHandler.kokoroReady) { console.log('Using Kokoro TTS as primary TTS system'); this.activeTTSHandler = this.kokoroHandler; this.usingKokoro = true; } // Fallback to browser TTS if available else if (this.browserTTSHandler) { console.log('Falling back to browser TTS.'); this.activeTTSHandler = this.browserTTSHandler; this.usingKokoro = false; } // No TTS available else { console.error('No TTS system available.'); this.activeTTSHandler = null; this.usingKokoro = false; } // Expose the active handler as the global ttsHandler window.ttsHandler = this.activeTTSHandler; // Log the active TTS system if (this.usingKokoro) { console.log('TTS Factory initialized with Kokoro TTS'); } else if (this.activeTTSHandler) { console.log('TTS Factory initialized with browser TTS'); } else { console.log('TTS Factory initialized with no available TTS'); } // Dispatch an event to notify the UI that TTS is ready (or not) const ttsReadyEvent = new CustomEvent('tts-ready', { detail: { available: !!this.activeTTSHandler, type: this.usingKokoro ? 'kokoro' : (this.activeTTSHandler ? 'browser' : 'none'), handler: this.activeTTSHandler } }); window.dispatchEvent(ttsReadyEvent); } /** * Get info about the active TTS system */ getActiveTTSInfo() { if (!this.activeTTSHandler) { return { available: false, type: 'none', name: 'None' }; } return { available: true, type: this.usingKokoro ? 'kokoro' : 'browser', name: this.usingKokoro ? 'Kokoro TTS' : 'Browser TTS' }; } /** * Force switching to a specific TTS system * @param {string} type - Either 'kokoro' or 'browser' */ switchTTS(type) { if (type === 'kokoro' && this.kokoroHandler && this.kokoroHandler.kokoroReady) { this.activeTTSHandler = this.kokoroHandler; this.usingKokoro = true; window.ttsHandler = this.activeTTSHandler; console.log('Switched to Kokoro TTS'); // Dispatch event on switch const ttsReadyEvent = new CustomEvent('tts-ready', { detail: { available: true, type: 'kokoro', handler: this.activeTTSHandler } }); window.dispatchEvent(ttsReadyEvent); return true; } else if (type === 'browser' && this.browserTTSHandler) { this.activeTTSHandler = this.browserTTSHandler; this.usingKokoro = false; window.ttsHandler = this.activeTTSHandler; console.log('Switched to browser TTS'); // Dispatch event on switch const ttsReadyEvent = new CustomEvent('tts-ready', { detail: { available: true, type: 'browser', handler: this.activeTTSHandler } }); window.dispatchEvent(ttsReadyEvent); return true; } console.error(`Failed to switch to ${type} TTS - not available`); return false; } } // Create the global TTS factory instance window.ttsFactory = new TTSFactory();