/** * TTS Player Module for AI Interactive Fiction * Handles Text-to-Speech functionality with resource-aware loading and progress reporting */ import { BaseModule, ModuleEvent } from './base-module.js'; import { moduleRegistry } from './module-registry.js'; class TTSPlayerModule extends BaseModule { constructor() { super('tts', 'Text-to-Speech'); this.ttsFactory = null; this.isInitialized = false; this.kokoroLoadingPromise = null; this.kokoroLoadingStarted = false; } /** * Load module dependencies * @returns {Promise} - Resolves when dependencies are loaded */ async loadDependencies() { try { // Import the TTS Factory module const { ttsFactory } = await import('./tts-factory.js'); this.ttsFactory = ttsFactory; this.reportProgress(20, "TTS Factory loaded"); // Set up event listeners window.addEventListener('tts-ready', this.handleTTSReadyEvent.bind(this)); // Create a Promise that resolves when Kokoro is loaded this.kokoroLoadingPromise = new Promise(resolve => { // Listen for when Kokoro starts loading window.addEventListener('kokoro-loading-started', () => { this.kokoroLoadingStarted = true; this.reportProgress(50, "Loading Kokoro TTS"); }); // Listen for when Kokoro completes loading window.addEventListener('kokoro-loading-complete', (event) => { // Check if loading was successful from the event details if (event.detail && event.detail.success === false) { this.reportProgress(95, "Kokoro TTS failed to load - using fallback"); console.warn("Kokoro failed to load:", event.detail?.error || "unknown error"); } else { this.reportProgress(95, "Kokoro TTS loaded"); } resolve(); }); }); return true; } catch (error) { console.error("Error loading TTS dependencies:", error); return false; } } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { // Initialize TTS Factory await this.ttsFactory.constructor.initializeInterface((percent, message) => { // Scale to 20-90% of our progress range const scaledPercent = 20 + (percent * 0.7); this.reportProgress(scaledPercent, message); }); // IMPORTANT: Always wait for Kokoro's loading promise to resolve this.reportProgress(90, "Waiting for Kokoro TTS to complete loading"); // Wait for the Kokoro loading promise to complete with a timeout try { // Add a timeout to prevent waiting forever const timeoutPromise = new Promise(resolve => setTimeout(() => { console.log("TTS Player: Kokoro loading timed out, continuing without Kokoro"); resolve(false); }, 10000)); // 10 second timeout // Race between normal completion and timeout await Promise.race([this.kokoroLoadingPromise, timeoutPromise]); this.reportProgress(95, "Kokoro TTS loading completed or timed out"); } catch (err) { console.warn("TTS Player: Error waiting for Kokoro:", err); this.reportProgress(95, "Error waiting for Kokoro, continuing anyway"); } this.isInitialized = true; // Final status check const ttsInfo = this.ttsFactory.getActiveTTSInfo(); if (ttsInfo.available) { this.reportProgress(100, `TTS Player initialized using ${ttsInfo.name}`); return true; } else { this.reportProgress(100, "TTS initialization complete but no voices available"); return true; // Still consider this a success, just with no voices } } catch (error) { console.error("Error initializing TTS Player:", error); this.reportProgress(100, "TTS initialization failed, continuing without TTS"); this.isInitialized = true; // Mark as initialized anyway to not block other modules return true; // Return true to not block the application } } /** * Handle TTS ready event from the factory * @param {CustomEvent} event - The TTS ready event */ handleTTSReadyEvent(event) { const { available, type } = event.detail; if (available && type) { this.reportProgress(95, `TTS system ready: ${type}`); } else { this.reportProgress(95, "No TTS system available"); } } // Public API methods /** * Get information about the active TTS system * @returns {Object} - TTS system info */ getTTSInfo() { if (!this.ttsFactory) return { available: false, type: 'none', name: 'None' }; return this.ttsFactory.getActiveTTSInfo(); } /** * Toggle TTS functionality on/off * @returns {boolean} - New TTS enabled state */ toggle() { if (!this.ttsFactory) return false; return this.ttsFactory.toggle(); } /** * Speak text using the active TTS system * @param {string} text - Text to speak * @param {Function} callback - Called when speech completes */ speak(text, callback) { if (!this.ttsFactory) { console.warn("TTS Factory not available for speak"); if (callback) callback("TTS not available"); return; } console.log(`TTS Player speaking: "${text}"`); this.ttsFactory.speak(text, (result) => { console.log("TTS Player speak complete", result); if (callback) callback(result); }); } /** * Stop any ongoing speech */ stop() { if (this.ttsFactory) { this.ttsFactory.stop(); } } /** * Set voice options for the active TTS system * @param {Object} options - Voice options */ setVoiceOptions(options) { if (this.ttsFactory) { this.ttsFactory.setVoiceOptions(options); } } /** * Set speech rate/speed * @param {number} speed - Speech rate (0.5-2.0) */ setSpeed(speed) { this.setVoiceOptions({ rate: speed }); } /** * Set the volume for speech * @param {number} volume - Volume level (0.0-1.0) */ setVolume(volume) { this.setVoiceOptions({ volume: volume }); } /** * Set the voice for speech * @param {string} voice - Voice identifier */ setVoice(voice) { this.setVoiceOptions({ voice: voice }); } /** * Switch to a specific TTS system * @param {string} type - The TTS system to use ('kokoro', 'browser', or 'api') * @returns {boolean} - Success status */ switchTTS(type) { if (!this.ttsFactory) return false; const result = this.ttsFactory.switchTTS(type); // If the switch was successful, refresh the voice list if (result) { // Notify listeners that the TTS system changed window.dispatchEvent(new CustomEvent('tts-system-changed', { detail: { type, info: this.getTTSInfo() } })); } return result; } /** * Get available TTS systems * @returns {Array} - Array of available TTS system IDs */ getAvailableSystems() { if (!this.ttsFactory) return []; const handlers = this.ttsFactory.getAvailableHandlers(); return Object.keys(handlers); } /** * Get available voices for the active TTS system * @returns {Promise} - Array of voice objects */ async getVoices() { if (!this.ttsFactory) return []; return this.ttsFactory.getVoices(); } /** * Is TTS enabled currently * @returns {boolean} - Whether TTS is enabled */ isEnabled() { if (!this.ttsFactory) return false; return this.ttsFactory.isEnabled(); } } // Create the singleton instance const TTSPlayer = new TTSPlayerModule(); // Register with the module registry moduleRegistry.register(TTSPlayer); // Export the module export { TTSPlayer }; // Keep a reference in window for loader system window.TTSPlayer = TTSPlayer;