Split everything up into dynamically loaded modules.
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* 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<boolean>} - 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>} - 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user