Checkpoint current interactive fiction state

This commit is contained in:
2026-05-14 21:17:43 +02:00
parent c745efd1d2
commit 873049f7e6
183 changed files with 13755 additions and 1459 deletions
+351 -91
View File
@@ -23,7 +23,10 @@ class TTSFactoryModule extends BaseModule {
this.initStatus = {};
this.activeHandler = null;
this.ttsAvailable = false;
this.speed = 1; // Default speed
this.speed = 1; // Speech speed multiplier. 1.0 is normal speed.
this.language = 'en-us';
this.voice = '';
this.volume = 1.0;
// IndexedDB Cache Configuration
this.db = null; // Will hold the DB connection
@@ -48,6 +51,7 @@ class TTSFactoryModule extends BaseModule {
'stop',
'pause',
'resume',
'fadeOut',
'getVoices',
'getPreference',
'isSpeaking',
@@ -76,22 +80,29 @@ class TTSFactoryModule extends BaseModule {
'registerHandlers',
'initializeHandlerSystem',
'debugLogAllRegisteredModules',
'debugTTSHandlers' // Added method
'debugTTSHandlers',
'emitProcessState',
'getEffectiveVoiceId',
'disableAfterCurrentPlayback',
'getHandlerStatuses',
'getVoicesForHandler',
'refreshHandlerStatus'
]);
// 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;
this.initStatus['kokoro-tts'] = 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
if ((this.activeHandler === 'kokoro-tts' || !this.activeHandler) && event.detail.success) {
// Only activate Kokoro when it was explicitly selected.
const ttsEnabled = this.getPreference('tts', 'enabled', false);
if (ttsEnabled) {
this.setActiveHandler('kokoro');
const preferredHandler = this.getPreference('tts', 'preferred_handler', 'none');
if (ttsEnabled && preferredHandler === 'kokoro-tts') {
this.setActiveHandler('kokoro-tts');
}
}
@@ -99,6 +110,14 @@ class TTSFactoryModule extends BaseModule {
this.updateTTSAvailability();
}
});
document.addEventListener('tts:speechCompleted', () => {
const persistenceManager = this.getModule('persistence-manager');
const enabled = persistenceManager?.getPreference('tts', 'enabled', false);
if (!enabled && this.activeHandler) {
this.setActiveHandler('none');
}
});
// Listen for handler availability changes
document.addEventListener('tts:handler:availabilityChanged', (event) => {
@@ -198,15 +217,29 @@ class TTSFactoryModule extends BaseModule {
}
});
// Listen for speed change events from UI
document.addEventListener('tts:speed:change', (event) => {
if (event.detail && typeof event.detail.speed === 'number') {
this.configure({ speed: event.detail.speed });
console.log(`TTS Factory: Speed updated to ${this.speed} from UI event`);
}
});
document.addEventListener('locale-changed', (event) => {
if (event.detail?.locale) {
this.configure({ language: event.detail.locale });
}
});
// Listen for kokoro error events
document.addEventListener('kokoro:error', (event) => {
console.error('TTS Factory: Received kokoro error event:', event.detail);
if (this.handlers['kokoro']) {
this.initStatus['kokoro'] = false;
if (this.handlers['kokoro-tts']) {
this.initStatus['kokoro-tts'] = false;
this.updateTTSAvailability();
// If kokoro was our active handler, try fallback
if (this.activeHandler === 'kokoro') {
if (this.activeHandler === 'kokoro-tts') {
console.warn('TTS Factory: Kokoro handler failed, falling back');
this.attemptFallbackHandler();
}
@@ -367,8 +400,8 @@ class TTSFactoryModule extends BaseModule {
// Default settings for first run
const defaults = {
'speed': 0.5, // Default speech rate (0-1 range)
'preferred_handler': 'kokoro', // Default to Kokoro TTS
'speed': 1.0, // Default speech speed multiplier
'preferred_handler': 'none', // Development default: TTS disabled
'enabled': false, // TTS disabled by default
'voice': '', // Empty default - will be selected based on handler
'language': 'en-US', // Default language
@@ -387,9 +420,10 @@ class TTSFactoryModule extends BaseModule {
}
}
// Load speech rate preference
// Load speech rate preference exactly as persisted. Do not migrate or
// rewrite this value on load; the UI must reflect the browser state.
const savedSpeed = persistenceManager.getPreference('tts', 'speed');
if (typeof savedSpeed === 'number') {
if (Number.isFinite(savedSpeed)) {
this.speed = savedSpeed;
console.log(`TTS Factory: Loaded speed preference: ${this.speed}`);
} else {
@@ -400,6 +434,9 @@ class TTSFactoryModule extends BaseModule {
// Load other preferences we need for initialization
const preferredHandler = persistenceManager.getPreference('tts', 'preferred_handler');
console.log(`TTS Factory: Loaded preferred handler: ${preferredHandler || 'none'}`);
this.language = persistenceManager.getPreference('tts', 'language', defaults.language);
this.voice = persistenceManager.getPreference('tts', 'voice', defaults.voice);
this.volume = persistenceManager.getPreference('tts', 'volume', defaults.volume);
// We'll handle the preferred handler in initializeHandlerSystem()
@@ -469,7 +506,14 @@ class TTSFactoryModule extends BaseModule {
if (persistenceManager) {
preferredHandler = persistenceManager.getPreference('tts', 'preferred_handler');
const ttsEnabled = persistenceManager.getPreference('tts', 'enabled', false);
console.log(`TTS Factory: Preferred handler from settings: ${preferredHandler || 'none'}`);
if (!ttsEnabled) {
console.log('TTS Factory: TTS toggle is disabled, starting with no active handler');
this.activeHandler = null;
this.updateTTSAvailability();
return true;
}
}
// Special case for 'none' preference
@@ -500,8 +544,11 @@ class TTSFactoryModule extends BaseModule {
}
}
// If we don't have a preferred handler or it's not registered, try fallbacks
return this.attemptFallbackHandler();
// Default to no TTS. Games or users can explicitly select a provider later.
console.log('TTS Factory: No preferred TTS handler selected, defaulting to none');
this.activeHandler = null;
this.updateTTSAvailability();
return true;
}
/**
@@ -509,8 +556,8 @@ class TTSFactoryModule extends BaseModule {
* @returns {Promise<boolean>} - Success status
*/
async attemptFallbackHandler() {
// Fallback order: Kokoro -> Browser -> None
const fallbackOrder = ['kokoro', 'browser'];
// Providers are opt-in. Keep the baseline as text-only unless explicitly selected.
const fallbackOrder = [];
// Try each fallback in order
for (const handlerId of fallbackOrder) {
@@ -529,8 +576,8 @@ class TTSFactoryModule extends BaseModule {
}
}
// If all fallbacks failed, update TTS availability
console.warn('TTS Factory: All handlers failed to initialize, TTS will be unavailable');
// If no explicit provider is selected, update TTS availability and continue.
console.log('TTS Factory: No fallback handler selected, TTS will be unavailable');
this.activeHandler = null;
this.updateTTSAvailability();
@@ -603,11 +650,16 @@ class TTSFactoryModule extends BaseModule {
persistenceManager.updatePreference('tts', 'preferred_handler', 'none');
}
// Dispatch event
// Dispatch events
document.dispatchEvent(new CustomEvent('tts:handler:changed', {
detail: { handler: 'none', available: false }
}));
// Also dispatch tts:engine:change for compatibility with Options UI
document.dispatchEvent(new CustomEvent('tts:engine:change', {
detail: { engine: 'none', handler: 'none', available: false }
}));
this.updateTTSAvailability();
return true;
}
@@ -629,17 +681,17 @@ class TTSFactoryModule extends BaseModule {
console.log(`TTS Factory: Setting active handler to ${id}`);
// Check if the handler is ready (just for logging)
if (this.handlers[id].isReady !== true) {
console.log(`TTS Factory: Initializing handler ${id} before activation`);
await this.initializeHandler(id);
}
// Check if the handler is ready after initialization
const isReady = this.handlers[id].isReady === true;
if (!isReady) {
console.warn(`TTS Factory: Handler ${id} is not ready - TTS will be considered disabled until ready`);
}
// Stop any current speech
if (this.activeHandler && this.handlers[this.activeHandler]) {
this.handlers[this.activeHandler].stop();
}
// Set the new active handler
this.activeHandler = id;
@@ -647,17 +699,34 @@ class TTSFactoryModule extends BaseModule {
const persistenceManager = this.getModule('persistence-manager');
if (persistenceManager) {
persistenceManager.updatePreference('tts', 'preferred_handler', id);
this.voice = persistenceManager.getPreference('tts', 'voice', this.voice || '');
this.language = persistenceManager.getPreference('tts', 'language', this.language || 'en-us');
this.speed = persistenceManager.getPreference('tts', 'speed', this.speed || 1.0);
}
const handler = this.handlers[id];
if (handler && typeof handler.setVoiceOptions === 'function') {
handler.setVoiceOptions({
voice: this.voice,
speed: this.speed,
language: this.language
});
}
// Dispatch event
// Dispatch events
const event = new CustomEvent('tts:handler:changed', {
detail: { handler: id, available: isReady }
});
document.dispatchEvent(event);
// Also dispatch tts:engine:change for compatibility with Options UI
document.dispatchEvent(new CustomEvent('tts:engine:change', {
detail: { engine: id, handler: id, available: isReady }
}));
// Update overall TTS availability
this.updateTTSAvailability();
return true;
}
@@ -725,7 +794,7 @@ class TTSFactoryModule extends BaseModule {
const preloadData = await handler.preloadSpeech(text);
if (preloadData && preloadData.success) {
// Cache the speech
await this.cacheSpeech(hash, preloadData);
await this.cacheSpeech(hash, preloadData.audioData, preloadData.duration);
// Speak the preloaded speech
return handler.speakPreloaded(preloadData, result => {
@@ -790,13 +859,13 @@ class TTSFactoryModule extends BaseModule {
// If the handler has a preloadSpeech method, use it
if (typeof this.handlers[this.activeHandler].preloadSpeech === 'function') {
const preloadData = await this.handlers[this.activeHandler].preloadSpeech(text);
// Cache the generated speech data
if (preloadData) {
await this.cacheSpeech(hash, preloadData);
// Cache the generated speech data (extract audioData from result object)
if (preloadData && preloadData.audioData) {
await this.cacheSpeech(hash, preloadData.audioData, preloadData.duration);
console.log(`TTS Factory: Added speech to cache for hash ${hash} (size: ${this.currentCacheSize}/${this.maxCacheSizeBytes})`);
}
return preloadData;
} else {
console.warn(`TTS Factory: Handler ${this.activeHandler} does not support preloading`);
@@ -862,6 +931,24 @@ class TTSFactoryModule extends BaseModule {
return false;
}
}
fadeOut(duration = 1000) {
const handlers = Object.values(this.handlers);
const fades = handlers.map(handler => {
if (!handler) return Promise.resolve(false);
if (typeof handler.fadeOutCurrentAudio === 'function') {
return handler.fadeOutCurrentAudio(duration);
} else if (handler.isSpeaking && typeof handler.stop === 'function') {
return new Promise(resolve => {
setTimeout(() => {
resolve(handler.stop());
}, duration);
});
}
return Promise.resolve(false);
});
return Promise.all(fades);
}
/**
* Get voices from the active handler
@@ -957,41 +1044,170 @@ class TTSFactoryModule extends BaseModule {
document.dispatchEvent(event);
}
}
disableAfterCurrentPlayback() {
const previousHandler = this.activeHandler;
if (previousHandler) {
const persistenceManager = this.getModule('persistence-manager');
if (persistenceManager) {
persistenceManager.updatePreference('tts', 'last_enabled_handler', previousHandler);
}
}
this.activeHandler = null;
console.log('TTS Factory: TTS disabled for future generation; current playback may finish');
document.dispatchEvent(new CustomEvent('tts:handler:changed', {
detail: { handler: 'none', available: false, previousHandler }
}));
document.dispatchEvent(new CustomEvent('tts:engine:change', {
detail: { engine: 'none', handler: 'none', available: false, previousHandler }
}));
this.updateTTSAvailability();
return true;
}
getHandlerStatuses() {
const statuses = [{
id: 'none',
name: 'None',
ready: true,
active: !this.activeHandler,
message: 'Text-only mode'
}];
for (const id in this.handlers) {
const handler = this.handlers[id];
statuses.push({
id,
name: typeof handler.getName === 'function' ? handler.getName() : id,
ready: handler.isReady === true,
active: this.activeHandler === id,
message: this.getHandlerStatusMessage(id, handler)
});
}
return statuses;
}
getHandlerStatusMessage(id, handler) {
if (!handler) return 'Not registered';
if (handler.isReady === true) return 'Ready';
if (id === 'kokoro-tts') return handler.state === 'INITIALIZING' ? 'Loading model' : 'Not loaded';
if (handler.apiKey === '') return 'API key missing';
if (handler.apiKey && handler.isReady !== true) return 'API unavailable or invalid settings';
return 'Not ready';
}
async refreshHandlerStatus(id) {
if (!id || id === 'none') {
this.updateTTSAvailability();
return true;
}
if (!this.handlers[id]) {
return false;
}
const handler = this.handlers[id];
if (id === 'kokoro-tts') {
this.updateTTSAvailability();
return handler.isReady === true;
}
const success = await this.initializeHandler(id);
this.updateTTSAvailability();
document.dispatchEvent(new CustomEvent('tts:status:updated', {
detail: { statuses: this.getHandlerStatuses() }
}));
return success;
}
async getVoicesForHandler(handlerId) {
if (!handlerId || handlerId === 'none' || !this.handlers[handlerId]) {
return [];
}
const handler = this.handlers[handlerId];
if (handler.isReady !== true && handlerId !== 'kokoro-tts') {
await this.initializeHandler(handlerId);
}
if (typeof handler.getVoices === 'function') {
return await handler.getVoices() || [];
}
return Array.isArray(handler.voices) ? handler.voices : [];
}
/**
* Configure TTS settings for all handlers
* @param {Object} options - TTS options
* @param {number} [options.speed] - Normalized speech rate (0-1 range)
* @param {number} [options.speed] - Speech speed multiplier
*/
configure(options = {}) {
if (!options || typeof options !== 'object') {
return;
}
const persistenceManager = this.getModule('persistence-manager');
const voiceOptions = {};
// Handle speed option
if (typeof options.speed === 'number') {
// Save speed setting
this.speed = Math.max(0.1, Math.min(3.0, options.speed));
// Save to preferences
const persistenceManager = this.getModule('persistence-manager');
this.speed = Math.max(0.5, Math.min(2.0, options.speed));
voiceOptions.speed = this.speed;
if (persistenceManager) {
persistenceManager.updatePreference('tts', 'speed', this.speed);
}
// Update all handlers
for (const id in this.handlers) {
const handler = this.handlers[id];
if (handler && typeof handler.setVoiceOptions === 'function') {
handler.setVoiceOptions({ speed: this.speed });
}
if (typeof options.voice === 'string' && options.voice) {
this.voice = options.voice;
voiceOptions.voice = options.voice;
if (persistenceManager) {
persistenceManager.updatePreference('tts', 'voice', options.voice);
if (this.activeHandler) {
persistenceManager.updatePreference('tts', `${this.activeHandler}_voice`, options.voice);
}
}
}
if (typeof options.language === 'string' && options.language) {
this.language = options.language.toLowerCase();
voiceOptions.language = this.language;
if (persistenceManager) {
persistenceManager.updatePreference('tts', 'language', this.language);
}
}
if (typeof options.volume === 'number') {
this.volume = Math.max(0, Math.min(1, options.volume));
voiceOptions.volume = this.volume;
if (persistenceManager) {
persistenceManager.updatePreference('tts', 'volume', this.volume);
}
}
for (const id in this.handlers) {
const handler = this.handlers[id];
if (handler && typeof handler.setVoiceOptions === 'function') {
handler.setVoiceOptions(voiceOptions);
} else if (handler && typeof handler.configure === 'function') {
handler.configure(voiceOptions);
}
if (voiceOptions.language && !voiceOptions.voice && handler && typeof handler.selectVoiceForLocale === 'function') {
handler.selectVoiceForLocale(voiceOptions.language);
}
}
// Update UI that TTS settings have changed
document.dispatchEvent(new CustomEvent('tts:configured', {
detail: {
options: { speed: this.speed },
options: {
speed: this.speed,
voice: this.voice,
language: this.language,
volume: this.volume
},
activeHandler: this.activeHandler
}
}));
@@ -1016,6 +1232,7 @@ class TTSFactoryModule extends BaseModule {
const cachedData = await this.getCachedSpeech(hash);
if (cachedData) {
console.log(`TTS Factory: Using cached speech for hash ${hash} (hits: ${this.cacheHits}, misses: ${this.cacheMisses})`);
this.emitProcessState('playing-ready', { reason: 'tts-cache-hit', hash });
// Move this item to the end of the Map to mark it as most recently used
// this.audioCache.delete(hash);
// this.audioCache.set(hash, cachedData);
@@ -1025,17 +1242,19 @@ class TTSFactoryModule extends BaseModule {
// Cache miss - need to generate new speech data
this.cacheMisses++;
this.emitProcessState('waiting-generating', { reason: 'tts-cache-miss', hash });
// If the handler has a preloadSpeech method, use it
if (typeof this.handlers[this.activeHandler].preloadSpeech === 'function') {
const preloadData = await this.handlers[this.activeHandler].preloadSpeech(text);
// Cache the generated speech data
if (preloadData) {
await this.cacheSpeech(hash, preloadData);
// Cache the generated speech data (extract audioData from result object)
if (preloadData && preloadData.audioData) {
await this.cacheSpeech(hash, preloadData.audioData, preloadData.duration);
console.log(`TTS Factory: Added speech to cache for hash ${hash} (size: ${this.currentCacheSize}/${this.maxCacheSizeBytes})`);
this.emitProcessState('playing-ready', { reason: 'tts-generated', hash });
}
return preloadData;
} else {
console.warn(`TTS Factory: Handler ${this.activeHandler} does not support preloading`);
@@ -1053,21 +1272,18 @@ class TTSFactoryModule extends BaseModule {
* @returns {Promise<string>} - Hash string
*/
async generateSpeechHash(text) {
// Get the active handler for voice information
const handler = this.getActiveHandler();
// Include handler ID and voice options in the hash to ensure uniqueness across voices
let voiceInfo = '';
if (handler && handler.voiceOptions && handler.voiceOptions.voice) {
// Use the voice ID or name to identify the voice
voiceInfo = handler.voiceOptions.voice.id || handler.voiceOptions.voice;
}
// Also include speed setting in the hash
const provider = this.activeHandler || 'none';
const voiceInfo = this.getEffectiveVoiceId(handler);
const speed = this.speed || 1.0;
// Create a composite key for hashing
const key = `${text}|${this.activeHandler}|${voiceInfo}|${speed}`;
const language = this.language || 'en-us';
const key = JSON.stringify({
provider,
voice: voiceInfo,
speed,
language,
text
});
try {
const encoder = new TextEncoder();
@@ -1117,17 +1333,22 @@ class TTSFactoryModule extends BaseModule {
console.warn('TTS Factory: Cache not ready, cannot retrieve cached speech');
return null;
}
try {
const item = await this._getDBItem(hash);
if (item && item.audioData) {
console.log(`TTS Factory: Found cached speech for hash ${hash}`);
return item.audioData;
// Return in the same format as handlers' preloadSpeech() method
return {
success: true,
audioData: item.audioData,
duration: item.duration || 0
};
}
} catch (error) {
console.error('TTS Factory: Error retrieving cached speech:', error);
}
return null;
}
@@ -1137,24 +1358,26 @@ class TTSFactoryModule extends BaseModule {
* @param {ArrayBuffer} audioData - Audio data to cache
* @returns {Promise<boolean>} - Success status
*/
async cacheSpeech(hash, audioData) {
async cacheSpeech(hash, audioData, duration = 0) {
if (!this.db || this.cacheStatus !== 'ready') {
console.warn('TTS Factory: Cache not ready, cannot cache speech');
return false;
}
if (!audioData) {
console.error('TTS Factory: No audio data provided to cache');
return false;
}
console.log(`TTS Factory: cacheSpeech called with audioData:`, audioData ? `${audioData.byteLength} bytes` : 'UNDEFINED', 'duration:', duration);
try {
// Make sure we have room in the cache
await this.manageCacheSize(audioData.byteLength);
// Store the speech data
await this._putDBItem(hash, audioData);
// Store the speech data with duration
await this._putDBItem(hash, audioData, duration);
console.log(`TTS Factory: Cached speech for hash ${hash}`);
return true;
} catch (error) {
@@ -1243,6 +1466,20 @@ class TTSFactoryModule extends BaseModule {
return false;
}
}
getEffectiveVoiceId(handler = this.getActiveHandler()) {
if (!handler) return this.voice || '';
const voice = handler.voiceOptions?.voice || handler.currentVoice || this.voice || '';
if (typeof voice === 'string') return voice;
return voice.id || voice.name || '';
}
emitProcessState(state, detail = {}) {
console.log(`Process state: ${state}`, detail);
document.dispatchEvent(new CustomEvent('story:process-state', {
detail: { state, ...detail }
}));
}
/**
* Opens and initializes the IndexedDB database.
@@ -1365,9 +1602,10 @@ class TTSFactoryModule extends BaseModule {
* Adds or updates an item in the IndexedDB store.
* @param {string} hash - The key (hash) of the item to store.
* @param {ArrayBuffer} audioData - The audio data to cache.
* @param {number} duration - Duration of the audio in milliseconds.
* @returns {Promise<void>}
*/
async _putDBItem(hash, audioData) {
async _putDBItem(hash, audioData, duration = 0) {
if (!this.db || this.cacheStatus !== 'ready') {
console.warn("IndexedDB not ready, cannot put item.");
return Promise.reject(new Error("IndexedDB not ready"));
@@ -1380,7 +1618,7 @@ class TTSFactoryModule extends BaseModule {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.put({ hash, audioData, size: audioData.byteLength, lastAccessed: Date.now() });
const request = store.put({ hash, audioData, size: audioData.byteLength, duration: duration || 0, lastAccessed: Date.now() });
request.onerror = (event) => {
console.error("Error putting item into IndexedDB:", event.target.error);
@@ -1459,8 +1697,27 @@ class TTSFactoryModule extends BaseModule {
if (typeof cursor.value.size === 'number') {
totalSize += cursor.value.size;
} else {
console.warn(`Item with hash ${cursor.key} missing or invalid size property.`);
// Optionally try to get blob size here, but might be slow
// Old cache entry without size - calculate it
if (cursor.value.audioData && cursor.value.audioData.byteLength) {
const calculatedSize = cursor.value.audioData.byteLength;
totalSize += calculatedSize;
// Update the entry with the size property
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const updatedValue = {
...cursor.value,
size: calculatedSize
};
store.put(updatedValue);
console.log(`Updated cache entry ${cursor.key} with size: ${calculatedSize} bytes`);
} else {
console.warn(`Item with hash ${cursor.key} missing size and cannot calculate - will be deleted`);
// Delete invalid entry
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
store.delete(cursor.key);
}
}
cursor.continue();
} else {
@@ -1593,8 +1850,11 @@ class TTSFactoryModule extends BaseModule {
}
}
// If we couldn't initialize the preferred handler, try fallbacks
return this.attemptFallbackHandler();
// Default to no TTS. Games or users can explicitly select a provider later.
console.log('TTS Factory: No preferred TTS handler selected, defaulting to none');
this.activeHandler = null;
this.updateTTSAvailability();
return true;
}
/**
@@ -1602,8 +1862,8 @@ class TTSFactoryModule extends BaseModule {
* @returns {Promise<boolean>} - Success status
*/
async attemptFallbackHandler() {
// Fallback order: Kokoro -> Browser -> None
const fallbackOrder = ['kokoro', 'browser'];
// Providers are opt-in. Keep the baseline as text-only unless explicitly selected.
const fallbackOrder = [];
// Try each fallback in order
for (const handlerId of fallbackOrder) {
@@ -1622,8 +1882,8 @@ class TTSFactoryModule extends BaseModule {
}
}
// If all fallbacks failed, update TTS availability
console.warn('TTS Factory: All handlers failed to initialize, TTS will be unavailable');
// If no explicit provider is selected, update TTS availability and continue.
console.log('TTS Factory: No fallback handler selected, TTS will be unavailable');
this.activeHandler = null;
this.updateTTSAvailability();
@@ -1737,4 +1997,4 @@ class TTSFactoryModule extends BaseModule {
const TTSFactory = new TTSFactoryModule();
// Export the module
export { TTSFactory };
export { TTSFactory };