Split everything up into dynamically loaded modules.

This commit is contained in:
2025-04-04 00:00:43 +00:00
parent 2f7cda4b6d
commit aa29a6fd93
32 changed files with 8768 additions and 3935 deletions
+280
View File
@@ -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;
}
}