| 168 | //#################// | 172 | //#################// |
| 169 | fetchTtsVoiceObjects() { | 173 | fetchTtsVoiceObjects() { |
| 170 | if (!('speechSynthesis' in window)) { | 174 | if (!('speechSynthesis' in window)) { |
| 171 | return []; | 175 | // Browser doesn't support speech synthesis |
| | 176 | return Promise.resolve([]); |
| 172 | } | 177 | } |
| 173 | | 178 | |
| 174 | return new Promise((resolve) => { | 179 | return new Promise((resolve) => { |
| | 180 | // Use a minimal timeout to allow the voice list to potentially populate |
| 175 | setTimeout(() => { | 181 | setTimeout(() => { |
| 176 | const voices = speechSynthesis | 182 | let voices = speechSynthesis.getVoices(); |
| 177 | .getVoices() | 183 | |
| | 184 | if (voices.length === 0) { |
| | 185 | // If no voices returned (e.g., Edge on first load), provide a default option |
| | 186 | console.warn('SystemTTS: getVoices() returned empty list. Providing browser default option.'); |
| | 187 | const defaultVoice = { |
| | 188 | name: SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME, |
| | 189 | voice_id: SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID, |
| | 190 | preview_url: false, |
| | 191 | // Try to guess the browser's default language |
| | 192 | lang: navigator.language || 'en-US', |
| | 193 | }; |
| | 194 | resolve([defaultVoice]); |
| | 195 | } else { |
| | 196 | // If voices are available, map them as before |
| | 197 | const mappedVoices = voices |
| 178 | .sort((a, b) => a.lang.localeCompare(b.lang) || a.name.localeCompare(b.name)) | 198 | .sort((a, b) => a.lang.localeCompare(b.lang) || a.name.localeCompare(b.name)) |
| 179 | .map(x => ({ name: x.name, voice_id: x.voiceURI, preview_url: false, lang: x.lang })); | 199 | .map(x => ({ name: x.name, voice_id: x.voiceURI, preview_url: false, lang: x.lang })); |
| 180 | | 200 | resolve(mappedVoices); |
| 181 | resolve(voices); | 201 | } |
| 182 | }, 1); | 202 | }, 50); // Increased timeout slightly just in case it helps voice population on some browsers |
| 183 | }); | 203 | }); |
| 184 | } | 204 | } |
| 185 | | 205 | |
| | 206 | |
| 186 | previewTtsVoice(voiceId) { | 207 | previewTtsVoice(voiceId) { |
| 187 | if (!('speechSynthesis' in window)) { | 208 | if (!('speechSynthesis' in window)) { |
| 188 | throw 'Speech synthesis API is not supported'; | 209 | throw new Error('Speech synthesis API is not supported'); // Keep Error type for consistency |
| 189 | } | 210 | } |
| 190 | | 211 | |
| 191 | const voice = speechSynthesis.getVoices().find(x => x.voiceURI === voiceId); | 212 | let voice = null; |
| 192 | | 213 | // Check if the requested voice is NOT the browser default |
| 193 | if (!voice) { | 214 | if (voiceId !== SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID) { |
| 194 | throw `TTS Voice id ${voiceId} not found`; | 215 | const voices = speechSynthesis.getVoices(); |
| | 216 | // Try to find the actual voice |
| | 217 | voice = voices.find(x => x.voiceURI === voiceId); |
| | 218 | |
| | 219 | if (!voice && voices.length > 0) { |
| | 220 | // If voices are loaded but the specific ID wasn't found, log a warning |
| | 221 | console.warn(`SystemTTS Preview: Voice ID "${voiceId}" not found among available voices. Using browser default.`); |
| | 222 | // Fallback to default (voice remains null) |
| | 223 | } else if (!voice && voices.length === 0) { |
| | 224 | // If no voices are loaded at all, we expect to use default |
| | 225 | console.warn('SystemTTS Preview: Voice list is empty. Using browser default.'); |
| | 226 | // Fallback to default (voice remains null) |
| | 227 | } |
| | 228 | } else { |
| | 229 | console.log('SystemTTS Preview: Using browser default voice as requested.'); |
| | 230 | // Use default (voice remains null) |
| 195 | } | 231 | } |
| 196 | | 232 | |
| 197 | speechSynthesis.cancel(); | 233 | speechSynthesis.cancel(); // Stop any previous speech |
| 198 | const text = getPreviewString(voice.lang); | 234 | // Use the language from the found voice if available, otherwise default to 'en-US' or browser lang for the preview text |
| | 235 | const langForPreview = voice ? voice.lang : (navigator.language || 'en-US'); |
| | 236 | const text = getPreviewString(langForPreview); |
| 199 | const utterance = new SpeechSynthesisUtterance(text); | 237 | const utterance = new SpeechSynthesisUtterance(text); |
| | 238 | |
| | 239 | // Only set the voice if we found a specific one and it wasn't the default request |
| | 240 | if (voice) { |
| 200 | utterance.voice = voice; | 241 | utterance.voice = voice; |
| | 242 | } |
| | 243 | // Otherwise, utterance.voice remains null/undefined, causing the browser to use its default |
| | 244 | |
| 201 | utterance.rate = this.settings.rate || 1; | 245 | utterance.rate = this.settings.rate || 1; |
| 202 | utterance.pitch = this.settings.pitch || 1; | 246 | utterance.pitch = this.settings.pitch || 1; |
| | 247 | |
| | 248 | // Add error handling for the speech itself |
| | 249 | utterance.onerror = (event) => { |
| | 250 | console.error(`SystemTTS Preview Error: ${event.error}`, event); |
| | 251 | // Potentially notify the user here |
| | 252 | }; |
| | 253 | |
| 203 | speechSynthesis.speak(utterance); | 254 | speechSynthesis.speak(utterance); |
| 204 | } | 255 | } |
| 205 | | 256 | |
| 206 | async getVoice(voiceName) { | 257 | async getVoice(voiceName) { |
| 207 | if (!('speechSynthesis' in window)) { | 258 | if (!('speechSynthesis' in window)) { |
| 208 | return { voice_id: null }; | 259 | // Return a predictable null-like structure if API not supported |
| | 260 | return { voice_id: null, name: 'API Not Supported' }; |
| | 261 | } |
| | 262 | |
| | 263 | // Check if the requested name is the browser default placeholder |
| | 264 | if (voiceName === SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME) { |
| | 265 | return { |
| | 266 | voice_id: SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID, |
| | 267 | name: SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME, |
| | 268 | }; |
| 209 | } | 269 | } |
| 210 | | 270 | |
| | 271 | // Attempt to get voices, might be async |
| | 272 | // Note: This relies on voices potentially being populated by now. |
| | 273 | // A more robust approach might involve re-calling fetchTtsVoiceObjects if needed, |
| | 274 | // but sticking to minimal changes based on original code structure. |
| 211 | const voices = speechSynthesis.getVoices(); | 275 | const voices = speechSynthesis.getVoices(); |
| | 276 | |
| | 277 | if (voices.length === 0) { |
| | 278 | // If voices are still empty, we can't find any specific name |
| | 279 | console.warn(`SystemTTS getVoice: Voice list empty, cannot find "${voiceName}". Falling back to browser default ID.`); |
| | 280 | // Return the default placeholder as a fallback in this edge case |
| | 281 | return { |
| | 282 | voice_id: SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID, |
| | 283 | name: SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME, |
| | 284 | }; |
| | 285 | } |
| | 286 | |
| 212 | const match = voices.find(x => x.name == voiceName); | 287 | const match = voices.find(x => x.name == voiceName); |
| 213 | | 288 | |
| 214 | if (!match) { | 289 | if (!match) { |
| 215 | throw `TTS Voice name ${voiceName} not found`; | 290 | // If voices are loaded but name not found, throw error as before |
| | 291 | throw new Error(`SystemTTS getVoice: TTS Voice name "${voiceName}" not found`); |
| 216 | } | 292 | } |
| 217 | | 293 | |
| 218 | return { voice_id: match.voiceURI, name: match.name }; | 294 | return { voice_id: match.voiceURI, name: match.name }; |