Files
ai.interactive.fiction/public/js/tts-player.js

366 lines
13 KiB
JavaScript

/**
* TTS Player Module
* Manages TTS functionality and interacts with available TTS handlers
*/
import { BaseModule } from './base-module.js';
import { moduleRegistry } from './module-registry.js';
class TTSPlayerModule extends BaseModule {
constructor() {
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'
]);
}
/**
* Initialize the module
* @returns {Promise<boolean>} - Resolves with success status
*/
async initialize() {
try {
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'}`);
}
});
// 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 }
}));
}
}
});
// Listen for TTS toggle events from UI - support both event names
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 }
}));
});
// Also listen for ui:tts:toggle events (from the main UI)
this.addEventListener(document, 'ui:tts:toggle', (event) => {
// If we have explicit enabled value, use it instead of toggling
if (event.detail && typeof event.detail.enabled === 'boolean') {
this.enabled = event.detail.enabled;
} else {
this.toggle();
}
// Dispatch state change event for UI to update
document.dispatchEvent(new CustomEvent('tts:stateChange', {
detail: { enabled: this.enabled, available: ttsFactory.ttsAvailable }
}));
});
// 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);
}
});
// 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);
return false;
}
}
/**
* Preload speech for a sentence
* @param {string} text - Text to preload
*/
preloadSpeech(text) {
if (!text || !this.enabled) return;
// 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();
}
}
/**
* Process the preload queue
*/
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
const preloadData = await ttsFactory.preloadSpeech(text);
if (preloadData) {
this.preloadedAudio.set(text, preloadData);
console.log(`TTS Player: Successfully preloaded speech for: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`);
} else {
console.warn(`TTS Player: Failed to preload speech for: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`);
}
}
} 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());
}
}
}
/**
* Speak a sentence
* @param {string} text - Text to speak
* @param {Function} callback - Callback for when speech completes
* @returns {boolean} - Success status
*/
speak(text, callback = null) {
// Check if TTS is enabled
if (!this.enabled) {
if (callback) {
setTimeout(() => callback({ success: false, reason: 'tts_disabled' }), 0);
}
return false;
}
// Get TTSFactory from module registry
const ttsFactory = this.getModule('tts-factory');
if (ttsFactory) {
this.pendingCallback = callback;
this.currentSpeech = text;
// Check if this text was preloaded
const preloadedData = this.preloadedAudio.get(text);
if (preloadedData) {
console.log("TTS Player: Using preloaded speech");
this.preloadedAudio.delete(text); // Remove from cache after use
// Use the preloaded speech data
ttsFactory.speakPreloaded(preloadedData, (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();
}
});
} else {
// Start TTS with regular speech if not preloaded
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 speaking
*/
stop() {
const ttsFactory = this.getModule('tts-factory');
if (ttsFactory) {
ttsFactory.stop();
}
this.currentSpeech = null;
this.pendingCallback = null;
}
/**
* Toggle TTS enabled state
*/
toggle() {
this.enabled = !this.enabled;
this.enable(this.enabled);
return this.enabled;
}
/**
* Enable or disable TTS
* @param {boolean} enabled - Whether TTS should be enabled
*/
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);
}
}
/**
* Check if TTS is enabled
* @returns {boolean} - Whether TTS is enabled
*/
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 [];
}
}
// Create the singleton instance
const TTSPlayer = new TTSPlayerModule();
// Register with the module registry
moduleRegistry.register(TTSPlayer);
// Export the module
export { TTSPlayer };