Fix TTS module initialization and dependency issues. Update module IDs for consistency, improve circular dependency detection, and fix UI Controller event handling.
This commit is contained in:
+571
-180
@@ -3,196 +3,587 @@
|
||||
* Implementation using the browser's Web Speech API
|
||||
*/
|
||||
import { TTSHandler } from './tts-handler.js';
|
||||
import { moduleRegistry } from './module-registry.js';
|
||||
|
||||
export class BrowserTTSHandler extends TTSHandler {
|
||||
constructor() {
|
||||
super(); // Initialize the base TTSHandler
|
||||
this.synth = window.speechSynthesis;
|
||||
this.utterance = null;
|
||||
this.voices = [];
|
||||
this.isReady = false;
|
||||
// Initialize voice options through base class
|
||||
this.voiceOptions = {
|
||||
voice: '',
|
||||
rate: 1.0,
|
||||
pitch: 1.0,
|
||||
volume: 1.0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if speech is currently playing
|
||||
* @returns {boolean} - True if speaking
|
||||
*/
|
||||
isSpeaking() {
|
||||
return this.synth && this.synth.speaking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of this provider
|
||||
* @returns {string} - Provider ID
|
||||
*/
|
||||
getId() {
|
||||
return 'browser';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the browser's speech synthesis
|
||||
* @param {Function} progressCallback - Optional callback for progress updates
|
||||
* @returns {Promise<boolean>} - Resolves to true if initialization was successful
|
||||
*/
|
||||
async initialize(progressCallback = null) {
|
||||
if (!this.synth) {
|
||||
console.warn('Web Speech API not supported in this browser');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (progressCallback) progressCallback(20, 'Loading speech synthesis');
|
||||
|
||||
// Get available voices
|
||||
this.voices = await this.getVoices();
|
||||
|
||||
if (progressCallback) progressCallback(80, 'Speech synthesis loaded');
|
||||
|
||||
// If we have voices, we're ready
|
||||
this.isReady = this.voices && this.voices.length > 0;
|
||||
|
||||
if (this.isReady) {
|
||||
console.log('Browser TTS initialized with', this.voices.length, 'voices');
|
||||
} else {
|
||||
console.warn('Browser TTS initialized but no voices available');
|
||||
}
|
||||
|
||||
if (progressCallback) progressCallback(100, 'Browser TTS ready');
|
||||
|
||||
return this.isReady;
|
||||
} catch (error) {
|
||||
console.error('Error initializing browser TTS:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available voices
|
||||
* @returns {Promise<Array>} - Array of available voices
|
||||
*/
|
||||
async getVoices() {
|
||||
return new Promise((resolve) => {
|
||||
// Some browsers get voices immediately, others need an event
|
||||
const voices = this.synth.getVoices();
|
||||
|
||||
if (voices && voices.length > 0) {
|
||||
resolve(voices);
|
||||
} else {
|
||||
// Wait for voiceschanged event
|
||||
const voicesChangedHandler = () => {
|
||||
this.synth.removeEventListener('voiceschanged', voicesChangedHandler);
|
||||
resolve(this.synth.getVoices());
|
||||
constructor() {
|
||||
super();
|
||||
this.id = 'browser';
|
||||
this.name = 'Browser TTS Handler';
|
||||
|
||||
// Voice options
|
||||
this.voiceOptions = {
|
||||
voice: null, // Will be set during initialization
|
||||
rate: 1.0,
|
||||
pitch: 1.0,
|
||||
volume: 1.0
|
||||
};
|
||||
|
||||
this.synth.addEventListener('voiceschanged', voicesChangedHandler);
|
||||
// State
|
||||
this.available = false;
|
||||
this.voices = [];
|
||||
this.currentUtterance = null;
|
||||
this.preloadCache = new Map();
|
||||
|
||||
// Safety mechanism: if after 3 seconds we still have no voices and no event,
|
||||
// resolve with whatever we have (or empty array)
|
||||
// This is not a setTimeout for synchronization, but a safety fallback
|
||||
const safetyCheckVoices = () => {
|
||||
const currentVoices = this.synth.getVoices() || [];
|
||||
console.log(`Safety check: Found ${currentVoices.length} voices`);
|
||||
resolve(currentVoices);
|
||||
};
|
||||
// Add dependencies
|
||||
this.dependencies = ['localization', 'persistence-manager'];
|
||||
|
||||
// Use requestIdleCallback if available, otherwise requestAnimationFrame
|
||||
if (window.requestIdleCallback) {
|
||||
window.requestIdleCallback(safetyCheckVoices, { timeout: 3000 });
|
||||
// Bind methods
|
||||
this.bindMethods([
|
||||
'initialize',
|
||||
'speak',
|
||||
'speakPreloaded',
|
||||
'preloadSpeech',
|
||||
'stop',
|
||||
'isAvailable',
|
||||
'getId',
|
||||
'getVoices',
|
||||
'setVoiceOptions',
|
||||
'onVoicesChanged',
|
||||
'getModule'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a module from the registry
|
||||
* @param {string} moduleId - ID of the module to get
|
||||
* @returns {Object|null} - The module or null if not found
|
||||
*/
|
||||
getModule(moduleId) {
|
||||
return moduleRegistry.getModule(moduleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the browser TTS handler
|
||||
* @param {Function} progressCallback - Callback for progress updates
|
||||
* @returns {Promise<boolean>} - Resolves with success status
|
||||
*/
|
||||
async initialize(progressCallback = null) {
|
||||
try {
|
||||
if (progressCallback) {
|
||||
progressCallback(10, "Initializing Browser TTS Handler");
|
||||
}
|
||||
|
||||
// Check if the browser supports speech synthesis
|
||||
if (!window.speechSynthesis) {
|
||||
console.error("Browser TTS: Speech synthesis not supported by browser");
|
||||
if (progressCallback) {
|
||||
progressCallback(100, "Browser TTS unavailable");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(30, "Loading voices");
|
||||
}
|
||||
|
||||
try {
|
||||
// Load available voices
|
||||
await this.loadVoices();
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(70, "Setting up voice");
|
||||
}
|
||||
|
||||
// Get localization module
|
||||
const localization = this.getModule('localization');
|
||||
const persistenceManager = this.getModule('persistence-manager');
|
||||
|
||||
// Get current locale and preferred voice
|
||||
let currentLocale = 'en-us';
|
||||
let preferredVoice = '';
|
||||
|
||||
if (localization) {
|
||||
currentLocale = localization.getLocale();
|
||||
} else {
|
||||
console.error("Browser TTS: Localization module not found");
|
||||
}
|
||||
|
||||
if (persistenceManager) {
|
||||
preferredVoice = persistenceManager.getPreference('tts', 'voice', '');
|
||||
} else {
|
||||
console.error("Browser TTS: Persistence Manager module not found");
|
||||
}
|
||||
|
||||
// Set voice based on locale and preferences
|
||||
await this.selectVoiceForLocale(currentLocale, preferredVoice);
|
||||
|
||||
// Check if we have a voice set
|
||||
if (this.voiceOptions.voice) {
|
||||
this.available = true;
|
||||
this.isReady = true;
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(100, "Browser TTS Handler ready");
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// Try one more time with a delay
|
||||
console.log("Browser TTS: No voice set, trying again after delay");
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(80, "Retrying voice loading");
|
||||
}
|
||||
|
||||
// Wait a bit and try again
|
||||
return new Promise(resolve => {
|
||||
setTimeout(async () => {
|
||||
await this.loadVoices();
|
||||
await this.selectVoiceForLocale(currentLocale, preferredVoice);
|
||||
|
||||
if (this.voiceOptions.voice) {
|
||||
this.available = true;
|
||||
this.isReady = true;
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(100, "Browser TTS Handler ready");
|
||||
}
|
||||
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error("Browser TTS: Failed to set voice after retry");
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(100, "Browser TTS initialization failed");
|
||||
}
|
||||
|
||||
resolve(false);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Browser TTS: Error loading voices:", error);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(100, "Browser TTS initialization failed");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Browser TTS: Initialization error:", error);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(100, "Browser TTS initialization failed");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle voices changed event
|
||||
*/
|
||||
async onVoicesChanged() {
|
||||
await this.loadVoices();
|
||||
const localization = this.getModule('localization');
|
||||
const persistenceManager = this.getModule('persistence-manager');
|
||||
let currentLocale = 'en-us';
|
||||
let preferredVoice = '';
|
||||
if (localization) {
|
||||
currentLocale = localization.getLocale();
|
||||
}
|
||||
if (persistenceManager) {
|
||||
preferredVoice = persistenceManager.getPreference('tts', 'voice', '');
|
||||
}
|
||||
await this.selectVoiceForLocale(currentLocale, preferredVoice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load available voices
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async loadVoices() {
|
||||
return new Promise(resolve => {
|
||||
// Get available voices
|
||||
const getVoices = () => {
|
||||
this.voices = speechSynthesis.getVoices() || [];
|
||||
console.log(`Browser TTS: Loaded ${this.voices.length} voices`);
|
||||
resolve();
|
||||
};
|
||||
|
||||
// Some browsers need a timeout to get voices
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (this.voices.length === 0) {
|
||||
this.voices = speechSynthesis.getVoices() || [];
|
||||
console.log(`Browser TTS: Loaded ${this.voices.length} voices after timeout`);
|
||||
resolve();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Try to get voices immediately
|
||||
this.voices = speechSynthesis.getVoices() || [];
|
||||
if (this.voices.length > 0) {
|
||||
clearTimeout(timeoutId);
|
||||
console.log(`Browser TTS: Loaded ${this.voices.length} voices immediately`);
|
||||
resolve();
|
||||
} else {
|
||||
// If no voices are available yet, set up the onvoiceschanged event
|
||||
speechSynthesis.onvoiceschanged = () => {
|
||||
clearTimeout(timeoutId);
|
||||
this.voices = speechSynthesis.getVoices() || [];
|
||||
console.log(`Browser TTS: Loaded ${this.voices.length} voices from event`);
|
||||
speechSynthesis.onvoiceschanged = null;
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set voice based on locale
|
||||
* @param {string} locale - Locale code (e.g., 'en-us', 'de', 'fr')
|
||||
* @param {string} preferredVoice - Optional preferred voice name
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async selectVoiceForLocale(locale = 'en-us', preferredVoice = '') {
|
||||
// Normalize locale for comparison
|
||||
const normalizedLocale = locale.toLowerCase().split('-')[0];
|
||||
|
||||
// If we have a preferred voice, try to use it first
|
||||
if (preferredVoice) {
|
||||
const matchingVoice = this.voices.find(voice =>
|
||||
voice.name === preferredVoice ||
|
||||
voice.voiceURI === preferredVoice
|
||||
);
|
||||
|
||||
if (matchingVoice) {
|
||||
this.voiceOptions.voice = matchingVoice;
|
||||
console.log(`Browser TTS: Using preferred voice: ${matchingVoice.name}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find voices matching the locale
|
||||
const localeVoices = this.voices.filter(voice => {
|
||||
const voiceLocale = voice.lang.toLowerCase();
|
||||
return voiceLocale.startsWith(normalizedLocale) ||
|
||||
voice.name.toLowerCase().includes(normalizedLocale);
|
||||
});
|
||||
|
||||
if (localeVoices.length > 0) {
|
||||
// Use the first matching voice
|
||||
this.voiceOptions.voice = localeVoices[0];
|
||||
console.log(`Browser TTS: Using ${normalizedLocale} voice: ${this.voiceOptions.voice.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no matching voice found, try to find any voice
|
||||
if (this.voices.length > 0) {
|
||||
// Look for a preferred language voice (English)
|
||||
const englishVoices = this.voices.filter(voice =>
|
||||
voice.lang.toLowerCase().startsWith('en')
|
||||
);
|
||||
|
||||
if (englishVoices.length > 0) {
|
||||
this.voiceOptions.voice = englishVoices[0];
|
||||
console.log(`Browser TTS: No ${normalizedLocale} voice found, using English voice: ${this.voiceOptions.voice.name}`);
|
||||
} else {
|
||||
// Use the first available voice
|
||||
this.voiceOptions.voice = this.voices[0];
|
||||
console.log(`Browser TTS: No ${normalizedLocale} or English voice found, using: ${this.voiceOptions.voice.name}`);
|
||||
}
|
||||
} else {
|
||||
// Schedule for next frame, but with longer delay
|
||||
setTimeout(safetyCheckVoices, 3000);
|
||||
console.log("Browser TTS: No voices available");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if browser TTS is available
|
||||
* @returns {boolean} - True if browser TTS is ready to use
|
||||
*/
|
||||
isAvailable() {
|
||||
return this.isReady && this.synth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speak text using browser TTS
|
||||
* @param {string} text - The text to speak
|
||||
* @param {Function} callback - Called when speech completes
|
||||
*/
|
||||
speak(text, callback = null) {
|
||||
if (!this.isAvailable() || !text) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop any current speech
|
||||
this.stop();
|
||||
|
||||
try {
|
||||
// Create a new utterance
|
||||
this.utterance = new SpeechSynthesisUtterance(text);
|
||||
|
||||
// Apply voice options
|
||||
if (this.voiceOptions.voice) {
|
||||
// Find the voice by name or URI
|
||||
const selectedVoice = this.voices.find(v =>
|
||||
v.name === this.voiceOptions.voice ||
|
||||
v.voiceURI === this.voiceOptions.voice
|
||||
);
|
||||
if (selectedVoice) {
|
||||
this.utterance.voice = selectedVoice;
|
||||
|
||||
/**
|
||||
* Preload speech for a text
|
||||
* @param {string} text - Text to preload
|
||||
* @returns {Promise<Object>} - Preloaded speech data
|
||||
*/
|
||||
async preloadSpeech(text) {
|
||||
if (!this.available || !text || !this.voiceOptions.voice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Process text for TTS
|
||||
const processedText = this.preprocessText(text);
|
||||
|
||||
console.log(`Browser TTS: Preloading speech for: "${processedText.substring(0, 50)}${processedText.length > 50 ? '...' : ''}"`);
|
||||
|
||||
// Create utterance but don't speak it yet
|
||||
const utterance = new SpeechSynthesisUtterance(processedText);
|
||||
|
||||
// Set voice and options
|
||||
utterance.voice = this.voiceOptions.voice;
|
||||
utterance.rate = this.voiceOptions.rate;
|
||||
utterance.pitch = this.voiceOptions.pitch;
|
||||
utterance.volume = this.voiceOptions.volume;
|
||||
utterance.lang = this.voiceOptions.voice.lang;
|
||||
|
||||
// Store preloaded data
|
||||
const preloadData = {
|
||||
utterance,
|
||||
text: processedText
|
||||
};
|
||||
|
||||
this.preloadCache.set(text, preloadData);
|
||||
return preloadData;
|
||||
} catch (error) {
|
||||
console.warn("Browser TTS: Error preloading speech:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply other options
|
||||
this.utterance.rate = this.voiceOptions.rate;
|
||||
this.utterance.pitch = this.voiceOptions.pitch;
|
||||
this.utterance.volume = this.voiceOptions.volume;
|
||||
|
||||
// Handle end of speech
|
||||
this.utterance.onend = () => {
|
||||
if (callback) callback();
|
||||
};
|
||||
|
||||
// Handle errors
|
||||
this.utterance.onerror = (e) => {
|
||||
console.error('Speech synthesis error:', e);
|
||||
if (callback) callback();
|
||||
};
|
||||
|
||||
// Start speaking
|
||||
this.synth.speak(this.utterance);
|
||||
} catch (error) {
|
||||
console.error('Error speaking with browser TTS:', error);
|
||||
if (callback) callback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop any ongoing speech
|
||||
*/
|
||||
stop() {
|
||||
if (this.synth) {
|
||||
this.synth.cancel();
|
||||
this.utterance = null;
|
||||
|
||||
/**
|
||||
* Speak text using preloaded utterance
|
||||
* @param {Object} preloadData - Preloaded speech data
|
||||
* @param {Function} callback - Callback for when speech completes
|
||||
* @returns {boolean} - Success status
|
||||
*/
|
||||
speakPreloaded(preloadData, callback = null) {
|
||||
if (!this.available || !preloadData || !preloadData.utterance) {
|
||||
if (callback) {
|
||||
setTimeout(() => callback({ success: false, reason: 'no_preloaded_data' }), 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Stop any current speech
|
||||
this.stop();
|
||||
|
||||
const { utterance, text } = preloadData;
|
||||
|
||||
// Dispatch start event
|
||||
this.dispatchEvent('tts:speak:start', { text });
|
||||
|
||||
// Set up event listeners
|
||||
utterance.onend = () => {
|
||||
this.currentUtterance = null;
|
||||
|
||||
// Dispatch end event
|
||||
this.dispatchEvent('tts:speak:end', { text });
|
||||
|
||||
if (callback) {
|
||||
callback({ success: true });
|
||||
}
|
||||
};
|
||||
|
||||
utterance.onerror = (error) => {
|
||||
this.currentUtterance = null;
|
||||
|
||||
// Dispatch error event
|
||||
this.dispatchEvent('tts:speak:error', {
|
||||
text,
|
||||
error: error.error || 'Unknown error'
|
||||
});
|
||||
|
||||
if (callback) {
|
||||
callback({ success: false, reason: 'synthesis_error', error });
|
||||
}
|
||||
};
|
||||
|
||||
// Store reference to current utterance
|
||||
this.currentUtterance = utterance;
|
||||
|
||||
// Speak the utterance
|
||||
speechSynthesis.speak(utterance);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Browser TTS: Error playing preloaded speech:", error);
|
||||
|
||||
// Dispatch error event
|
||||
this.dispatchEvent('tts:speak:error', {
|
||||
text: preloadData.text,
|
||||
error: error.message || 'Unknown error'
|
||||
});
|
||||
|
||||
if (callback) {
|
||||
setTimeout(() => callback({ success: false, reason: 'synthesis_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
|
||||
*/
|
||||
speak(text, callback = null) {
|
||||
if (!this.available || !this.voiceOptions.voice) {
|
||||
if (callback) {
|
||||
setTimeout(() => callback({ success: false, reason: 'not_available' }), 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Stop any current speech
|
||||
this.stop();
|
||||
|
||||
// Check if we have this in the preload cache
|
||||
if (this.preloadCache.has(text)) {
|
||||
const preloadData = this.preloadCache.get(text);
|
||||
this.preloadCache.delete(text); // Remove from cache
|
||||
return this.speakPreloaded(preloadData, callback);
|
||||
}
|
||||
|
||||
// Process text for TTS
|
||||
const processedText = this.preprocessText(text);
|
||||
|
||||
// Create utterance
|
||||
const utterance = new SpeechSynthesisUtterance(processedText);
|
||||
|
||||
// Set voice and options
|
||||
utterance.voice = this.voiceOptions.voice;
|
||||
utterance.rate = this.voiceOptions.rate;
|
||||
utterance.pitch = this.voiceOptions.pitch;
|
||||
utterance.volume = this.voiceOptions.volume;
|
||||
utterance.lang = this.voiceOptions.voice.lang;
|
||||
|
||||
// Dispatch start event
|
||||
this.dispatchEvent('tts:speak:start', { text: processedText });
|
||||
|
||||
// Set up event listeners
|
||||
utterance.onend = () => {
|
||||
this.currentUtterance = null;
|
||||
|
||||
// Dispatch end event
|
||||
this.dispatchEvent('tts:speak:end', { text: processedText });
|
||||
|
||||
if (callback) {
|
||||
callback({ success: true });
|
||||
}
|
||||
};
|
||||
|
||||
utterance.onerror = (error) => {
|
||||
this.currentUtterance = null;
|
||||
|
||||
// Dispatch error event
|
||||
this.dispatchEvent('tts:speak:error', {
|
||||
text: processedText,
|
||||
error: error.error || 'Unknown error'
|
||||
});
|
||||
|
||||
if (callback) {
|
||||
callback({ success: false, reason: 'synthesis_error', error });
|
||||
}
|
||||
};
|
||||
|
||||
// Store reference to current utterance
|
||||
this.currentUtterance = utterance;
|
||||
|
||||
// Speak the utterance
|
||||
speechSynthesis.speak(utterance);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Browser TTS: 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: 'synthesis_error', error }), 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess text for TTS
|
||||
* @param {string} text - Text to preprocess
|
||||
* @returns {string} - Processed text
|
||||
*/
|
||||
preprocessText(text) {
|
||||
if (!text) return '';
|
||||
|
||||
// Trim whitespace
|
||||
let processed = text.trim();
|
||||
|
||||
// Replace multiple spaces with a single space
|
||||
processed = processed.replace(/\s+/g, ' ');
|
||||
|
||||
// Add a period at the end if there's no punctuation
|
||||
if (!/[.!?]$/.test(processed)) {
|
||||
processed += '.';
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop speaking
|
||||
*/
|
||||
stop() {
|
||||
if (speechSynthesis) {
|
||||
speechSynthesis.cancel();
|
||||
this.currentUtterance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if TTS is available
|
||||
* @returns {boolean} - True if TTS is available
|
||||
*/
|
||||
isAvailable() {
|
||||
return this.available && this.voiceOptions.voice !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get handler ID
|
||||
* @returns {string} - Handler ID
|
||||
*/
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available voices
|
||||
* @returns {Array} - Array of voice objects
|
||||
*/
|
||||
getVoices() {
|
||||
return this.voices.map(voice => ({
|
||||
id: voice.voiceURI,
|
||||
name: voice.name,
|
||||
language: voice.lang
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set voice options
|
||||
* @param {Object} options - Voice options
|
||||
*/
|
||||
setVoiceOptions(options = {}) {
|
||||
if (options.voice) {
|
||||
// Find the voice by ID or name
|
||||
const voice = this.voices.find(v =>
|
||||
v.voiceURI === options.voice ||
|
||||
v.name === options.voice
|
||||
);
|
||||
|
||||
if (voice) {
|
||||
this.voiceOptions.voice = voice;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.rate === 'number') {
|
||||
// Clamp rate between 0.1 and 10
|
||||
this.voiceOptions.rate = Math.max(0.1, Math.min(10, options.rate));
|
||||
}
|
||||
|
||||
if (typeof options.pitch === 'number') {
|
||||
// Clamp pitch between 0 and 2
|
||||
this.voiceOptions.pitch = Math.max(0, Math.min(2, options.pitch));
|
||||
}
|
||||
|
||||
if (typeof options.volume === 'number') {
|
||||
// Clamp volume between 0 and 1
|
||||
this.voiceOptions.volume = Math.max(0, Math.min(1, options.volume));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set voice options
|
||||
* @param {Object} options - Voice options
|
||||
*/
|
||||
setVoiceOptions(options = {}) {
|
||||
if (options.voice !== undefined) this.voiceOptions.voice = options.voice;
|
||||
if (options.rate !== undefined) this.voiceOptions.rate = options.rate;
|
||||
if (options.pitch !== undefined) this.voiceOptions.pitch = options.pitch;
|
||||
if (options.volume !== undefined) this.voiceOptions.volume = options.volume;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user