Completed options menu and got kokoro to load.
This commit is contained in:
+120
-20
@@ -722,34 +722,134 @@ ol.choice {
|
|||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* UI Effects */
|
/* Options Modal Styling */
|
||||||
.effects-overlay {
|
.options-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none; /* Allow clicks to pass through */
|
display: none;
|
||||||
z-index: 998; /* Below lighting but above other elements */
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.candle-effect {
|
.options-content {
|
||||||
position: absolute;
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
top: 0;
|
border-radius: 5px;
|
||||||
left: 0;
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 80%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: var(--book-font);
|
||||||
|
color: #333;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--book-font);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-close:hover {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-section h3 {
|
||||||
|
font-family: var(--book-font);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-row label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-row select,
|
||||||
|
.options-row input[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style range inputs to match the top-left menu */
|
||||||
|
.options-row input[type="range"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 0.5rem;
|
||||||
opacity: 0.3;
|
background-color: transparent;
|
||||||
pointer-events: none;
|
box-sizing: border-box;
|
||||||
mix-blend-mode: screen;
|
border: 1px solid black;
|
||||||
background: radial-gradient(circle at center, rgba(255,230,150,0.2) 0%, rgba(0,0,0,0) 70%);
|
border-radius: 0.25rem;
|
||||||
animation: candle-flicker 4s infinite alternate;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes candle-flicker {
|
.options-row input[type="range"]::-webkit-slider-thumb {
|
||||||
0% { opacity: 0.2; transform: scale(1.02); }
|
-webkit-appearance: none;
|
||||||
25% { opacity: 0.3; }
|
appearance: none;
|
||||||
50% { opacity: 0.25; transform: scale(0.98); }
|
height: 0.5rem;
|
||||||
75% { opacity: 0.3; }
|
width: 0.5rem;
|
||||||
100% { opacity: 0.35; transform: scale(1); }
|
border-radius: 0.25rem;
|
||||||
|
background-color: rgba(0,0,0,0.9);
|
||||||
|
border: none;
|
||||||
|
box-shadow: -407px 0 0 400px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-row input[type="range"]::-webkit-slider-runnable-track {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reload notice styled like the book theme */
|
||||||
|
.reload-notice {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reload-notice span {
|
||||||
|
font-family: var(--book-font);
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|||||||
+665
-629
File diff suppressed because it is too large
Load Diff
@@ -1,123 +0,0 @@
|
|||||||
/**
|
|
||||||
* Kokoro Web Worker
|
|
||||||
* Handles TTS processing in a separate thread to keep UI responsive
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Global variables
|
|
||||||
let kokoroLoaded = false;
|
|
||||||
let isProcessing = false;
|
|
||||||
let voiceOptions = {
|
|
||||||
voice: 'bf_alice',
|
|
||||||
speed: 1.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize when receiving init message
|
|
||||||
self.onmessage = function(e) {
|
|
||||||
const message = e.data;
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (message.type) {
|
|
||||||
case 'init':
|
|
||||||
// Just acknowledge initialization - actual model loading happens on first generate call
|
|
||||||
self.postMessage({ type: 'ready' });
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'generate':
|
|
||||||
if (!message.text) {
|
|
||||||
self.postMessage({
|
|
||||||
type: 'error',
|
|
||||||
error: 'No text provided for generation'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store voice options
|
|
||||||
if (message.voice) voiceOptions.voice = message.voice;
|
|
||||||
if (message.speed) voiceOptions.speed = message.speed;
|
|
||||||
|
|
||||||
// Generate speech
|
|
||||||
generateSpeech(message.text)
|
|
||||||
.then(result => {
|
|
||||||
self.postMessage({
|
|
||||||
type: 'generated',
|
|
||||||
result: result
|
|
||||||
}, [result.audio.buffer]);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
self.postMessage({
|
|
||||||
type: 'error',
|
|
||||||
error: `Generation error: ${error.message || error}`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
self.postMessage({
|
|
||||||
type: 'error',
|
|
||||||
error: `Unknown message type: ${message.type}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
self.postMessage({
|
|
||||||
type: 'error',
|
|
||||||
error: `Worker error: ${error.message || error}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate speech from text
|
|
||||||
* @param {string} text - Text to convert to speech
|
|
||||||
*/
|
|
||||||
async function generateSpeech(text) {
|
|
||||||
if (isProcessing) {
|
|
||||||
throw new Error('Already processing another request');
|
|
||||||
}
|
|
||||||
|
|
||||||
isProcessing = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Load Kokoro if not already loaded
|
|
||||||
if (!kokoroLoaded) {
|
|
||||||
try {
|
|
||||||
// Load the Kokoro script
|
|
||||||
self.importScripts('/js/kokoro.js');
|
|
||||||
|
|
||||||
if (!self.Kokoro) {
|
|
||||||
throw new Error('Kokoro failed to load correctly');
|
|
||||||
}
|
|
||||||
|
|
||||||
kokoroLoaded = true;
|
|
||||||
console.log('Kokoro loaded in worker');
|
|
||||||
} catch (loadError) {
|
|
||||||
console.error('Error loading Kokoro in worker:', loadError);
|
|
||||||
throw new Error(`Failed to load Kokoro: ${loadError.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate speech using Kokoro
|
|
||||||
const result = await self.Kokoro(text, {
|
|
||||||
voice: voiceOptions.voice,
|
|
||||||
speed: voiceOptions.speed,
|
|
||||||
autoPlay: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extract audio data
|
|
||||||
const audioContext = new (self.AudioContext || self.webkitAudioContext)();
|
|
||||||
const audioBuffer = await audioContext.decodeAudioData(result.buffer);
|
|
||||||
|
|
||||||
// Get audio data as Float32Array
|
|
||||||
const audioData = audioBuffer.getChannelData(0);
|
|
||||||
|
|
||||||
// Return the result
|
|
||||||
return {
|
|
||||||
audio: audioData,
|
|
||||||
sampling_rate: audioBuffer.sampleRate
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating speech in worker:', error);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
isProcessing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+37
-26
@@ -22,20 +22,7 @@ class LocalizationModule extends BaseModule {
|
|||||||
this.languageNames = {
|
this.languageNames = {
|
||||||
'en-us': 'English (US)',
|
'en-us': 'English (US)',
|
||||||
'en-gb': 'English (UK)',
|
'en-gb': 'English (UK)',
|
||||||
'de': 'Deutsch',
|
'de-de': 'Deutsch (Deutschland)'
|
||||||
'de-de': 'Deutsch (Deutschland)',
|
|
||||||
'fr': 'Français',
|
|
||||||
'fr-fr': 'Français (France)',
|
|
||||||
'es': 'Español',
|
|
||||||
'es-es': 'Español (España)',
|
|
||||||
'it': 'Italiano',
|
|
||||||
'ja': 'Japanese',
|
|
||||||
'ko': 'Korean',
|
|
||||||
'zh': 'Chinese (Simplified)',
|
|
||||||
'zh-tw': 'Chinese (Traditional)',
|
|
||||||
'ru': 'Russian',
|
|
||||||
'pt': 'Portuguese',
|
|
||||||
'pt-br': 'Portuguese (Brazil)'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bind methods
|
// Bind methods
|
||||||
@@ -57,23 +44,47 @@ class LocalizationModule extends BaseModule {
|
|||||||
try {
|
try {
|
||||||
this.reportProgress(10, "Initializing localization");
|
this.reportProgress(10, "Initializing localization");
|
||||||
|
|
||||||
// Load default translations
|
|
||||||
await this.loadTranslations('en-us');
|
|
||||||
|
|
||||||
// Try to load browser locale if available
|
// Get stored locale from persistence manager if available
|
||||||
const browserLocale = navigator.language.toLowerCase();
|
const persistenceManager = this.getModule('persistence-manager');
|
||||||
if (browserLocale && browserLocale !== 'en-us') {
|
let storedLocale = null;
|
||||||
|
|
||||||
|
if (persistenceManager) {
|
||||||
try {
|
try {
|
||||||
this.reportProgress(50, `Loading browser locale: ${browserLocale}`);
|
storedLocale = persistenceManager.getPreference('app', 'locale');
|
||||||
await this.loadTranslations(browserLocale);
|
if (storedLocale) {
|
||||||
this.currentLocale = browserLocale;
|
console.log(`Localization: Found stored locale: ${storedLocale}`);
|
||||||
} catch (localeError) {
|
await this.loadTranslations(storedLocale);
|
||||||
console.warn(`Failed to load browser locale ${browserLocale}:`, localeError);
|
this.currentLocale = storedLocale;
|
||||||
|
} else {
|
||||||
|
// If no stored locale, ensure English is the default and persist it
|
||||||
|
console.log('Localization: No stored locale found, defaulting to en-us');
|
||||||
|
await this.loadTranslations('en-us');
|
||||||
|
persistenceManager.updatePreference('app', 'locale', 'en-us');
|
||||||
|
persistenceManager.updatePreference('tts', 'language', 'en-us');
|
||||||
|
this.currentLocale = 'en-us';
|
||||||
|
}
|
||||||
|
} catch (persistError) {
|
||||||
|
console.warn(`Failed to load stored locale:`, persistError);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If browser locale is available, just load it as a fallback but keep English as default
|
||||||
|
const browserLocale = navigator.language.toLowerCase();
|
||||||
|
if (browserLocale && browserLocale !== 'en-us') {
|
||||||
|
try {
|
||||||
|
this.reportProgress(50, `Loading browser locale as fallback: ${browserLocale}`);
|
||||||
|
await this.loadTranslations(browserLocale);
|
||||||
|
// Do NOT set browser locale as current - keep English as default
|
||||||
|
} catch (localeError) {
|
||||||
|
console.warn(`Failed to load browser locale ${browserLocale}:`, localeError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't check for persistence manager here to avoid circular dependency
|
// Dispatch event to notify about loaded locale
|
||||||
// The persistence manager will update our locale after it initializes if needed
|
document.dispatchEvent(new CustomEvent('localization:languageChanged', {
|
||||||
|
detail: { locale: this.currentLocale }
|
||||||
|
}));
|
||||||
|
|
||||||
this.reportProgress(100, "Localization ready");
|
this.reportProgress(100, "Localization ready");
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+708
-928
File diff suppressed because it is too large
Load Diff
@@ -181,7 +181,15 @@ class TextProcessorModule extends BaseModule {
|
|||||||
// Define a custom loader for the patterns
|
// Define a custom loader for the patterns
|
||||||
loader: (file) => {
|
loader: (file) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const patternPath = `/js/patterns/${file}`;
|
// Determine correct pattern file based on locale
|
||||||
|
let patternFile = file;
|
||||||
|
|
||||||
|
// Special handling for 'en' locale - use en-us.wasm if available
|
||||||
|
if (file === 'en.wasm') {
|
||||||
|
patternFile = 'en-us.wasm';
|
||||||
|
}
|
||||||
|
|
||||||
|
const patternPath = `/js/patterns/${patternFile}`;
|
||||||
console.log(`Loading hyphenation pattern: ${patternPath}`);
|
console.log(`Loading hyphenation pattern: ${patternPath}`);
|
||||||
|
|
||||||
fetch(patternPath)
|
fetch(patternPath)
|
||||||
|
|||||||
+163
-84
@@ -15,21 +15,33 @@ class TTSFactoryModule extends BaseModule {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super('tts-factory', 'TTS Factory');
|
super('tts-factory', 'TTS Factory');
|
||||||
|
|
||||||
// Available TTS handlers
|
this.dependencies = ['persistence-manager', 'localization'];
|
||||||
this.handlers = {};
|
this.handlers = {};
|
||||||
|
this.initStatus = {};
|
||||||
// Current active handler
|
|
||||||
this.activeHandler = null;
|
this.activeHandler = null;
|
||||||
|
|
||||||
// Handler initialization status
|
|
||||||
this.initStatus = {
|
|
||||||
browser: false,
|
|
||||||
api: false,
|
|
||||||
kokoro: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// TTS availability flag
|
|
||||||
this.ttsAvailable = false;
|
this.ttsAvailable = false;
|
||||||
|
this.speed = 1; // Default speed
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const ttsEnabled = this.getPreference('tts', 'enabled', false);
|
||||||
|
if (ttsEnabled) {
|
||||||
|
this.setActiveHandler('kokoro');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update overall TTS availability
|
||||||
|
this.updateTTSAvailability();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Bind methods
|
// Bind methods
|
||||||
this.bindMethods([
|
this.bindMethods([
|
||||||
@@ -45,11 +57,9 @@ class TTSFactoryModule extends BaseModule {
|
|||||||
'resume',
|
'resume',
|
||||||
'getVoices',
|
'getVoices',
|
||||||
'getPreference',
|
'getPreference',
|
||||||
'isSpeaking'
|
'isSpeaking',
|
||||||
|
'configure'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Add dependencies
|
|
||||||
this.dependencies = ['persistence-manager', 'localization'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,19 +85,37 @@ class TTSFactoryModule extends BaseModule {
|
|||||||
this.registerHandler('api', new ApiTTSHandler());
|
this.registerHandler('api', new ApiTTSHandler());
|
||||||
this.registerHandler('kokoro', new KokoroHandler());
|
this.registerHandler('kokoro', new KokoroHandler());
|
||||||
|
|
||||||
|
console.log('TTS Factory: Registered handlers:', Object.keys(this.handlers));
|
||||||
this.reportProgress(30, "Registered TTS handlers");
|
this.reportProgress(30, "Registered TTS handlers");
|
||||||
|
|
||||||
|
// Force the initialization of all handlers for diagnostics
|
||||||
|
// This ensures they're all initialized even if not selected
|
||||||
|
const initPromises = [];
|
||||||
|
for (const id of Object.keys(this.handlers)) {
|
||||||
|
console.log(`TTS Factory: Initializing handler ${id}`);
|
||||||
|
initPromises.push(this.initializeHandler(id).then(success => {
|
||||||
|
console.log(`TTS Factory: Handler ${id} initialization ${success ? 'succeeded' : 'failed'}`);
|
||||||
|
return { id, success };
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all handlers to initialize
|
||||||
|
const results = await Promise.all(initPromises);
|
||||||
|
console.log('TTS Factory: All handler initialization results:', results);
|
||||||
|
|
||||||
// Get user preferences
|
// Get user preferences
|
||||||
const ttsEnabled = this.getPreference('tts', 'enabled', false);
|
const ttsEnabled = this.getPreference('tts', 'enabled', false);
|
||||||
const preferredProvider = this.getPreference('tts', 'provider', 'browser');
|
const preferredProvider = this.getPreference('tts', 'provider', 'browser');
|
||||||
|
|
||||||
|
console.log(`TTS Factory: User preferences - enabled: ${ttsEnabled}, provider: ${preferredProvider}`);
|
||||||
|
|
||||||
// Initialize handlers based on preferences
|
// Initialize handlers based on preferences
|
||||||
let initSuccess = false;
|
let initSuccess = false;
|
||||||
|
|
||||||
if (ttsEnabled) {
|
if (ttsEnabled) {
|
||||||
// Try to initialize preferred handler first
|
// Try to initialize preferred handler first
|
||||||
this.reportProgress(50, `Initializing preferred TTS handler: ${preferredProvider}`);
|
this.reportProgress(50, `Initializing preferred TTS handler: ${preferredProvider}`);
|
||||||
initSuccess = await this.initializeHandler(preferredProvider);
|
initSuccess = this.initStatus[preferredProvider] || false;
|
||||||
|
|
||||||
if (initSuccess) {
|
if (initSuccess) {
|
||||||
this.setActiveHandler(preferredProvider);
|
this.setActiveHandler(preferredProvider);
|
||||||
@@ -96,71 +124,44 @@ class TTSFactoryModule extends BaseModule {
|
|||||||
console.warn(`Failed to initialize preferred TTS handler: ${preferredProvider}, trying alternatives`);
|
console.warn(`Failed to initialize preferred TTS handler: ${preferredProvider}, trying alternatives`);
|
||||||
|
|
||||||
// Try Kokoro TTS as fallback if not already tried
|
// Try Kokoro TTS as fallback if not already tried
|
||||||
if (preferredProvider !== 'kokoro') {
|
if (preferredProvider !== 'kokoro' && this.initStatus.kokoro) {
|
||||||
this.reportProgress(60, "Trying Kokoro TTS as fallback");
|
this.reportProgress(60, "Using Kokoro TTS as fallback");
|
||||||
initSuccess = await this.initializeHandler('kokoro');
|
this.setActiveHandler('kokoro');
|
||||||
if (initSuccess) {
|
// Update preference to Kokoro since it worked
|
||||||
this.setActiveHandler('kokoro');
|
this.getModule('persistence-manager').updatePreference('tts', 'provider', 'kokoro');
|
||||||
// Update preference to Kokoro since it worked
|
initSuccess = true;
|
||||||
this.getModule('persistence-manager').updatePreference('tts', 'provider', 'kokoro');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Try Browser TTS as fallback if not already tried
|
||||||
// If Kokoro TTS failed, try Browser TTS
|
else if (preferredProvider !== 'browser' && this.initStatus.browser) {
|
||||||
if (!initSuccess && preferredProvider !== 'browser') {
|
this.reportProgress(70, "Using Browser TTS as fallback");
|
||||||
this.reportProgress(70, "Trying Browser TTS as fallback");
|
this.setActiveHandler('browser');
|
||||||
initSuccess = await this.initializeHandler('browser');
|
// Update preference to Browser since it worked
|
||||||
if (initSuccess) {
|
this.getModule('persistence-manager').updatePreference('tts', 'provider', 'browser');
|
||||||
this.setActiveHandler('browser');
|
initSuccess = true;
|
||||||
// Update preference to browser since it worked
|
}
|
||||||
this.getModule('persistence-manager').updatePreference('tts', 'provider', 'browser');
|
else {
|
||||||
}
|
// If all failed, disable TTS
|
||||||
|
this.reportProgress(80, "All TTS handlers failed, disabling TTS");
|
||||||
|
this.getModule('persistence-manager').updatePreference('tts', 'enabled', false);
|
||||||
|
this.getModule('persistence-manager').updatePreference('tts', 'provider', 'none');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: API TTS is not used as a fallback as it requires manual configuration
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Even if TTS is disabled, initialize handlers in the background
|
|
||||||
// so they're ready if the user enables TTS later
|
|
||||||
this.reportProgress(50, "TTS disabled, initializing handlers in background");
|
|
||||||
|
|
||||||
// Initialize Kokoro and Browser handlers in parallel (not API as it requires configuration)
|
|
||||||
const initPromises = [
|
|
||||||
this.initializeHandler('kokoro'),
|
|
||||||
this.initializeHandler('browser')
|
|
||||||
];
|
|
||||||
|
|
||||||
// Wait for all handlers to initialize
|
|
||||||
await Promise.allSettled(initPromises);
|
|
||||||
|
|
||||||
// Check if any handler initialized successfully
|
|
||||||
initSuccess = this.initStatus.kokoro || this.initStatus.browser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set TTS availability flag and dispatch event
|
// Determine overall TTS availability
|
||||||
this.ttsAvailable = initSuccess;
|
this.ttsAvailable = this.initStatus.kokoro || this.initStatus.browser;
|
||||||
|
|
||||||
// Dispatch event to notify UI about TTS availability
|
// Dispatch TTS availability event
|
||||||
document.dispatchEvent(new CustomEvent('tts:availability', {
|
window.dispatchEvent(new CustomEvent('tts:availability', {
|
||||||
detail: { available: this.ttsAvailable }
|
detail: { available: this.ttsAvailable }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.reportProgress(100, initSuccess ? "TTS factory ready" : "TTS factory ready (no handlers available)");
|
this.reportProgress(100, "TTS factory initialized");
|
||||||
|
return true; // TTS is optional, so always return true
|
||||||
// Always return true since TTS is optional for the application
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error initializing TTS factory:", error);
|
console.error("TTS Factory: Error during initialization:", error);
|
||||||
this.reportProgress(100, "TTS factory failed");
|
this.reportProgress(100, "TTS factory failed");
|
||||||
|
return true; // TTS is optional, so always return true
|
||||||
// Set TTS availability to false and dispatch event
|
|
||||||
this.ttsAvailable = false;
|
|
||||||
document.dispatchEvent(new CustomEvent('tts:availability', {
|
|
||||||
detail: { available: false }
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Still return true since TTS is optional
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,27 +229,43 @@ class TTSFactoryModule extends BaseModule {
|
|||||||
* @returns {boolean} - Success status
|
* @returns {boolean} - Success status
|
||||||
*/
|
*/
|
||||||
setActiveHandler(id) {
|
setActiveHandler(id) {
|
||||||
if (!id || !this.handlers[id] || !this.initStatus[id]) {
|
// Handle 'none' option specially
|
||||||
console.warn(`Cannot set active handler to ${id}: handler not found or not initialized`);
|
if (id === 'none') {
|
||||||
|
this.activeHandler = null;
|
||||||
|
|
||||||
|
// Update TTS availability state
|
||||||
|
this.ttsAvailable = false;
|
||||||
|
|
||||||
|
// Notify about TTS availability change
|
||||||
|
document.dispatchEvent(new CustomEvent('tts:availability', {
|
||||||
|
detail: { available: false }
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log("TTS Factory: TTS disabled (none selected)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.handlers[id]) {
|
||||||
|
console.error(`TTS Factory: Handler not found: ${id}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop current handler if active
|
if (!this.initStatus[id]) {
|
||||||
if (this.activeHandler) {
|
console.error(`TTS Factory: Handler not initialized: ${id}`);
|
||||||
this.handlers[this.activeHandler].stop();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set new active handler
|
|
||||||
this.activeHandler = id;
|
this.activeHandler = id;
|
||||||
|
|
||||||
// Update preference
|
// Update TTS availability state
|
||||||
this.getModule('persistence-manager').updatePreference('tts', 'provider', id);
|
this.ttsAvailable = true;
|
||||||
|
|
||||||
// Dispatch event
|
// Notify about TTS availability change
|
||||||
this.dispatchEvent('tts-handler-changed', {
|
document.dispatchEvent(new CustomEvent('tts:availability', {
|
||||||
handler: id
|
detail: { available: true }
|
||||||
});
|
}));
|
||||||
|
|
||||||
|
console.log(`TTS Factory: Active handler set to ${id}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,8 +285,16 @@ class TTSFactoryModule extends BaseModule {
|
|||||||
getAvailableHandlers() {
|
getAvailableHandlers() {
|
||||||
const available = {};
|
const available = {};
|
||||||
|
|
||||||
|
// Debug logging for diagnostic purposes
|
||||||
|
console.log('TTS Factory: getAvailableHandlers called');
|
||||||
|
console.log('TTS Factory: Current initialization status:', this.initStatus);
|
||||||
|
console.log('TTS Factory: Registered handlers:', Object.keys(this.handlers).join(', '));
|
||||||
|
|
||||||
for (const id in this.handlers) {
|
for (const id in this.handlers) {
|
||||||
available[id] = this.initStatus[id];
|
// Add the handler to the available list even if it's not initialized yet
|
||||||
|
// This ensures all registered handlers appear in the options
|
||||||
|
available[id] = true;
|
||||||
|
console.log(`TTS Factory: Including handler ${id} in options`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return available;
|
return available;
|
||||||
@@ -387,6 +412,60 @@ class TTSFactoryModule extends BaseModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update overall TTS availability
|
||||||
|
*/
|
||||||
|
updateTTSAvailability() {
|
||||||
|
this.ttsAvailable = this.initStatus.kokoro || this.initStatus.browser;
|
||||||
|
|
||||||
|
// Dispatch TTS availability event
|
||||||
|
window.dispatchEvent(new CustomEvent('tts:availability', {
|
||||||
|
detail: { available: this.ttsAvailable }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure TTS settings for all handlers
|
||||||
|
* @param {Object} options - TTS options
|
||||||
|
* @param {number} [options.speed] - Normalized speech rate (0-1 range)
|
||||||
|
*/
|
||||||
|
configure(options = {}) {
|
||||||
|
// If speed is provided, convert the normalized speed (0-1) to the appropriate scale for each handler
|
||||||
|
if (typeof options.speed === 'number') {
|
||||||
|
const normalizedSpeed = Math.max(0, Math.min(1, options.speed));
|
||||||
|
|
||||||
|
// Scale for each handler type
|
||||||
|
for (const id in this.handlers) {
|
||||||
|
// Ensure the handler exists and has the setVoiceOptions method
|
||||||
|
if (this.handlers[id] && typeof this.handlers[id].setVoiceOptions === 'function') {
|
||||||
|
let scaledOptions = {};
|
||||||
|
|
||||||
|
// Scale the speed value appropriately for each handler type
|
||||||
|
if (id === 'browser') {
|
||||||
|
// Browser TTS uses rate from 0.1 to 2.0
|
||||||
|
scaledOptions.rate = 0.1 + (normalizedSpeed * 1.9);
|
||||||
|
} else if (id === 'kokoro') {
|
||||||
|
// Kokoro uses rate from 0.5 to 1.5
|
||||||
|
scaledOptions.rate = 0.5 + (normalizedSpeed);
|
||||||
|
} else if (id === 'api') {
|
||||||
|
// API uses speed from 0.5 to 2.0
|
||||||
|
scaledOptions.speed = 0.5 + (normalizedSpeed * 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the scaled options to the handler
|
||||||
|
this.handlers[id].setVoiceOptions(scaledOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the normalized value
|
||||||
|
this.speed = normalizedSpeed;
|
||||||
|
|
||||||
|
console.log(`TTS Factory: Speed set to ${normalizedSpeed} (normalized), ${Math.round(normalizedSpeed * 100)}/100`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up when module is disposed
|
* Clean up when module is disposed
|
||||||
*/
|
*/
|
||||||
|
|||||||
+189
-78
@@ -166,41 +166,102 @@ class UIController extends BaseModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Listen for command events from input handler - use arrow function to preserve context
|
// Set up event listeners for menu buttons
|
||||||
document.addEventListener('ui:command', (event) => {
|
const saveButton = document.getElementById('save');
|
||||||
this.handleCommand(event.detail);
|
const loadButton = document.getElementById('reload');
|
||||||
});
|
const restartButton = document.getElementById('rewind');
|
||||||
|
const speechToggle = document.getElementById('speech');
|
||||||
|
const optionsButton = document.getElementById('options');
|
||||||
|
|
||||||
// Listen for text display events - use arrow function to preserve context
|
// Get persistence manager module
|
||||||
document.addEventListener('ui:text:complete', (event) => {
|
const persistenceManager = this.getModule('persistence-manager');
|
||||||
console.log('UIController: Text complete event received, ready for next text');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for socket connection events
|
// Set up save button
|
||||||
document.addEventListener('socket:connected', () => {
|
if (saveButton) {
|
||||||
console.log('UIController: Socket connected');
|
saveButton.addEventListener('click', () => {
|
||||||
this.updateButtonStates();
|
document.dispatchEvent(new CustomEvent('ui:game:save'));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('socket:disconnected', () => {
|
// Set up load button
|
||||||
console.log('UIController: Socket disconnected');
|
if (loadButton) {
|
||||||
this.updateButtonStates();
|
loadButton.addEventListener('click', () => {
|
||||||
});
|
document.dispatchEvent(new CustomEvent('ui:game:load'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for TTS state change events
|
// Set up restart button
|
||||||
document.addEventListener('tts:stateChange', (event) => {
|
if (restartButton) {
|
||||||
if (event.detail) {
|
restartButton.addEventListener('click', () => {
|
||||||
if (typeof event.detail.enabled === 'boolean') {
|
document.dispatchEvent(new CustomEvent('ui:game:restart'));
|
||||||
this.ttsEnabled = event.detail.enabled;
|
});
|
||||||
}
|
}
|
||||||
if (typeof event.detail.available === 'boolean') {
|
|
||||||
this.ttsAvailable = event.detail.available;
|
// Set up speech toggle button
|
||||||
}
|
if (speechToggle) {
|
||||||
|
// Initialize ttsEnabled from persistence manager
|
||||||
|
if (persistenceManager) {
|
||||||
|
const prefs = persistenceManager.getAllPreferences();
|
||||||
|
this.ttsEnabled = prefs.tts?.enabled ?? false;
|
||||||
|
|
||||||
|
// Update button state
|
||||||
this.updateButtonStates();
|
this.updateButtonStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
speechToggle.addEventListener('click', () => {
|
||||||
|
// Toggle TTS state
|
||||||
|
this.ttsEnabled = !this.ttsEnabled;
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
this.updateButtonStates();
|
||||||
|
|
||||||
|
// Save preference
|
||||||
|
if (persistenceManager) {
|
||||||
|
persistenceManager.updatePreference('tts', 'enabled', this.ttsEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify other components
|
||||||
|
document.dispatchEvent(new CustomEvent('ui:tts:toggle', {
|
||||||
|
detail: { enabled: this.ttsEnabled }
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up options button
|
||||||
|
if (optionsButton) {
|
||||||
|
optionsButton.addEventListener('click', () => {
|
||||||
|
document.dispatchEvent(new CustomEvent('ui:options:toggle'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for book events
|
||||||
|
document.addEventListener('book:ready', () => {
|
||||||
|
this.updateButtonStates({
|
||||||
|
canSave: true,
|
||||||
|
canLoad: true,
|
||||||
|
canRestart: true
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for TTS availability events
|
// Listen for restart events
|
||||||
|
document.addEventListener('story:restart', () => {
|
||||||
|
this.updateButtonStates({
|
||||||
|
canSave: true,
|
||||||
|
canLoad: false,
|
||||||
|
canRestart: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for save events
|
||||||
|
document.addEventListener('story:save', () => {
|
||||||
|
this.updateButtonStates({
|
||||||
|
canSave: true,
|
||||||
|
canLoad: true,
|
||||||
|
canRestart: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for TTS availability changes
|
||||||
document.addEventListener('tts:availability', (event) => {
|
document.addEventListener('tts:availability', (event) => {
|
||||||
if (event.detail && typeof event.detail.available === 'boolean') {
|
if (event.detail && typeof event.detail.available === 'boolean') {
|
||||||
this.ttsAvailable = event.detail.available;
|
this.ttsAvailable = event.detail.available;
|
||||||
@@ -208,45 +269,92 @@ class UIController extends BaseModule {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add options button to controls section
|
// Listen for TTS engine changes
|
||||||
const controlsSection = document.getElementById('controls');
|
document.addEventListener('tts:engine:change', (event) => {
|
||||||
if (controlsSection) {
|
// Update button states since TTS engine changed
|
||||||
// Check if options button already exists
|
this.updateButtonStates();
|
||||||
if (!document.getElementById('options-button')) {
|
|
||||||
const optionsButton = document.createElement('a');
|
|
||||||
optionsButton.id = 'options-button';
|
|
||||||
optionsButton.href = '#';
|
|
||||||
optionsButton.textContent = 'options';
|
|
||||||
optionsButton.title = 'Show game options';
|
|
||||||
optionsButton.className = 'control-button';
|
|
||||||
optionsButton.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
document.dispatchEvent(new CustomEvent('ui:showOptions'));
|
|
||||||
});
|
|
||||||
controlsSection.appendChild(optionsButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add speech toggle button
|
|
||||||
const speechToggle = document.getElementById('speech-toggle');
|
|
||||||
if (speechToggle) {
|
|
||||||
speechToggle.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
// Dispatch an event for the TTS module to handle instead of calling directly
|
|
||||||
document.dispatchEvent(new CustomEvent('tts:toggle'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for window resize events
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
this.applyBookSizing();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for key events
|
// Listen for TTS toggle events from other components
|
||||||
document.addEventListener('keydown', (event) => {
|
document.addEventListener('tts:enabled:change', (event) => {
|
||||||
// Pass to input handler
|
if (event.detail && typeof event.detail.enabled === 'boolean') {
|
||||||
if (this.inputHandler) {
|
this.ttsEnabled = event.detail.enabled;
|
||||||
this.inputHandler.handleKeyboardInput(event);
|
this.updateButtonStates();
|
||||||
|
|
||||||
|
// Ensure persistence is updated
|
||||||
|
if (persistenceManager) {
|
||||||
|
persistenceManager.updatePreference('tts', 'enabled', this.ttsEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up speed slider in main UI
|
||||||
|
const speedSlider = document.getElementById('speed');
|
||||||
|
const speedReset = document.getElementById('speed_reset');
|
||||||
|
|
||||||
|
if (speedSlider) {
|
||||||
|
// Initialize speed from persistence manager
|
||||||
|
if (persistenceManager) {
|
||||||
|
const prefs = persistenceManager.getAllPreferences();
|
||||||
|
// Get the unified speed value (0-1 range)
|
||||||
|
const speed = prefs.tts?.speed ?? 0.5;
|
||||||
|
// Convert to slider range (0-100)
|
||||||
|
speedSlider.value = Math.round(speed * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
speedSlider.addEventListener('input', (e) => {
|
||||||
|
// Convert slider value (0-100) to normalized speed (0-1)
|
||||||
|
const speed = parseInt(e.target.value) / 100;
|
||||||
|
|
||||||
|
// Scale for different TTS engines
|
||||||
|
// This value is used for real-time preview only
|
||||||
|
const rate = this.ttsEnabled ? speed * 2 : 1;
|
||||||
|
|
||||||
|
// Update animation speed
|
||||||
|
document.dispatchEvent(new CustomEvent('animation:speed:change', {
|
||||||
|
detail: { speed: rate }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update TTS speed
|
||||||
|
document.dispatchEvent(new CustomEvent('tts:speed:change', {
|
||||||
|
detail: { speed: speed }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Save preference
|
||||||
|
if (persistenceManager) {
|
||||||
|
persistenceManager.updatePreference('tts', 'speed', speed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (speedReset) {
|
||||||
|
speedReset.addEventListener('click', () => {
|
||||||
|
// Reset to default speed (0.5)
|
||||||
|
if (speedSlider) {
|
||||||
|
// Default value is 0.5 in normalized form (0-1),
|
||||||
|
// which is 50 in slider range (0-100)
|
||||||
|
speedSlider.value = 50;
|
||||||
|
|
||||||
|
// Trigger the input event to update all components
|
||||||
|
speedSlider.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for speed change events from other components
|
||||||
|
document.addEventListener('tts:speed:change', (event) => {
|
||||||
|
if (event.detail && typeof event.detail.speed === 'number') {
|
||||||
|
// Update the main UI speed slider
|
||||||
|
const speedSlider = document.getElementById('speed');
|
||||||
|
if (speedSlider) {
|
||||||
|
// Convert normalized speed (0-1) to slider range (0-100)
|
||||||
|
speedSlider.value = Math.round(event.detail.speed * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to persistence manager
|
||||||
|
if (persistenceManager) {
|
||||||
|
persistenceManager.updatePreference('tts', 'speed', event.detail.speed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -342,7 +450,6 @@ class UIController extends BaseModule {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update UI button states based on game state
|
* Update UI button states based on game state
|
||||||
* @param {Object} state - Game state information
|
|
||||||
*/
|
*/
|
||||||
updateButtonStates(state = {}) {
|
updateButtonStates(state = {}) {
|
||||||
const { canSave, canLoad, canRestart } = state;
|
const { canSave, canLoad, canRestart } = state;
|
||||||
@@ -351,7 +458,7 @@ class UIController extends BaseModule {
|
|||||||
const saveButton = document.getElementById('save');
|
const saveButton = document.getElementById('save');
|
||||||
const loadButton = document.getElementById('reload');
|
const loadButton = document.getElementById('reload');
|
||||||
const restartButton = document.getElementById('rewind');
|
const restartButton = document.getElementById('rewind');
|
||||||
const speechToggle = document.getElementById('speech-toggle');
|
const speechToggle = document.getElementById('speech');
|
||||||
|
|
||||||
// Update save button state
|
// Update save button state
|
||||||
if (saveButton) {
|
if (saveButton) {
|
||||||
@@ -382,21 +489,25 @@ class UIController extends BaseModule {
|
|||||||
|
|
||||||
// Update speech toggle button state
|
// Update speech toggle button state
|
||||||
if (speechToggle) {
|
if (speechToggle) {
|
||||||
// Update the button appearance based on TTS state
|
// Update the button appearance based on TTS state using existing styles
|
||||||
if (this.ttsEnabled) {
|
if (!this.ttsAvailable) {
|
||||||
speechToggle.classList.add('active');
|
// TTS is not available, disable the button
|
||||||
speechToggle.title = 'Disable speech';
|
|
||||||
} else {
|
|
||||||
speechToggle.classList.remove('active');
|
|
||||||
speechToggle.title = 'Enable speech';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable the button completely if TTS is not available
|
|
||||||
if (this.ttsAvailable === false) {
|
|
||||||
speechToggle.setAttribute('disabled', 'disabled');
|
speechToggle.setAttribute('disabled', 'disabled');
|
||||||
speechToggle.title = 'Speech not available';
|
speechToggle.title = 'Text-to-speech is not available';
|
||||||
} else {
|
} else {
|
||||||
|
// TTS is available, remove disabled attribute
|
||||||
speechToggle.removeAttribute('disabled');
|
speechToggle.removeAttribute('disabled');
|
||||||
|
|
||||||
|
// Update based on whether TTS is enabled
|
||||||
|
if (this.ttsEnabled) {
|
||||||
|
speechToggle.style.fontWeight = 'bold';
|
||||||
|
speechToggle.style.color = '#000';
|
||||||
|
speechToggle.title = 'Disable speech';
|
||||||
|
} else {
|
||||||
|
speechToggle.style.fontWeight = 'normal';
|
||||||
|
speechToggle.style.color = '#999';
|
||||||
|
speechToggle.title = 'Enable speech';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ class UIDisplayHandler extends BaseModule {
|
|||||||
<a class="l10n-restart" id="rewind" title="Restart story from beginning" disabled="disabled">restart</a>
|
<a class="l10n-restart" id="rewind" title="Restart story from beginning" disabled="disabled">restart</a>
|
||||||
<a class="l10n-save" id="save" title="Save progress">save</a>
|
<a class="l10n-save" id="save" title="Save progress">save</a>
|
||||||
<a class="l10n-load" id="reload" title="Reload from save point" disabled="disabled">load</a>
|
<a class="l10n-load" id="reload" title="Reload from save point" disabled="disabled">load</a>
|
||||||
|
<a class="l10n-options" id="options" title="Options">options</a>
|
||||||
`;
|
`;
|
||||||
this.pageLeft.appendChild(controls);
|
this.pageLeft.appendChild(controls);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Kokoro Loader</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
#status {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
#log {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.error { color: red; }
|
||||||
|
.info { color: blue; }
|
||||||
|
.success { color: green; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="status">Loading Kokoro...</div>
|
||||||
|
<div id="log"></div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
// Import the KokoroTTS class from the module
|
||||||
|
import { KokoroTTS } from '/js/kokoro-js.js';
|
||||||
|
|
||||||
|
// Logging function
|
||||||
|
function log(message, type = 'info') {
|
||||||
|
console.log(message);
|
||||||
|
const logElement = document.getElementById('log');
|
||||||
|
const entry = document.createElement('div');
|
||||||
|
entry.className = type;
|
||||||
|
entry.textContent = `[${new Date().toISOString()}] ${message}`;
|
||||||
|
logElement.appendChild(entry);
|
||||||
|
logElement.scrollTop = logElement.scrollHeight;
|
||||||
|
|
||||||
|
// Also send to parent
|
||||||
|
if (window.parent !== window) {
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'kokoro-log',
|
||||||
|
logType: type,
|
||||||
|
message: message
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a global object to store Kokoro instance
|
||||||
|
window.KokoroLoader = {
|
||||||
|
loaded: false,
|
||||||
|
error: null,
|
||||||
|
instance: null,
|
||||||
|
kokoroTTS: null,
|
||||||
|
voices: null,
|
||||||
|
callbacks: [],
|
||||||
|
progress: 0,
|
||||||
|
progressMessage: 'Initializing...',
|
||||||
|
|
||||||
|
// Register a callback for when Kokoro is loaded
|
||||||
|
onLoad: function(callback) {
|
||||||
|
if (this.loaded) {
|
||||||
|
callback(this.instance);
|
||||||
|
} else if (this.error) {
|
||||||
|
callback(null, this.error);
|
||||||
|
} else {
|
||||||
|
this.callbacks.push(callback);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update progress
|
||||||
|
updateProgress: function(progress, message) {
|
||||||
|
this.progress = progress;
|
||||||
|
this.progressMessage = message || 'Loading...';
|
||||||
|
const progressPercent = Math.round(progress * 100);
|
||||||
|
document.getElementById('status').textContent = `${this.progressMessage} (${isNaN(progressPercent) ? 0 : progressPercent}%)`;
|
||||||
|
log(`Progress: ${this.progressMessage} (${isNaN(progressPercent) ? 0 : progressPercent}%)`);
|
||||||
|
|
||||||
|
// Notify parent window
|
||||||
|
if (window.parent !== window) {
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'kokoro-progress',
|
||||||
|
progress: isNaN(progress) ? 0 : progress,
|
||||||
|
message: this.progressMessage
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get default voices
|
||||||
|
getDefaultVoices: function() {
|
||||||
|
return [
|
||||||
|
// American Female voices
|
||||||
|
{ id: 'af_heart', name: 'Heart', lang: 'en-US', gender: 'female' },
|
||||||
|
{ id: 'af_daisy', name: 'Daisy', lang: 'en-US', gender: 'female' },
|
||||||
|
{ id: 'af_soft', name: 'Soft', lang: 'en-US', gender: 'female' },
|
||||||
|
{ id: 'af_glados', name: 'GLaDOS', lang: 'en-US', gender: 'female' },
|
||||||
|
{ id: 'af_southern_belle', name: 'Southern Belle', lang: 'en-US', gender: 'female' },
|
||||||
|
{ id: 'af_dramatic', name: 'Dramatic', lang: 'en-US', gender: 'female' },
|
||||||
|
{ id: 'af_valley_girl', name: 'Valley Girl', lang: 'en-US', gender: 'female' },
|
||||||
|
{ id: 'af_british', name: 'British', lang: 'en-US', gender: 'female' },
|
||||||
|
{ id: 'af_russian', name: 'Russian', lang: 'en-US', gender: 'female' },
|
||||||
|
{ id: 'af_german', name: 'German', lang: 'en-US', gender: 'female' },
|
||||||
|
{ id: 'af_cheeky_cute', name: 'Cheeky Cute', lang: 'en-US', gender: 'female' },
|
||||||
|
|
||||||
|
// American Male voices
|
||||||
|
{ id: 'am_bruce', name: 'Bruce', lang: 'en-US', gender: 'male' },
|
||||||
|
{ id: 'am_announcer', name: 'Announcer', lang: 'en-US', gender: 'male' },
|
||||||
|
{ id: 'am_radio_host', name: 'Radio Host', lang: 'en-US', gender: 'male' },
|
||||||
|
|
||||||
|
// British Female voices
|
||||||
|
{ id: 'bf_charlotte', name: 'Charlotte', lang: 'en-GB', gender: 'female' },
|
||||||
|
{ id: 'bf_elizabeth', name: 'Elizabeth', lang: 'en-GB', gender: 'female' },
|
||||||
|
{ id: 'bf_lily', name: 'Lily', lang: 'en-GB', gender: 'female' },
|
||||||
|
{ id: 'bf_olivia', name: 'Olivia', lang: 'en-GB', gender: 'female' },
|
||||||
|
{ id: 'bf_victoria', name: 'Victoria', lang: 'en-GB', gender: 'female' },
|
||||||
|
|
||||||
|
// British Male voices
|
||||||
|
{ id: 'bm_william', name: 'William', lang: 'en-GB', gender: 'male' },
|
||||||
|
{ id: 'bm_arthur', name: 'Arthur', lang: 'en-GB', gender: 'male' },
|
||||||
|
{ id: 'bm_george', name: 'George', lang: 'en-GB', gender: 'male' },
|
||||||
|
{ id: 'bm_harry', name: 'Harry', lang: 'en-GB', gender: 'male' },
|
||||||
|
{ id: 'bm_jack', name: 'Jack', lang: 'en-GB', gender: 'male' }
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
// Initialize Kokoro
|
||||||
|
init: async function() {
|
||||||
|
try {
|
||||||
|
log('Starting Kokoro initialization');
|
||||||
|
this.updateProgress(0.1, 'Loading Kokoro library...');
|
||||||
|
|
||||||
|
// Store the KokoroTTS class
|
||||||
|
this.kokoroTTS = KokoroTTS;
|
||||||
|
log('Kokoro library loaded successfully', 'success');
|
||||||
|
this.updateProgress(0.3, 'Initializing Kokoro model...');
|
||||||
|
|
||||||
|
// Initialize the model
|
||||||
|
const model_id = "onnx-community/Kokoro-82M-v1.0-ONNX";
|
||||||
|
this.instance = await this.kokoroTTS.from_pretrained(model_id, {
|
||||||
|
dtype: "q8", // Use quantized model for better performance
|
||||||
|
device: "wasm", // Use WebAssembly for compatibility
|
||||||
|
progress_callback: (progress) => {
|
||||||
|
// Map progress from 0-1 to 30-90
|
||||||
|
const mappedProgress = 0.3 + (progress * 0.6);
|
||||||
|
this.updateProgress(mappedProgress, `Loading Kokoro model: ${Math.round(progress * 100)}%`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch available voices
|
||||||
|
log('Fetching available voices...');
|
||||||
|
this.updateProgress(0.8, 'Fetching voices...');
|
||||||
|
|
||||||
|
// Use default voices directly since the list_voices method is unreliable
|
||||||
|
log('Using predefined voice list instead of attempting to fetch from model');
|
||||||
|
this.voices = this.getDefaultVoices();
|
||||||
|
log(`Using ${this.voices.length} predefined voices`, 'success');
|
||||||
|
|
||||||
|
log('Testing Kokoro with a simple text');
|
||||||
|
this.updateProgress(0.95, 'Testing Kokoro...');
|
||||||
|
|
||||||
|
// Test with a simple text
|
||||||
|
// Use the first available voice for testing
|
||||||
|
const testVoice = this.voices && this.voices.length > 0 ? this.voices[0].id : 'af_heart';
|
||||||
|
await this.instance.generate('Test', { voice: testVoice });
|
||||||
|
|
||||||
|
log('Kokoro initialized successfully', 'success');
|
||||||
|
this.loaded = true;
|
||||||
|
this.updateProgress(1.0, 'Kokoro ready');
|
||||||
|
|
||||||
|
// Notify parent window
|
||||||
|
if (window.parent !== window) {
|
||||||
|
log('Notifying parent window of successful initialization');
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'kokoro-ready',
|
||||||
|
success: true,
|
||||||
|
voices: this.voices
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call all callbacks
|
||||||
|
log(`Calling ${this.callbacks.length} registered callbacks`);
|
||||||
|
this.callbacks.forEach(callback => callback(this.instance));
|
||||||
|
|
||||||
|
document.getElementById('status').textContent = 'Kokoro loaded and ready!';
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error.message || 'Unknown error';
|
||||||
|
log(`Error initializing Kokoro: ${errorMsg}`, 'error');
|
||||||
|
console.error('Error initializing Kokoro:', error);
|
||||||
|
this.error = error;
|
||||||
|
|
||||||
|
// Notify parent window
|
||||||
|
if (window.parent !== window) {
|
||||||
|
log('Notifying parent window of initialization failure');
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'kokoro-ready',
|
||||||
|
success: false,
|
||||||
|
error: errorMsg
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call all callbacks with error
|
||||||
|
log(`Calling ${this.callbacks.length} registered callbacks with error`);
|
||||||
|
this.callbacks.forEach(callback => callback(null, error));
|
||||||
|
|
||||||
|
document.getElementById('status').textContent = `Error loading Kokoro: ${errorMsg}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize Kokoro when the page loads
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
log('Page loaded, initializing Kokoro');
|
||||||
|
window.KokoroLoader.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle messages from parent window
|
||||||
|
window.addEventListener('message', function(event) {
|
||||||
|
if (event.source !== window.parent) return;
|
||||||
|
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
if (data.type === 'kokoro-generate') {
|
||||||
|
// Generate speech in a non-blocking way
|
||||||
|
if (!window.KokoroLoader.loaded) {
|
||||||
|
log(`Cannot process generation request ${data.id}: Kokoro not loaded`, 'error');
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'kokoro-generated',
|
||||||
|
id: data.id,
|
||||||
|
success: false,
|
||||||
|
error: 'Kokoro not loaded'
|
||||||
|
}, '*');
|
||||||
|
} else {
|
||||||
|
log(`Processing generation request ${data.id}`);
|
||||||
|
|
||||||
|
// Make the generation asynchronous to avoid freezing the UI
|
||||||
|
setTimeout(() => {
|
||||||
|
window.KokoroLoader.instance.generate(data.text, { voice: data.voice, speed: data.speed })
|
||||||
|
.then(result => {
|
||||||
|
log(`Generation successful for request ${data.id}`, 'success');
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'kokoro-generated',
|
||||||
|
id: data.id,
|
||||||
|
success: true,
|
||||||
|
result: result
|
||||||
|
}, '*');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
log(`Generation failed for request ${data.id}: ${error.message}`, 'error');
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'kokoro-generated',
|
||||||
|
id: data.id,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
Reference in New Issue
Block a user