From 6a96b6ba04d8a5c8d1080b4af455f8004bf61d94 Mon Sep 17 00:00:00 2001 From: Drini Cami Date: Fri, 15 May 2026 22:45:38 +0200 Subject: [PATCH] Exclude iOS joke voices from ReadAloud --- src/plugins/tts/WebTTSEngine.js | 20 +++++++++++++++++++- tests/jest/plugins/tts/WebTTSEngine.test.js | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/plugins/tts/WebTTSEngine.js b/src/plugins/tts/WebTTSEngine.js index b40a67279..708c4be22 100644 --- a/src/plugins/tts/WebTTSEngine.js +++ b/src/plugins/tts/WebTTSEngine.js @@ -16,6 +16,24 @@ export default class WebTTSEngine extends AbstractTTSEngine { return typeof(window.speechSynthesis) !== 'undefined'; } + /** + * @param {SpeechSynthesisVoice} voice + * @returns {boolean} + */ + static isGoodVoice(voice) { + const badVoicePrefixes = [ + // Exclude known novelty/undesired macOS voice families (e.g. joke/system variants) + 'com.apple.speech.synthesis.voice.', + 'com.apple.eloquence.', + 'com.apple.voice.super-compact.', + ]; + + return ( + voice.voiceURI + && !badVoicePrefixes.some(prefix => voice.voiceURI.startsWith(prefix)) + ); + } + /** @param {TTSEngineOptions} options */ constructor(options) { super(options); @@ -80,7 +98,7 @@ export default class WebTTSEngine extends AbstractTTSEngine { /** @override */ getVoices() { - const voices = speechSynthesis.getVoices(); + const voices = speechSynthesis.getVoices().filter(WebTTSEngine.isGoodVoice); if (voices.filter(v => v.default).length != 1) { // iOS bug where the default system voice is sometimes // missing from the list diff --git a/tests/jest/plugins/tts/WebTTSEngine.test.js b/tests/jest/plugins/tts/WebTTSEngine.test.js index 6eeb0fb59..621bec2bb 100644 --- a/tests/jest/plugins/tts/WebTTSEngine.test.js +++ b/tests/jest/plugins/tts/WebTTSEngine.test.js @@ -23,6 +23,24 @@ afterEach(() => { }); describe('WebTTSEngine', () => { + test('isGoodVoice excludes known macOS joke voices', () => { + const voices = [ + { voiceURI: 'com.apple.speech.synthesis.voice.Bad News', name: 'Bad News' }, + { voiceURI: 'com.apple.eloquence.Eddy', name: 'Eddy' }, + { voiceURI: 'com.apple.voice.super-compact.en-US.Samantha', name: 'Samantha (SC)' }, + { voiceURI: 'com.apple.voice.compact.en-US.Samantha', name: 'Samantha' }, + { voiceURI: 'urn:moz-tts:sapi:David', name: 'David' }, + { name: 'No URI Voice' }, + ]; + + const filteredVoices = voices.filter(WebTTSEngine.isGoodVoice); + + expect(filteredVoices).toEqual([ + { voiceURI: 'com.apple.voice.compact.en-US.Samantha', name: 'Samantha' }, + { voiceURI: 'urn:moz-tts:sapi:David', name: 'David' }, + ]); + }); + test('getVoices should include default voice when no actual default', () => { // iOS devices set all the voices to default -_- speechSynthesis.getVoices = () => [