Fix TTS module initialization and dependency issues. Update module IDs for consistency, improve circular dependency detection, and fix UI Controller event handling.
This commit is contained in:
+271
-198
@@ -1,59 +1,41 @@
|
||||
/**
|
||||
* TTS Player Module for AI Interactive Fiction
|
||||
* Handles Text-to-Speech functionality with resource-aware loading and progress reporting
|
||||
* TTS Player Module
|
||||
* Manages TTS functionality and interacts with available TTS handlers
|
||||
*/
|
||||
import { BaseModule, ModuleEvent } from './base-module.js';
|
||||
import { BaseModule } 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;
|
||||
}
|
||||
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'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,197 +44,291 @@ class TTSPlayerModule extends BaseModule {
|
||||
*/
|
||||
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);
|
||||
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'}`);
|
||||
}
|
||||
});
|
||||
|
||||
// IMPORTANT: Always wait for Kokoro's loading promise to resolve
|
||||
this.reportProgress(90, "Waiting for Kokoro TTS to complete loading");
|
||||
// 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 }
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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");
|
||||
}
|
||||
// Listen for TTS toggle events from UI
|
||||
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 }
|
||||
}));
|
||||
});
|
||||
|
||||
this.isInitialized = true;
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
}
|
||||
// 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);
|
||||
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
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle TTS ready event from the factory
|
||||
* @param {CustomEvent} event - The TTS ready event
|
||||
* Preload speech for a sentence
|
||||
* @param {string} text - Text to preload
|
||||
*/
|
||||
handleTTSReadyEvent(event) {
|
||||
const { available, type } = event.detail;
|
||||
preloadSpeech(text) {
|
||||
if (!text || !this.enabled) return;
|
||||
|
||||
if (available && type) {
|
||||
this.reportProgress(95, `TTS system ready: ${type}`);
|
||||
} else {
|
||||
this.reportProgress(95, "No TTS system available");
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
// Public API methods
|
||||
|
||||
/**
|
||||
* Get information about the active TTS system
|
||||
* @returns {Object} - TTS system info
|
||||
* Process the preload queue
|
||||
*/
|
||||
getTTSInfo() {
|
||||
if (!this.ttsFactory) return { available: false, type: 'none', name: 'None' };
|
||||
return this.ttsFactory.getActiveTTSInfo();
|
||||
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 if available
|
||||
if (typeof ttsFactory.preloadSpeech === 'function') {
|
||||
await ttsFactory.preloadSpeech(text);
|
||||
this.preloadedAudio.set(text, true);
|
||||
} else {
|
||||
// Fallback: use normal speak method with a dummy callback
|
||||
ttsFactory.speak(text, () => {
|
||||
ttsFactory.stop(); // Stop immediately after generation
|
||||
this.preloadedAudio.set(text, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Speak text
|
||||
* @param {string} text - Text to speak
|
||||
* @param {Function} callback - Called when speech completes
|
||||
* @param {Function} callback - Optional callback for when speech completes
|
||||
* @returns {boolean} - True if speech started successfully
|
||||
*/
|
||||
speak(text, callback) {
|
||||
if (!this.ttsFactory) {
|
||||
console.warn("TTS Factory not available for speak");
|
||||
if (callback) callback("TTS not available");
|
||||
return;
|
||||
speak(text, callback = null) {
|
||||
if (!text) return false;
|
||||
|
||||
console.log(`TTS Player: Speaking "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`, this.enabled ? "(TTS enabled)" : "(TTS disabled)");
|
||||
|
||||
// Store the current speech text
|
||||
this.currentSpeech = text;
|
||||
|
||||
if (!this.enabled) {
|
||||
console.log("TTS Player: TTS is disabled, not speaking");
|
||||
if (callback) {
|
||||
setTimeout(() => callback({ success: false, reason: 'tts_disabled' }), 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`TTS Player speaking: "${text}"`);
|
||||
this.ttsFactory.speak(text, (result) => {
|
||||
console.log("TTS Player speak complete", result);
|
||||
if (callback) callback(result);
|
||||
});
|
||||
// Get TTSFactory from module registry
|
||||
const ttsFactory = this.getModule('tts-factory');
|
||||
if (ttsFactory) {
|
||||
this.pendingCallback = callback;
|
||||
|
||||
// Check if this text was preloaded
|
||||
const wasPreloaded = this.preloadedAudio.has(text);
|
||||
if (wasPreloaded) {
|
||||
console.log("TTS Player: Using preloaded speech");
|
||||
this.preloadedAudio.delete(text); // Remove from cache after use
|
||||
}
|
||||
|
||||
// Start TTS with minimal delay to synchronize with text rendering
|
||||
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 any ongoing speech
|
||||
* Stop speaking
|
||||
*/
|
||||
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()
|
||||
}
|
||||
}));
|
||||
const ttsFactory = this.getModule('tts-factory');
|
||||
if (ttsFactory) {
|
||||
ttsFactory.stop();
|
||||
}
|
||||
|
||||
return result;
|
||||
this.currentSpeech = null;
|
||||
this.pendingCallback = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available TTS systems
|
||||
* @returns {Array<string>} - Array of available TTS system IDs
|
||||
* Toggle TTS enabled state
|
||||
*/
|
||||
getAvailableSystems() {
|
||||
if (!this.ttsFactory) return [];
|
||||
const handlers = this.ttsFactory.getAvailableHandlers();
|
||||
return Object.keys(handlers);
|
||||
toggle() {
|
||||
this.enabled = !this.enabled;
|
||||
this.enable(this.enabled);
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available voices for the active TTS system
|
||||
* @returns {Promise<Array>} - Array of voice objects
|
||||
* Enable or disable TTS
|
||||
* @param {boolean} enabled - Whether TTS should be enabled
|
||||
*/
|
||||
async getVoices() {
|
||||
if (!this.ttsFactory) return [];
|
||||
return this.ttsFactory.getVoices();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is TTS enabled currently
|
||||
* Check if TTS is enabled
|
||||
* @returns {boolean} - Whether TTS is enabled
|
||||
*/
|
||||
isEnabled() {
|
||||
if (!this.ttsFactory) return false;
|
||||
return this.ttsFactory.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<Array>} - Resolves with array of voice objects
|
||||
*/
|
||||
async getVoices() {
|
||||
const ttsFactory = this.getModule('tts-factory');
|
||||
if (ttsFactory) {
|
||||
return ttsFactory.getVoices();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +340,3 @@ moduleRegistry.register(TTSPlayer);
|
||||
|
||||
// Export the module
|
||||
export { TTSPlayer };
|
||||
|
||||
// Keep a reference in window for loader system
|
||||
window.TTSPlayer = TTSPlayer;
|
||||
|
||||
Reference in New Issue
Block a user