247 lines
7.9 KiB
JavaScript
247 lines
7.9 KiB
JavaScript
/**
|
|
* 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<boolean>} - 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<Object>} - 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;
|