Fix Edge Browser TTS Compatibility Edge-compatible fallback for empty Web Speech voice lists

d511875db99ebd441563ec5e057e6dc102dbf345

YunZLu <130174259+YunZLu@users.noreply.github.com>

Signed
1 files changed, +94 -18Ignore whitespace
public/scripts/extensions/tts/system.js+94 -18
@@ -79,6 +79,10 @@ class SystemTtsProvider {
79 // Config //79 // Config //
80 //########//80 //########//
8181
82 // Static constants for the simulated default voice
83 static BROWSER_DEFAULT_VOICE_ID = '__browser_default__';
84 static BROWSER_DEFAULT_VOICE_NAME = 'System Default Voice';
85
82 settings;86 settings;
83 ready = false;87 ready = false;
84 voices = [];88 voices = [];
@@ -168,51 +172,123 @@ class SystemTtsProvider {
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 }
173178
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 = speechSynthesis182 let voices = speechSynthesis.getVoices();
177 .getVoices()183
178 .sort((a, b) => a.lang.localeCompare(b.lang) || a.name.localeCompare(b.name))184 if (voices.length === 0) {
179 .map(x => ({ name: x.name, voice_id: x.voiceURI, preview_url: false, lang: x.lang }));185 // If no voices returned (e.g., Edge on first load), provide a default option
180186 console.warn('SystemTTS: getVoices() returned empty list. Providing browser default option.');
181 resolve(voices);187 const defaultVoice = {
182 }, 1);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
198 .sort((a, b) => a.lang.localeCompare(b.lang) || a.name.localeCompare(b.name))
199 .map(x => ({ name: x.name, voice_id: x.voiceURI, preview_url: false, lang: x.lang }));
200 resolve(mappedVoices);
201 }
202 }, 50); // Increased timeout slightly just in case it helps voice population on some browsers
183 });203 });
184 }204 }
185205
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 }
190211
191 const voice = speechSynthesis.getVoices().find(x => x.voiceURI === voiceId);212 let voice = null;
192213 // 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 }
196232
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);
200 utterance.voice = voice;238
239 // Only set the voice if we found a specific one and it wasn't the default request
240 if (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 }
205256
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' };
209 }261 }
210262
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 };
269 }
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);
213288
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 }
217293
218 return { voice_id: match.voiceURI, name: match.name };294 return { voice_id: match.voiceURI, name: match.name };