Files
ai.interactive.fiction/public/js/tts-factory.js
T

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 };