diff --git a/src/cthulhu/settings_manager.py b/src/cthulhu/settings_manager.py index fbccfc4..ebe02fa 100644 --- a/src/cthulhu/settings_manager.py +++ b/src/cthulhu/settings_manager.py @@ -112,6 +112,7 @@ class SettingsManager(object): self._appPronunciations: Dict[str, Any] = {} self._appKeybindings: Dict[str, Any] = {} self._lastRoleSoundPresentation: Optional[Any] = None + self._session_settings: Dict[str, Any] = {} if not self._loadBackend(): raise Exception('SettingsManager._loadBackend failed.') @@ -383,6 +384,12 @@ class SettingsManager(object): def setSetting(self, settingName: str, settingValue: Any) -> None: self._setSettingsRuntime({settingName:settingValue}) + def set_session_setting(self, setting_name: str, setting_value: Any) -> None: + """Set a process-lifetime setting which survives application changes.""" + + self._session_settings[setting_name] = copy.deepcopy(setting_value) + self._setSettingsRuntime({setting_name: setting_value}) + def getSetting(self, settingName: str) -> Any: return getattr(settings, settingName, None) @@ -582,6 +589,8 @@ class SettingsManager(object): self._getCustomizedSettings() for key, value in self.customizedSettings.items(): setattr(settings, str(key), value) + for key, value in self._session_settings.items(): + setattr(settings, str(key), copy.deepcopy(value)) msg = 'SETTINGS MANAGER: Runtime settings set.' debug.printMessage(debug.LEVEL_INFO, msg, True) diff --git a/src/cthulhu/speech_and_verbosity_manager.py b/src/cthulhu/speech_and_verbosity_manager.py index 9864731..0729f48 100644 --- a/src/cthulhu/speech_and_verbosity_manager.py +++ b/src/cthulhu/speech_and_verbosity_manager.py @@ -375,6 +375,7 @@ class SpeechAndVerbosityManager: } default_voice[acss.ACSS.FAMILY] = family_dict default_voice['established'] = True + self.app.getSettingsManager().set_session_setting("voices", settings.voices) def _get_current_speech_setting(self): order = self._get_speech_settings_order() @@ -405,6 +406,7 @@ class SpeechAndVerbosityManager: default_voice = self._get_default_voice() default_voice[acss.ACSS.RATE] = int(value) default_voice['established'] = True + self.app.getSettingsManager().set_session_setting("voices", settings.voices) def _get_pitch_value(self): from . import acss @@ -416,6 +418,7 @@ class SpeechAndVerbosityManager: default_voice = self._get_default_voice() default_voice[acss.ACSS.AVERAGE_PITCH] = float(value) default_voice['established'] = True + self.app.getSettingsManager().set_session_setting("voices", settings.voices) def _get_volume_value(self): from . import acss @@ -427,6 +430,7 @@ class SpeechAndVerbosityManager: default_voice = self._get_default_voice() default_voice[acss.ACSS.GAIN] = float(value) default_voice['established'] = True + self.app.getSettingsManager().set_session_setting("voices", settings.voices) def _get_current_voice_name(self, server=None): from . import acss @@ -708,6 +712,10 @@ class SpeechAndVerbosityManager: debug.printMessage(debug.LEVEL_WARNING, f"Error setting output module: {e}", True) return True settings.speechServerInfo = [new_module, new_module] + self.app.getSettingsManager().set_session_setting( + "speechServerInfo", + settings.speechServerInfo, + ) msg = f"SPEECH AND VERBOSITY MANAGER: speechServerInfo set to {settings.speechServerInfo}" debug.printMessage(debug.LEVEL_INFO, msg, True) diff --git a/tests/test_speechdispatcher_voice_selection_regressions.py b/tests/test_speechdispatcher_voice_selection_regressions.py index e48697c..8832713 100644 --- a/tests/test_speechdispatcher_voice_selection_regressions.py +++ b/tests/test_speechdispatcher_voice_selection_regressions.py @@ -6,12 +6,121 @@ from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from cthulhu import guilabels +from cthulhu import settings +from cthulhu import settings_manager from cthulhu import speech_and_verbosity_manager from cthulhu import speechdispatcherfactory from cthulhu import speechserver class SpeechDispatcherVoiceSelectionRegressionTests(unittest.TestCase): + def test_session_speech_settings_override_profile_reload(self): + manager = settings_manager.SettingsManager.__new__( + settings_manager.SettingsManager + ) + manager._session_settings = { + "speechServerInfo": ["doubletalk", "doubletalk"], + "voices": { + settings.DEFAULT_VOICE: { + "family": { + speechserver.VoiceFamily.NAME: "Perfect Paul", + speechserver.VoiceFamily.LANG: "en", + speechserver.VoiceFamily.DIALECT: "US", + speechserver.VoiceFamily.VARIANT: "none", + } + } + }, + } + manager.customizedSettings = {} + manager._getCustomizedSettings = mock.Mock(return_value={}) + manager._logRoleSoundPresentationChange = mock.Mock() + saved_settings = { + "speechServerInfo": ["rhvoice", "rhvoice"], + "voices": { + settings.DEFAULT_VOICE: { + "family": { + speechserver.VoiceFamily.NAME: "Previous Voice", + } + } + }, + } + + with mock.patch.object( + settings, + "speechServerInfo", + saved_settings["speechServerInfo"], + ), mock.patch.object(settings, "voices", saved_settings["voices"]): + manager._setSettingsRuntime(saved_settings) + + self.assertEqual( + ["doubletalk", "doubletalk"], + settings.speechServerInfo, + ) + self.assertEqual( + "Perfect Paul", + settings.voices[settings.DEFAULT_VOICE]["family"][ + speechserver.VoiceFamily.NAME + ], + ) + + def test_quick_module_switch_preserves_module_and_voice_for_session(self): + current_module = {"name": "rhvoice"} + perfect_paul = speechserver.VoiceFamily( + { + speechserver.VoiceFamily.NAME: "Perfect Paul", + speechserver.VoiceFamily.LANG: "en", + speechserver.VoiceFamily.DIALECT: "US", + speechserver.VoiceFamily.VARIANT: "none", + } + ) + server = mock.Mock() + server.list_output_modules.return_value = ["rhvoice", "doubletalk"] + server.getOutputModule.side_effect = lambda: current_module["name"] + server.setOutputModule.side_effect = ( + lambda module: current_module.update(name=module) + ) + server.getVoiceFamilies.return_value = [perfect_paul] + + session_settings = mock.Mock() + app = mock.Mock() + app.getSettingsManager.return_value = session_settings + manager = speech_and_verbosity_manager.SpeechAndVerbosityManager.__new__( + speech_and_verbosity_manager.SpeechAndVerbosityManager + ) + manager.app = app + manager._get_server = mock.Mock(return_value=server) + manager._present_message = mock.Mock() + + runtime_voices = { + settings.DEFAULT_VOICE: { + "family": { + speechserver.VoiceFamily.NAME: "Previous Voice", + } + } + } + with mock.patch.object(settings, "voices", runtime_voices), \ + mock.patch.object( + settings, + "speechServerInfo", + ["rhvoice", "rhvoice"], + ): + manager._adjust_module(script=None, decrease=False) + + session_values = { + call.args[0]: call.args[1] + for call in session_settings.set_session_setting.call_args_list + } + self.assertEqual( + ["doubletalk", "doubletalk"], + session_values["speechServerInfo"], + ) + self.assertEqual( + "Perfect Paul", + session_values["voices"][settings.DEFAULT_VOICE]["family"][ + speechserver.VoiceFamily.NAME + ], + ) + def test_output_module_updates_the_synthetic_default_voice_name(self): server = speechdispatcherfactory.SpeechServer.__new__( speechdispatcherfactory.SpeechServer