import { EventEmitter } from '$lib/utils/EventEmitter'; export interface SpeechOptions { pitch?: number; rate?: number; volume?: number; voice?: SpeechSynthesisVoice; } export class AccessibilityManager extends EventEmitter { private isSpeechEnabled: boolean = false; private speechSynthesis: SpeechSynthesis | null = null; private speechOptions: SpeechOptions = { pitch: 1, rate: 1, volume: 0.8 }; constructor() { super(); console.log('AccessibilityManager constructed'); // Simple initialization - no speech synthesis by default if (typeof window !== 'undefined' && window.speechSynthesis) { this.speechSynthesis = window.speechSynthesis; console.log('Speech synthesis is available'); } } /** * Enable or disable speech */ public setSpeechEnabled(enabled: boolean): void { console.log('Setting speech enabled:', enabled); this.isSpeechEnabled = enabled; // Cancel speech if disabling if (!enabled && this.speechSynthesis) { try { this.speechSynthesis.cancel(); } catch (error) { console.error('Error cancelling speech:', error); } } this.emit('speechEnabledChanged', enabled); // Test speech synthesis if enabling was successful if (this.isSpeechEnabled) { try { console.log('Text-to-speech is now enabled'); } catch (error) { console.error('Error testing speech:', error); } } } /** * Update speech options */ public updateSpeechOptions(options: SpeechOptions): void { console.log('Speech options updated:', options); // Update stored options if (options.rate !== undefined) this.speechOptions.rate = options.rate; if (options.pitch !== undefined) this.speechOptions.pitch = options.pitch; if (options.volume !== undefined) this.speechOptions.volume = options.volume; if (options.voice !== undefined) this.speechOptions.voice = options.voice; console.log('New speech options:', this.speechOptions); this.emit('speechOptionsChanged', this.speechOptions); } /** * Speak text using text-to-speech */ public speak(text: string): void { console.log('AccessibilityManager.speak() called with text:', text.substring(0, 50) + (text.length > 50 ? '...' : '')); // Skip if speech is disabled if (!this.isSpeechEnabled || !this.speechSynthesis) { console.log('Speech is disabled or speechSynthesis is null:', { isSpeechEnabled: this.isSpeechEnabled, speechSynthesis: !!this.speechSynthesis }); return; } try { // Clean and truncate text to prevent issues with large blocks const cleanText = this.cleanTextForSpeech(text); console.log('Cleaned text for speech:', cleanText.substring(0, 50) + (cleanText.length > 50 ? '...' : '')); // Only speak if there's meaningful text after cleaning if (cleanText && cleanText.trim().length > 0) { console.log('Speaking text with options:', { rate: this.speechOptions.rate, pitch: this.speechOptions.pitch, volume: this.speechOptions.volume }); const utterance = new SpeechSynthesisUtterance(cleanText); // Explicitly set options utterance.rate = Number(this.speechOptions.rate) || 1; utterance.pitch = Number(this.speechOptions.pitch) || 1; utterance.volume = Number(this.speechOptions.volume) || 0.8; console.log('Created utterance with:', { rate: utterance.rate, pitch: utterance.pitch, volume: utterance.volume }); // Add event handlers for debugging utterance.onstart = () => console.log('Speech started'); utterance.onend = () => console.log('Speech ended'); utterance.onerror = (e) => console.error('Speech error:', e); // Apply voice if set if (this.speechOptions.voice) { utterance.voice = this.speechOptions.voice; } // Speak the text console.log('Calling speechSynthesis.speak()'); this.speechSynthesis.speak(utterance); console.log('speechSynthesis.speak() called successfully'); } else { console.log('Not speaking - cleaned text is empty'); } } catch (error) { console.error('Error in speak:', error); } } /** * Clean text for speech synthesis */ private cleanTextForSpeech(text: string): string { if (!text) return ''; try { // Limit text length to prevent freezes with very long text let cleanText = text.length > 200 ? text.substring(0, 200) : text; // Remove ANSI color codes cleanText = cleanText.replace(/\u001b\[\d+(;\d+)*m/g, ''); // Remove HTML tags cleanText = cleanText.replace(/<[^>]*>/g, ''); return cleanText.trim(); } catch (error) { console.error('Error cleaning text for speech:', error); return ''; } } /** * Stop current speech */ public stopSpeech(): void { if (!this.speechSynthesis) return; try { // Make sure we cancel any pending or active speech this.speechSynthesis.cancel(); console.log('Speech stopped'); this.emit('speechStopped'); // Force a small delay to ensure the speech engine has time to process the cancel setTimeout(() => { if (this.speechSynthesis && this.speechSynthesis.speaking) { console.log('Force stopping speech again after delay'); this.speechSynthesis.cancel(); } }, 50); } catch (error) { console.error('Error stopping speech:', error); } } /** * Check if speech is currently speaking */ public isSpeaking(): boolean { // Ensure the speech synthesis is available if (!this.speechSynthesis) return false; // Check if speech is currently active try { return this.speechSynthesis.speaking || this.speechSynthesis.pending; } catch (error) { console.error('Error checking if speech is speaking:', error); return false; } } /** * Check if speech is currently enabled */ public isSpeechActive(): boolean { return this.isSpeechEnabled; } }