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