/** * SentenceQueueModule * Manages the preparation pipeline for sentences, including TTS generation */ import { BaseModule } from './base-module.js'; class SentenceQueueModule extends BaseModule { constructor() { super('sentence-queue', 'Sentence Queue'); // Dependencies this.dependencies = ['text-buffer', 'tts-factory', 'tts-player']; // Queue state this.sentenceQueue = []; this.isProcessing = false; this.onSentenceReadyCallback = null; // Bind methods this.bindMethods([ 'initialize', 'addSentence', 'processNextSentence', 'setOnSentenceReady', 'completeSentence' ]); } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { // Get dependencies const textBuffer = this.getModule('text-buffer'); if (!textBuffer) { console.error("SentenceQueue: TextBuffer dependency not found"); return false; } // Set up the text buffer to send sentences to this queue textBuffer.setOnSentenceReady((sentence, callback) => { this.addSentence(sentence, callback); }); this.reportProgress(100, "Sentence queue ready"); return true; } catch (error) { console.error("Error initializing Sentence Queue:", error); return false; } } /** * Set callback for when a sentence is ready for display * @param {Function} callback - Function to call with prepared sentence */ setOnSentenceReady(callback) { if (typeof callback === 'function') { this.onSentenceReadyCallback = callback; } } /** * Add a sentence to the queue * @param {string} sentence - Sentence to add * @param {Function} callback - Callback to call when sentence is processed */ addSentence(sentence, callback) { this.sentenceQueue.push({ text: sentence, callback: callback }); // Process the queue if not already processing if (!this.isProcessing) { this.processNextSentence(); } } /** * Process the next sentence in the queue */ async processNextSentence() { if (this.sentenceQueue.length === 0 || this.isProcessing) { return; } this.isProcessing = true; const item = this.sentenceQueue[0]; // Don't remove yet try { // Get TTS Factory const ttsFactory = this.getModule('tts-factory'); if (!ttsFactory) { console.error("SentenceQueue: TTSFactory dependency not found"); this.completeSentence(item, { success: false, reason: 'no_tts_factory' }); return; } // Create a speech metadata object const speechMetadata = await this.prepareSpeechMetadata(item.text); // If we have a callback for ready sentences, call it with the metadata if (this.onSentenceReadyCallback) { this.onSentenceReadyCallback(item.text, speechMetadata, () => { // Remove from queue and process next this.completeSentence(item, { success: true }); }); } else { // No callback, just complete this.completeSentence(item, { success: true }); } } catch (error) { console.error("Error processing sentence:", error); this.completeSentence(item, { success: false, reason: error.message }); } } /** * Prepare speech metadata for a sentence * @param {string} text - Text to prepare speech for * @returns {Promise} - Speech metadata object */ async prepareSpeechMetadata(text) { const ttsFactory = this.getModule('tts-factory'); const ttsPlayer = this.getModule('tts-player'); if (!ttsFactory || !ttsPlayer) { throw new Error("TTS dependencies not found"); } // Check if TTS is enabled const isTtsEnabled = ttsPlayer.isEnabled(); // If TTS is disabled, estimate duration based on character count if (!isTtsEnabled) { return this.estimateSpeechDuration(text); } try { // Preload the speech to get metadata const result = await ttsFactory.preloadSpeech(text); if (!result.success) { console.warn("SentenceQueue: Speech preload failed, using estimated duration"); return this.estimateSpeechDuration(text); } // Create a speech metadata object return { text: text, duration: result.duration || this.estimateSpeechDuration(text).duration, handler: ttsFactory.getActiveHandler() ? ttsFactory.getActiveHandler().id : null, play: async () => { return ttsFactory.speak(text); }, stop: () => { return ttsFactory.stop(); }, isTtsEnabled: isTtsEnabled }; } catch (error) { console.error("Error preparing speech metadata:", error); return this.estimateSpeechDuration(text); } } /** * Estimate speech duration based on character count * @param {string} text - Text to estimate duration for * @returns {Object} - Speech metadata object with estimated duration */ estimateSpeechDuration(text) { // Average reading speed is about 14-15 characters per second // We'll use a slightly slower rate for TTS const charactersPerSecond = 12; const ttsPlayer = this.getModule('tts-player'); // Get the current speed setting if available let speedMultiplier = 1.0; if (ttsPlayer) { const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { // Get the current speed setting (typically 0.5-2.0) const speed = ttsFactory.speed || 1.0; speedMultiplier = speed; } } // Calculate estimated duration in milliseconds const charCount = text.length; const durationSeconds = charCount / (charactersPerSecond * speedMultiplier); const durationMs = Math.max(durationSeconds * 1000, 500); // Minimum 500ms return { text: text, duration: durationMs, handler: null, play: async () => ({ success: false, reason: 'tts_disabled' }), stop: () => true, isTtsEnabled: false, isEstimated: true }; } /** * Complete processing of a sentence * @param {Object} item - Queue item * @param {Object} result - Processing result */ completeSentence(item, result) { // Remove from queue this.sentenceQueue.shift(); // Call the original callback if (item.callback) { item.callback(result); } // Reset processing flag this.isProcessing = false; // Process next sentence if any if (this.sentenceQueue.length > 0) { this.processNextSentence(); } } } // Create the singleton instance const SentenceQueue = new SentenceQueueModule(); // Export the module export { SentenceQueue }; // Register with the module registry if (window.moduleRegistry) { window.moduleRegistry.register(SentenceQueue); } // Keep a reference in window for loader system window.SentenceQueue = SentenceQueue;