/** * Cross-platform TTS Voice Manager for Electron * Handles voice selection with platform-specific fallbacks */ class VoiceManager { constructor() { this.synth = window.speechSynthesis; this.selectedVoice = null; this.fallbackVoices = this.getPlatformFallbacks(); } /** * Get platform-specific fallback voice names in order of preference */ getPlatformFallbacks() { const platform = this.detectPlatform(); switch (platform) { case 'windows': return [ 'Microsoft Zira Desktop', 'Microsoft Zira', // Windows default female 'Microsoft Hazel Desktop', 'Microsoft Hazel', // Alternative 'Microsoft Eva Desktop', 'Microsoft Eva', // Another option 'Microsoft Aria', 'Microsoft Jenny', // Newer voices if available 'Zira', 'Hazel', 'Eva', 'Aria', 'Jenny' // Short names as fallback ]; case 'mac': return [ 'Samantha', // macOS default female voice 'Victoria', // Alternative female 'Karen', // Another option 'Fiona', // Additional choice 'Moira', // Backup 'Tessa', // Last resort 'Alex' // Can work as female with pitch adjustment ]; default: return [ 'Samantha', 'Zira', // Try both platform defaults 'Victoria', 'Hazel', // Alternatives 'Karen', 'Eva', // More options 'female', 'woman' // Generic indicators ]; } } /** * Detect current platform */ detectPlatform() { const userAgent = navigator.userAgent.toLowerCase(); if (userAgent.includes('win')) return 'windows'; if (userAgent.includes('mac')) return 'mac'; if (userAgent.includes('linux')) return 'linux'; return 'unknown'; } /** * Check if a voice is female */ isFemaleVoice(voice) { const name = voice.name.toLowerCase(); // Platform-specific female voice names const femaleVoiceNames = [ // Windows voices 'zira', 'hazel', 'eva', 'aria', 'jenny', // macOS voices 'samantha', 'victoria', 'karen', 'fiona', 'moira', 'tessa', // Generic indicators 'female', 'woman', 'girl', 'lady' ]; return femaleVoiceNames.some(femaleName => name.includes(femaleName)); } /** * Get all available female voices */ getAvailableFemaleVoices() { const voices = this.synth.getVoices(); return voices.filter(voice => voice.lang.startsWith('en') && this.isFemaleVoice(voice) ); } /** * Select the best available female voice for this platform */ selectBestVoice() { const availableVoices = this.getAvailableFemaleVoices(); if (availableVoices.length === 0) { console.warn('No female voices available'); return null; } // Try to find voices in order of preference for (const preferredName of this.fallbackVoices) { const voice = availableVoices.find(v => v.name.toLowerCase().includes(preferredName.toLowerCase()) ); if (voice) { this.selectedVoice = voice; return voice; } } // If no preferred voice found, use the first available female voice this.selectedVoice = availableVoices[0]; return this.selectedVoice; } /** * Initialize voice selection (call after voices are loaded) */ async initialize() { return new Promise((resolve) => { // Wait for voices to be loaded if (this.synth.getVoices().length > 0) { this.selectBestVoice(); resolve(this.selectedVoice); } else { this.synth.addEventListener('voiceschanged', () => { this.selectBestVoice(); resolve(this.selectedVoice); }, { once: true }); } }); } /** * Speak text with the selected voice */ speak(text, options = {}) { if (!this.selectedVoice) { this.selectBestVoice(); } const utterance = new SpeechSynthesisUtterance(text); // Apply voice if (this.selectedVoice) { utterance.voice = this.selectedVoice; } // Apply settings with defaults utterance.rate = options.rate || 1.0; utterance.pitch = options.pitch || 1.0; utterance.volume = options.volume || 1.0; // Platform-specific pitch adjustments for better female sound if (this.selectedVoice) { const voiceName = this.selectedVoice.name.toLowerCase(); // Adjust pitch for Alex on Mac to sound more feminine if (voiceName.includes('alex')) { utterance.pitch = Math.max(utterance.pitch * 1.2, 1.5); } } // Add event listeners if provided if (options.onStart) utterance.onstart = options.onStart; if (options.onEnd) utterance.onend = options.onEnd; if (options.onError) utterance.onerror = options.onError; this.synth.speak(utterance); return utterance; } /** * Get current voice information */ getVoiceInfo() { if (!this.selectedVoice) { return { name: 'No voice selected', platform: this.detectPlatform(), available: false }; } return { name: this.selectedVoice.name, lang: this.selectedVoice.lang, platform: this.detectPlatform(), available: true, localService: this.selectedVoice.localService, voiceURI: this.selectedVoice.voiceURI }; } /** * Stop current speech */ stop() { this.synth.cancel(); } /** * Check if speech synthesis is supported */ isSupported() { return 'speechSynthesis' in window; } } // Export for use in other modules window.VoiceManager = VoiceManager;