Added support for openai api tts.

This commit is contained in:
2025-04-05 14:40:56 +00:00
parent b8e2e6e238
commit e8eb93ae1b
11 changed files with 2063 additions and 989 deletions
+262 -30
View File
@@ -1,12 +1,13 @@
/**
* TTS Factory Module
* Creates and manages TTS handler instances
* Manages TTS handler instances
*/
import { BaseModule } from './base-module.js';
import { moduleRegistry } from './module-registry.js';
import { BrowserTTSHandler } from './browser-tts-handler.js';
import { ApiTTSHandler } from './api-tts-handler.js';
import { KokoroHandler } from './kokoro-handler.js';
import { ElevenLabsTTSHandler } from './elevenlabs-tts-handler.js';
import { OpenAITTSHandler } from './openai-tts-handler.js';
class TTSFactoryModule extends BaseModule {
/**
@@ -22,6 +23,12 @@ class TTSFactoryModule extends BaseModule {
this.ttsAvailable = false;
this.speed = 1; // Default speed
// LRU Cache for preloaded speech
this.audioCache = new Map();
this.maxCacheSize = 20; // Maximum number of cached items
this.cacheHits = 0;
this.cacheMisses = 0;
// Listen for kokoro:ready event
document.addEventListener('kokoro:ready', (event) => {
if (event.detail && typeof event.detail.success === 'boolean') {
@@ -43,6 +50,15 @@ class TTSFactoryModule extends BaseModule {
}
});
// Listen for handler availability changes
document.addEventListener('tts:handler:availabilityChanged', (event) => {
if (event && event.detail) {
const { handlerId, available } = event.detail;
console.log(`TTS Factory: Handler ${handlerId} availability changed to ${available}`);
this.updateTTSAvailability();
}
});
// Bind methods
this.bindMethods([
'registerHandler',
@@ -58,7 +74,15 @@ class TTSFactoryModule extends BaseModule {
'getVoices',
'getPreference',
'isSpeaking',
'configure'
'configure',
'preloadSpeech',
'generateSpeechHash',
'speakPreloaded',
'getCachedSpeech',
'addToCache',
'manageCacheSize',
'cacheSpeech',
'isSpeechCached'
]);
}
@@ -80,16 +104,23 @@ class TTSFactoryModule extends BaseModule {
return false;
}
// Register available handlers
// Reset any previous state
this.initStatus = {};
for (const id in this.handlers) {
this.initStatus[id] = false;
}
// Register all available handlers (this will overwrite any existing handlers)
console.log('TTS Factory: Registering all handlers');
this.registerHandler('browser', new BrowserTTSHandler());
this.registerHandler('api', new ApiTTSHandler());
this.registerHandler('elevenlabs', new ElevenLabsTTSHandler());
this.registerHandler('openai', new OpenAITTSHandler());
this.registerHandler('kokoro', new KokoroHandler());
console.log('TTS Factory: Registered handlers:', Object.keys(this.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
// Initialize all handlers in parallel for efficiency
const initPromises = [];
for (const id of Object.keys(this.handlers)) {
console.log(`TTS Factory: Initializing handler ${id}`);
@@ -105,7 +136,13 @@ class TTSFactoryModule extends BaseModule {
// Get user preferences
const ttsEnabled = this.getPreference('tts', 'enabled', false);
const preferredProvider = this.getPreference('tts', 'provider', 'browser');
let preferredProvider = this.getPreference('tts', 'provider', '');
// Default to browser if no provider is set
if (!preferredProvider || preferredProvider === 'none') {
preferredProvider = 'browser';
persistenceManager.updatePreference('tts', 'provider', 'browser');
}
console.log(`TTS Factory: User preferences - enabled: ${ttsEnabled}, provider: ${preferredProvider}`);
@@ -128,7 +165,7 @@ class TTSFactoryModule extends BaseModule {
this.reportProgress(60, "Using Kokoro TTS as fallback");
this.setActiveHandler('kokoro');
// Update preference to Kokoro since it worked
this.getModule('persistence-manager').updatePreference('tts', 'provider', 'kokoro');
persistenceManager.updatePreference('tts', 'provider', 'kokoro');
initSuccess = true;
}
// Try Browser TTS as fallback if not already tried
@@ -136,20 +173,24 @@ class TTSFactoryModule extends BaseModule {
this.reportProgress(70, "Using Browser TTS as fallback");
this.setActiveHandler('browser');
// Update preference to Browser since it worked
this.getModule('persistence-manager').updatePreference('tts', 'provider', 'browser');
persistenceManager.updatePreference('tts', 'provider', 'browser');
initSuccess = true;
}
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');
// If all failed, set to none but don't disable TTS entirely
// This allows configuring API-based TTS later
this.reportProgress(80, "No working TTS handlers found");
persistenceManager.updatePreference('tts', 'provider', 'none');
}
}
}
// Determine overall TTS availability
this.ttsAvailable = this.initStatus.kokoro || this.initStatus.browser;
// Any handler that's initialized should count towards availability
this.ttsAvailable = Object.values(this.initStatus).some(status => status === true);
console.log('TTS Factory: Overall TTS availability:', this.ttsAvailable);
console.log('TTS Factory: Handler status:', this.initStatus);
// Dispatch TTS availability event
window.dispatchEvent(new CustomEvent('tts:availability', {
@@ -290,25 +331,33 @@ class TTSFactoryModule extends BaseModule {
}
/**
* Get all available TTS handlers
* @returns {Object} - Map of handler IDs to initialization status
* Get available TTS handlers
* @returns {Array} - Array of handler objects
*/
getAvailableHandlers() {
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(', '));
const availableHandlers = [];
// Always show all initialized handlers in the options dropdown,
// regardless of availability status. This ensures API handlers are configurable
// even when the API key is not set.
for (const id in this.handlers) {
// 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`);
// Only include handlers that have been initialized
if (this.handlers[id] && this.initStatus[id]) {
console.log(`TTS Factory: Handler ${id} is initialized, adding to available handlers list`);
availableHandlers.push({
id: id,
handler: this.handlers[id]
});
}
}
return available;
if (availableHandlers.length === 0) {
console.warn('TTS Factory: No available handlers found - something is wrong!');
} else {
console.log(`TTS Factory: Found ${availableHandlers.length} available handlers`);
}
return availableHandlers;
}
/**
@@ -471,8 +520,8 @@ class TTSFactoryModule extends BaseModule {
} 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
} else if (id === 'elevenlabs' || id === 'openai') {
// ElevenLabs and OpenAI use speed from 0.5 to 2.0
scaledOptions.speed = 0.5 + (normalizedSpeed * 1.5);
}
@@ -490,6 +539,189 @@ class TTSFactoryModule extends BaseModule {
return true;
}
/**
* Preload speech for a text
* @param {string} text - Text to preload
* @returns {Promise<Object>} - Resolves with preloaded speech data
*/
async preloadSpeech(text) {
if (!this.activeHandler) {
console.warn("TTS Factory: No active TTS handler for preload");
return null;
}
try {
// Generate a hash for this speech request
const hash = await this.generateSpeechHash(text);
// Check if we already have this audio in cache
const cachedData = this.getCachedSpeech(hash);
if (cachedData) {
console.log(`TTS Factory: Using cached speech for hash ${hash} (hits: ${this.cacheHits}, misses: ${this.cacheMisses})`);
// Move this item to the end of the Map to mark it as most recently used
this.audioCache.delete(hash);
this.audioCache.set(hash, cachedData);
this.cacheHits++;
return cachedData;
}
// Cache miss - need to generate new speech data
this.cacheMisses++;
// If the handler has a preloadSpeech method, use it
if (typeof this.handlers[this.activeHandler].preloadSpeech === 'function') {
const preloadData = await this.handlers[this.activeHandler].preloadSpeech(text);
// Cache the generated speech data
if (preloadData) {
this.addToCache(hash, preloadData);
console.log(`TTS Factory: Added speech to cache for hash ${hash} (size: ${this.audioCache.size}/${this.maxCacheSize})`);
}
return preloadData;
} else {
console.warn(`TTS Factory: Handler ${this.activeHandler} does not support preloading`);
return null;
}
} catch (error) {
console.error("TTS Factory: Error preloading speech:", error);
return null;
}
}
/**
* Generate a unique hash for a speech request
* @param {string} text - Text to generate hash for
* @returns {Promise<string>} - Hash string
*/
async generateSpeechHash(text) {
if (!this.activeHandler) return null;
// Get voice ID and other parameters
const handler = this.handlers[this.activeHandler];
const handlerId = this.activeHandler;
const voiceId = handler.voiceOptions?.voice?.id || 'default';
const speed = this.speed;
// Create a string to hash
const dataToHash = `${handlerId}_${voiceId}_${speed}_${text}`;
// Use SubtleCrypto to create a SHA-256 hash if available
try {
const encoder = new TextEncoder();
const data = encoder.encode(dataToHash);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
// Convert to hex string
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
} catch (error) {
// Fallback to simple string hash if SubtleCrypto is not available
console.warn('TTS Factory: Unable to generate crypto hash, using fallback', error);
let hash = 0;
for (let i = 0; i < dataToHash.length; i++) {
const char = dataToHash.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash).toString(16);
}
}
/**
* Speak using preloaded speech data
* @param {Object} preloadData - Preloaded speech data
* @param {Object} options - Speech options
* @returns {Promise<boolean>} - Success status
*/
async speakPreloaded(preloadData, options = {}) {
if (!this.activeHandler) {
console.warn("TTS Factory: No active TTS handler for speak preloaded");
return false;
}
// If the handler has a speakPreloaded method, use it
if (typeof this.handlers[this.activeHandler].speakPreloaded === 'function') {
return await this.handlers[this.activeHandler].speakPreloaded(preloadData, options);
} else {
console.warn(`TTS Factory: Handler ${this.activeHandler} does not support speaking preloaded data`);
return false;
}
}
/**
* Get cached speech data
* @param {string} hash - Hash of the speech data
* @returns {Object|null} - Cached speech data or null if not found
*/
getCachedSpeech(hash) {
if (!this.audioCache || !this.audioCache.has(hash)) return null;
return this.audioCache.get(hash);
}
/**
* Add speech data to the cache
* @param {string} hash - Hash of the speech data
* @param {Object} data - Speech data to cache
*/
addToCache(hash, data) {
if (!this.audioCache) this.audioCache = new Map();
this.audioCache.set(hash, data);
this.cacheMisses++;
// Manage cache size
this.manageCacheSize();
}
/**
* Manage cache size
*/
manageCacheSize() {
if (!this.audioCache) return;
// Check if cache size exceeds the maximum allowed
if (this.audioCache.size > this.maxCacheSize) {
// Remove the oldest item from the cache
const oldestKey = this.audioCache.keys().next().value;
this.audioCache.delete(oldestKey);
}
}
/**
* Generate a hash for a speech request
* @param {string} text - Text to generate hash for
* @returns {Promise<string>} - Hash value
*/
async generateSpeechHash(text) {
// For now, just use the text as the hash
// In a more complex implementation, you could include voice ID and other parameters
// You could also use a proper hashing function
return `${this.activeHandler}-${text}`;
}
/**
* Check if speech is cached by text
* @param {string} text - Text to check
* @returns {boolean} - True if cached
*/
async isSpeechCached(text) {
const hash = await this.generateSpeechHash(text);
return this.audioCache && this.audioCache.has(hash);
}
/**
* Cache speech data with text as key
* @param {string} text - Text used for the speech
* @param {Object} audioData - The audio data to cache
*/
async cacheSpeech(text, audioData) {
const hash = await this.generateSpeechHash(text);
this.addToCache(hash, audioData);
}
/**
* Clean up when module is disposed
*/