498 lines
18 KiB
JavaScript
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 }; |