| 168 | 172 | //#################// |
| 169 | 173 | fetchTtsVoiceObjects() { |
| 170 | 174 | if (!('speechSynthesis' in window)) { |
| 171 | 175 | return Promise.resolve([]); |
| 172 | 176 | } |
| 173 | 177 | |
| 174 | 178 | return new Promise((resolve) => { |
| 175 | 179 | setTimeout(() => { |
| 176 | 180 | constlet voices = speechSynthesis.getVoices(); |
| 177 | | - .getVoices() |
| 181 | + |
| 178 | | - .sort((a, b) => a.lang.localeCompare(b.lang) || a.name.localeCompare(b.name)) |
| 182 | + if (voices.length === 0) { |
| 179 | | - .map(x => ({ name: x.name, voice_id: x.voiceURI, preview_url: false, lang: x.lang })); |
| 183 | + // Edge compat: Provide default when voices empty |
| 180 | | - |
| 184 | + console.warn('SystemTTS: getVoices() returned empty list. Providing browser default option.'); |
| 181 | | - resolve(voices); |
| 185 | + const defaultVoice = { |
| 182 | | - }, 1); |
| 186 | + name: SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME, |
| 187 | + voice_id: SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID, |
| 188 | + preview_url: false, |
| 189 | + lang: navigator.language || 'en-US', |
| 190 | + }; |
| 191 | + resolve([defaultVoice]); |
| 192 | + } else { |
| 193 | + const mappedVoices = voices |
| 194 | + .sort((a, b) => a.lang.localeCompare(b.lang) || a.name.localeCompare(b.name)) |
| 195 | + .map(x => ({ name: x.name, voice_id: x.voiceURI, preview_url: false, lang: x.lang })); |
| 196 | + resolve(mappedVoices); |
| 197 | + } |
| 198 | + }, 50); |
| 183 | 199 | }); |
| 184 | 200 | } |
| 185 | 201 | |
| 186 | 202 | previewTtsVoice(voiceId) { |
| 187 | 203 | if (!('speechSynthesis' in window)) { |
| 188 | 204 | throw new Error('Speech synthesis API is not supported'); |
| 189 | 205 | } |
| 190 | 206 | |
| 191 | | - const voice = speechSynthesis.getVoices().find(x => x.voiceURI === voiceId); |
| 207 | + let voice = null; |
| 208 | + if (voiceId !== SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID) { |
| 209 | + const voices = speechSynthesis.getVoices(); |
| 210 | + voice = voices.find(x => x.voiceURI === voiceId); |
| 192 | 211 | |
| 193 | 212 | if (!voice && voices.length > 0) { |
| 194 | | - throw `TTS Voice id ${voiceId} not found`; |
| 213 | + console.warn(`SystemTTS Preview: Voice ID "${voiceId}" not found among available voices. Using browser default.`); |
| 214 | + } else if (!voice && voices.length === 0) { |
| 215 | + console.warn('SystemTTS Preview: Voice list is empty. Using browser default.'); |
| 216 | + } |
| 217 | + } else { |
| 218 | + console.log('SystemTTS Preview: Using browser default voice as requested.'); |
| 195 | 219 | } |
| 196 | 220 | |
| 197 | 221 | speechSynthesis.cancel(); |
| 198 | | - const text = getPreviewString(voice.lang); |
| 222 | + const langForPreview = voice ? voice.lang : (navigator.language || 'en-US'); |
| 223 | + const text = getPreviewString(langForPreview); |
| 199 | 224 | const utterance = new SpeechSynthesisUtterance(text); |
| 200 | | - utterance.voice = voice; |
| 225 | + |
| 226 | + if (voice) { |
| 227 | + utterance.voice = voice; |
| 228 | + } |
| 229 | + |
| 201 | 230 | utterance.rate = this.settings.rate || 1; |
| 202 | 231 | utterance.pitch = this.settings.pitch || 1; |
| 232 | + |
| 233 | + utterance.onerror = (event) => { |
| 234 | + console.error(`SystemTTS Preview Error: ${event.error}`, event); |
| 235 | + }; |
| 236 | + |
| 203 | 237 | speechSynthesis.speak(utterance); |
| 204 | 238 | } |
| 205 | 239 | |
| 206 | 240 | async getVoice(voiceName) { |
| 207 | 241 | if (!('speechSynthesis' in window)) { |
| 208 | 242 | return { voice_id: null, name: 'API Not Supported' }; |
| 243 | + } |
| 244 | + |
| 245 | + if (voiceName === SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME) { |
| 246 | + return { |
| 247 | + voice_id: SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID, |
| 248 | + name: SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME, |
| 249 | + }; |
| 209 | 250 | } |
| 210 | 251 | |
| 211 | 252 | const voices = speechSynthesis.getVoices(); |
| 253 | + |
| 254 | + if (voices.length === 0) { |
| 255 | + console.warn('SystemTTS: Empty voice list, using default fallback'); |
| 256 | + return { |
| 257 | + voice_id: SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID, |
| 258 | + name: SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME, |
| 259 | + }; |
| 260 | + } |
| 261 | + |
| 212 | 262 | const match = voices.find(x => x.name == voiceName); |
| 213 | 263 | |
| 214 | 264 | if (!match) { |
| 215 | 265 | throw new Error(`SystemTTS getVoice: TTS Voice name "${voiceName}" not found`); |
| 216 | 266 | } |
| 217 | 267 | |
| 218 | 268 | return { voice_id: match.voiceURI, name: match.name }; |