/** * ApiTTSHandler for AI Interactive Fiction * Implementation using external TTS APIs like ElevenLabs */ import { TTSHandler } from './tts-handler.js'; export class ApiTTSHandler extends TTSHandler { constructor() { super(); // Initialize the base TTSHandler this.isReady = false; this.enabled = false; // Disabled by default until options panel is implemented this.audioElement = null; // Set voice options through base class this.voiceOptions = { voice: '8JNqTOY3RaSYcHTVJZ0G', // Default ElevenLabs voice ID model: 'eleven_multilingual_v1', stability: 0, similarityBoost: 0, style: 0.5, useSpeakerBoost: true }; this.apiKey = 'd191e27c2e5b07573b39fe70f0783f48'; // From speech.js this.apiUrl = 'https://api.elevenlabs.io/v1/text-to-speech'; this.voicesApiUrl = 'https://api.elevenlabs.io/v1/voices'; // Separate URL for voices endpoint this.cache = new Map(); this.currentCallback = null; } /** * Get the ID of this provider * @returns {string} - Provider ID */ getId() { return 'api'; } /** * Initialize the API TTS system * @param {Function} progressCallback - Optional callback for progress updates * @returns {Promise} - Resolves to true if initialization was successful */ async initialize(progressCallback = null) { try { if (progressCallback) progressCallback(20, 'Setting up API TTS'); // Create audio element for playback this.audioElement = new Audio(); // Set up audio event listeners this.audioElement.onended = () => { if (this.currentCallback) { const callback = this.currentCallback; this.currentCallback = null; callback(); } }; this.audioElement.onerror = (error) => { console.error('Audio playback error:', error); if (this.currentCallback) { const callback = this.currentCallback; this.currentCallback = null; callback(); } }; if (progressCallback) progressCallback(80, 'API TTS ready'); // Only check API if enabled if (this.enabled) { // Check if the API is reachable with a simple request try { const testResponse = await fetch(this.voicesApiUrl, { method: 'GET', headers: { 'xi-api-key': this.apiKey } }); if (testResponse.ok) { this.isReady = true; console.log('API TTS initialized successfully'); } else { console.warn('API TTS initialized but API may not be accessible'); } } catch (apiError) { console.warn('Could not verify API access, but continuing:', apiError); // We'll still mark as ready and try when speak is called this.isReady = true; } } else { console.log('API TTS is disabled by default. Enable via options panel when implemented.'); } if (progressCallback) progressCallback(100, 'API TTS initialization complete'); return this.isReady; } catch (error) { console.error('Error initializing API TTS:', error); return false; } } /** * Check if API TTS is available * @returns {boolean} - True if API TTS is ready to use */ isAvailable() { return this.isReady && this.enabled; } /** * Generate an MD5 hash for text caching * @param {string} text - Text to hash * @returns {string} - MD5 hash */ generateHash(text) { // Simple hash function for client-side use // For production, consider using a proper hashing library let hash = 0; if (text.length === 0) return hash.toString(); for (let i = 0; i < text.length; i++) { const char = text.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash).toString(16); } /** * Convert text to speech via API and play it * @param {string} text - Text to speak * @param {Function} callback - Called when speech completes */ async speak(text, callback = null) { if (!this.isAvailable() || !text) { if (callback) callback(); return; } // Stop any current speech this.stop(); // Set new callback this.currentCallback = callback; try { // Check cache first const cacheKey = this.generateHash(text + JSON.stringify(this.voiceOptions)); let audioUrl = this.cache.get(cacheKey); if (!audioUrl) { // Make API request to get audio const response = await fetch(`${this.apiUrl}/${this.voiceOptions.voice}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'xi-api-key': this.apiKey }, body: JSON.stringify({ text: text, model_id: this.voiceOptions.model, voice_settings: { stability: this.voiceOptions.stability, similarity_boost: this.voiceOptions.similarityBoost, style: this.voiceOptions.style, use_speaker_boost: this.voiceOptions.useSpeakerBoost } }) }); if (!response.ok) { throw new Error(`API returned ${response.status}: ${response.statusText}`); } // Get the audio data as blob const audioBlob = await response.blob(); audioUrl = URL.createObjectURL(audioBlob); // Store in cache this.cache.set(cacheKey, audioUrl); } // Play the audio this.audioElement.src = audioUrl; await this.audioElement.play(); } catch (error) { console.error('Error speaking with API TTS:', error); if (this.currentCallback) { const callback = this.currentCallback; this.currentCallback = null; callback(); } } } /** * Stop any ongoing speech */ stop() { if (this.audioElement) { this.audioElement.pause(); this.audioElement.currentTime = 0; } if (this.currentCallback) { const callback = this.currentCallback; this.currentCallback = null; callback(); } } /** * Set voice options * @param {Object} options - Voice options */ setVoiceOptions(options = {}) { if (options.voice !== undefined) this.voiceOptions.voice = options.voice; if (options.model !== undefined) this.voiceOptions.model = options.model; if (options.stability !== undefined) this.voiceOptions.stability = options.stability; if (options.similarityBoost !== undefined) this.voiceOptions.similarityBoost = options.similarityBoost; if (options.style !== undefined) this.voiceOptions.style = options.style; if (options.useSpeakerBoost !== undefined) this.voiceOptions.useSpeakerBoost = options.useSpeakerBoost; } /** * Get available voices from the API * @returns {Promise} - Array of available voices */ async getVoices() { if (!this.enabled) { return []; } try { const response = await fetch(this.voicesApiUrl, { method: 'GET', headers: { 'xi-api-key': this.apiKey } }); if (!response.ok) { throw new Error(`API returned ${response.status}: ${response.statusText}`); } const data = await response.json(); return data.voices || []; } catch (error) { console.error('Error getting voices from API:', error); return []; } } /** * Enable or disable the API TTS * @param {boolean} enabled - Whether the API TTS should be enabled */ setEnabled(enabled) { this.enabled = enabled; if (enabled && !this.isReady) { // Re-initialize if enabled this.initialize(); } } /** * Check if speech is currently playing * @returns {boolean} - True if speaking */ isSpeaking() { return this.audioElement !== null && !this.audioElement.paused && !this.audioElement.ended; } }