358 lines
12 KiB
JavaScript
358 lines
12 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 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());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Speak text
|
|
* @param {string} text - Text to speak
|
|
* @param {Function} callback - Optional callback for when speech completes
|
|
* @returns {boolean} - True if speech started successfully
|
|
*/
|
|
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;
|
|
}
|
|
|
|
// 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 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 };
|