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

498 lines
18 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');
this.dependencies = ['persistence-manager', 'localization'];
this.handlers = {};
this.initStatus = {};
this.activeHandler = null;
this.ttsAvailable = false;
this.speed = 1; // Default speed
// Listen for kokoro:ready event
document.addEventListener('kokoro:ready', (event) => {
if (event.detail && typeof event.detail.success === 'boolean') {
console.log('TTS Factory: Received kokoro:ready event with success =', event.detail.success);
this.initStatus['kokoro'] = event.detail.success;
// If this is the current active handler or we don't have an active handler yet,
// try to activate Kokoro if it's now ready
if ((this.activeHandler === 'kokoro' || !this.activeHandler) && event.detail.success) {
// Only attempt to set active handler if TTS is enabled
const ttsEnabled = this.getPreference('tts', 'enabled', false);
if (ttsEnabled) {
this.setActiveHandler('kokoro');
}
}
// Update overall TTS availability
this.updateTTSAvailability();
}
});
// Bind methods
this.bindMethods([
'registerHandler',
'initializeHandler',
'getHandler',
'setActiveHandler',
'getActiveHandler',
'getAvailableHandlers',
'speak',
'stop',
'pause',
'resume',
'getVoices',
'getPreference',
'isSpeaking',
'configure'
]);
}
/**
* 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());
console.log('TTS Factory: Registered handlers:', Object.keys(this.handlers));
this.reportProgress(30, "Registered TTS handlers");
// Force the initialization of all handlers for diagnostics
// This ensures they're all initialized even if not selected
const initPromises = [];
for (const id of Object.keys(this.handlers)) {
console.log(`TTS Factory: Initializing handler ${id}`);
initPromises.push(this.initializeHandler(id).then(success => {
console.log(`TTS Factory: Handler ${id} initialization ${success ? 'succeeded' : 'failed'}`);
return { id, success };
}));
}
// Wait for all handlers to initialize
const results = await Promise.all(initPromises);
console.log('TTS Factory: All handler initialization results:', results);
// Get user preferences
const ttsEnabled = this.getPreference('tts', 'enabled', false);
const preferredProvider = this.getPreference('tts', 'provider', 'browser');
console.log(`TTS Factory: User preferences - enabled: ${ttsEnabled}, provider: ${preferredProvider}`);
// 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 = this.initStatus[preferredProvider] || false;
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.initStatus.kokoro) {
this.reportProgress(60, "Using Kokoro TTS as fallback");
this.setActiveHandler('kokoro');
// Update preference to Kokoro since it worked
this.getModule('persistence-manager').updatePreference('tts', 'provider', 'kokoro');
initSuccess = true;
}
// Try Browser TTS as fallback if not already tried
else if (preferredProvider !== 'browser' && this.initStatus.browser) {
this.reportProgress(70, "Using Browser TTS as fallback");
this.setActiveHandler('browser');
// Update preference to Browser since it worked
this.getModule('persistence-manager').updatePreference('tts', 'provider', 'browser');
initSuccess = true;
}
else {
// If all failed, disable TTS
this.reportProgress(80, "All TTS handlers failed, disabling TTS");
this.getModule('persistence-manager').updatePreference('tts', 'enabled', false);
this.getModule('persistence-manager').updatePreference('tts', 'provider', 'none');
}
}
}
// Determine overall TTS availability
this.ttsAvailable = this.initStatus.kokoro || this.initStatus.browser;
// Dispatch TTS availability event
window.dispatchEvent(new CustomEvent('tts:availability', {
detail: { available: this.ttsAvailable }
}));
this.reportProgress(100, "TTS factory initialized");
return true; // TTS is optional, so always return true
} catch (error) {
console.error("TTS Factory: Error during initialization:", error);
this.reportProgress(100, "TTS factory failed");
return true; // TTS is optional, so always 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) {
// Handle 'none' option specially
if (id === 'none') {
this.activeHandler = null;
// Update TTS availability state
this.ttsAvailable = false;
// Notify about TTS availability change
document.dispatchEvent(new CustomEvent('tts:availability', {
detail: { available: false }
}));
console.log("TTS Factory: TTS disabled (none selected)");
return true;
}
if (!this.handlers[id]) {
console.error(`TTS Factory: Handler not found: ${id}`);
return false;
}
if (!this.initStatus[id]) {
console.error(`TTS Factory: Handler not initialized: ${id}`);
return false;
}
this.activeHandler = id;
// Update TTS availability state
this.ttsAvailable = true;
// Notify about TTS availability change
document.dispatchEvent(new CustomEvent('tts:availability', {
detail: { available: true }
}));
console.log(`TTS Factory: Active handler set to ${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 = {};
// Debug logging for diagnostic purposes
console.log('TTS Factory: getAvailableHandlers called');
console.log('TTS Factory: Current initialization status:', this.initStatus);
console.log('TTS Factory: Registered handlers:', Object.keys(this.handlers).join(', '));
for (const id in this.handlers) {
// Add the handler to the available list even if it's not initialized yet
// This ensures all registered handlers appear in the options
available[id] = true;
console.log(`TTS Factory: Including handler ${id} in options`);
}
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;
}
/**
* Check if any TTS handler is currently speaking
* @returns {boolean} - True if speaking, false otherwise
*/
isSpeaking() {
if (!this.activeHandler || !this.handlers[this.activeHandler]) {
return false;
}
try {
return this.handlers[this.activeHandler].isSpeaking();
} catch (error) {
console.error("Error checking speaking status:", error);
return false;
}
}
/**
* Update overall TTS availability
*/
updateTTSAvailability() {
this.ttsAvailable = this.initStatus.kokoro || this.initStatus.browser;
// Dispatch TTS availability event
window.dispatchEvent(new CustomEvent('tts:availability', {
detail: { available: this.ttsAvailable }
}));
}
/**
* Configure TTS settings for all handlers
* @param {Object} options - TTS options
* @param {number} [options.speed] - Normalized speech rate (0-1 range)
*/
configure(options = {}) {
// If speed is provided, convert the normalized speed (0-1) to the appropriate scale for each handler
if (typeof options.speed === 'number') {
const normalizedSpeed = Math.max(0, Math.min(1, options.speed));
// Scale for each handler type
for (const id in this.handlers) {
// Ensure the handler exists and has the setVoiceOptions method
if (this.handlers[id] && typeof this.handlers[id].setVoiceOptions === 'function') {
let scaledOptions = {};
// Scale the speed value appropriately for each handler type
if (id === 'browser') {
// Browser TTS uses rate from 0.1 to 2.0
scaledOptions.rate = 0.1 + (normalizedSpeed * 1.9);
} else if (id === 'kokoro') {
// Kokoro uses rate from 0.5 to 1.5
scaledOptions.rate = 0.5 + (normalizedSpeed);
} else if (id === 'api') {
// API uses speed from 0.5 to 2.0
scaledOptions.speed = 0.5 + (normalizedSpeed * 1.5);
}
// Apply the scaled options to the handler
this.handlers[id].setVoiceOptions(scaledOptions);
}
}
// Store the normalized value
this.speed = normalizedSpeed;
console.log(`TTS Factory: Speed set to ${normalizedSpeed} (normalized), ${Math.round(normalizedSpeed * 100)}/100`);
}
return true;
}
/**
* 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 };