401 lines
13 KiB
JavaScript
401 lines
13 KiB
JavaScript
/**
|
|
* TTS Factory Module
|
|
* Creates and manages TTS handler instances
|
|
*/
|
|
import { BaseModule } from './base-module.js';
|
|
import { moduleRegistry } from './module-registry.js';
|
|
import { BrowserTTSHandler } from './browser-tts-handler.js';
|
|
import { ApiTTSHandler } from './api-tts-handler.js';
|
|
import { KokoroHandler } from './kokoro-handler.js';
|
|
|
|
class TTSFactoryModule extends BaseModule {
|
|
/**
|
|
* Create a new TTS factory
|
|
*/
|
|
constructor() {
|
|
super('tts-factory', 'TTS Factory');
|
|
|
|
// Available TTS handlers
|
|
this.handlers = {};
|
|
|
|
// Current active handler
|
|
this.activeHandler = null;
|
|
|
|
// Handler initialization status
|
|
this.initStatus = {
|
|
browser: false,
|
|
api: false,
|
|
kokoro: false
|
|
};
|
|
|
|
// TTS availability flag
|
|
this.ttsAvailable = false;
|
|
|
|
// Bind methods
|
|
this.bindMethods([
|
|
'registerHandler',
|
|
'initializeHandler',
|
|
'getHandler',
|
|
'setActiveHandler',
|
|
'getActiveHandler',
|
|
'getAvailableHandlers',
|
|
'speak',
|
|
'stop',
|
|
'pause',
|
|
'resume',
|
|
'getVoices',
|
|
'getPreference'
|
|
]);
|
|
|
|
// Add dependencies
|
|
this.dependencies = ['persistence-manager', 'localization'];
|
|
}
|
|
|
|
/**
|
|
* Initialize the module
|
|
* @returns {Promise<boolean>} - Resolves with success status
|
|
*/
|
|
async initialize() {
|
|
try {
|
|
this.reportProgress(10, "Initializing TTS factory");
|
|
|
|
// Get dependencies
|
|
const persistenceManager = this.getModule('persistence-manager');
|
|
const localization = this.getModule('localization');
|
|
|
|
if (!persistenceManager || !localization) {
|
|
console.error("TTS Factory: Required dependencies not found");
|
|
this.reportProgress(100, "TTS factory failed - missing dependencies");
|
|
return false;
|
|
}
|
|
|
|
// Register available handlers
|
|
this.registerHandler('browser', new BrowserTTSHandler());
|
|
this.registerHandler('api', new ApiTTSHandler());
|
|
this.registerHandler('kokoro', new KokoroHandler());
|
|
|
|
this.reportProgress(30, "Registered TTS handlers");
|
|
|
|
// Get user preferences
|
|
const ttsEnabled = this.getPreference('tts', 'enabled', false);
|
|
const preferredProvider = this.getPreference('tts', 'provider', 'browser');
|
|
|
|
// Initialize handlers based on preferences
|
|
let initSuccess = false;
|
|
|
|
if (ttsEnabled) {
|
|
// Try to initialize preferred handler first
|
|
this.reportProgress(50, `Initializing preferred TTS handler: ${preferredProvider}`);
|
|
initSuccess = await this.initializeHandler(preferredProvider);
|
|
|
|
if (initSuccess) {
|
|
this.setActiveHandler(preferredProvider);
|
|
} else {
|
|
// If preferred handler failed, try alternatives based on priority: Kokoro -> Browser -> None
|
|
console.warn(`Failed to initialize preferred TTS handler: ${preferredProvider}, trying alternatives`);
|
|
|
|
// Try Kokoro TTS as fallback if not already tried
|
|
if (preferredProvider !== 'kokoro') {
|
|
this.reportProgress(60, "Trying Kokoro TTS as fallback");
|
|
initSuccess = await this.initializeHandler('kokoro');
|
|
if (initSuccess) {
|
|
this.setActiveHandler('kokoro');
|
|
// Update preference to Kokoro since it worked
|
|
this.getModule('persistence-manager').updatePreference('tts', 'provider', 'kokoro');
|
|
}
|
|
}
|
|
|
|
// If Kokoro TTS failed, try Browser TTS
|
|
if (!initSuccess && preferredProvider !== 'browser') {
|
|
this.reportProgress(70, "Trying Browser TTS as fallback");
|
|
initSuccess = await this.initializeHandler('browser');
|
|
if (initSuccess) {
|
|
this.setActiveHandler('browser');
|
|
// Update preference to browser since it worked
|
|
this.getModule('persistence-manager').updatePreference('tts', 'provider', 'browser');
|
|
}
|
|
}
|
|
|
|
// Note: API TTS is not used as a fallback as it requires manual configuration
|
|
}
|
|
} else {
|
|
// Even if TTS is disabled, initialize handlers in the background
|
|
// so they're ready if the user enables TTS later
|
|
this.reportProgress(50, "TTS disabled, initializing handlers in background");
|
|
|
|
// Initialize Kokoro and Browser handlers in parallel (not API as it requires configuration)
|
|
const initPromises = [
|
|
this.initializeHandler('kokoro'),
|
|
this.initializeHandler('browser')
|
|
];
|
|
|
|
// Wait for all handlers to initialize
|
|
await Promise.allSettled(initPromises);
|
|
|
|
// Check if any handler initialized successfully
|
|
initSuccess = this.initStatus.kokoro || this.initStatus.browser;
|
|
}
|
|
|
|
// Set TTS availability flag and dispatch event
|
|
this.ttsAvailable = initSuccess;
|
|
|
|
// Dispatch event to notify UI about TTS availability
|
|
document.dispatchEvent(new CustomEvent('tts:availability', {
|
|
detail: { available: this.ttsAvailable }
|
|
}));
|
|
|
|
this.reportProgress(100, initSuccess ? "TTS factory ready" : "TTS factory ready (no handlers available)");
|
|
|
|
// Always return true since TTS is optional for the application
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Error initializing TTS factory:", error);
|
|
this.reportProgress(100, "TTS factory failed");
|
|
|
|
// Set TTS availability to false and dispatch event
|
|
this.ttsAvailable = false;
|
|
document.dispatchEvent(new CustomEvent('tts:availability', {
|
|
detail: { available: false }
|
|
}));
|
|
|
|
// Still return true since TTS is optional
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a TTS handler
|
|
* @param {string} id - Handler ID
|
|
* @param {Object} handler - TTS handler instance
|
|
*/
|
|
registerHandler(id, handler) {
|
|
if (!id || !handler) return;
|
|
this.handlers[id] = handler;
|
|
}
|
|
|
|
/**
|
|
* Initialize a specific TTS handler
|
|
* @param {string} id - Handler ID
|
|
* @returns {Promise<boolean>} - Success status
|
|
*/
|
|
async initializeHandler(id) {
|
|
if (!id || !this.handlers[id]) {
|
|
console.error(`TTS Factory: Handler '${id}' not found`);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
this.reportProgress(0, `Initializing ${id} TTS handler`);
|
|
|
|
// Initialize the handler
|
|
const success = await this.handlers[id].initialize(
|
|
(progress, message) => {
|
|
this.reportProgress(progress, message);
|
|
}
|
|
);
|
|
|
|
// Update initialization status
|
|
this.initStatus[id] = success;
|
|
|
|
if (success) {
|
|
console.log(`TTS Factory: Successfully initialized ${id} TTS handler`);
|
|
} else {
|
|
console.error(`TTS Factory: Failed to initialize ${id} TTS handler`);
|
|
}
|
|
|
|
return success;
|
|
} catch (error) {
|
|
console.error(`TTS Factory: Error initializing ${id} TTS handler:`, error);
|
|
this.initStatus[id] = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a TTS handler by ID
|
|
* @param {string} id - Handler ID
|
|
* @returns {Object|null} - TTS handler instance or null if not found
|
|
*/
|
|
getHandler(id) {
|
|
if (!id || !this.handlers[id]) return null;
|
|
return this.handlers[id];
|
|
}
|
|
|
|
/**
|
|
* Set the active TTS handler
|
|
* @param {string} id - Handler ID
|
|
* @returns {boolean} - Success status
|
|
*/
|
|
setActiveHandler(id) {
|
|
if (!id || !this.handlers[id] || !this.initStatus[id]) {
|
|
console.warn(`Cannot set active handler to ${id}: handler not found or not initialized`);
|
|
return false;
|
|
}
|
|
|
|
// Stop current handler if active
|
|
if (this.activeHandler) {
|
|
this.handlers[this.activeHandler].stop();
|
|
}
|
|
|
|
// Set new active handler
|
|
this.activeHandler = id;
|
|
|
|
// Update preference
|
|
this.getModule('persistence-manager').updatePreference('tts', 'provider', id);
|
|
|
|
// Dispatch event
|
|
this.dispatchEvent('tts-handler-changed', {
|
|
handler: id
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get the active TTS handler
|
|
* @returns {Object|null} - Active TTS handler instance or null if none active
|
|
*/
|
|
getActiveHandler() {
|
|
if (!this.activeHandler) return null;
|
|
return this.handlers[this.activeHandler];
|
|
}
|
|
|
|
/**
|
|
* Get all available TTS handlers
|
|
* @returns {Object} - Map of handler IDs to initialization status
|
|
*/
|
|
getAvailableHandlers() {
|
|
const available = {};
|
|
|
|
for (const id in this.handlers) {
|
|
available[id] = this.initStatus[id];
|
|
}
|
|
|
|
return available;
|
|
}
|
|
|
|
/**
|
|
* Speak text using the active TTS handler
|
|
* @param {string} text - Text to speak
|
|
* @param {Object} options - TTS options
|
|
* @returns {Promise<boolean>} - Success status
|
|
*/
|
|
async speak(text, options = {}) {
|
|
if (!this.activeHandler) {
|
|
console.warn("No active TTS handler");
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return await this.handlers[this.activeHandler].speak(text, options);
|
|
} catch (error) {
|
|
console.error("Error speaking text:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop speaking
|
|
* @returns {boolean} - Success status
|
|
*/
|
|
stop() {
|
|
if (!this.activeHandler) return false;
|
|
|
|
try {
|
|
return this.handlers[this.activeHandler].stop();
|
|
} catch (error) {
|
|
console.error("Error stopping TTS:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pause speaking
|
|
* @returns {boolean} - Success status
|
|
*/
|
|
pause() {
|
|
if (!this.activeHandler) return false;
|
|
|
|
try {
|
|
return this.handlers[this.activeHandler].pause();
|
|
} catch (error) {
|
|
console.error("Error pausing TTS:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resume speaking
|
|
* @returns {boolean} - Success status
|
|
*/
|
|
resume() {
|
|
if (!this.activeHandler) return false;
|
|
|
|
try {
|
|
return this.handlers[this.activeHandler].resume();
|
|
} catch (error) {
|
|
console.error("Error resuming TTS:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get available voices for the active TTS handler
|
|
* @returns {Array} - Array of voice objects
|
|
*/
|
|
getVoices() {
|
|
if (!this.activeHandler) return [];
|
|
|
|
try {
|
|
return this.handlers[this.activeHandler].getVoices();
|
|
} catch (error) {
|
|
console.error("Error getting voices:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a preference from the persistence manager
|
|
* @param {string} category - Preference category
|
|
* @param {string} key - Preference key
|
|
* @param {*} defaultValue - Default value if preference doesn't exist
|
|
* @returns {*} - Preference value
|
|
*/
|
|
getPreference(category, key, defaultValue) {
|
|
const persistenceManager = this.getModule('persistence-manager');
|
|
if (persistenceManager) {
|
|
return persistenceManager.getPreference(category, key, defaultValue);
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/**
|
|
* Clean up when module is disposed
|
|
*/
|
|
dispose() {
|
|
// Stop any active TTS
|
|
if (this.activeHandler) {
|
|
this.handlers[this.activeHandler].stop();
|
|
}
|
|
|
|
// Dispose all handlers
|
|
for (const id in this.handlers) {
|
|
if (this.handlers[id].dispose) {
|
|
this.handlers[id].dispose();
|
|
}
|
|
}
|
|
|
|
// Clear handlers
|
|
this.handlers = {};
|
|
this.activeHandler = null;
|
|
}
|
|
}
|
|
|
|
// Create the singleton instance
|
|
const TTSFactory = new TTSFactoryModule();
|
|
|
|
// Register with the module registry
|
|
moduleRegistry.register(TTSFactory);
|
|
|
|
// Export the module
|
|
export { TTSFactory }; |