Fixed kokoro loading process.

This commit is contained in:
2025-04-07 06:51:45 +00:00
parent 0842cbfefc
commit b1387f4833
13 changed files with 905 additions and 789 deletions
+15 -2
View File
@@ -13,6 +13,7 @@ class AudioManagerModule extends BaseModule {
this.masterVolume = 1.0;
this.musicVolume = 1.0;
this.sfxVolume = 1.0;
this.ttsVolume = 1.0;
// Add persistence-manager as a dependency
this.dependencies = ['persistence-manager'];
@@ -193,7 +194,19 @@ class AudioManagerModule extends BaseModule {
this.masterVolume = Math.max(0, Math.min(1, volume));
this.updateVolumes();
}
/**
* Set the speech volume
* @param {number} volume - The volume level (0.0 to 1.0)
*/
setTtsVolume(volume) {
this.ttsVolume = Math.max(0, Math.min(1, volume));
// Apply to current non-loop audio if it exists
if (this.currentAudio) {
this.currentAudio.volume = this.masterVolume * this.ttsVolume;
}
}
/**
* Set the music volume
* @param {number} volume - The volume level (0.0 to 1.0)
@@ -318,7 +331,7 @@ class AudioManagerModule extends BaseModule {
}
// Apply master volume and speech volume
audio.volume = this.masterVolume * speechVolume;
audio.volume = this.masterVolume * speechVolume * this._ttsVolume;
// Set up cleanup
audio.onended = () => {
+1 -1
View File
@@ -27,7 +27,7 @@ export class BrowserTTSModule extends TTSHandlerModule {
this.currentUtterance = null;
// Bind additional methods
this.bindMethods(['onVoicesChanged', 'handleVoicePreferenceChanged']);
this.bindMethods(['handleVoicePreferenceChanged']);
}
/**
+7 -8
View File
@@ -41,7 +41,6 @@ export class KokoroTTSModule extends TTSHandlerModule {
async initialize() {
try {
console.log('Kokoro TTS: Initializing');
this.state = 'INITIALIZING';
// Get dependencies
this.reportProgress(10, 'Loading dependencies');
@@ -195,21 +194,21 @@ export class KokoroTTSModule extends TTSHandlerModule {
case 'kokoro:error':
console.error('Kokoro TTS: Error from iframe:', event.data.error);
this.state = 'ERROR';
// this.changeState('ERROR');
break;
case 'kokoro:speech-generated':
case 'kokoro-generated':
// Handle speech generation completion
if (event.data.id !== undefined && this.pendingGenerations.has(event.data.id)) {
const resolver = this.pendingGenerations.get(event.data.id);
this.pendingGenerations.delete(event.data.id);
if (event.data.error) {
resolver.reject(new Error(event.data.error));
if (!event.data.success || event.data.error) {
resolver.reject(new Error(event.data.error || 'Speech generation failed'));
} else {
resolver.resolve({
success: true,
audioData: event.data.audioData,
audioData: event.data.result && event.data.result.buffer,
duration: event.data.duration || 0
});
}
@@ -541,10 +540,10 @@ export class KokoroTTSModule extends TTSHandlerModule {
// Send request to iframe
this.iframe.contentWindow.postMessage({
type: 'kokoro:generate-speech',
type: 'kokoro-generate',
text: processedText,
id,
voiceId: this.currentVoice ? this.currentVoice.id : null
voice: this.currentVoice ? this.currentVoice.id : null
}, '*');
});
}
+1 -14
View File
@@ -51,20 +51,7 @@ const ModuleLoader = (function() {
}
console.log('Module Loader: Initialization started');
// Check for circular dependencies before proceeding
const circularDependencies = moduleRegistry.checkForCircularDependencies();
if (circularDependencies) {
const errorMsg = `Circular dependency detected: ${circularDependencies.join(' -> ')} -> ${circularDependencies[0]}`;
console.error(errorMsg);
document.body.innerHTML = `<div style="padding: 20px; color: white; background-color: #ff3333;">
<h2>Fatal Error: Circular Module Dependency</h2>
<p>${errorMsg}</p>
<p>Please check the browser console for more details.</p>
</div>`;
return;
}
// Create the loading overlay
createLoadingOverlay();
+1 -102
View File
@@ -7,9 +7,6 @@ export class ModuleRegistry {
this.modules = {};
this.readyPromises = {};
this.moduleDependencies = new Map(); // Track module dependencies
this.visitedModules = new Set(); // For circular dependency detection
this.recursionStack = new Set(); // For circular dependency detection
this.untrackedDependencies = new Map(); // Track unregistered dependencies
}
/**
@@ -42,16 +39,6 @@ export class ModuleRegistry {
this.moduleDependencies.set(module.id, []);
}
// Check for circular dependencies
this.visitedModules.clear();
this.recursionStack.clear();
const circularDependency = this.detectCircularDependency(module.id);
if (circularDependency) {
const errorMsg = `Circular dependency detected: ${circularDependency.join(' -> ')} -> ${circularDependency[0]}`;
console.error(errorMsg);
throw new Error(errorMsg);
}
// Create a promise that will resolve when this module is ready
this.readyPromises[module.id] = new Promise((resolve) => {
// Set up a state change listener for this module
@@ -70,77 +57,7 @@ export class ModuleRegistry {
}
});
}
/**
* Detect circular dependencies using DFS algorithm
* @param {string} moduleId - Starting module ID
* @param {Array<string>} [path=[]] - Current dependency path
* @returns {Array<string>|null} - Array representing the circular dependency path, or null if none
*/
detectCircularDependency(moduleId, path = []) {
// If we've already checked this module completely, no need to check again
if (this.visitedModules.has(moduleId)) {
return null;
}
// If we're already visiting this module in the current path, we found a cycle
if (this.recursionStack.has(moduleId)) {
// Return the path that forms the cycle
const cycleStartIndex = path.indexOf(moduleId);
if (cycleStartIndex >= 0) {
return path.slice(cycleStartIndex);
}
return path;
}
// Add to recursion stack to mark as being visited
this.recursionStack.add(moduleId);
path.push(moduleId);
// Get dependencies for this module
const dependencies = this.getDependencies(moduleId);
// Check each dependency
for (const depId of dependencies) {
// Even if the dependency isn't registered yet, we need to track it
// for potential circular dependencies that will manifest later
// Create a temporary placeholder in the path for unregistered dependencies
const depPath = [...path];
if (!this.modules[depId]) {
// Log that we're tracking an unregistered dependency
console.log(`Module Registry: Tracking potential circular dependency with unregistered module: ${depId}`);
// Add to the dependency tracking for future checks
this.trackDependency(moduleId, depId);
continue;
}
const result = this.detectCircularDependency(depId, depPath);
if (result) {
return result;
}
}
// Remove from recursion stack as we're done with this module
this.recursionStack.delete(moduleId);
// Mark as fully visited
this.visitedModules.add(moduleId);
return null;
}
/**
* Track an unregistered dependency
* @param {string} moduleId - Module ID
* @param {string} depId - Unregistered dependency ID
*/
trackDependency(moduleId, depId) {
if (!this.untrackedDependencies.has(moduleId)) {
this.untrackedDependencies.set(moduleId, new Set());
}
this.untrackedDependencies.get(moduleId).add(depId);
}
/**
* Get a module by id
* @param {string} id - Module id
@@ -166,25 +83,7 @@ export class ModuleRegistry {
getDependencies(id) {
return this.moduleDependencies.get(id) || [];
}
/**
* Check if the dependency graph has any circular dependencies
* @returns {Array<string>|null} - Array representing the circular dependency path, or null if none
*/
checkForCircularDependencies() {
this.visitedModules.clear();
for (const moduleId in this.modules) {
this.recursionStack.clear();
const result = this.detectCircularDependency(moduleId);
if (result) {
return result;
}
}
return null;
}
/**
* Wait for a module to be ready (in FINISHED state)
* @param {string} id - Module id to wait for
+15 -3
View File
@@ -11,7 +11,7 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
// Voice options specific to OpenAI
this.voiceOptions = {
voice: 'alloy', // Default voice for OpenAI
model: 'tts-1', // Standard model
model: 'tts-1-hd', // Standard model
speed: 1.0,
response_format: 'mp3' // OpenAI supports mp3, opus, aac, and flac (not wav)
};
@@ -19,11 +19,16 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
// Predefined voices - OpenAI has a fixed set
this.voices = [
{ id: 'alloy', name: 'Alloy', language: 'en' },
{ id: 'ash', name: 'Ash', language: 'en' },
{ id: 'ballad', name: 'Ballad', language: 'en' },
{ id: 'coral', name: 'Coral', language: 'en' },
{ id: 'echo', name: 'Echo', language: 'en' },
{ id: 'fable', name: 'Fable', language: 'en' },
{ id: 'onyx', name: 'Onyx', language: 'en' },
{ id: 'nova', name: 'Nova', language: 'en' },
{ id: 'shimmer', name: 'Shimmer', language: 'en' }
{ id: 'sage', name: 'Sage', language: 'en' },
{ id: 'shimmer', name: 'Shimmer', language: 'en' },
{ id: 'verse', name: 'Verse', language: 'en' }
];
}
@@ -208,7 +213,14 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
}
if (typeof options.speed === 'number') {
this.voiceOptions.speed = Math.max(0.5, Math.min(2.0, options.speed));
// OpenAI API supports speed values from 0.25 to 4.0 with 1 as default
if (options.speed <= 0.5) {
// Map [0, 0.5] -> [0.25, 1]
this.voiceOptions.speed = 0.25 + (1 - 0.25) * (options.speed / 0.5);
} else {
// Map [0.5, 1] -> [1, 4]
this.voiceOptions.speed = 1 + (4 - 1) * ((options.speed - 0.5) / 0.5);
}
}
// Handle OpenAI-specific options
File diff suppressed because it is too large Load Diff
+209 -11
View File
@@ -31,8 +31,12 @@ class PersistenceManagerModule extends BaseModule {
this.defaultPreferences = {
tts: {
enabled: false,
provider: 'none',
preferred_handler: 'none',
voice: '',
'elevenlabs-tts_api_key': '',
'elevenlabs-tts_api_url': 'https://api.elevenlabs.io/v1',
'openai-tts_api_key': '',
'openai-tts_api_url': 'https://api.openai.com/v1'
},
audio: {
masterVolume: 1.0,
@@ -58,7 +62,11 @@ class PersistenceManagerModule extends BaseModule {
'createSaveSlot',
'loadSaveSlot',
'deleteSaveSlot',
'getAllSaveSlots'
'getAllSaveSlots',
'createBinding',
'updateElementFromPreference',
'updatePreferenceFromElement',
'setupBindings'
]);
// Remove circular dependency
@@ -246,19 +254,14 @@ class PersistenceManagerModule extends BaseModule {
* @returns {boolean} - Success status
*/
updatePreference(category, setting, value) {
if (!category || !setting) return false;
if (!this.preferences) return false;
// Ensure preferences are loaded
if (!this.preferences) {
this.loadPreferences();
}
// Create category if it doesn't exist
// Ensure category exists
if (!this.preferences[category]) {
this.preferences[category] = {};
}
// Update preference
// Store value
this.preferences[category][setting] = value;
// Save preferences
@@ -268,7 +271,7 @@ class PersistenceManagerModule extends BaseModule {
// Dispatch event
this.dispatchEvent('preference-updated', {
category,
setting,
key: setting,
value,
timestamp: new Date().toISOString()
});
@@ -469,6 +472,201 @@ class PersistenceManagerModule extends BaseModule {
return this.saveSlots;
}
/**
* Create a binding between a DOM element and a preference
* @param {HTMLElement} element - Element to bind to
* @param {string} category - Preference category
* @param {string} key - Preference key
* @param {Object} options - Additional options (transformers, etc)
* @returns {Object} - Binding control object
*/
createBinding(element, category, key, options = {}) {
if (!element) return null;
const transformer = options.transformer || {
toElement: (value) => value,
toPreference: (value) => value
};
// Store binding info on the element
element._prefBinding = { category, key, transformer };
// Set initial value
this.updateElementFromPreference(element);
// Set up event listeners
const eventHandler = () => this.updatePreferenceFromElement(element);
// Choose appropriate events based on element type
let events = ['change'];
if (element.type !== 'checkbox' && element.type !== 'radio' && element.tagName !== 'SELECT') {
events.push('input');
}
// Attach event listeners
events.forEach(event => {
element.addEventListener(event, eventHandler);
});
// Return control object
return {
update: () => this.updateElementFromPreference(element),
destroy: () => {
events.forEach(event => {
element.removeEventListener(event, eventHandler);
});
delete element._prefBinding;
}
};
}
/**
* Update an element value from its bound preference
* @param {HTMLElement} element - The bound element
*/
updateElementFromPreference(element) {
if (!element || !element._prefBinding) return;
const { category, key, transformer } = element._prefBinding;
const value = this.getPreference(category, key);
const transformedValue = transformer.toElement(value);
// Set element value based on its type
if (element.type === 'checkbox') {
element.checked = !!transformedValue;
} else if (element.type === 'radio') {
element.checked = element.value === String(transformedValue);
} else if (element.tagName === 'SELECT') {
element.value = transformedValue;
} else {
element.value = transformedValue;
}
}
/**
* Update a preference from its bound element
* @param {HTMLElement} element - The bound element
*/
updatePreferenceFromElement(element) {
if (!element || !element._prefBinding) return;
const { category, key, transformer } = element._prefBinding;
let value;
// Get element value based on its type
if (element.type === 'checkbox') {
value = element.checked;
} else if (element.type === 'radio') {
value = element.checked ? element.value : null;
} else {
value = element.value;
}
const transformedValue = transformer.toPreference(value);
this.updatePreference(category, key, transformedValue);
}
/**
* Set up bindings for all elements with data-pref-bind attributes
* @param {string} rootSelector - Root selector to search within
* @returns {Array} - Array of binding control objects
*/
setupBindings(rootSelector = 'body') {
const root = document.querySelector(rootSelector);
if (!root) return [];
const bindings = [];
const elements = root.querySelectorAll('[data-pref-bind]');
elements.forEach(element => {
const bindingStr = element.dataset.prefBind;
if (!bindingStr) return;
const [category, key] = bindingStr.split('.');
if (!category || !key) return;
// Parse transformer if specified
let transformer = {
toElement: (value) => value,
toPreference: (value) => value
};
// Handle range transformations
if (element.type === 'range' && element.hasAttribute('min') && element.hasAttribute('max')) {
const min = parseInt(element.getAttribute('min'), 10) || 0;
const max = parseInt(element.getAttribute('max'), 10) || 100;
transformer = {
toElement: (value) => {
// Convert from 0-1 to min-max
return Math.round(value * (max - min) + min);
},
toPreference: (value) => {
// Convert from min-max to 0-1
return (parseInt(value, 10) - min) / (max - min);
}
};
}
// Custom transformer via data attribute
if (element.dataset.prefTransform) {
try {
// Check if it's a range transformer in format 'range:min,max'
if (element.dataset.prefTransform.startsWith('range:')) {
const rangeValues = element.dataset.prefTransform.substring(6).split(',');
if (rangeValues.length === 2) {
const min = parseFloat(rangeValues[0]);
const max = parseFloat(rangeValues[1]);
if (!isNaN(min) && !isNaN(max)) {
transformer = {
toElement: (value) => {
// Convert from min-max to 0-100 for the slider
return Math.round(((value - min) / (max - min)) * 100);
},
toPreference: (value) => {
// Convert from 0-100 to min-max
return min + (parseInt(value, 10) / 100) * (max - min);
}
};
}
}
} else {
// Try to parse as JSON for backward compatibility
const customTransformer = JSON.parse(element.dataset.prefTransform);
if (customTransformer && typeof customTransformer === 'object') {
transformer = customTransformer;
}
}
} catch (e) {
console.warn('Invalid transformer data attribute', e);
}
}
const binding = this.createBinding(element, category, key, { transformer });
if (binding) {
bindings.push(binding);
}
});
// Set up event listener for preference changes from other sources
document.addEventListener('preference-updated', (event) => {
const { category, key } = event.detail;
// Update any matching elements
elements.forEach(element => {
if (!element._prefBinding) return;
if (element._prefBinding.category === category &&
element._prefBinding.key === key) {
this.updateElementFromPreference(element);
}
});
});
return bindings;
}
/**
* Clean up when module is disposed
*/
+35 -3
View File
@@ -676,9 +676,9 @@ class TTSFactoryModule extends BaseModule {
* Speak text using the active TTS handler
* @param {string} text - Text to speak
* @param {Object} options - TTS options
* @returns {boolean} - Success status
* @returns {Promise<boolean>} - Success status
*/
speak(text, options = {}) {
async speak(text, options = {}) {
// Check if we have an active handler
if (!this.activeHandler) {
console.warn('TTS Factory: No active handler set');
@@ -705,7 +705,39 @@ class TTSFactoryModule extends BaseModule {
effectiveOptions.speed = this.speed;
}
// Call the handler's speak method
// Check if we have this speech cached
const hash = await this.generateSpeechHash(text);
const cached = await this.getCachedSpeech(hash);
if (cached && cached.success) {
console.log(`TTS Factory: Using cached speech for hash ${hash}`);
// Use cached speech
return handler.speakPreloaded(cached, result => {
document.dispatchEvent(new CustomEvent('tts:speechCompleted', {
detail: { success: result?.success === true, error: result?.error }
}));
});
}
// Not cached, generate and cache
if (typeof handler.preloadSpeech === 'function') {
console.log(`TTS Factory: Generating and caching speech for hash ${hash}`);
const preloadData = await handler.preloadSpeech(text);
if (preloadData && preloadData.success) {
// Cache the speech
await this.cacheSpeech(hash, preloadData);
// Speak the preloaded speech
return handler.speakPreloaded(preloadData, result => {
document.dispatchEvent(new CustomEvent('tts:speechCompleted', {
detail: { success: result?.success === true, error: result?.error }
}));
});
}
}
// Fallback to direct speak if preloading failed or not supported
console.log(`TTS Factory: Falling back to direct speak (no caching)`);
return handler.speak(text, result => {
// Forward speech completion event
document.dispatchEvent(new CustomEvent('tts:speechCompleted', {