Added support for openai api tts.
This commit is contained in:
+262
-30
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user