Reference: Semi broken tts before refactoring

This commit is contained in:
2025-04-05 17:23:01 +00:00
parent e8eb93ae1b
commit e5a3016846
9 changed files with 909 additions and 333 deletions
+150 -180
View File
@@ -83,6 +83,10 @@ export class ApiTTSHandlerBase extends TTSHandler {
persistenceManager.updatePreference('tts', `${this.id}_api_url`, defaultApiUrl);
}
// Log the current values for debugging
console.log(`${this.name} API KEY: ${this.apiKey ? '[SET]' : '[EMPTY]'}`);
console.log(`${this.name} API URL: ${this.apiBaseUrl}`);
if (progressCallback) {
progressCallback(40, `${this.name} API URL set to: ${this.apiBaseUrl}`);
}
@@ -114,7 +118,7 @@ export class ApiTTSHandlerBase extends TTSHandler {
this.isReady = true;
if (progressCallback) {
const statusMessage = this.available ?
const statusMessage = this.apiKey ?
`${this.name} initialized successfully` :
`${this.name} initialized but unavailable (API key missing)`;
progressCallback(100, statusMessage);
@@ -207,189 +211,157 @@ export class ApiTTSHandlerBase extends TTSHandler {
}
/**
* Preload speech for a text
* @param {string} text - Text to preload
* @returns {Promise<Object>} - Preloaded audio data
* Generate speech audio blob for the given text using the API.
* Does not handle caching or playback, returns the Blob directly.
* @param {string} text - The text to synthesize.
* @returns {Promise<Blob|null>} - A promise that resolves with the audio Blob, or null on failure.
*/
async preloadSpeech(text) {
// Don't try to preload if handler isn't ready, available, or if no text or API key
if (!this.isReady || !this.available || !text || !this.apiKey) {
if (!this.apiKey) {
console.log(`${this.name}: Skipping preload speech - no API key set`);
}
async generateSpeechAudio(text) {
if (!this.apiKey) {
console.error(`${this.name}: API key is not set.`);
return null;
}
if (!this.isReady || !this.currentVoice) {
console.error(`${this.name}: Handler not ready or no voice selected.`);
return null;
}
const requestUrl = this.getApiRequestUrl();
const requestBody = this.getApiRequestBody(text);
const requestHeaders = this.getApiRequestHeaders();
console.log(`${this.name}: Requesting speech generation...`);
// Log sensitive info only if debug enabled (assuming a global DEBUG flag or similar)
// if (DEBUG) {
// console.debug(`${this.name}: URL: ${requestUrl}`);
// console.debug(`${this.name}: Headers:`, JSON.stringify(requestHeaders));
// console.debug(`${this.name}: Body:`, JSON.stringify(requestBody));
// }
try {
// Process text for TTS
const processedText = this.preprocessText(text);
// Generate speech audio data
const audioData = await this.generateSpeechAudio(processedText);
const response = await fetch(requestUrl, {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(requestBody)
});
if (!response.ok) {
let errorBody = 'Unknown error';
try {
errorBody = await response.text(); // Try to get text first
const errorJson = JSON.parse(errorBody); // Try to parse as JSON
errorBody = errorJson.error?.message || errorJson.detail || JSON.stringify(errorJson);
} catch (e) {
// If parsing fails or it's not JSON, use the raw text
console.warn(`${this.name}: Could not parse error response as JSON. Raw text: ${errorBody}`);
}
throw new Error(`API Error (${response.status} ${response.statusText}): ${errorBody}`);
}
// --- Response Handling (Specific to API - Override if necessary) ---
// Default assumes response IS the audio blob
const audioBlob = await response.blob();
console.log(`${this.name}: Received audio blob, size: ${audioBlob.size}`);
// -------------------------------------------------------------------
if (!audioBlob || audioBlob.size === 0) {
throw new Error('Received empty audio blob from API.');
}
// Return the audio data blob
return audioBlob;
} catch (error) {
console.error(`${this.name}: Error generating speech audio:`, error);
this.handleApiError(error);
return null;
}
}
/**
* Plays preloaded audio data.
* @param {Blob} audioData - The audio data Blob to play.
* @param {Function} [callback=null] - Optional callback function.
*/
speakPreloaded(audioData, callback = null) {
// This method might now be redundant if the factory handles all playback.
// However, keeping it in case direct playback of preloaded data is needed elsewhere.
// Or, it could be simplified to just return the blob if factory always handles play.
// For now, let's keep the playback logic but it might be unused by the factory flow.
console.log(`${this.name}: Playing preloaded audio...`);
const audioManager = this.getModule('audio-manager');
if (audioManager && audioData) {
// This assumes audioManager.play handles Blobs
audioManager.play(audioData, callback);
} else {
console.error(`${this.name}: AudioManager not found or no audio data to play.`);
if (callback) callback(false, "Playback error");
}
}
/**
* Stops the currently playing audio.
*/
stop() {
console.log(`${this.name}: Stop requested.`);
const audioManager = this.getModule('audio-manager');
if (audioManager) {
audioManager.stop();
}
// Reset any internal state if needed
this.currentAudio = null;
}
/**
* Speak the given text using the API.
* This method now primarily calls generateSpeechAudio and returns the result.
* Caching and playback are handled by TTSFactoryModule.
* @param {string} text - The text to speak.
* @returns {Promise<Blob|null>} - A promise resolving to the audio Blob or null on failure.
*/
async speak(text) {
console.log(`${this.name}: speak called for text: ${text.substring(0, 30)}...`);
try {
// Generate audio data
const audioData = await this.generateSpeechAudio(text);
if (!audioData) {
console.error(`${this.name}: Failed to generate audio data for preloading`);
console.error(`${this.name}: Failed to generate audio for speak.`);
return null;
}
// Store in centralized TTSFactory cache
const ttsFactory = this.getModule('tts-factory');
if (ttsFactory) {
ttsFactory.cacheSpeech(text, audioData);
}
// Return audio data
// Return the Blob for the factory to handle
return audioData;
} catch (error) {
console.error(`${this.name}: Preload speech error:`, error);
console.error(`${this.name}: Error in speak method:`, error);
return null;
}
}
/**
* Generate speech audio data
* @param {string} text - Text to generate speech for
* @returns {Promise<Object>} - Audio data (Blob)
* Preloads speech for the given text.
* Generates the audio data but does not play it.
* Returns the generated Blob for the factory to cache.
* @param {string} text - The text to preload.
* @returns {Promise<Blob|null>} - A promise resolving to the audio Blob or null on failure.
*/
async generateSpeechAudio(text) {
// Should be implemented by subclasses
return null;
}
/**
* Speak text using preloaded audio
* @param {Object} preloadData - Preloaded audio data
* @param {Function} callback - Callback for when speech completes
* @returns {boolean} - Success status
*/
speakPreloaded(preloadData, callback = null) {
if (!this.isReady || !this.available || !preloadData) {
if (callback) {
setTimeout(() => callback({ success: false, reason: 'not_available' }), 0);
}
return false;
}
async preloadSpeech(text) {
console.log(`${this.name}: preloadSpeech called for text: ${text.substring(0, 30)}...`);
try {
// Stop any current audio
this.stop();
// Create Blob URL
const audioUrl = URL.createObjectURL(preloadData);
// Create new audio element
const audio = new Audio(audioUrl);
// Set up event handlers
audio.addEventListener('ended', () => {
// Clean up URL object
URL.revokeObjectURL(audioUrl);
// Clear current audio reference
if (this.currentAudio === audio) {
this.currentAudio = null;
}
// Dispatch completion event
this.dispatchEvent('tts:speak:complete', {});
if (callback) {
callback({ success: true });
}
}, { once: true });
audio.addEventListener('error', (error) => {
console.error(`${this.name}: Playback error:`, error);
// Clean up URL object
URL.revokeObjectURL(audioUrl);
// Dispatch error event
this.dispatchEvent('tts:speak:error', { error: error.message || 'Unknown error' });
if (callback) {
callback({ success: false, reason: 'playback_error', error });
}
}, { once: true });
// Store reference to current audio
this.currentAudio = audio;
// Play the audio
audio.play();
return true;
// Generate audio data using the main generation method
const audioData = await this.generateSpeechAudio(text);
if (audioData) {
console.log(`${this.name}: Successfully preloaded speech (blob generated).`);
return audioData; // Return the Blob for the factory
} else {
console.error(`${this.name}: Failed to generate audio for preload.`);
return null;
}
} catch (error) {
console.error(`${this.name}: Error playing preloaded audio:`, error);
// Dispatch error event
this.dispatchEvent('tts:speak:error', {
error: error.message || 'Unknown error'
});
if (callback) {
setTimeout(() => callback({ success: false, reason: 'playback_error', error }), 0);
}
return false;
}
}
/**
* Speak text
* @param {string} text - Text to speak
* @param {Function} callback - Callback for when speech completes
* @returns {boolean} - Success status
*/
async speak(text, callback = null) {
if (!this.isReady || !this.available || !text) {
if (callback) {
setTimeout(() => callback({ success: false, reason: 'not_available' }), 0);
}
return false;
}
try {
// Process text for TTS
const processedText = this.preprocessText(text);
// Check if already preloaded
const ttsFactory = this.getModule('tts-factory');
if (ttsFactory && ttsFactory.isSpeechCached(text)) {
return this.speakPreloaded(ttsFactory.getCachedSpeech(text), callback);
}
// Generate audio data
const audioData = await this.generateSpeechAudio(processedText);
if (!audioData) {
if (callback) {
setTimeout(() => callback({ success: false, reason: 'generation_failed' }), 0);
}
return false;
}
// Store in centralized TTSFactory cache
if (ttsFactory) {
ttsFactory.cacheSpeech(text, audioData);
}
// Play the audio
return this.speakPreloaded(audioData, callback);
} catch (error) {
console.error(`${this.name}: Error generating speech:`, error);
// Dispatch error event
this.dispatchEvent('tts:speak:error', {
text,
error: error.message || 'Unknown error'
});
if (callback) {
setTimeout(() => callback({ success: false, reason: 'generation_error', error }), 0);
}
return false;
console.error(`${this.name}: Error during preloadSpeech:`, error);
return null;
}
}
@@ -415,20 +387,6 @@ export class ApiTTSHandlerBase extends TTSHandler {
return processed;
}
/**
* Stop speaking
*/
stop() {
if (this.currentAudio) {
try {
this.currentAudio.pause();
this.currentAudio = null;
} catch (error) {
console.error(`${this.name}: Error stopping speech:`, error);
}
}
}
/**
* Check if TTS is available
* @returns {boolean} - True if TTS is available
@@ -485,9 +443,21 @@ export class ApiTTSHandlerBase extends TTSHandler {
if (event && event.detail && event.detail.provider === this.id) {
const newKey = event.detail.key || '';
// Security check - never use a URL as an API key
if (newKey && newKey.startsWith('http')) {
console.error(`${this.name}: Received URL instead of API key, ignoring it`);
return; // Don't update API key
}
// Update API key
this.apiKey = newKey;
// Save to preferences
const persistenceManager = this.getModule('persistence-manager');
if (persistenceManager) {
persistenceManager.updatePreference('tts', `${this.id}_api_key`, newKey);
}
// Update functionality status but don't make it unavailable
// We want it to stay in the dropdown for configuration
const wasFullyFunctional = this.available;