Files
ai.interactive.fiction/public/js/browser-tts-handler.js
T

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;
}
}