270 lines
8.8 KiB
JavaScript
270 lines
8.8 KiB
JavaScript
/**
|
|
* 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<boolean>} - 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<string>} - 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>} - 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;
|