201 lines
5.7 KiB
TypeScript
201 lines
5.7 KiB
TypeScript
|
|
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 {
|
||
|
|
// Skip if speech is disabled
|
||
|
|
if (!this.isSpeechEnabled || !this.speechSynthesis) {
|
||
|
|
// console.log('Speech is disabled, not speaking');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Clean and truncate text to prevent issues with large blocks
|
||
|
|
const cleanText = this.cleanTextForSpeech(text);
|
||
|
|
|
||
|
|
// 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
|
||
|
|
this.speechSynthesis.speak(utterance);
|
||
|
|
}
|
||
|
|
} 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;
|
||
|
|
}
|
||
|
|
}
|