feat: Integrate Kokoro TTS with WebGPU and fallback

This commit is contained in:
2025-04-01 10:34:24 +00:00
parent 113e3b995d
commit 1882acac8c
111 changed files with 9143 additions and 4447 deletions
+189
View File
@@ -0,0 +1,189 @@
/**
* TTS Factory for AI Interactive Fiction
* Attempts to use Kokoro TTS first, then falls back to browser TTS if needed
*/
class TTSFactory {
constructor() {
this.activeTTSHandler = null;
this.kokoroHandler = null;
this.browserTTSHandler = null;
this.initializationAttempted = false;
this.usingKokoro = false;
this.initializationPromise = null; // Promise for the factory initialization
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.initialize());
} else {
// Use requestAnimationFrame to ensure scripts are parsed
requestAnimationFrame(() => this.initialize());
}
}
/**
* Initialize available TTS handlers
*/
async initialize() {
// Prevent multiple initializations
if (this.initializationAttempted) return this.initializationPromise;
this.initializationAttempted = true;
console.log('Initializing TTS Factory...');
this.initializationPromise = new Promise(async (resolve) => {
let kokoroInitialized = false;
// Try to initialize Kokoro first (preferred option)
try {
// Check if KokoroHandler class is defined (loaded via script tag)
if (typeof KokoroHandler !== 'undefined') {
console.log('Attempting to initialize Kokoro TTS...');
this.kokoroHandler = new KokoroHandler();
// --- Increase Timeout for Kokoro Initialization ---
// Wait for KokoroHandler's internal initialization promise
// Use Promise.race to add a longer timeout specifically for Kokoro init
const kokoroTimeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Kokoro initialization timed out in factory')), 60000) // 60 seconds timeout
);
try {
kokoroInitialized = await Promise.race([
this.kokoroHandler.initializationPromise,
kokoroTimeoutPromise
]);
} catch (timeoutError) {
console.error(timeoutError.message); // Log the timeout error
kokoroInitialized = false;
}
// --- End Increase Timeout ---
if (kokoroInitialized) {
console.log('Kokoro Handler reported successful initialization.');
} else {
console.warn('Kokoro Handler reported failed or timed out initialization.');
}
} else {
console.warn('KokoroHandler class not found when factory initialized.');
}
} catch (error) {
console.error('Error creating or initializing Kokoro Handler:', error);
kokoroInitialized = false; // Ensure it's marked as failed
}
// Initialize browser TTS as fallback (can happen in parallel)
try {
if (typeof TTSHandler !== 'undefined') {
console.log('Initializing browser TTS as fallback...');
this.browserTTSHandler = new TTSHandler();
} else {
console.warn('TTSHandler class not found when factory initialized.');
}
} catch (error) {
console.error('Error initializing browser TTS:', error);
}
// Decide which handler to use based on Kokoro's success
this.selectActiveHandler(kokoroInitialized);
resolve(); // Resolve the factory's promise
});
return this.initializationPromise;
}
// Removed waitForKokoroInitialization as KokoroHandler now manages its own promise
/**
* Select which TTS handler to use
* @param {boolean} kokoroInitialized - Whether Kokoro initialization succeeded
*/
selectActiveHandler(kokoroInitialized) {
// First choice: Kokoro if it's available and initialized successfully
if (kokoroInitialized && this.kokoroHandler && this.kokoroHandler.kokoroReady) {
console.log('Using Kokoro TTS as primary TTS system');
this.activeTTSHandler = this.kokoroHandler;
this.usingKokoro = true;
}
// Fallback to browser TTS if available
else if (this.browserTTSHandler) {
console.log('Falling back to browser TTS.');
this.activeTTSHandler = this.browserTTSHandler;
this.usingKokoro = false;
}
// No TTS available
else {
console.error('No TTS system available.');
this.activeTTSHandler = null;
this.usingKokoro = false;
}
// Expose the active handler as the global ttsHandler
window.ttsHandler = this.activeTTSHandler;
// Log the active TTS system
if (this.usingKokoro) {
console.log('TTS Factory initialized with Kokoro TTS');
} else if (this.activeTTSHandler) {
console.log('TTS Factory initialized with browser TTS');
} else {
console.log('TTS Factory initialized with no available TTS');
}
// Dispatch an event to notify the UI that TTS is ready (or not)
const ttsReadyEvent = new CustomEvent('tts-ready', {
detail: {
available: !!this.activeTTSHandler,
type: this.usingKokoro ? 'kokoro' : (this.activeTTSHandler ? 'browser' : 'none'),
handler: this.activeTTSHandler
}
});
window.dispatchEvent(ttsReadyEvent);
}
/**
* Get info about the active TTS system
*/
getActiveTTSInfo() {
if (!this.activeTTSHandler) {
return { available: false, type: 'none', name: 'None' };
}
return {
available: true,
type: this.usingKokoro ? 'kokoro' : 'browser',
name: this.usingKokoro ? 'Kokoro TTS' : 'Browser TTS'
};
}
/**
* Force switching to a specific TTS system
* @param {string} type - Either 'kokoro' or 'browser'
*/
switchTTS(type) {
if (type === 'kokoro' && this.kokoroHandler && this.kokoroHandler.kokoroReady) {
this.activeTTSHandler = this.kokoroHandler;
this.usingKokoro = true;
window.ttsHandler = this.activeTTSHandler;
console.log('Switched to Kokoro TTS');
// Dispatch event on switch
const ttsReadyEvent = new CustomEvent('tts-ready', { detail: { available: true, type: 'kokoro', handler: this.activeTTSHandler } });
window.dispatchEvent(ttsReadyEvent);
return true;
} else if (type === 'browser' && this.browserTTSHandler) {
this.activeTTSHandler = this.browserTTSHandler;
this.usingKokoro = false;
window.ttsHandler = this.activeTTSHandler;
console.log('Switched to browser TTS');
// Dispatch event on switch
const ttsReadyEvent = new CustomEvent('tts-ready', { detail: { available: true, type: 'browser', handler: this.activeTTSHandler } });
window.dispatchEvent(ttsReadyEvent);
return true;
}
console.error(`Failed to switch to ${type} TTS - not available`);
return false;
}
}
// Create the global TTS factory instance
window.ttsFactory = new TTSFactory();