Fixed kokoro loading process.
This commit is contained in:
+16
-16
@@ -723,7 +723,7 @@ ol.choice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Options Modal Styling */
|
/* Options Modal Styling */
|
||||||
.options-modal {
|
.modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -736,7 +736,7 @@ ol.choice {
|
|||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-content {
|
.modal-content {
|
||||||
background-color: rgba(255, 252, 245, 0.97);
|
background-color: rgba(255, 252, 245, 0.97);
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
box-shadow: 0 0 1.5rem rgba(0, 0, 0, 0.4);
|
box-shadow: 0 0 1.5rem rgba(0, 0, 0, 0.4);
|
||||||
@@ -754,7 +754,7 @@ ol.choice {
|
|||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -763,7 +763,7 @@ ol.choice {
|
|||||||
padding-bottom: 0.7rem;
|
padding-bottom: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-header h2 {
|
.modal-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'EB Garamond', var(--book-font), serif;
|
font-family: 'EB Garamond', var(--book-font), serif;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -772,7 +772,7 @@ ol.choice {
|
|||||||
letter-spacing: 0.02rem;
|
letter-spacing: 0.02rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-close {
|
.close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
@@ -781,7 +781,7 @@ ol.choice {
|
|||||||
font-family: 'EB Garamond', var(--book-font), serif;
|
font-family: 'EB Garamond', var(--book-font), serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-close:hover {
|
.close:hover {
|
||||||
color: #5a3921;
|
color: #5a3921;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,7 +800,7 @@ ol.choice {
|
|||||||
color: #5a3921;
|
color: #5a3921;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-row {
|
.option-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -808,14 +808,14 @@ ol.choice {
|
|||||||
padding: 0.25rem 0;
|
padding: 0.25rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-row label {
|
.option-item label {
|
||||||
font-family: 'EB Garamond', var(--book-font), serif;
|
font-family: 'EB Garamond', var(--book-font), serif;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: #4a4234;
|
color: #4a4234;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Elegant checkboxes */
|
/* Elegant checkboxes */
|
||||||
.options-row input[type="checkbox"] {
|
.option-item input[type="checkbox"] {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
@@ -829,7 +829,7 @@ ol.choice {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-row input[type="checkbox"]:checked::before {
|
.option-item input[type="checkbox"]:checked::before {
|
||||||
content: "✓";
|
content: "✓";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-family: 'EB Garamond', var(--book-font), serif;
|
font-family: 'EB Garamond', var(--book-font), serif;
|
||||||
@@ -840,7 +840,7 @@ ol.choice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Elegant select dropdowns */
|
/* Elegant select dropdowns */
|
||||||
.options-row select {
|
.option-item select {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -858,12 +858,12 @@ ol.choice {
|
|||||||
min-width: 8rem;
|
min-width: 8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-row select:focus {
|
.option-item select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Range inputs (sliders) - match the main menu style */
|
/* Range inputs (sliders) - match the main menu style */
|
||||||
.options-row input[type="range"] {
|
.option-item input[type="range"] {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 8rem;
|
width: 8rem;
|
||||||
@@ -878,7 +878,7 @@ ol.choice {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-row input[type="range"]::-webkit-slider-thumb {
|
.option-item input[type="range"]::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
@@ -889,7 +889,7 @@ ol.choice {
|
|||||||
box-shadow: -407px 0 0 400px rgba(0,0,0,0.3);
|
box-shadow: -407px 0 0 400px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-row input[type="range"]::-moz-range-thumb {
|
.option-item input[type="range"]::-moz-range-thumb {
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
width: 0.5rem;
|
width: 0.5rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
@@ -912,7 +912,7 @@ ol.choice {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* API Settings in Options Panel */
|
/* API Settings in options Panel */
|
||||||
.api-settings-container {
|
.api-settings-container {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|||||||
+7
-3
@@ -281,16 +281,20 @@
|
|||||||
<script>
|
<script>
|
||||||
// Redefine console.log to expose browser logs to model
|
// Redefine console.log to expose browser logs to model
|
||||||
const originalLog = console.log;
|
const originalLog = console.log;
|
||||||
console.log = function(message) {
|
console.log = function(...args) {
|
||||||
if (typeof debug !== 'undefined' && debug) {
|
if (typeof debug !== 'undefined' && debug) {
|
||||||
const debugContent = document.getElementById('debug-content');
|
const debugContent = document.getElementById('debug-content');
|
||||||
if (debugContent) {
|
if (debugContent) {
|
||||||
const logMsg = document.createElement('div');
|
const logMsg = document.createElement('div');
|
||||||
logMsg.textContent = message;
|
// Convert all arguments to string and join them
|
||||||
|
logMsg.textContent = args.map(arg =>
|
||||||
|
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
||||||
|
).join(' ');
|
||||||
debugContent.appendChild(logMsg);
|
debugContent.appendChild(logMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
originalLog(message);
|
// Pass all arguments to the original console.log
|
||||||
|
originalLog.apply(console, args);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="/js/loader.js"></script>
|
<script type="module" src="/js/loader.js"></script>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class AudioManagerModule extends BaseModule {
|
|||||||
this.masterVolume = 1.0;
|
this.masterVolume = 1.0;
|
||||||
this.musicVolume = 1.0;
|
this.musicVolume = 1.0;
|
||||||
this.sfxVolume = 1.0;
|
this.sfxVolume = 1.0;
|
||||||
|
this.ttsVolume = 1.0;
|
||||||
|
|
||||||
// Add persistence-manager as a dependency
|
// Add persistence-manager as a dependency
|
||||||
this.dependencies = ['persistence-manager'];
|
this.dependencies = ['persistence-manager'];
|
||||||
@@ -193,7 +194,19 @@ class AudioManagerModule extends BaseModule {
|
|||||||
this.masterVolume = Math.max(0, Math.min(1, volume));
|
this.masterVolume = Math.max(0, Math.min(1, volume));
|
||||||
this.updateVolumes();
|
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
|
* Set the music volume
|
||||||
* @param {number} volume - The volume level (0.0 to 1.0)
|
* @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
|
// Apply master volume and speech volume
|
||||||
audio.volume = this.masterVolume * speechVolume;
|
audio.volume = this.masterVolume * speechVolume * this._ttsVolume;
|
||||||
|
|
||||||
// Set up cleanup
|
// Set up cleanup
|
||||||
audio.onended = () => {
|
audio.onended = () => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export class BrowserTTSModule extends TTSHandlerModule {
|
|||||||
this.currentUtterance = null;
|
this.currentUtterance = null;
|
||||||
|
|
||||||
// Bind additional methods
|
// Bind additional methods
|
||||||
this.bindMethods(['onVoicesChanged', 'handleVoicePreferenceChanged']);
|
this.bindMethods(['handleVoicePreferenceChanged']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export class KokoroTTSModule extends TTSHandlerModule {
|
|||||||
async initialize() {
|
async initialize() {
|
||||||
try {
|
try {
|
||||||
console.log('Kokoro TTS: Initializing');
|
console.log('Kokoro TTS: Initializing');
|
||||||
this.state = 'INITIALIZING';
|
|
||||||
|
|
||||||
// Get dependencies
|
// Get dependencies
|
||||||
this.reportProgress(10, 'Loading dependencies');
|
this.reportProgress(10, 'Loading dependencies');
|
||||||
@@ -195,21 +194,21 @@ export class KokoroTTSModule extends TTSHandlerModule {
|
|||||||
|
|
||||||
case 'kokoro:error':
|
case 'kokoro:error':
|
||||||
console.error('Kokoro TTS: Error from iframe:', event.data.error);
|
console.error('Kokoro TTS: Error from iframe:', event.data.error);
|
||||||
this.state = 'ERROR';
|
// this.changeState('ERROR');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'kokoro:speech-generated':
|
case 'kokoro-generated':
|
||||||
// Handle speech generation completion
|
// Handle speech generation completion
|
||||||
if (event.data.id !== undefined && this.pendingGenerations.has(event.data.id)) {
|
if (event.data.id !== undefined && this.pendingGenerations.has(event.data.id)) {
|
||||||
const resolver = this.pendingGenerations.get(event.data.id);
|
const resolver = this.pendingGenerations.get(event.data.id);
|
||||||
this.pendingGenerations.delete(event.data.id);
|
this.pendingGenerations.delete(event.data.id);
|
||||||
|
|
||||||
if (event.data.error) {
|
if (!event.data.success || event.data.error) {
|
||||||
resolver.reject(new Error(event.data.error));
|
resolver.reject(new Error(event.data.error || 'Speech generation failed'));
|
||||||
} else {
|
} else {
|
||||||
resolver.resolve({
|
resolver.resolve({
|
||||||
success: true,
|
success: true,
|
||||||
audioData: event.data.audioData,
|
audioData: event.data.result && event.data.result.buffer,
|
||||||
duration: event.data.duration || 0
|
duration: event.data.duration || 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -541,10 +540,10 @@ export class KokoroTTSModule extends TTSHandlerModule {
|
|||||||
|
|
||||||
// Send request to iframe
|
// Send request to iframe
|
||||||
this.iframe.contentWindow.postMessage({
|
this.iframe.contentWindow.postMessage({
|
||||||
type: 'kokoro:generate-speech',
|
type: 'kokoro-generate',
|
||||||
text: processedText,
|
text: processedText,
|
||||||
id,
|
id,
|
||||||
voiceId: this.currentVoice ? this.currentVoice.id : null
|
voice: this.currentVoice ? this.currentVoice.id : null
|
||||||
}, '*');
|
}, '*');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-14
@@ -51,20 +51,7 @@ const ModuleLoader = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Module Loader: Initialization started');
|
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
|
// Create the loading overlay
|
||||||
createLoadingOverlay();
|
createLoadingOverlay();
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ export class ModuleRegistry {
|
|||||||
this.modules = {};
|
this.modules = {};
|
||||||
this.readyPromises = {};
|
this.readyPromises = {};
|
||||||
this.moduleDependencies = new Map(); // Track module dependencies
|
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, []);
|
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
|
// Create a promise that will resolve when this module is ready
|
||||||
this.readyPromises[module.id] = new Promise((resolve) => {
|
this.readyPromises[module.id] = new Promise((resolve) => {
|
||||||
// Set up a state change listener for this module
|
// 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
|
* Get a module by id
|
||||||
* @param {string} id - Module id
|
* @param {string} id - Module id
|
||||||
@@ -166,25 +83,7 @@ export class ModuleRegistry {
|
|||||||
getDependencies(id) {
|
getDependencies(id) {
|
||||||
return this.moduleDependencies.get(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)
|
* Wait for a module to be ready (in FINISHED state)
|
||||||
* @param {string} id - Module id to wait for
|
* @param {string} id - Module id to wait for
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
|
|||||||
// Voice options specific to OpenAI
|
// Voice options specific to OpenAI
|
||||||
this.voiceOptions = {
|
this.voiceOptions = {
|
||||||
voice: 'alloy', // Default voice for OpenAI
|
voice: 'alloy', // Default voice for OpenAI
|
||||||
model: 'tts-1', // Standard model
|
model: 'tts-1-hd', // Standard model
|
||||||
speed: 1.0,
|
speed: 1.0,
|
||||||
response_format: 'mp3' // OpenAI supports mp3, opus, aac, and flac (not wav)
|
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
|
// Predefined voices - OpenAI has a fixed set
|
||||||
this.voices = [
|
this.voices = [
|
||||||
{ id: 'alloy', name: 'Alloy', language: 'en' },
|
{ 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: 'echo', name: 'Echo', language: 'en' },
|
||||||
{ id: 'fable', name: 'Fable', language: 'en' },
|
{ id: 'fable', name: 'Fable', language: 'en' },
|
||||||
{ id: 'onyx', name: 'Onyx', language: 'en' },
|
{ id: 'onyx', name: 'Onyx', language: 'en' },
|
||||||
{ id: 'nova', name: 'Nova', 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') {
|
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
|
// Handle OpenAI-specific options
|
||||||
|
|||||||
+486
-624
File diff suppressed because it is too large
Load Diff
@@ -31,8 +31,12 @@ class PersistenceManagerModule extends BaseModule {
|
|||||||
this.defaultPreferences = {
|
this.defaultPreferences = {
|
||||||
tts: {
|
tts: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
provider: 'none',
|
preferred_handler: 'none',
|
||||||
voice: '',
|
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: {
|
audio: {
|
||||||
masterVolume: 1.0,
|
masterVolume: 1.0,
|
||||||
@@ -58,7 +62,11 @@ class PersistenceManagerModule extends BaseModule {
|
|||||||
'createSaveSlot',
|
'createSaveSlot',
|
||||||
'loadSaveSlot',
|
'loadSaveSlot',
|
||||||
'deleteSaveSlot',
|
'deleteSaveSlot',
|
||||||
'getAllSaveSlots'
|
'getAllSaveSlots',
|
||||||
|
'createBinding',
|
||||||
|
'updateElementFromPreference',
|
||||||
|
'updatePreferenceFromElement',
|
||||||
|
'setupBindings'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Remove circular dependency
|
// Remove circular dependency
|
||||||
@@ -246,19 +254,14 @@ class PersistenceManagerModule extends BaseModule {
|
|||||||
* @returns {boolean} - Success status
|
* @returns {boolean} - Success status
|
||||||
*/
|
*/
|
||||||
updatePreference(category, setting, value) {
|
updatePreference(category, setting, value) {
|
||||||
if (!category || !setting) return false;
|
if (!this.preferences) return false;
|
||||||
|
|
||||||
// Ensure preferences are loaded
|
// Ensure category exists
|
||||||
if (!this.preferences) {
|
|
||||||
this.loadPreferences();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create category if it doesn't exist
|
|
||||||
if (!this.preferences[category]) {
|
if (!this.preferences[category]) {
|
||||||
this.preferences[category] = {};
|
this.preferences[category] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update preference
|
// Store value
|
||||||
this.preferences[category][setting] = value;
|
this.preferences[category][setting] = value;
|
||||||
|
|
||||||
// Save preferences
|
// Save preferences
|
||||||
@@ -268,7 +271,7 @@ class PersistenceManagerModule extends BaseModule {
|
|||||||
// Dispatch event
|
// Dispatch event
|
||||||
this.dispatchEvent('preference-updated', {
|
this.dispatchEvent('preference-updated', {
|
||||||
category,
|
category,
|
||||||
setting,
|
key: setting,
|
||||||
value,
|
value,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
@@ -469,6 +472,201 @@ class PersistenceManagerModule extends BaseModule {
|
|||||||
return this.saveSlots;
|
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
|
* Clean up when module is disposed
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -676,9 +676,9 @@ class TTSFactoryModule extends BaseModule {
|
|||||||
* Speak text using the active TTS handler
|
* Speak text using the active TTS handler
|
||||||
* @param {string} text - Text to speak
|
* @param {string} text - Text to speak
|
||||||
* @param {Object} options - TTS options
|
* @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
|
// Check if we have an active handler
|
||||||
if (!this.activeHandler) {
|
if (!this.activeHandler) {
|
||||||
console.warn('TTS Factory: No active handler set');
|
console.warn('TTS Factory: No active handler set');
|
||||||
@@ -705,7 +705,39 @@ class TTSFactoryModule extends BaseModule {
|
|||||||
effectiveOptions.speed = this.speed;
|
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 => {
|
return handler.speak(text, result => {
|
||||||
// Forward speech completion event
|
// Forward speech completion event
|
||||||
document.dispatchEvent(new CustomEvent('tts:speechCompleted', {
|
document.dispatchEvent(new CustomEvent('tts:speechCompleted', {
|
||||||
|
|||||||
@@ -95,8 +95,8 @@
|
|||||||
// Initialize the model
|
// Initialize the model
|
||||||
const model_id = "onnx-community/Kokoro-82M-v1.0-ONNX";
|
const model_id = "onnx-community/Kokoro-82M-v1.0-ONNX";
|
||||||
this.instance = await this.kokoroTTS.from_pretrained(model_id, {
|
this.instance = await this.kokoroTTS.from_pretrained(model_id, {
|
||||||
dtype: "q8", // Use quantized model for better performance
|
dtype: "q8",
|
||||||
device: "webgpu", // Use WebGL for better performance
|
device: "webgpu",
|
||||||
progress_callback: (progress) => {
|
progress_callback: (progress) => {
|
||||||
// Skip progress updates if progress is NaN/undefined (cache loading)
|
// Skip progress updates if progress is NaN/undefined (cache loading)
|
||||||
if (progress === undefined || isNaN(progress)) {
|
if (progress === undefined || isNaN(progress)) {
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<div id="options-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Options</h2>
|
||||||
|
<span class="close">×</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- TTS Section -->
|
||||||
|
<div class="options-section">
|
||||||
|
<h3>Text-to-Speech</h3>
|
||||||
|
|
||||||
|
<div class="option-item">
|
||||||
|
<label>Enable TTS:</label>
|
||||||
|
<input type="checkbox" id="tts-enabled" data-pref-bind="tts.enabled">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-item">
|
||||||
|
<label>TTS System:</label>
|
||||||
|
<select id="tts-system" data-pref-bind="tts.preferred_handler"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-item">
|
||||||
|
<label>Voice:</label>
|
||||||
|
<select id="tts-voice" data-pref-bind="tts.voice"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-item">
|
||||||
|
<label>Speech:</label>
|
||||||
|
<span class="slider-value">100%</span>
|
||||||
|
<input type="range" id="tts-speed" min="50" max="200" value="100"
|
||||||
|
data-pref-bind="app.speed" data-pref-transform="range:0.5,2.0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Settings -->
|
||||||
|
<div id="api-settings" class="options-section" style="display: none;">
|
||||||
|
<!-- ElevenLabs Settings -->
|
||||||
|
<div class="api-settings eleven-labs-settings" style="display: none;">
|
||||||
|
<h3>ElevenLabs API Settings</h3>
|
||||||
|
|
||||||
|
<div class="option-item">
|
||||||
|
<label>API Key:</label>
|
||||||
|
<input type="password" id="elevenlabs-api-key" data-pref-bind="tts.elevenlabs-tts_api_key">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-item">
|
||||||
|
<label>API URL:</label>
|
||||||
|
<input type="text" id="elevenlabs-api-url" data-pref-bind="tts.elevenlabs-tts_api_url">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- OpenAI Settings -->
|
||||||
|
<div class="api-settings openai-settings" style="display: none;">
|
||||||
|
<h3>OpenAI API Settings</h3>
|
||||||
|
|
||||||
|
<div class="option-item">
|
||||||
|
<label>API Key:</label>
|
||||||
|
<input type="password" id="openai-api-key" data-pref-bind="tts.openai-tts_api_key">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-item">
|
||||||
|
<label>API URL:</label>
|
||||||
|
<input type="text" id="openai-api-url" data-pref-bind="tts.openai-tts_api_url">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Audio Section -->
|
||||||
|
<div class="options-section">
|
||||||
|
<h3>Audio</h3>
|
||||||
|
|
||||||
|
<div class="option-item">
|
||||||
|
<label>Master Volume:</label>
|
||||||
|
<span class="slider-value">100%</span>
|
||||||
|
<input type="range" id="master-volume" min="0" max="100" value="100"
|
||||||
|
data-pref-bind="audio.masterVolume" data-pref-transform="range:0,1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-item">
|
||||||
|
<label>Music Volume:</label>
|
||||||
|
<span class="slider-value">70%</span>
|
||||||
|
<input type="range" id="music-volume" min="0" max="100" value="70"
|
||||||
|
data-pref-bind="audio.musicVolume" data-pref-transform="range:0,1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-item">
|
||||||
|
<label>Sound Effects Volume:</label>
|
||||||
|
<span class="slider-value">100%</span>
|
||||||
|
<input type="range" id="sfx-volume" min="0" max="100" value="100"
|
||||||
|
data-pref-bind="audio.sfxVolume" data-pref-transform="range:0,1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Language Section -->
|
||||||
|
<div class="options-section">
|
||||||
|
<h3>Language</h3>
|
||||||
|
|
||||||
|
<div class="option-item">
|
||||||
|
<label>Language:</label>
|
||||||
|
<select id="language" data-pref-bind="app.locale"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="close-options">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user