Checkpoint current interactive fiction state
This commit is contained in:
+351
-91
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user