Split everything up into dynamically loaded modules.
This commit is contained in:
+510
-127
@@ -1,172 +1,555 @@
|
||||
/**
|
||||
* TTS Factory for AI Interactive Fiction
|
||||
* Attempts to use Kokoro TTS first, then falls back to browser TTS if needed
|
||||
* Manages different TTS implementations with a common interface
|
||||
*/
|
||||
import { kokoroHandler } from './kokoro-handler.js';
|
||||
import { browserTtsHandler } from './tts-handler.js';
|
||||
|
||||
export class TTSFactory {
|
||||
class TTSFactory {
|
||||
constructor() {
|
||||
this.activeTTSHandler = null;
|
||||
this.ttsHandler = null;
|
||||
this.handlers = {};
|
||||
this.initializationAttempted = false;
|
||||
this.usingKokoro = false;
|
||||
this.initializationPromise = null; // Promise for the factory initialization
|
||||
this.initializationPromise = null;
|
||||
this.ttsEnabled = true;
|
||||
this.progressCallback = null;
|
||||
this.persistenceManager = null;
|
||||
}
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => this.initialize());
|
||||
} else {
|
||||
// Use requestAnimationFrame to ensure scripts are parsed
|
||||
requestAnimationFrame(() => this.initialize());
|
||||
/**
|
||||
* Initialize the TTS Factory - Static method for the module loader
|
||||
* @param {Function} reportProgress - Function to report loading progress to the loader
|
||||
* @returns {Promise} - Resolves when TTS is initialized
|
||||
*/
|
||||
static async initializeInterface(reportProgress = null) {
|
||||
console.log('TTS Factory: Initializing interface');
|
||||
|
||||
// Create singleton instance if needed
|
||||
if (!window.ttsFactory) {
|
||||
window.ttsFactory = new TTSFactory();
|
||||
}
|
||||
|
||||
// Initialize TTS with the progress callback
|
||||
window.ttsFactory.progressCallback = reportProgress;
|
||||
|
||||
try {
|
||||
// Start initialization process
|
||||
await window.ttsFactory.initialize();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error initializing TTS Factory:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize available TTS handlers
|
||||
* Initialize the TTS Factory
|
||||
* This will load and initialize all available TTS handlers
|
||||
* @returns {Promise} - Resolves when initialization is complete
|
||||
*/
|
||||
async initialize() {
|
||||
// Prevent multiple initializations
|
||||
if (this.initializationAttempted) return this.initializationPromise;
|
||||
this.initializationAttempted = true;
|
||||
|
||||
console.log('Initializing TTS Factory...');
|
||||
if (this.initializationPromise) {
|
||||
return this.initializationPromise;
|
||||
}
|
||||
|
||||
this.initializationPromise = new Promise(async (resolve) => {
|
||||
let kokoroInitialized = false;
|
||||
// Try to initialize Kokoro first (preferred option)
|
||||
try {
|
||||
console.log('Attempting to initialize Kokoro TTS...');
|
||||
|
||||
// --- Increase Timeout for Kokoro Initialization ---
|
||||
// Wait for KokoroHandler's internal initialization promise
|
||||
// Use Promise.race to add a longer timeout specifically for Kokoro init
|
||||
const kokoroTimeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Kokoro initialization timed out in factory')), 60000) // 60 seconds timeout
|
||||
);
|
||||
this.initializationAttempted = true;
|
||||
|
||||
const reportProgress = (percent, message) => {
|
||||
console.log(`TTS progress: ${percent}% - ${message}`);
|
||||
if (this.progressCallback && typeof this.progressCallback === 'function') {
|
||||
this.progressCallback(percent, message);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
kokoroInitialized = await Promise.race([
|
||||
kokoroHandler.initializationPromise,
|
||||
kokoroTimeoutPromise
|
||||
]);
|
||||
} catch (timeoutError) {
|
||||
console.error(timeoutError.message); // Log the timeout error
|
||||
kokoroInitialized = false;
|
||||
}
|
||||
// --- End Increase Timeout ---
|
||||
try {
|
||||
// Report starting initialization
|
||||
reportProgress(10, 'Loading TTS modules');
|
||||
|
||||
// Get persistence manager if available
|
||||
if (window.PersistenceManager) {
|
||||
this.persistenceManager = window.PersistenceManager;
|
||||
reportProgress(15, 'Persistence manager found, loading preferences');
|
||||
|
||||
if (kokoroInitialized) {
|
||||
console.log('Kokoro Handler reported successful initialization.');
|
||||
} else {
|
||||
console.warn('Kokoro Handler reported failed or timed out initialization.');
|
||||
// Load preferences to determine TTS enabled state and preferred provider
|
||||
const prefs = this.persistenceManager.getAllPreferences();
|
||||
if (prefs && prefs.tts) {
|
||||
this.ttsEnabled = prefs.tts.enabled;
|
||||
console.log(`TTS Factory: Setting initial TTS enabled state to ${this.ttsEnabled ? 'enabled' : 'disabled'} from preferences`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing Kokoro Handler:', error);
|
||||
kokoroInitialized = false; // Ensure it's marked as failed
|
||||
}
|
||||
|
||||
// Import needed modules dynamically
|
||||
const [{ BrowserTTSHandler }, { KokoroHandler }, { ApiTTSHandler }] = await Promise.all([
|
||||
import('./browser-tts-handler.js'),
|
||||
import('./kokoro-handler.js'),
|
||||
import('./api-tts-handler.js')
|
||||
]);
|
||||
|
||||
reportProgress(20, 'TTS modules loaded');
|
||||
|
||||
// Create handlers
|
||||
const browserHandler = new BrowserTTSHandler();
|
||||
const kokoroHandler = new KokoroHandler();
|
||||
const apiHandler = new ApiTTSHandler();
|
||||
|
||||
// Store handlers
|
||||
this.handlers = {
|
||||
browser: browserHandler,
|
||||
kokoro: kokoroHandler,
|
||||
api: apiHandler
|
||||
};
|
||||
|
||||
// Get preferred TTS mode from options
|
||||
const preferredTTSMode = this.getPreferredTTSMode();
|
||||
|
||||
// Initialize the preferred handler first
|
||||
if (preferredTTSMode === 'browser') {
|
||||
// User prefers browser TTS
|
||||
await this.initializeBrowserTTS(browserHandler, reportProgress);
|
||||
} else if (preferredTTSMode === 'api') {
|
||||
// User prefers API TTS
|
||||
await this.initializeApiTTS(apiHandler, reportProgress);
|
||||
|
||||
// Fallback to browser TTS if API fails
|
||||
if (!apiHandler.isAvailable()) {
|
||||
await this.initializeBrowserTTS(browserHandler, reportProgress);
|
||||
}
|
||||
} else {
|
||||
// Default flow: prefer Kokoro, with browser as immediate fallback
|
||||
// Initialize browser TTS immediately for a responsive experience
|
||||
await this.initializeBrowserTTS(browserHandler, reportProgress);
|
||||
|
||||
// Then schedule Kokoro loading in the background
|
||||
reportProgress(75, 'Scheduling Kokoro TTS initialization');
|
||||
this.scheduleKokoroInitialization(kokoroHandler, reportProgress).then((kokoroAvailable) => {
|
||||
if (kokoroAvailable) {
|
||||
// Switch to Kokoro as it's the best option and set as preferred
|
||||
this.ttsHandler = kokoroHandler;
|
||||
this.setPreferredTTSMode('kokoro');
|
||||
this.dispatchTTSReadyEvent(true, 'kokoro', kokoroHandler);
|
||||
reportProgress(100, 'Kokoro TTS ready');
|
||||
|
||||
// Apply voice settings from preferences if available
|
||||
this.applyVoiceSettingsFromPreferences();
|
||||
} else if (!this.getPreferredTTSMode()) {
|
||||
// If Kokoro failed and no preference was previously set,
|
||||
// set browser as preferred mode
|
||||
this.setPreferredTTSMode('browser');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Decide which handler to use based on Kokoro's success
|
||||
this.selectActiveHandler(kokoroInitialized);
|
||||
resolve(); // Resolve the factory's promise
|
||||
// Apply voice settings from preferences for initial handler
|
||||
this.applyVoiceSettingsFromPreferences();
|
||||
|
||||
// Resolve initialization even though Kokoro is still loading in background
|
||||
reportProgress(80, 'TTS interface ready' +
|
||||
(preferredTTSMode !== 'kokoro' ? '' : ' (Kokoro loading in background)'));
|
||||
resolve(true);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing TTS Factory:', error);
|
||||
|
||||
// If we have any handler working, consider initialization successful
|
||||
if (this.ttsHandler) {
|
||||
reportProgress(100, `Using ${this.ttsHandler.getId()} TTS (fallback)`);
|
||||
resolve(true);
|
||||
} else {
|
||||
this.dispatchTTSReadyEvent(false);
|
||||
reportProgress(100, 'TTS initialization failed');
|
||||
resolve(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return this.initializationPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select which TTS handler to use
|
||||
* @param {boolean} kokoroInitialized - Whether Kokoro initialization succeeded
|
||||
* Apply stored voice settings from preferences
|
||||
* @private
|
||||
*/
|
||||
selectActiveHandler(kokoroInitialized) {
|
||||
// First choice: Kokoro if it's available and initialized successfully
|
||||
if (kokoroInitialized && kokoroHandler.kokoroReady) {
|
||||
console.log('Using Kokoro TTS as primary TTS system');
|
||||
this.activeTTSHandler = kokoroHandler;
|
||||
this.usingKokoro = true;
|
||||
}
|
||||
// Fallback to browser TTS if available
|
||||
else if (browserTtsHandler) {
|
||||
console.log('Falling back to browser TTS.');
|
||||
this.activeTTSHandler = browserTtsHandler;
|
||||
this.usingKokoro = false;
|
||||
}
|
||||
// No TTS available
|
||||
else {
|
||||
console.error('No TTS system available.');
|
||||
this.activeTTSHandler = null;
|
||||
this.usingKokoro = false;
|
||||
}
|
||||
|
||||
// Expose the active handler as the global ttsHandler for compatibility
|
||||
window.ttsHandler = this.activeTTSHandler;
|
||||
|
||||
// Log the active TTS system
|
||||
if (this.usingKokoro) {
|
||||
console.log('TTS Factory initialized with Kokoro TTS');
|
||||
} else if (this.activeTTSHandler) {
|
||||
console.log('TTS Factory initialized with browser TTS');
|
||||
} else {
|
||||
console.log('TTS Factory initialized with no available TTS');
|
||||
}
|
||||
applyVoiceSettingsFromPreferences() {
|
||||
if (!this.ttsHandler || !this.persistenceManager) return;
|
||||
|
||||
// Dispatch an event to notify the UI that TTS is ready (or not)
|
||||
const ttsReadyEvent = new CustomEvent('tts-ready', {
|
||||
detail: {
|
||||
available: !!this.activeTTSHandler,
|
||||
type: this.usingKokoro ? 'kokoro' : (this.activeTTSHandler ? 'browser' : 'none'),
|
||||
handler: this.activeTTSHandler
|
||||
const prefs = this.persistenceManager.getAllPreferences();
|
||||
if (prefs && prefs.tts) {
|
||||
if (prefs.tts.voice) {
|
||||
console.log(`TTS Factory: Setting voice to ${prefs.tts.voice} from preferences`);
|
||||
// Check if setVoice exists, otherwise try setting through voiceOptions
|
||||
if (typeof this.ttsHandler.setVoice === 'function') {
|
||||
this.ttsHandler.setVoice(prefs.tts.voice);
|
||||
} else if (typeof this.ttsHandler.setVoiceOptions === 'function') {
|
||||
this.ttsHandler.setVoiceOptions({ voice: prefs.tts.voice });
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(ttsReadyEvent);
|
||||
}
|
||||
|
||||
if (prefs.tts.rate !== undefined) {
|
||||
console.log(`TTS Factory: Setting speech rate to ${prefs.tts.rate} from preferences`);
|
||||
// Check if setSpeed exists, otherwise try setting through voiceOptions
|
||||
if (typeof this.ttsHandler.setSpeed === 'function') {
|
||||
this.ttsHandler.setSpeed(prefs.tts.rate);
|
||||
} else if (typeof this.ttsHandler.setVoiceOptions === 'function') {
|
||||
this.ttsHandler.setVoiceOptions({ rate: prefs.tts.rate });
|
||||
}
|
||||
}
|
||||
|
||||
if (prefs.tts.volume !== undefined && typeof this.ttsHandler.setVolume === 'function') {
|
||||
console.log(`TTS Factory: Setting volume to ${prefs.tts.volume} from preferences`);
|
||||
this.ttsHandler.setVolume(prefs.tts.volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get info about the active TTS system
|
||||
* Initialize browser TTS
|
||||
* @param {BrowserTTSHandler} handler - The browser TTS handler
|
||||
* @param {Function} reportProgress - Progress reporting function
|
||||
* @returns {Promise<boolean>} - Resolves with availability status
|
||||
*/
|
||||
getActiveTTSInfo() {
|
||||
if (!this.activeTTSHandler) {
|
||||
return { available: false, type: 'none', name: 'None' };
|
||||
async initializeBrowserTTS(handler, reportProgress) {
|
||||
reportProgress(30, 'Initializing browser TTS');
|
||||
const browserAvailable = await handler.initialize();
|
||||
|
||||
if (browserAvailable) {
|
||||
this.ttsHandler = handler;
|
||||
this.dispatchTTSReadyEvent(true, 'browser', handler);
|
||||
reportProgress(40, 'Browser TTS ready');
|
||||
} else {
|
||||
reportProgress(40, 'Browser TTS not available');
|
||||
}
|
||||
|
||||
return {
|
||||
available: true,
|
||||
type: this.usingKokoro ? 'kokoro' : 'browser',
|
||||
name: this.usingKokoro ? 'Kokoro TTS' : 'Browser TTS'
|
||||
};
|
||||
return browserAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize API TTS
|
||||
* @param {ApiTTSHandler} handler - The API TTS handler
|
||||
* @param {Function} reportProgress - Progress reporting function
|
||||
* @returns {Promise<boolean>} - Resolves with availability status
|
||||
*/
|
||||
async initializeApiTTS(handler, reportProgress) {
|
||||
reportProgress(50, 'Initializing API TTS');
|
||||
const apiAvailable = await handler.initialize();
|
||||
|
||||
if (apiAvailable) {
|
||||
this.ttsHandler = handler;
|
||||
this.dispatchTTSReadyEvent(true, 'api', handler);
|
||||
reportProgress(70, 'API TTS ready');
|
||||
}
|
||||
|
||||
return apiAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force switching to a specific TTS system
|
||||
* @param {string} type - Either 'kokoro' or 'browser'
|
||||
* Get preferred TTS mode from storage
|
||||
* @returns {string|null} - Preferred TTS mode or null if not set
|
||||
*/
|
||||
switchTTS(type) {
|
||||
if (type === 'kokoro' && kokoroHandler && kokoroHandler.kokoroReady) {
|
||||
this.activeTTSHandler = kokoroHandler;
|
||||
this.usingKokoro = true;
|
||||
window.ttsHandler = this.activeTTSHandler;
|
||||
console.log('Switched to Kokoro TTS');
|
||||
// Dispatch event on switch
|
||||
const ttsReadyEvent = new CustomEvent('tts-ready', { detail: { available: true, type: 'kokoro', handler: this.activeTTSHandler } });
|
||||
window.dispatchEvent(ttsReadyEvent);
|
||||
return true;
|
||||
} else if (type === 'browser' && browserTtsHandler) {
|
||||
this.activeTTSHandler = browserTtsHandler;
|
||||
this.usingKokoro = false;
|
||||
window.ttsHandler = this.activeTTSHandler;
|
||||
console.log('Switched to browser TTS');
|
||||
// Dispatch event on switch
|
||||
const ttsReadyEvent = new CustomEvent('tts-ready', { detail: { available: true, type: 'browser', handler: this.activeTTSHandler } });
|
||||
window.dispatchEvent(ttsReadyEvent);
|
||||
return true;
|
||||
getPreferredTTSMode() {
|
||||
// First check persistent settings if available
|
||||
if (this.persistenceManager) {
|
||||
const prefs = this.persistenceManager.getAllPreferences();
|
||||
if (prefs && prefs.tts && prefs.tts.provider) {
|
||||
console.log(`TTS Factory: Using preferred TTS mode '${prefs.tts.provider}' from persistence manager`);
|
||||
return prefs.tts.provider;
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Failed to switch to ${type} TTS - not available`);
|
||||
return false;
|
||||
// Fallback to localStorage if persistence manager is not available
|
||||
try {
|
||||
const savedMode = localStorage.getItem('preferred-tts-mode');
|
||||
if (savedMode) {
|
||||
console.log(`TTS Factory: Using preferred TTS mode '${savedMode}' from localStorage`);
|
||||
return savedMode;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not read TTS preference from localStorage');
|
||||
}
|
||||
|
||||
// Default to Kokoro if no preference is found
|
||||
return "kokoro";
|
||||
}
|
||||
|
||||
/**
|
||||
* Set preferred TTS mode in storage
|
||||
* @param {string} mode - The TTS mode to save as preferred
|
||||
*/
|
||||
setPreferredTTSMode(mode) {
|
||||
// Update in persistence manager if available
|
||||
if (this.persistenceManager) {
|
||||
this.persistenceManager.updatePreference('tts', 'provider', mode);
|
||||
console.log(`TTS Factory: Saved preferred TTS mode '${mode}' to persistence manager`);
|
||||
}
|
||||
|
||||
// Also save to localStorage as backup
|
||||
try {
|
||||
localStorage.setItem('preferred-tts-mode', mode);
|
||||
} catch (e) {
|
||||
console.warn('Could not save TTS preference to localStorage');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule Kokoro initialization during idle time
|
||||
* @param {Object} kokoroHandler - The Kokoro handler instance
|
||||
* @param {Function} reportProgress - Progress reporting function
|
||||
* @returns {Promise<boolean>} - Resolves with success status
|
||||
*/
|
||||
scheduleKokoroInitialization(kokoroHandler, reportProgress) {
|
||||
// Immediately dispatch the loading started event so tts-player can catch it
|
||||
window.dispatchEvent(new CustomEvent('kokoro-loading-started'));
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Create the initialization function
|
||||
const startKokoroInit = async () => {
|
||||
try {
|
||||
// Initialize Kokoro with progress callback
|
||||
const kokoroAvailable = await kokoroHandler.initialize((percent, message) => {
|
||||
// Scale progress to 80-95% range for the TTS module's overall progress
|
||||
const scaledProgress = 80 + Math.floor(percent * 0.15);
|
||||
reportProgress(scaledProgress, message || `Loading Kokoro TTS: ${percent}%`);
|
||||
});
|
||||
|
||||
// Mark completion
|
||||
if (kokoroAvailable) {
|
||||
reportProgress(95, "Kokoro TTS initialized successfully");
|
||||
} else {
|
||||
reportProgress(95, "Kokoro TTS unavailable - using fallback");
|
||||
}
|
||||
|
||||
// Always dispatch event to indicate completion status
|
||||
window.dispatchEvent(new CustomEvent('kokoro-loading-complete', {
|
||||
detail: { success: kokoroAvailable }
|
||||
}));
|
||||
|
||||
resolve(kokoroAvailable);
|
||||
} catch (error) {
|
||||
console.error('Error initializing Kokoro:', error);
|
||||
reportProgress(95, 'Kokoro TTS failed to initialize - using fallback');
|
||||
|
||||
// Dispatch completion event with error information
|
||||
window.dispatchEvent(new CustomEvent('kokoro-loading-complete', {
|
||||
detail: { success: false, error: error.message }
|
||||
}));
|
||||
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Add timeout protection with a reasonable timeout (30 seconds for resource-intensive operations)
|
||||
const timeoutId = setTimeout(() => {
|
||||
reportProgress(95, 'Kokoro initialization timed out - using fallback');
|
||||
window.dispatchEvent(new CustomEvent('kokoro-loading-complete', {
|
||||
detail: { success: false, error: "Timeout" }
|
||||
}));
|
||||
resolve(false);
|
||||
}, 30000); // Increased timeout to 30 seconds since model loading is resource intensive
|
||||
|
||||
// Use requestIdleCallback to start initialization during idle time
|
||||
if (window.requestIdleCallback) {
|
||||
reportProgress(75, 'Scheduling Kokoro TTS for background loading');
|
||||
|
||||
window.requestIdleCallback(() => {
|
||||
startKokoroInit().then(() => clearTimeout(timeoutId));
|
||||
}, { timeout: 10000 });
|
||||
} else {
|
||||
reportProgress(75, 'Background loading not available, loading Kokoro normally');
|
||||
|
||||
// Use a microtask to avoid blocking the UI thread
|
||||
Promise.resolve().then(() => startKokoroInit().then(() => clearTimeout(timeoutId)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a custom event when TTS is ready
|
||||
* @param {boolean} available - Whether TTS is available
|
||||
* @param {string} type - The type of TTS
|
||||
* @param {Object} handler - The TTS handler object
|
||||
*/
|
||||
dispatchTTSReadyEvent(available, type = null, handler = null) {
|
||||
const event = new CustomEvent('tts-ready', {
|
||||
detail: {
|
||||
available,
|
||||
type,
|
||||
handler,
|
||||
enabled: this.ttsEnabled
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the active TTS system
|
||||
* @returns {Object} - TTS system info
|
||||
*/
|
||||
getActiveTTSInfo() {
|
||||
if (!this.ttsHandler) {
|
||||
return { available: false, type: 'none', name: 'None' };
|
||||
}
|
||||
|
||||
const id = this.ttsHandler.getId();
|
||||
const name = {
|
||||
'browser': 'Browser TTS',
|
||||
'kokoro': 'Kokoro Neural TTS',
|
||||
'api': 'ElevenLabs API TTS'
|
||||
}[id] || 'Unknown TTS';
|
||||
|
||||
return {
|
||||
available: true,
|
||||
type: id,
|
||||
name: name
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a specific TTS handler
|
||||
* @param {string} type - The handler ID to use
|
||||
* @returns {boolean} - Success status
|
||||
*/
|
||||
switchTTS(type) {
|
||||
if (!this.handlers[type] || !this.handlers[type].isAvailable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.ttsHandler = this.handlers[type];
|
||||
this.dispatchTTSReadyEvent(true, type, this.ttsHandler);
|
||||
|
||||
// Update preferred TTS mode
|
||||
this.setPreferredTTSMode(type);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speak text using the active TTS handler
|
||||
* @param {string} text - Text to speak
|
||||
* @param {Function} callback - Called when speech completes
|
||||
* @returns {boolean} - True if speech started successfully
|
||||
*/
|
||||
speak(text, callback = null) {
|
||||
if (!this.ttsEnabled || !this.ttsHandler) {
|
||||
console.warn("TTSFactory: No active TTS handler available or TTS disabled");
|
||||
if (callback) callback("No TTS handler");
|
||||
return false;
|
||||
}
|
||||
|
||||
const handlerType = this.ttsHandler.getId();
|
||||
console.log(`TTSFactory: Using ${handlerType} handler to speak "${text}"`);
|
||||
|
||||
try {
|
||||
this.ttsHandler.speak(text, (result) => {
|
||||
console.log(`TTSFactory: Speech completed using ${handlerType}`, result);
|
||||
if (callback) callback(result);
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error speaking:', error);
|
||||
if (callback) callback(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop any ongoing speech
|
||||
*/
|
||||
stop() {
|
||||
if (this.ttsHandler) {
|
||||
this.ttsHandler.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set voice options for the active handler
|
||||
* @param {Object} options - Voice options
|
||||
*/
|
||||
setVoiceOptions(options = {}) {
|
||||
if (this.ttsHandler && typeof this.ttsHandler.setVoiceOptions === 'function') {
|
||||
this.ttsHandler.setVoiceOptions(options);
|
||||
|
||||
// Save settings to persistence manager if available
|
||||
if (this.persistenceManager) {
|
||||
if (options.voice !== undefined) {
|
||||
this.persistenceManager.updatePreference('tts', 'voice', options.voice, false);
|
||||
}
|
||||
if (options.rate !== undefined) {
|
||||
this.persistenceManager.updatePreference('tts', 'rate', options.rate, false);
|
||||
}
|
||||
if (options.volume !== undefined) {
|
||||
this.persistenceManager.updatePreference('tts', 'volume', options.volume, false);
|
||||
}
|
||||
// Save all changes at once
|
||||
this.persistenceManager.savePreferences();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle TTS on/off
|
||||
* @returns {boolean} - New TTS enabled state
|
||||
*/
|
||||
toggle() {
|
||||
this.ttsEnabled = !this.ttsEnabled;
|
||||
console.log(`TTS Factory: Toggling TTS to ${this.ttsEnabled ? 'enabled' : 'disabled'}`);
|
||||
|
||||
if (!this.ttsEnabled && this.ttsHandler) {
|
||||
this.ttsHandler.stop();
|
||||
}
|
||||
|
||||
// Save the new state to preferences if persistence manager is available
|
||||
if (this.persistenceManager) {
|
||||
this.persistenceManager.updatePreference('tts', 'enabled', this.ttsEnabled);
|
||||
console.log(`TTS Factory: Saved enabled state (${this.ttsEnabled}) to persistence manager`);
|
||||
}
|
||||
|
||||
return this.ttsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if TTS is enabled
|
||||
* @returns {boolean} - Current TTS enabled state
|
||||
*/
|
||||
isEnabled() {
|
||||
return this.ttsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available handlers
|
||||
* @returns {Object} - Map of available handlers
|
||||
*/
|
||||
getAvailableHandlers() {
|
||||
const available = {};
|
||||
|
||||
Object.entries(this.handlers).forEach(([id, handler]) => {
|
||||
if (handler.isAvailable()) {
|
||||
available[id] = handler;
|
||||
}
|
||||
});
|
||||
|
||||
return available;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available voices from active handler
|
||||
* @returns {Promise<Array>} - Array of available voices
|
||||
*/
|
||||
async getVoices() {
|
||||
if (!this.ttsHandler || typeof this.ttsHandler.getVoices !== 'function') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.ttsHandler.getVoices();
|
||||
} catch (error) {
|
||||
console.error('Error getting voices:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export singleton instance
|
||||
export const ttsFactory = new TTSFactory();
|
||||
// Create singleton instance
|
||||
const ttsFactory = new TTSFactory();
|
||||
|
||||
// Keep a reference in window for compatibility with existing code
|
||||
// Export the factory
|
||||
export { ttsFactory };
|
||||
|
||||
// Keep global reference
|
||||
window.ttsFactory = ttsFactory;
|
||||
Reference in New Issue
Block a user