From 6ec512ea335fe838c9251bfbb80fcd043fc3938c Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sat, 25 Jul 2026 04:52:17 -0400 Subject: [PATCH] Fixed 2 speech-dispatcher bugs. First, multiword voice names were not available. Next, when the module changed the old voice name was carried in causing some modules to fail. Ported over fixes from Fenrir. This mostly affected the quick speech keys. --- src/cthulhu/speech_and_verbosity_manager.py | 15 +++ src/cthulhu/speechdispatcherfactory.py | 2 + ...hdispatcher_voice_selection_regressions.py | 103 ++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 tests/test_speechdispatcher_voice_selection_regressions.py diff --git a/src/cthulhu/speech_and_verbosity_manager.py b/src/cthulhu/speech_and_verbosity_manager.py index 5ff8a6d..9864731 100644 --- a/src/cthulhu/speech_and_verbosity_manager.py +++ b/src/cthulhu/speech_and_verbosity_manager.py @@ -36,6 +36,7 @@ from . import cmdnames from . import cthulhu from . import dbus_service from . import debug +from . import guilabels from . import input_event from . import keybindings from . import messages @@ -479,6 +480,20 @@ class SpeechAndVerbosityManager: if not filtered: return [] + if hasattr(server, "getOutputModule"): + try: + module = server.getOutputModule() or "" + except Exception: + module = "" + if module: + default_name = guilabels.SPEECH_DEFAULT_VOICE % module + real_voices = [ + voice for voice in filtered + if voice.get(speechserver.VoiceFamily.NAME) != default_name + ] + if real_voices: + filtered = real_voices + if not self._should_filter_voices_by_locale(server): return filtered diff --git a/src/cthulhu/speechdispatcherfactory.py b/src/cthulhu/speechdispatcherfactory.py index a40b4e3..355611c 100644 --- a/src/cthulhu/speechdispatcherfactory.py +++ b/src/cthulhu/speechdispatcherfactory.py @@ -823,6 +823,8 @@ class SpeechServer(speechserver.SpeechServer): def setOutputModule(self, module): self._client.set_output_module(module) + self._default_voice_name = guilabels.SPEECH_DEFAULT_VOICE % module + self._current_voice_properties.pop(ACSS.FAMILY, None) def stop(self): self._cancel() diff --git a/tests/test_speechdispatcher_voice_selection_regressions.py b/tests/test_speechdispatcher_voice_selection_regressions.py new file mode 100644 index 0000000..e48697c --- /dev/null +++ b/tests/test_speechdispatcher_voice_selection_regressions.py @@ -0,0 +1,103 @@ +import sys +import unittest +from pathlib import Path +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from cthulhu import guilabels +from cthulhu import speech_and_verbosity_manager +from cthulhu import speechdispatcherfactory +from cthulhu import speechserver + + +class SpeechDispatcherVoiceSelectionRegressionTests(unittest.TestCase): + def test_output_module_updates_the_synthetic_default_voice_name(self): + server = speechdispatcherfactory.SpeechServer.__new__( + speechdispatcherfactory.SpeechServer + ) + server._client = mock.Mock() + server._default_voice_name = guilabels.SPEECH_DEFAULT_VOICE % "default" + server._current_voice_properties = { + speechdispatcherfactory.ACSS.FAMILY: {"name": "Previous Voice"} + } + + server.setOutputModule("doubletalk") + + server._client.set_output_module.assert_called_once_with("doubletalk") + self.assertEqual( + guilabels.SPEECH_DEFAULT_VOICE % "doubletalk", + server._default_voice_name, + ) + self.assertNotIn( + speechdispatcherfactory.ACSS.FAMILY, + server._current_voice_properties, + ) + + def test_quick_voice_list_prefers_real_doubletalk_voices(self): + default_name = guilabels.SPEECH_DEFAULT_VOICE % "doubletalk" + voice_names = [ + "Perfect Paul", + "Vader", + "Big Bob", + "Precise Pete", + "Ricochet", + "Biff", + "Skip", + "Robo Robert", + ] + voices = [ + speechserver.VoiceFamily( + { + speechserver.VoiceFamily.NAME: default_name, + speechserver.VoiceFamily.LANG: "en", + speechserver.VoiceFamily.DIALECT: "US", + speechserver.VoiceFamily.VARIANT: None, + } + ), + ] + voices.extend( + speechserver.VoiceFamily( + { + speechserver.VoiceFamily.NAME: voice_name, + speechserver.VoiceFamily.LANG: "en", + speechserver.VoiceFamily.DIALECT: "US", + speechserver.VoiceFamily.VARIANT: "none", + } + ) + for voice_name in voice_names + ) + server = mock.Mock() + server.getOutputModule.return_value = "doubletalk" + server.getVoiceFamilies.return_value = voices + manager = speech_and_verbosity_manager.SpeechAndVerbosityManager.__new__( + speech_and_verbosity_manager.SpeechAndVerbosityManager + ) + + available = manager._get_available_voices(server) + + self.assertEqual( + voice_names, + [voice[speechserver.VoiceFamily.NAME] for voice in available], + ) + + def test_quick_voice_list_keeps_default_when_no_real_voice_exists(self): + default_name = guilabels.SPEECH_DEFAULT_VOICE % "generic" + default_voice = speechserver.VoiceFamily( + { + speechserver.VoiceFamily.NAME: default_name, + speechserver.VoiceFamily.LANG: "en", + } + ) + server = mock.Mock() + server.getOutputModule.return_value = "generic" + server.getVoiceFamilies.return_value = [default_voice] + manager = speech_and_verbosity_manager.SpeechAndVerbosityManager.__new__( + speech_and_verbosity_manager.SpeechAndVerbosityManager + ) + + self.assertEqual([default_voice], manager._get_available_voices(server)) + + +if __name__ == "__main__": + unittest.main()