789 lines
30 KiB
JavaScript
789 lines
30 KiB
JavaScript
/**
|
|
* Kokoro TTS Handler
|
|
* Handles text-to-speech using the Kokoro library
|
|
*/
|
|
|
|
import { TTSHandler } from './tts-handler.js';
|
|
import { moduleRegistry } from './module-registry.js';
|
|
|
|
export class KokoroHandler extends TTSHandler {
|
|
/**
|
|
* Constructor
|
|
* @param {Object} options - Options for the handler
|
|
*/
|
|
constructor(options = {}) {
|
|
super(options);
|
|
|
|
// Set default options
|
|
this.options = {
|
|
rate: 1.0,
|
|
volume: 1.0,
|
|
...options
|
|
};
|
|
|
|
// Initialize properties
|
|
this.id = 'kokoro';
|
|
this.name = 'Kokoro TTS Handler';
|
|
this.available = false;
|
|
this.loading = false;
|
|
this.iframe = null;
|
|
this.currentAudio = null;
|
|
this.currentVoice = null;
|
|
this.pendingGenerations = new Map();
|
|
this.generationCounter = 0;
|
|
|
|
// Default voices (will be replaced by dynamically fetched voices)
|
|
this.voices = [];
|
|
|
|
// Dependencies
|
|
this.dependencies = ['localization', 'persistence-manager'];
|
|
|
|
// Bind methods
|
|
this.initialize = this.initialize.bind(this);
|
|
this.speak = this.speak.bind(this);
|
|
this.stop = this.stop.bind(this);
|
|
this.getVoices = this.getVoices.bind(this);
|
|
this.setVoice = this.setVoice.bind(this);
|
|
this.generateSpeech = this.generateSpeech.bind(this);
|
|
this.preprocessText = this.preprocessText.bind(this);
|
|
this.speakPreloaded = this.speakPreloaded.bind(this);
|
|
this.preloadSpeech = this.preloadSpeech.bind(this);
|
|
this.pause = this.pause.bind(this);
|
|
this.resume = this.resume.bind(this);
|
|
this.setOptions = this.setOptions.bind(this);
|
|
this.setupVoiceFromPreferences = this.setupVoiceFromPreferences.bind(this);
|
|
this.getId = this.getId.bind(this);
|
|
this.handleIframeMessage = this.handleIframeMessage.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Get the ID of the handler
|
|
* @returns {string} - Handler ID
|
|
*/
|
|
getId() {
|
|
return 'kokoro';
|
|
}
|
|
|
|
/**
|
|
* Get a module from the registry
|
|
* @param {string} id - Module ID
|
|
* @returns {Object} - Module instance
|
|
*/
|
|
getModule(id) {
|
|
return moduleRegistry.getModule(id);
|
|
}
|
|
|
|
/**
|
|
* Initialize the handler
|
|
* @param {Function} progressCallback - Callback for progress updates
|
|
* @returns {Promise<boolean>} - Resolves with success status
|
|
*/
|
|
async initialize(progressCallback) {
|
|
try {
|
|
console.log('Kokoro TTS: Initializing...');
|
|
|
|
// Check if already initialized
|
|
if (this.available && this.isReady) {
|
|
console.log('Kokoro TTS: Already initialized and ready');
|
|
return true;
|
|
}
|
|
|
|
// Ensure we have at least default voices ready
|
|
if (!this.voices || this.voices.length === 0) {
|
|
console.log('Kokoro TTS: No voices set, initializing with defaults');
|
|
this.voices = this.getDefaultVoices();
|
|
}
|
|
|
|
// Set loading flag
|
|
this.loading = true;
|
|
this.isReady = false; // Explicitly set to false during initialization
|
|
|
|
// Create iframe if not already created
|
|
if (!this.iframe) {
|
|
console.log('Kokoro TTS: Creating iframe');
|
|
// Create iframe
|
|
this.iframe = document.createElement('iframe');
|
|
this.iframe.style.display = 'none';
|
|
this.iframe.src = '/kokoro-loader.html';
|
|
document.body.appendChild(this.iframe);
|
|
|
|
// Add message listener - IMPORTANT: Use an arrow function to preserve 'this'
|
|
window.addEventListener('message', (event) => this.handleIframeMessage(event));
|
|
}
|
|
|
|
// Set up event handler for configuration changes
|
|
document.addEventListener('tts:configure', (event) => {
|
|
if (event.detail) {
|
|
if (typeof event.detail.rate === 'number') {
|
|
this.options.rate = event.detail.rate;
|
|
console.log(`Kokoro TTS: Rate updated to ${this.options.rate}`);
|
|
}
|
|
if (typeof event.detail.volume === 'number') {
|
|
this.options.volume = event.detail.volume;
|
|
console.log(`Kokoro TTS: Volume updated to ${this.options.volume}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Wait for Kokoro to load
|
|
return new Promise((resolve) => {
|
|
// Set a timeout to prevent hanging indefinitely
|
|
const timeout = setTimeout(() => {
|
|
console.error('Kokoro TTS: Initialization timed out');
|
|
this.loading = false;
|
|
this.isReady = false;
|
|
this.available = false;
|
|
resolve(false);
|
|
}, 30000); // 30 second timeout
|
|
|
|
// Handle progress updates
|
|
const handleProgress = (progress, message) => {
|
|
console.log(`Kokoro TTS: Progress ${progress * 100}% - ${message}`);
|
|
if (progressCallback) {
|
|
progressCallback(progress, message);
|
|
}
|
|
};
|
|
|
|
// Handle message events
|
|
const messageHandler = (event) => {
|
|
if (event.source !== this.iframe.contentWindow) {
|
|
return;
|
|
}
|
|
|
|
const data = event.data;
|
|
|
|
if (data.type === 'kokoro-progress') {
|
|
handleProgress(data.progress, data.message);
|
|
} else if (data.type === 'kokoro-ready') {
|
|
console.log('Kokoro TTS: Received ready message from iframe', data);
|
|
// Remove the message listener
|
|
window.removeEventListener('message', messageHandler);
|
|
|
|
// Clear the timeout
|
|
clearTimeout(timeout);
|
|
|
|
// Set availability based on success
|
|
this.available = data.success;
|
|
this.loading = false;
|
|
this.isReady = data.success; // Set isReady flag based on success
|
|
|
|
// Store voices if provided
|
|
if (data.success && data.voices && Array.isArray(data.voices)) {
|
|
console.log(`Kokoro TTS: Received ${data.voices.length} voices from Kokoro iframe during initialization`);
|
|
this.voices = data.voices;
|
|
} else {
|
|
console.warn('Kokoro TTS: No voices received during initialization or invalid voices data');
|
|
if (data.success) {
|
|
// Even though we already set the default voices, check and update if needed
|
|
if (!this.voices || this.voices.length === 0) {
|
|
this.voices = this.getDefaultVoices();
|
|
console.log('Kokoro TTS: Using default voices as fallback');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set up voice from preferences
|
|
if (data.success) {
|
|
this.setupVoiceFromPreferences().then(() => {
|
|
console.log('Kokoro TTS: Voice set up from preferences during initialization');
|
|
}).catch(error => {
|
|
console.error('Kokoro TTS: Error setting up voice from preferences during initialization:',
|
|
error ? (error.message || error) : 'Unknown error');
|
|
});
|
|
}
|
|
|
|
// Resolve with success status
|
|
resolve(data.success);
|
|
}
|
|
};
|
|
|
|
// Add the message handler
|
|
window.addEventListener('message', messageHandler);
|
|
|
|
// Initial progress update
|
|
handleProgress(0.1, 'Starting Kokoro initialization');
|
|
});
|
|
} catch (error) {
|
|
console.error('Kokoro TTS: Error during initialization:', error);
|
|
this.loading = false;
|
|
this.isReady = false;
|
|
this.available = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle messages from the iframe
|
|
* @param {MessageEvent} event - Message event
|
|
*/
|
|
handleIframeMessage(event) {
|
|
// Only process messages from our iframe
|
|
if (!this.iframe || event.source !== this.iframe.contentWindow) {
|
|
return;
|
|
}
|
|
|
|
const data = event.data;
|
|
console.log('Kokoro TTS: Received message from iframe:', data.type);
|
|
|
|
switch (data.type) {
|
|
case 'kokoro-log':
|
|
console.log(`Kokoro Loader: ${data.message}`);
|
|
break;
|
|
|
|
case 'kokoro-ready':
|
|
console.log('Kokoro TTS: Received ready message from iframe. Success:', data.success, 'Voices:', data.voices ? data.voices.length : 0);
|
|
|
|
// Store availability
|
|
this.loading = false;
|
|
this.available = data.success;
|
|
this.isReady = data.success; // Important to set this for the base handler
|
|
|
|
// Store voices
|
|
if (data.success && data.voices && Array.isArray(data.voices)) {
|
|
console.log(`Kokoro TTS: Storing ${data.voices.length} voices from iframe`);
|
|
this.voices = data.voices;
|
|
} else if (data.success) {
|
|
// If success but no voices, use defaults
|
|
console.warn('Kokoro TTS: No voices received from iframe, using defaults');
|
|
this.voices = this.getDefaultVoices();
|
|
}
|
|
|
|
// Set up voice from preferences if ready
|
|
if (this.available) {
|
|
this.setupVoiceFromPreferences().then(() => {
|
|
console.log('Kokoro TTS: Voice set up from preferences');
|
|
});
|
|
}
|
|
|
|
// Dispatch ready event
|
|
this.dispatchEvent('tts:ready', { success: data.success });
|
|
break;
|
|
|
|
case 'kokoro-generated':
|
|
// Handle generated speech
|
|
if (data.id && this.pendingGenerations.has(data.id)) {
|
|
const { resolve, reject } = this.pendingGenerations.get(data.id);
|
|
this.pendingGenerations.delete(data.id);
|
|
|
|
if (data.success && data.result) {
|
|
// Create an audio element from the result
|
|
try {
|
|
// Create a blob from the buffer
|
|
const blob = new Blob([data.result.buffer], { type: 'audio/wav' });
|
|
|
|
// Create audio element
|
|
const audio = new Audio(URL.createObjectURL(blob));
|
|
|
|
// Create a play function
|
|
const play = () => {
|
|
audio.play().catch(error => {
|
|
console.error('Error playing Kokoro audio:', error);
|
|
});
|
|
};
|
|
|
|
resolve({ audio, play, blob });
|
|
} catch (error) {
|
|
console.error('Error processing Kokoro audio:', error);
|
|
reject(error);
|
|
}
|
|
} else {
|
|
console.error('Kokoro TTS: Invalid speech generation result');
|
|
reject(new Error(data.error || 'Unknown error generating speech'));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'kokoro-progress':
|
|
// Progress updates are handled during initialization
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set up the voice from preferences
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async setupVoiceFromPreferences() {
|
|
try {
|
|
console.log('Kokoro TTS: Setting up voice from preferences, available voices:', this.voices ? this.voices.length : 0);
|
|
|
|
// If no voices are available yet, use default voice
|
|
if (!this.voices || this.voices.length === 0) {
|
|
console.warn('Kokoro TTS: No voices available yet, using default voice');
|
|
return;
|
|
}
|
|
|
|
// Get persistence manager
|
|
const persistenceManager = this.getModule('persistence-manager');
|
|
if (!persistenceManager) {
|
|
console.warn('Kokoro TTS: Persistence manager not available');
|
|
this.currentVoice = this.voices[0]; // Default to first voice
|
|
return;
|
|
}
|
|
|
|
// Get localization
|
|
const localization = this.getModule('localization');
|
|
if (!localization) {
|
|
console.warn('Kokoro TTS: Localization not available');
|
|
this.currentVoice = this.voices[0]; // Default to first voice
|
|
return;
|
|
}
|
|
|
|
// Get current locale
|
|
let currentLocale = 'en-us'; // Default locale
|
|
if (localization && typeof localization.getLocale === 'function') {
|
|
currentLocale = localization.getLocale();
|
|
console.log('Kokoro TTS: Current locale from localization:', currentLocale);
|
|
} else {
|
|
console.warn('Kokoro TTS: getLocale method not available, using default locale');
|
|
}
|
|
|
|
// Get voice preference
|
|
const voiceId = persistenceManager.getPreference('tts-voice-kokoro');
|
|
console.log('Kokoro TTS: Preferred voice ID:', voiceId);
|
|
|
|
// Find voice
|
|
if (voiceId) {
|
|
const voice = this.voices.find(v => v.id === voiceId);
|
|
if (voice) {
|
|
console.log('Kokoro TTS: Found preferred voice:', voice.id, voice.name);
|
|
this.currentVoice = voice;
|
|
return;
|
|
} else {
|
|
console.warn('Kokoro TTS: Preferred voice not found:', voiceId);
|
|
}
|
|
}
|
|
|
|
// Find voice for current locale
|
|
if (currentLocale) {
|
|
// Standardize locale format (compare lowercase and handle hyphens/underscores)
|
|
const normalizedLocale = currentLocale.toLowerCase().replace('_', '-');
|
|
const localePrefix = normalizedLocale.split('-')[0]; // Get language prefix (en, de, etc.)
|
|
|
|
// First try exact locale match
|
|
let localeVoice = this.voices.find(v => v.lang && v.lang.toLowerCase().replace('_', '-') === normalizedLocale);
|
|
|
|
// If no exact match, try prefix match (en-US with en-GB for example)
|
|
if (!localeVoice) {
|
|
localeVoice = this.voices.find(v => {
|
|
if (!v.lang) return false;
|
|
const voiceLocale = v.lang.toLowerCase().replace('_', '-');
|
|
return voiceLocale.startsWith(localePrefix + '-');
|
|
});
|
|
}
|
|
|
|
if (localeVoice) {
|
|
console.log('Kokoro TTS: Found locale voice:', localeVoice.id, localeVoice.name, 'for locale:', normalizedLocale);
|
|
this.currentVoice = localeVoice;
|
|
return;
|
|
} else {
|
|
console.warn('Kokoro TTS: No voice found for locale:', normalizedLocale);
|
|
}
|
|
}
|
|
|
|
// Default to first voice if available
|
|
if (this.voices.length > 0) {
|
|
console.log('Kokoro TTS: Using first available voice:', this.voices[0].id, this.voices[0].name);
|
|
this.currentVoice = this.voices[0];
|
|
} else {
|
|
console.warn('Kokoro TTS: No voices available after all checks');
|
|
}
|
|
} catch (error) {
|
|
// Log detailed error information
|
|
console.error('Kokoro TTS: Error setting up voice from preferences:', error ? error.message || error : 'Unknown error');
|
|
|
|
// Default to first voice if available
|
|
if (this.voices && this.voices.length > 0) {
|
|
console.log('Kokoro TTS: Falling back to first voice after error');
|
|
this.currentVoice = this.voices[0];
|
|
} else {
|
|
console.warn('Kokoro TTS: No voices available to fall back to after error');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set voice for TTS
|
|
* @param {Object} voice - Voice to set
|
|
* @returns {boolean} - Success status
|
|
*/
|
|
setVoice(voice) {
|
|
if (!voice || !voice.id) {
|
|
return false;
|
|
}
|
|
|
|
// Find voice
|
|
const foundVoice = this.voices.find(v => v.id === voice.id);
|
|
if (!foundVoice) {
|
|
return false;
|
|
}
|
|
|
|
// Set voice
|
|
this.currentVoice = foundVoice;
|
|
|
|
// Save preference
|
|
try {
|
|
const persistenceManager = this.getModule('persistence-manager');
|
|
if (persistenceManager) {
|
|
persistenceManager.setPreference('tts-voice-kokoro', foundVoice.id);
|
|
}
|
|
} catch (error) {
|
|
console.error('Kokoro TTS: Error saving voice preference:', error);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set options for TTS
|
|
* @param {Object} options - Options to set
|
|
* @returns {boolean} - Success status
|
|
*/
|
|
setOptions(options) {
|
|
if (!options) {
|
|
return false;
|
|
}
|
|
|
|
// Update options
|
|
this.options = {
|
|
...this.options,
|
|
...options
|
|
};
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get available voices
|
|
* @returns {Array} - Array of voice objects
|
|
*/
|
|
getVoices() {
|
|
if (!this.voices || this.voices.length === 0) {
|
|
return this.getDefaultVoices();
|
|
}
|
|
return this.voices;
|
|
}
|
|
|
|
/**
|
|
* Preprocess text for TTS
|
|
* @param {string} text - Text to preprocess
|
|
* @returns {string} - Preprocessed text
|
|
*/
|
|
preprocessText(text) {
|
|
if (!text) {
|
|
return '';
|
|
}
|
|
|
|
// Remove HTML tags
|
|
let processed = text.replace(/<[^>]*>/g, '');
|
|
|
|
// Replace special characters
|
|
processed = processed.replace(/ /g, ' ');
|
|
processed = processed.replace(/&/g, '&');
|
|
processed = processed.replace(/</g, '<');
|
|
processed = processed.replace(/>/g, '>');
|
|
processed = processed.replace(/"/g, '"');
|
|
processed = processed.replace(/'/g, "'");
|
|
|
|
return processed;
|
|
}
|
|
|
|
/**
|
|
* Preload speech for later playback
|
|
* @param {string} text - Text to preload
|
|
* @returns {Promise<Object>} - Resolves with preloaded audio data
|
|
*/
|
|
async preloadSpeech(text) {
|
|
if (!this.available) {
|
|
console.warn('Kokoro TTS: Not available');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// No longer check the local cache as we're using TTSFactory's centralized cache
|
|
// Generate speech directly
|
|
const result = await this.generateSpeech(text);
|
|
|
|
// Return result for centralized caching in TTSFactory
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Kokoro TTS: Error preloading speech:', error);
|
|
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.available) {
|
|
console.warn('Kokoro TTS: Not available');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Stop any current speech
|
|
this.stop();
|
|
|
|
// Create audio element if not already created
|
|
const audio = preloadData.audio;
|
|
|
|
// Set up event handlers
|
|
audio.onended = () => {
|
|
this.currentAudio = null;
|
|
if (callback) callback();
|
|
};
|
|
|
|
audio.onerror = (error) => {
|
|
console.error('Kokoro TTS: Audio playback error:', error);
|
|
this.currentAudio = null;
|
|
if (callback) callback(error);
|
|
};
|
|
|
|
// Set volume
|
|
audio.volume = this.options.volume;
|
|
|
|
// Store current audio
|
|
this.currentAudio = audio;
|
|
|
|
// Play audio
|
|
if (preloadData.play) {
|
|
preloadData.play();
|
|
} else {
|
|
audio.play().catch(error => {
|
|
console.error('Kokoro TTS: Error playing audio:', error);
|
|
this.currentAudio = null;
|
|
if (callback) callback(error);
|
|
});
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Kokoro TTS: Error speaking preloaded audio:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Speak text
|
|
* @param {string} text - Text to speak
|
|
* @param {Object} options - Speech options
|
|
* @returns {Promise<boolean>} - Resolves with success status
|
|
*/
|
|
async speak(text, options = {}) {
|
|
if (!this.available) {
|
|
console.warn('Kokoro TTS: Not available');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Stop any current speech
|
|
this.stop();
|
|
|
|
console.log('Kokoro TTS: Generating speech for:', text);
|
|
|
|
// Generate speech
|
|
const result = await this.generateSpeech(text);
|
|
|
|
if (!result || !result.audio) {
|
|
console.error('Kokoro TTS: Invalid speech generation result');
|
|
return false;
|
|
}
|
|
|
|
// Set up event handlers
|
|
result.audio.onended = () => {
|
|
console.log('Kokoro TTS: Audio playback ended');
|
|
this.currentAudio = null;
|
|
// Dispatch event for completion
|
|
window.dispatchEvent(new CustomEvent('tts:speak-completed'));
|
|
};
|
|
|
|
result.audio.onerror = (error) => {
|
|
console.error('Kokoro TTS: Audio playback error:', error);
|
|
this.currentAudio = null;
|
|
// Dispatch event for error
|
|
window.dispatchEvent(new CustomEvent('tts:speak-error', {
|
|
detail: { error: error }
|
|
}));
|
|
};
|
|
|
|
// Set volume
|
|
result.audio.volume = this.options.volume;
|
|
|
|
// Store current audio
|
|
this.currentAudio = result.audio;
|
|
|
|
console.log('Kokoro TTS: Attempting to play audio');
|
|
|
|
// Play audio with better error handling
|
|
try {
|
|
if (result.play && typeof result.play === 'function') {
|
|
await result.play();
|
|
} else {
|
|
await result.audio.play();
|
|
}
|
|
console.log('Kokoro TTS: Audio playback started successfully');
|
|
return true;
|
|
} catch (playError) {
|
|
console.error('Error playing Kokoro audio:', playError);
|
|
this.currentAudio = null;
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.error('Kokoro TTS: Error speaking:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate speech using the iframe
|
|
* @param {string} text - Text to generate speech for
|
|
* @returns {Promise<Object>} - Resolves with audio data
|
|
*/
|
|
async generateSpeech(text) {
|
|
if (!this.iframe || !this.iframe.contentWindow) {
|
|
throw new Error('Kokoro iframe not initialized');
|
|
}
|
|
|
|
// Preprocess text
|
|
const processedText = this.preprocessText(text);
|
|
|
|
// Ensure we have a valid voice
|
|
let voiceId = 'af_heart'; // Default fallback
|
|
if (this.currentVoice && this.currentVoice.id) {
|
|
voiceId = this.currentVoice.id;
|
|
} else if (this.voices && this.voices.length > 0) {
|
|
// Default to first voice if none selected
|
|
this.currentVoice = this.voices[0];
|
|
voiceId = this.currentVoice.id;
|
|
console.log(`Kokoro TTS: No voice set, defaulting to ${voiceId}`);
|
|
}
|
|
|
|
console.log(`Kokoro TTS: Generating speech with voice ${voiceId}`);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
// Generate unique ID for this request
|
|
const id = `gen-${++this.generationCounter}`;
|
|
|
|
// Store the pending generation
|
|
this.pendingGenerations.set(id, { resolve, reject });
|
|
|
|
// Send the generation request to the iframe
|
|
this.iframe.contentWindow.postMessage({
|
|
type: 'kokoro-generate',
|
|
id: id,
|
|
text: processedText,
|
|
voice: voiceId,
|
|
speed: this.options.rate
|
|
}, '*');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stop current speech
|
|
* @returns {boolean} - Success status
|
|
*/
|
|
stop() {
|
|
if (this.currentAudio) {
|
|
try {
|
|
this.currentAudio.pause();
|
|
this.currentAudio.currentTime = 0;
|
|
this.currentAudio = null;
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Kokoro TTS: Error stopping speech:', error);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Pause current speech
|
|
* @returns {boolean} - Success status
|
|
*/
|
|
pause() {
|
|
if (this.currentAudio) {
|
|
try {
|
|
this.currentAudio.pause();
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Kokoro TTS: Error pausing speech:', error);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Resume current speech
|
|
* @returns {boolean} - Success status
|
|
*/
|
|
resume() {
|
|
if (this.currentAudio) {
|
|
try {
|
|
this.currentAudio.play();
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Kokoro TTS: Error resuming speech:', error);
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get default voices for current locale
|
|
* @returns {Array} Default voices
|
|
*/
|
|
getDefaultVoices() {
|
|
// Check if localization module is available
|
|
const localization = this.getModule('localization');
|
|
let locale = 'en-us'; // Default fallback
|
|
|
|
if (localization) {
|
|
locale = localization.getLocale();
|
|
console.log(`Kokoro TTS: Getting default voices for locale: ${locale}`);
|
|
} else {
|
|
console.log('Kokoro TTS: Localization module not available, using default locale: en-us');
|
|
}
|
|
|
|
// Use the actual voices defined in the Kokoro loader
|
|
return [
|
|
// American Female voices
|
|
{ id: 'af_heart', name: 'Heart', lang: 'en-US', gender: 'female' },
|
|
{ id: 'af_daisy', name: 'Daisy', lang: 'en-US', gender: 'female' },
|
|
{ id: 'af_soft', name: 'Soft', lang: 'en-US', gender: 'female' },
|
|
{ id: 'af_glados', name: 'GLaDOS', lang: 'en-US', gender: 'female' },
|
|
{ id: 'af_southern_belle', name: 'Southern Belle', lang: 'en-US', gender: 'female' },
|
|
{ id: 'af_dramatic', name: 'Dramatic', lang: 'en-US', gender: 'female' },
|
|
{ id: 'af_valley_girl', name: 'Valley Girl', lang: 'en-US', gender: 'female' },
|
|
{ id: 'af_british', name: 'British', lang: 'en-US', gender: 'female' },
|
|
{ id: 'af_russian', name: 'Russian', lang: 'en-US', gender: 'female' },
|
|
{ id: 'af_german', name: 'German', lang: 'en-US', gender: 'female' },
|
|
{ id: 'af_cheeky_cute', name: 'Cheeky Cute', lang: 'en-US', gender: 'female' },
|
|
|
|
// American Male voices
|
|
{ id: 'am_bruce', name: 'Bruce', lang: 'en-US', gender: 'male' },
|
|
{ id: 'am_announcer', name: 'Announcer', lang: 'en-US', gender: 'male' },
|
|
{ id: 'am_radio_host', name: 'Radio Host', lang: 'en-US', gender: 'male' },
|
|
|
|
// British Female voices
|
|
{ id: 'bf_charlotte', name: 'Charlotte', lang: 'en-GB', gender: 'female' },
|
|
{ id: 'bf_elizabeth', name: 'Elizabeth', lang: 'en-GB', gender: 'female' },
|
|
{ id: 'bf_lily', name: 'Lily', lang: 'en-GB', gender: 'female' },
|
|
{ id: 'bf_olivia', name: 'Olivia', lang: 'en-GB', gender: 'female' },
|
|
{ id: 'bf_victoria', name: 'Victoria', lang: 'en-GB', gender: 'female' },
|
|
|
|
// British Male voices
|
|
{ id: 'bm_william', name: 'William', lang: 'en-GB', gender: 'male' },
|
|
{ id: 'bm_arthur', name: 'Arthur', lang: 'en-GB', gender: 'male' },
|
|
{ id: 'bm_george', name: 'George', lang: 'en-GB', gender: 'male' },
|
|
{ id: 'bm_harry', name: 'Harry', lang: 'en-GB', gender: 'male' },
|
|
{ id: 'bm_jack', name: 'Jack', lang: 'en-GB', gender: 'male' }
|
|
];
|
|
}
|
|
} |