/** * ElevenLabsTTSModule * Provides TTS via ElevenLabs API */ import { ApiTTSModuleBase } from './api-tts-module-base.js'; export class ElevenLabsTTSModule extends ApiTTSModuleBase { constructor() { super('elevenlabs-tts', 'ElevenLabs TTS'); // Voice options specific to ElevenLabs this.voiceOptions = { voice: 'pNInz6obpgDQGcFmaJgB', // Default voice ID for ElevenLabs model: 'eleven_multilingual_v2', // Use the multilingual model speed: 1.0 }; } /** * Initialize the ElevenLabs TTS module * @returns {Promise} - Resolves with success status */ async initialize() { try { this.reportProgress(10, 'Initializing ElevenLabs TTS'); // Initialize parent const parentInit = await super.initialize(); if (!parentInit) { console.error('ElevenLabs TTS: Parent initialization failed'); return false; } // Get required dependencies const persistenceManager = this.getModule('persistence-manager'); if (!persistenceManager) { console.error('ElevenLabs TTS: Required dependency persistence-manager not found'); return false; } // Check for API key const apiKey = persistenceManager.getPreference('elevenlabs', 'api_key', ''); if (!apiKey) { console.error('ElevenLabs TTS: API key not configured'); return false; } // Load voices from ElevenLabs try { this.reportProgress(50, 'Loading ElevenLabs voices'); await this.loadVoices(apiKey); } catch (error) { console.error('ElevenLabs TTS: Failed to load voices:', error); return false; } // Load preferences const preferredVoice = persistenceManager.getPreference('elevenlabs', 'voice', this.voiceOptions.voice); if (preferredVoice) { this.voiceOptions.voice = preferredVoice; } const preferredModel = persistenceManager.getPreference('elevenlabs', 'model', this.voiceOptions.model); if (preferredModel) { this.voiceOptions.model = preferredModel; } const preferredSpeed = persistenceManager.getPreference('elevenlabs', 'speed', this.voiceOptions.speed); if (typeof preferredSpeed === 'number') { this.voiceOptions.speed = preferredSpeed; } this.isReady = true; this.reportProgress(100, 'ElevenLabs TTS initialized'); return true; } catch (error) { console.error('ElevenLabs TTS: Initialization error:', error); this.isReady = false; return false; } } /** * Get the default API base URL for ElevenLabs * @returns {string} - Default API base URL */ getDefaultApiBaseUrl() { return 'https://api.elevenlabs.io/v1'; } /** * Load available voices from ElevenLabs API * @param {string} apiKey - API key for authentication * @returns {Promise} - Resolves with success status */ async loadVoices(apiKey) { // Set default voices that will be used if API call fails this.voices = [ { id: 'pNInz6obpgDQGcFmaJgB', name: 'Rachel', language: 'en' }, { id: '21m00Tcm4TlvDq8ikWAM', name: 'Adam', language: 'en' }, { id: 'AZnzlk1XvdvUeBnXmlld', name: 'Antoni', language: 'en' }, { id: 'EXAVITQu4vr4xnSDxMaL', name: 'Bella', language: 'en' }, { id: 'ErXwobaYiN019PkySvjV', name: 'Daniel', language: 'en' } ]; // Only load from API if we have an API key if (!apiKey) { return true; } try { const response = await fetch(`${this.apiBaseUrl}/voices`, { method: 'GET', headers: { 'Accept': 'application/json', 'xi-api-key': apiKey } }); if (!response.ok) { console.error(`ElevenLabs TTS: API error ${response.status} ${response.statusText}`); return true; // Continue with default voices } const data = await response.json(); if (data && data.voices && Array.isArray(data.voices)) { // Map API voices to our format this.voices = data.voices.map(voice => ({ id: voice.voice_id, name: voice.name, language: voice.language || 'en', gender: 'unknown', preview_url: voice.preview_url })); return true; } return true; // Continue with default voices } catch (error) { console.error('ElevenLabs TTS: Error loading voices:', error); return true; // Continue with default voices } } /** * Select a voice for the given locale * @param {string} locale - Locale code * @returns {boolean} - Success status */ selectVoiceForLocale(locale) { // Extract language code from locale (e.g., 'en-US' -> 'en') const langCode = locale.split('-')[0].toLowerCase(); // For English locales, select 'Rachel' if available if (langCode === 'en') { const defaultVoice = this.voices.find(v => v.id === 'pNInz6obpgDQGcFmaJgB'); if (defaultVoice) { this.voiceOptions.voice = defaultVoice.id; return true; } } return this.selectDefaultVoice(); } /** * Generate speech audio data using ElevenLabs API * @param {string} text - Text to generate speech for * @returns {Promise} - Audio data object */ async generateSpeechAudio(text) { // Don't attempt to call the API if no API key is set or text is empty if (!text || !this.apiKey) { return { success: false, reason: 'missing_api_key_or_text' }; } try { // Process the text const processedText = this.preprocessText(text); // Create request payload const payload = { text: processedText, model_id: this.voiceOptions.model || 'eleven_multilingual_v2', voice_settings: { stability: 0.5, similarity_boost: 0.75, style: 0.0, use_speaker_boost: true, speed: this.voiceOptions.speed || 1.0 } }; // Make API request const response = await fetch(`${this.apiBaseUrl}/text-to-speech/${this.voiceOptions.voice}?optimize_streaming_latency=0`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'xi-api-key': this.apiKey, 'Accept': 'audio/wav' }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`API error: ${response.status} ${response.statusText}`); } // Get audio blob from response const audioBlob = await response.blob(); // Convert to array buffer for consistency with other modules const arrayBuffer = await audioBlob.arrayBuffer(); return { success: true, audioData: arrayBuffer }; } catch (error) { console.error('ElevenLabs TTS: Error generating speech:', error); return { success: false, reason: 'api_error', error: error.message }; } } /** * Set voice options * @param {Object} options - Voice options */ setVoiceOptions(options = {}) { // Call parent method for common options if (options.voice) { this.voiceOptions.voice = options.voice; // Save voice preference const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('tts', 'elevenlabs_voice', options.voice); } } if (typeof options.speed === 'number') { this.voiceOptions.speed = Math.max(0.5, Math.min(2.0, options.speed)); } // Handle ElevenLabs-specific options if (options.model) { this.voiceOptions.model = options.model; // Save model preference const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('tts', 'elevenlabs_model', options.model); } } } } const elevenLabsTTSModule = new ElevenLabsTTSModule(); export { elevenLabsTTSModule };