Split everything up into dynamically loaded modules.
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* BrowserTTSHandler for AI Interactive Fiction
|
||||
* Implementation using the browser's Web Speech API
|
||||
*/
|
||||
import { TTSHandler } from './tts-handler.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());
|
||||
};
|
||||
|
||||
this.synth.addEventListener('voiceschanged', voicesChangedHandler);
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
// Use requestIdleCallback if available, otherwise requestAnimationFrame
|
||||
if (window.requestIdleCallback) {
|
||||
window.requestIdleCallback(safetyCheckVoices, { timeout: 3000 });
|
||||
} else {
|
||||
// Schedule for next frame, but with longer delay
|
||||
setTimeout(safetyCheckVoices, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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