199 lines
5.6 KiB
JavaScript
199 lines
5.6 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|
|
}
|