Reference: Semi broken tts before refactoring
This commit is contained in:
+150
-180
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user