From 7588d5b44e4ab1bbe0f08c7acdefbe526f7b9f46 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 10 Sep 2024 20:18:56 +0100 Subject: [PATCH 1/6] chore:semver_versioning (#262) --- requirements/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 9ae8badd..439a14dd 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,6 @@ -ovos-utils < 0.2.0, >=0.0.38 -ovos-bus-client < 0.2.0, >=0.0.9 -ovos-config < 0.2.0, >=0.0.12 +ovos-utils>=0.0.38,<1.0.0 +ovos_bus_client>=0.0.8,<1.0.0 +ovos-config>=0.0.12,<1.0.0 combo_lock~=0.2 requests~=2.26 quebra_frases From 9c3f41f6b2a20f5a7a4232740c5705ad8bc75035 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 10 Sep 2024 19:19:14 +0000 Subject: [PATCH 2/6] Increment Version to 0.2.0a1 --- ovos_plugin_manager/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_plugin_manager/version.py b/ovos_plugin_manager/version.py index 45c15967..59c1ec1b 100644 --- a/ovos_plugin_manager/version.py +++ b/ovos_plugin_manager/version.py @@ -2,5 +2,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 2 VERSION_BUILD = 0 -VERSION_ALPHA = 0 -# END_VERSION_BLOCK +VERSION_ALPHA = 1 +# END_VERSION_BLOCK \ No newline at end of file From fee6afc3359aa084894f87e2f58d54c9076756ae Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 10 Sep 2024 19:19:41 +0000 Subject: [PATCH 3/6] Update Changelog --- CHANGELOG.md | 46 ++-------------------------------------------- 1 file changed, 2 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02402cb4..59ea4379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,53 +2,11 @@ ## [0.2.0a1](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/0.2.0a1) (2024-09-10) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/0.1.1a1...0.2.0a1) - -**Implemented enhancements:** - -- feat/triples\_plugins [\#257](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/257) ([JarbasAl](https://github.com/JarbasAl)) - -## [0.1.1a1](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/0.1.1a1) (2024-09-10) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.1.1...0.1.1a1) - -**Implemented enhancements:** - -- feat/units\_kwarg\_solvers [\#247](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/247) ([JarbasAl](https://github.com/JarbasAl)) -- feat/metadata [\#246](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/246) ([JarbasAl](https://github.com/JarbasAl)) -- feat/embeddings\_metadata\_support [\#245](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/245) ([JarbasAl](https://github.com/JarbasAl)) -- feat/add\_rerank\_method [\#243](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/243) ([JarbasAl](https://github.com/JarbasAl)) -- feat/pipeline\_intent\_match [\#242](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/242) ([JarbasAl](https://github.com/JarbasAl)) -- feat/pipeline\_plugin\_placeholder [\#241](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/241) ([JarbasAl](https://github.com/JarbasAl)) -- feat/embeddings plugins [\#240](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/240) ([JarbasAl](https://github.com/JarbasAl)) -- feat/alternative\_transcripts [\#236](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/236) ([JarbasAl](https://github.com/JarbasAl)) -- feat/lang\_detection\_plugin [\#220](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/220) ([JarbasAl](https://github.com/JarbasAl)) -- feat/restore phonetic spellings [\#195](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/195) ([JarbasAl](https://github.com/JarbasAl)) - -**Fixed bugs:** - -- fix/restore\_dead\_code [\#256](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/256) ([JarbasAl](https://github.com/JarbasAl)) -- fix/context\_kwarg\_backwards\_compat [\#248](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/248) ([JarbasAl](https://github.com/JarbasAl)) -- refactor/solver\_decorators [\#244](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/244) ([JarbasAl](https://github.com/JarbasAl)) -- fix/missing\_property [\#239](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/239) ([JarbasAl](https://github.com/JarbasAl)) -- ensure cache dir exists [\#232](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/232) ([JarbasAl](https://github.com/JarbasAl)) -- fix/playback\_time\_not\_abstract [\#230](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/230) ([JarbasAl](https://github.com/JarbasAl)) -- fix/legacy\_playlist\_queue [\#227](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/227) ([JarbasAl](https://github.com/JarbasAl)) -- hotfix/voice\_kwarg [\#223](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/223) ([JarbasAl](https://github.com/JarbasAl)) -- hotfix/clean\_shutdown [\#222](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/222) ([JarbasAl](https://github.com/JarbasAl)) -- fix/tts\_reload [\#219](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/219) ([JarbasAl](https://github.com/JarbasAl)) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/0.2.0...0.2.0a1) **Merged pull requests:** -- chore:semver\_automations [\#259](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/259) ([JarbasAl](https://github.com/JarbasAl)) -- license compliance [\#255](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/255) ([JarbasAl](https://github.com/JarbasAl)) -- planned\_deprecations [\#254](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/254) ([JarbasAl](https://github.com/JarbasAl)) -- refactor/improve\_readwritestream [\#234](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/234) ([JarbasAl](https://github.com/JarbasAl)) -- refactor/deprecation\_warnings [\#233](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/233) ([JarbasAl](https://github.com/JarbasAl)) -- fix/py3.12 [\#231](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/231) ([JarbasAl](https://github.com/JarbasAl)) -- refactor/legacy\_audio [\#226](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/226) ([JarbasAl](https://github.com/JarbasAl)) -- Fix file path handling in setup.py [\#225](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/225) ([NeonDaniel](https://github.com/NeonDaniel)) -- Update for ovos-utils 0.0.X compat. [\#224](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/224) ([NeonDaniel](https://github.com/NeonDaniel)) +- chore:semver\_versioning [\#262](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/262) ([JarbasAl](https://github.com/JarbasAl)) From 4e447d84793c57b64401a2e12205470f96b6c5be Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 11 Sep 2024 22:23:17 +0100 Subject: [PATCH 4/6] feat:fallback_plugins (#263) * feat:fallback_plugins similar to WakeWords, allows defining a plugin to load if the primary fails this is DIFFERENT from fallback TTS/STT, it isnt loaded at same time but instead when main plugin fails to load for any reason * unittests * unittests * unittests --- ovos_plugin_manager/g2p.py | 21 ++- ovos_plugin_manager/language.py | 76 +++-------- ovos_plugin_manager/microphone.py | 13 +- ovos_plugin_manager/vad.py | 33 ++--- ovos_plugin_manager/wakewords.py | 10 -- test/unittests/test_g2p.py | 71 ++++++++++- test/unittests/test_language.py | 204 ++++++++++++++---------------- test/unittests/test_microphone.py | 42 ++++++ test/unittests/test_vad.py | 43 ++++++- test/unittests/test_wakewords.py | 6 +- 10 files changed, 303 insertions(+), 216 deletions(-) diff --git a/ovos_plugin_manager/g2p.py b/ovos_plugin_manager/g2p.py index 64497a15..8a8bcdfe 100644 --- a/ovos_plugin_manager/g2p.py +++ b/ovos_plugin_manager/g2p.py @@ -77,12 +77,6 @@ def get_g2p_config(config: Optional[dict] = None) -> dict: class OVOSG2PFactory: - """ replicates the base mycroft class, but uses only OPM enabled plugins""" - MAPPINGS = { - "dummy": "ovos-g2p-plugin-dummy", - "phoneme_guesser": "neon-g2p-plugin-phoneme-guesser", - "gruut": "neon-g2p-plugin-gruut" - } @staticmethod def get_class(config=None): @@ -97,15 +91,13 @@ def get_class(config=None): """ config = get_g2p_config(config) g2p_module = config.get("module") or 'dummy' - if g2p_module in OVOSG2PFactory.MAPPINGS: - g2p_module = OVOSG2PFactory.MAPPINGS[g2p_module] - if g2p_module == 'ovos-g2p-plugin-dummy': + if g2p_module == 'dummy': return Grapheme2PhonemePlugin return load_g2p_plugin(g2p_module) - @staticmethod - def create(config=None): + @classmethod + def create(cls, config=None): """Factory method to create a G2P engine based on configuration. The configuration file ``mycroft.conf`` contains a ``g2p`` section with @@ -115,13 +107,20 @@ def create(config=None): "module": } """ + if "g2p" in config: + config = config["g2p"] g2p_config = get_g2p_config(config) g2p_module = g2p_config.get('module', 'dummy') + fallback = g2p_config.get("fallback_module") try: clazz = OVOSG2PFactory.get_class(g2p_config) g2p = clazz(g2p_config) LOG.debug(f'Loaded plugin {g2p_module}') except Exception: LOG.exception('The selected G2P plugin could not be loaded.') + if fallback in config and fallback != g2p_module: + LOG.info(f"Attempting to load fallback plugin instead: {fallback}") + config["module"] = fallback + return cls.create(config) raise return g2p diff --git a/ovos_plugin_manager/language.py b/ovos_plugin_manager/language.py index ebf2a25c..3caee3e3 100644 --- a/ovos_plugin_manager/language.py +++ b/ovos_plugin_manager/language.py @@ -83,24 +83,7 @@ def get_lang_detect_module_configs(module_name: str): return load_plugin_configs(module_name, PluginConfigTypes.LANG_DETECT) -_fallback_lang_detect_plugin = "ovos-lang-detect-ngram-lm" -_fallback_translate_plugin = "ovos-translate-plugin-server" - - class OVOSLangDetectionFactory: - """ - replicates the base neon class, but uses only OPM enabled plugins - """ - MAPPINGS = { - "libretranslate": "libretranslate_detection_plug", - "google": "googletranslate_detection_plug", - "amazon": "amazontranslate_detection_plug", - "cld2": "cld2_plug", - "cld3": "cld3_plug", - "langdetect": "langdetect_plug", - "fastlang": "fastlang_plug", - "lingua_podre": "lingua_podre_plug" - } @staticmethod def get_class(config=None): @@ -120,12 +103,10 @@ def get_class(config=None): lang_module = config.get("detection_module", config.get("module")) if not lang_module: raise ValueError("`language.detection_module` not configured") - if lang_module in OVOSLangDetectionFactory.MAPPINGS: - lang_module = OVOSLangDetectionFactory.MAPPINGS[lang_module] return load_lang_detect_plugin(lang_module) - @staticmethod - def create(config=None) -> LanguageDetector: + @classmethod + def create(cls, config=None) -> LanguageDetector: """ Factory method to create a LangDetection engine based on configuration @@ -140,37 +121,26 @@ def create(config=None) -> LanguageDetector: if "language" in config: config = config["language"] lang_module = config.get("detection_module", config.get("module")) + cfg = config.get(lang_module, {}) + fallback = cfg.get("fallback_module") try: + config["module"] = lang_module clazz = OVOSLangDetectionFactory.get_class(config) if clazz is None: raise ValueError(f"Failed to load module: {lang_module}") LOG.info(f'Loaded the Language Detection plugin {lang_module}') - if lang_module in OVOSLangDetectionFactory.MAPPINGS: - lang_module = OVOSLangDetectionFactory.MAPPINGS[lang_module] return clazz(config=get_plugin_config(config, "language", lang_module)) except Exception: - # The Language Detection backend failed to start, fall back if appropriate. - if lang_module != _fallback_lang_detect_plugin: - lang_module = _fallback_lang_detect_plugin - LOG.error(f'Language Detection plugin {lang_module} not found. ' - f'Falling back to {_fallback_lang_detect_plugin}') - clazz = load_lang_detect_plugin(_fallback_lang_detect_plugin) - if clazz: - return clazz(config=get_plugin_config(config, "language", - lang_module)) - + LOG.exception(f'Language Detection plugin {lang_module} could not be loaded!') + if fallback in config and fallback != lang_module: + LOG.info(f"Attempting to load fallback plugin instead: {fallback}") + config["detection_module"] = fallback + return cls.create(config) raise class OVOSLangTranslationFactory: - """ replicates the base neon class, but uses only OPM enabled plugins""" - MAPPINGS = { - "libretranslate": "libretranslate_plug", - "google": "googletranslate_plug", - "amazon": "amazontranslate_plug", - "apertium": "apertium_plug" - } @staticmethod def get_class(config=None): @@ -190,12 +160,10 @@ def get_class(config=None): lang_module = config.get("translation_module", config.get("module")) if not lang_module: raise ValueError("`language.translation_module` not configured") - if lang_module in OVOSLangTranslationFactory.MAPPINGS: - lang_module = OVOSLangTranslationFactory.MAPPINGS[lang_module] return load_tx_plugin(lang_module) - @staticmethod - def create(config=None) -> LanguageTranslator: + @classmethod + def create(cls, config=None) -> LanguageTranslator: """ Factory method to create a LangTranslation engine based on configuration @@ -210,24 +178,20 @@ def create(config=None) -> LanguageTranslator: if "language" in config: config = config["language"] lang_module = config.get("translation_module", config.get("module")) + cfg = config.get(lang_module, {}) + fallback = cfg.get("fallback_module") try: + config["module"] = lang_module clazz = OVOSLangTranslationFactory.get_class(config) if clazz is None: raise ValueError(f"Failed to load module: {lang_module}") LOG.info(f'Loaded the Language Translation plugin {lang_module}') - if lang_module in OVOSLangTranslationFactory.MAPPINGS: - lang_module = OVOSLangTranslationFactory.MAPPINGS[lang_module] return clazz(config=get_plugin_config(config, "language", lang_module)) except Exception: - # The Language Translation backend failed to start, fall back if appropriate. - if lang_module != _fallback_translate_plugin: - lang_module = _fallback_translate_plugin - LOG.error(f'Language Translation plugin {lang_module} ' - f'not found. Falling back to {_fallback_translate_plugin}') - clazz = load_tx_plugin(_fallback_translate_plugin) - if clazz: - return clazz(config=get_plugin_config(config, "language", - lang_module)) - + LOG.exception(f'Language Translation plugin {lang_module} could not be loaded!') + if fallback in config and fallback != lang_module: + LOG.info(f"Attempting to load fallback plugin instead: {fallback}") + config["translation_module"] = fallback + return cls.create(config) raise diff --git a/ovos_plugin_manager/microphone.py b/ovos_plugin_manager/microphone.py index 2e8f0c84..f649b130 100644 --- a/ovos_plugin_manager/microphone.py +++ b/ovos_plugin_manager/microphone.py @@ -49,8 +49,8 @@ def get_class(config=None): microphone_module = config.get("module") return load_microphone_plugin(microphone_module) - @staticmethod - def create(config=None): + @classmethod + def create(cls, config=None): """Factory method to create a microphone engine based on configuration. The configuration file ``mycroft.conf`` contains a ``microphone`` section with @@ -60,8 +60,11 @@ def create(config=None): "module": } """ + if "microphone" in config: + config = config["microphone"] microphone_config = get_microphone_config(config) microphone_module = microphone_config.get('module') + fallback = microphone_config.get("fallback_module") try: clazz = OVOSMicrophoneFactory.get_class(microphone_config) # Note that configuration is expanded for this class of plugins @@ -69,9 +72,15 @@ def create(config=None): # as other plugin types microphone_config.pop('lang') microphone_config.pop('module') + if fallback: + microphone_config.pop('fallback_module') microphone = clazz(**microphone_config) LOG.debug(f'Loaded microphone plugin {microphone_module}') except Exception: LOG.exception('The selected microphone plugin could not be loaded.') + if fallback in config and fallback != microphone_module: + LOG.info(f"Attempting to load fallback plugin instead: {fallback}") + config["module"] = fallback + return cls.create(config) raise return microphone diff --git a/ovos_plugin_manager/vad.py b/ovos_plugin_manager/vad.py index 966c3edf..5d4b2589 100644 --- a/ovos_plugin_manager/vad.py +++ b/ovos_plugin_manager/vad.py @@ -61,11 +61,6 @@ def get_vad_config(config: dict = None) -> dict: class OVOSVADFactory: - """ replicates the base mycroft class, but uses only OPM enabled plugins""" - MAPPINGS = { - "silero": "ovos-vad-plugin-silero", - "webrtcvad": "ovos-vad-plugin-webrtcvad" - } @staticmethod def get_class(config=None): @@ -84,12 +79,10 @@ def get_class(config=None): raise ValueError(f"VAD Plugin not configured in: {config}") if vad_module == "dummy": return VADEngine - if vad_module in OVOSVADFactory.MAPPINGS: - vad_module = OVOSVADFactory.MAPPINGS[vad_module] return load_vad_plugin(vad_module) - @staticmethod - def create(config=None): + @classmethod + def create(cls, config=None): """Factory method to create a VAD engine based on configuration. The configuration file ``mycroft.conf`` contains a ``VAD`` section with @@ -99,16 +92,24 @@ def create(config=None): "module": } """ - vad_config = get_vad_config(config) - plugin = vad_config.get("module") + if "listener" in config: + config = config["listener"] + if "VAD" in config: + config = config["VAD"] + plugin = config.get("module") if not plugin: - raise ValueError(f"VAD Plugin not configured in: {vad_config}") + raise ValueError(f"VAD Plugin not configured in: {config}") + + plugin_config = config.get(plugin, {}) + fallback = plugin_config.get("fallback_module") + try: - clazz = OVOSVADFactory.get_class(vad_config) - # module name not expected in config; don't change passed config - plugin_config = dict(vad_config) - plugin_config.pop("module") + clazz = OVOSVADFactory.get_class(config) return clazz(plugin_config) except Exception: LOG.exception(f'VAD plugin {plugin} could not be loaded!') + if fallback in config and fallback != plugin: + LOG.info(f"Attempting to load fallback plugin instead: {fallback}") + config["module"] = fallback + return cls.create(config) raise diff --git a/ovos_plugin_manager/wakewords.py b/ovos_plugin_manager/wakewords.py index f098b64a..fa777217 100644 --- a/ovos_plugin_manager/wakewords.py +++ b/ovos_plugin_manager/wakewords.py @@ -105,14 +105,6 @@ def get_wws(scan=False): class OVOSWakeWordFactory: - """ replicates the base mycroft class, but uses only OPM enabled plugins""" - MAPPINGS = { - "dummy": "ovos-ww-plugin-dummy", - "pocketsphinx": "ovos-ww-plugin-pocketsphinx", - "precise": "ovos-ww-plugin-precise", - "snowboy": "ovos-ww-plugin-snowboy", - "porcupine": "porcupine_wakeword_plug" - } @staticmethod def get_class(hotword: str, config: Optional[dict] = None) -> type: @@ -128,8 +120,6 @@ def get_class(hotword: str, config: Optional[dict] = None) -> type: f"Returning base HotWordEngine") return HotWordEngine ww_module = hotword_config[hotword]["module"] - if ww_module in OVOSWakeWordFactory.MAPPINGS: - ww_module = OVOSWakeWordFactory.MAPPINGS[ww_module] return load_wake_word_plugin(ww_module) @staticmethod diff --git a/test/unittests/test_g2p.py b/test/unittests/test_g2p.py index 40b1964a..51a068e8 100644 --- a/test/unittests/test_g2p.py +++ b/test/unittests/test_g2p.py @@ -1,9 +1,24 @@ import unittest - -from unittest.mock import patch +from copy import deepcopy from enum import Enum +from unittest.mock import patch, Mock + from ovos_plugin_manager.utils import PluginTypes, PluginConfigTypes +_TEST_CONFIG = { + "g2p": { + "module": "good", + "good": {"a": "b"} + } +} +_FALLBACK_CONFIG = { + "g2p": { + "module": "bad", + "bad": {"fallback_module": "good"}, + "good": {"a": "b"} + } +} + class TestG2PTemplate(unittest.TestCase): def test_phoneme_alphabet(self): @@ -72,5 +87,53 @@ def test_get_config(self, get_config): class TestG2PFactory(unittest.TestCase): - from ovos_plugin_manager.g2p import OVOSG2PFactory - # TODO + def test_create_g2p(self): + from ovos_plugin_manager.g2p import OVOSG2PFactory + real_get_class = OVOSG2PFactory.get_class + mock_class = Mock() + call_args = None + + def _copy_args(*args): + nonlocal call_args + call_args = deepcopy(args) + return mock_class + + mock_get_class = Mock(side_effect=_copy_args) + OVOSG2PFactory.get_class = mock_get_class + + OVOSG2PFactory.create(config=_TEST_CONFIG) + mock_get_class.assert_called_once() + self.assertEqual(call_args, ({**_TEST_CONFIG['g2p']['good'], + **{"module": "good", + "lang": "en-us"}},)) + mock_class.assert_called_once_with({**_TEST_CONFIG['g2p']['good'], + **{"module": "good", + "lang": "en-us"}}) + OVOSG2PFactory.get_class = real_get_class + + def test_create_fallback(self): + from ovos_plugin_manager.g2p import OVOSG2PFactory + real_get_class = OVOSG2PFactory.get_class + mock_class = Mock() + call_args = None + bad_call_args = None + + def _copy_args(*args): + nonlocal call_args, bad_call_args + if args[0]["module"] == "bad": + bad_call_args = deepcopy(args) + return None + call_args = deepcopy(args) + return mock_class + + mock_get_class = Mock(side_effect=_copy_args) + OVOSG2PFactory.get_class = mock_get_class + + OVOSG2PFactory.create(config=_FALLBACK_CONFIG) + mock_get_class.assert_called() + self.assertEqual(call_args[0]["module"], 'good') + self.assertEqual(bad_call_args[0]["module"], 'bad') + mock_class.assert_called_once_with({**_FALLBACK_CONFIG['g2p']['good'], + **{"module": "good", + "lang": "en-us"}}) + OVOSG2PFactory.get_class = real_get_class diff --git a/test/unittests/test_language.py b/test/unittests/test_language.py index ed46e1e3..ebd3195b 100644 --- a/test/unittests/test_language.py +++ b/test/unittests/test_language.py @@ -3,6 +3,22 @@ from unittest.mock import patch, Mock from ovos_plugin_manager.utils import PluginTypes, PluginConfigTypes +_TEST_CONFIG = { + "language": { + "detection_module": "good", + "translation_module": "good", + "good": {"a": "b"} + } +} +_FALLBACK_CONFIG = { + "language": { + "detection_module": "bad", + "translation_module": "bad", + "bad": {"fallback_module": "good"}, + "good": {"a": "b"} + } +} + class TestLanguageTemplate(unittest.TestCase): def test_language_detector(self): @@ -83,39 +99,15 @@ def test_get_module_configs(self, load_plugin_configs): class TestLangDetectionFactory(unittest.TestCase): - def test_mappings(self): - from ovos_plugin_manager.language import OVOSLangDetectionFactory - self.assertIsInstance(OVOSLangDetectionFactory.MAPPINGS, dict) - for conf in OVOSLangDetectionFactory.MAPPINGS: - self.assertIsInstance(conf, str) - self.assertIsInstance(OVOSLangDetectionFactory.MAPPINGS[conf], - str) - self.assertNotEqual(conf, OVOSLangDetectionFactory.MAPPINGS[conf]) - @patch("ovos_plugin_manager.language.load_lang_detect_plugin") - @patch("ovos_plugin_manager.language.Configuration") - def test_get_class(self, config, load_plugin): + def test_get_class(self, load_plugin): from ovos_plugin_manager.language import OVOSLangDetectionFactory - test_config = {"language": { - "detection_module": "libretranslate" - }} + mock_class = Mock() - config.return_value = test_config load_plugin.return_value = mock_class - # Test mapped plugin from config - self.assertEquals(OVOSLangDetectionFactory.get_class(), mock_class) - load_plugin.assert_called_with("libretranslate_detection_plug") - - # Test explicitly specified mapped plugin - conf = {"module": "google"} - self.assertEquals(OVOSLangDetectionFactory.get_class(conf), mock_class) - load_plugin.assert_called_with("googletranslate_detection_plug") - - # Test unmapped plugin - conf = {"language": {"detection_module": "real-detect-plug"}} - self.assertEquals(OVOSLangDetectionFactory.get_class(conf), mock_class) - load_plugin.assert_called_with("real-detect-plug") + self.assertEqual(OVOSLangDetectionFactory.get_class(_TEST_CONFIG), mock_class) + load_plugin.assert_called_with("good") # Test invalid module config conf = {"language": {}} @@ -128,78 +120,64 @@ def test_create(self, config, load_plugin): from ovos_plugin_manager.language import OVOSLangDetectionFactory plug_instance = Mock() mock_plugin = Mock(return_value=plug_instance) - default_config = { - "lang": "core_lang", - "language": { - "detection_module": "google", - "lang": "detect" - } - } - config.return_value = default_config + + config.return_value = _TEST_CONFIG load_plugin.return_value = mock_plugin # Create from core config plug = OVOSLangDetectionFactory.create() - load_plugin.assert_called_once_with('googletranslate_detection_plug') - mock_plugin.assert_called_once_with( - config={'lang': "detect", - "module": "googletranslate_detection_plug"}) + load_plugin.assert_called_once_with('good') + mock_plugin.assert_called_once_with(config={**_TEST_CONFIG["language"]["good"], + **{'module': 'good', 'lang': 'en-us'}}) self.assertEquals(plug_instance, plug) # Create plugin fully specified in passed config - config_with_module = {"detection_module": "detect-plugin", - "lang": "lang"} - plug = OVOSLangDetectionFactory.create(config_with_module) - load_plugin.assert_called_with("detect-plugin") - mock_plugin.assert_called_with(config={"module": "detect-plugin", - "lang": "lang"}) + mock_plugin.reset_mock() + plug = OVOSLangDetectionFactory.create(_TEST_CONFIG) + load_plugin.assert_called_with("good") + mock_plugin.assert_called_once_with(config={**_TEST_CONFIG["language"]["good"], + **{'module': 'good', 'lang': 'en-us'}}) self.assertEquals(plug_instance, plug) - # Create plugin fallback module config parsing - config_with_fallback_module = {"module": "test-detect-plugin", - "lang": "lang"} - plug = OVOSLangDetectionFactory.create(config_with_fallback_module) - load_plugin.assert_called_with("test-detect-plugin") - mock_plugin.assert_called_with(config=config_with_fallback_module) - self.assertEquals(plug_instance, plug) - # TODO: Test exception handling fallback to libretranslate + def test_create_fallback(self): + from ovos_plugin_manager.language import OVOSLangDetectionFactory + real_get_class = OVOSLangDetectionFactory.get_class + mock_class = Mock() + call_args = None + bad_call_args = None + from copy import deepcopy + + def _copy_args(*args): + nonlocal call_args, bad_call_args + if args[0]["module"] == "bad": + bad_call_args = deepcopy(args) + return None + call_args = deepcopy(args) + return mock_class + + mock_get_class = Mock(side_effect=_copy_args) + OVOSLangDetectionFactory.get_class = mock_get_class + + OVOSLangDetectionFactory.create(config=_FALLBACK_CONFIG) + mock_get_class.assert_called() + self.assertEqual(call_args[0]["module"], 'good') + self.assertEqual(bad_call_args[0]["module"], 'bad') + mock_class.assert_called_once_with(config={**_TEST_CONFIG["language"]["good"], + **{'module': 'good', 'lang': 'en-us'}}) + OVOSLangDetectionFactory.get_class = real_get_class class TestLangTranslationFactory(unittest.TestCase): - def test_mappings(self): - from ovos_plugin_manager.language import OVOSLangTranslationFactory - self.assertIsInstance(OVOSLangTranslationFactory.MAPPINGS, dict) - for conf in OVOSLangTranslationFactory.MAPPINGS: - self.assertIsInstance(conf, str) - self.assertIsInstance(OVOSLangTranslationFactory.MAPPINGS[conf], - str) - self.assertNotEqual(conf, OVOSLangTranslationFactory.MAPPINGS[conf]) @patch("ovos_plugin_manager.language.load_tx_plugin") - @patch("ovos_plugin_manager.language.Configuration") - def test_get_class(self, config, load_plugin): + def test_get_class(self, load_plugin): from ovos_plugin_manager.language import OVOSLangTranslationFactory - test_config = {"language": { - "translation_module": "libretranslate" - }} + mock_class = Mock() - config.return_value = test_config load_plugin.return_value = mock_class - # Test mapped plugin from config - self.assertEquals(OVOSLangTranslationFactory.get_class(), mock_class) - load_plugin.assert_called_with("libretranslate_plug") - - # Test explicitly specified mapped plugin - conf = {"module": "google"} - self.assertEquals(OVOSLangTranslationFactory.get_class(conf), - mock_class) - load_plugin.assert_called_with("googletranslate_plug") - - # Test unmapped plugin - conf = {"language": {"translation_module": "real-detect-plug"}} - self.assertEquals(OVOSLangTranslationFactory.get_class(conf), mock_class) - load_plugin.assert_called_with("real-detect-plug") + self.assertEqual(OVOSLangTranslationFactory.get_class(_TEST_CONFIG), mock_class) + load_plugin.assert_called_with("good") # Test invalid module config conf = {"language": {}} @@ -212,38 +190,48 @@ def test_create(self, config, load_plugin): from ovos_plugin_manager.language import OVOSLangTranslationFactory plug_instance = Mock() mock_plugin = Mock(return_value=plug_instance) - default_config = { - "lang": "core_lang", - "language": { - "translation_module": "google", - "lang": "tx" - } - } - config.return_value = default_config + + config.return_value = _TEST_CONFIG load_plugin.return_value = mock_plugin # Create from core config plug = OVOSLangTranslationFactory.create() - load_plugin.assert_called_once_with('googletranslate_plug') - mock_plugin.assert_called_once_with( - config={'lang': "tx", "module": "googletranslate_plug"}) + load_plugin.assert_called_once_with('good') + mock_plugin.assert_called_once_with(config={**_TEST_CONFIG["language"]["good"], + **{'module': 'good', 'lang': 'en-us'}}) self.assertEquals(plug_instance, plug) # Create plugin fully specified in passed config - config_with_module = {"translation_module": "translate-plugin", - "lang": "lang"} - plug = OVOSLangTranslationFactory.create(config_with_module) - load_plugin.assert_called_with("translate-plugin") - mock_plugin.assert_called_with(config={"module": "translate-plugin", - "lang": "lang"}) - self.assertEquals(plug_instance, plug) - - # Create plugin fallback module config parsing - config_with_fallback_module = {"module": "test-translate-plugin", - "lang": "lang"} - plug = OVOSLangTranslationFactory.create(config_with_fallback_module) - load_plugin.assert_called_with("test-translate-plugin") - mock_plugin.assert_called_with(config=config_with_fallback_module) + mock_plugin.reset_mock() + plug = OVOSLangTranslationFactory.create(_TEST_CONFIG) + load_plugin.assert_called_with("good") + mock_plugin.assert_called_once_with(config={**_TEST_CONFIG["language"]["good"], + **{'module': 'good', 'lang': 'en-us'}}) self.assertEquals(plug_instance, plug) - # TODO: Test exception handling fallback to libretranslate + def test_create_fallback(self): + from ovos_plugin_manager.language import OVOSLangTranslationFactory + real_get_class = OVOSLangTranslationFactory.get_class + mock_class = Mock() + call_args = None + bad_call_args = None + from copy import deepcopy + + def _copy_args(*args): + nonlocal call_args, bad_call_args + if args[0]["module"] == "bad": + bad_call_args = deepcopy(args) + return None + call_args = deepcopy(args) + return mock_class + + mock_get_class = Mock(side_effect=_copy_args) + OVOSLangTranslationFactory.get_class = mock_get_class + + OVOSLangTranslationFactory.create(config=_FALLBACK_CONFIG) + mock_get_class.assert_called() + self.assertEqual(call_args[0]["module"], 'good') + self.assertEqual(bad_call_args[0]["module"], 'bad') + mock_class.assert_called_once_with(config={**_TEST_CONFIG["language"]["good"], + **{'module': 'good', 'lang': 'en-us'}}) + OVOSLangTranslationFactory.get_class = real_get_class diff --git a/test/unittests/test_microphone.py b/test/unittests/test_microphone.py index dbc203ef..50001136 100644 --- a/test/unittests/test_microphone.py +++ b/test/unittests/test_microphone.py @@ -19,6 +19,23 @@ } } } +_FALLBACK_CONFIG = { + "lang": "en-us", + "microphone": { + "module": "bad", + "bad": { + "sample_width": 1, + "sample_channels": 1, + "chunk_size": 2048, + "fallback_module": "dummy" + }, + "dummy": { + "sample_width": 1, + "sample_channels": 1, + "chunk_size": 2048 + }, + } +} class TestMicrophoneTemplate(unittest.TestCase): @@ -145,6 +162,31 @@ def _copy_args(*args): mock_class.assert_called_once_with(**_TEST_CONFIG['microphone']['dummy']) OVOSMicrophoneFactory.get_class = real_get_class + def test_create_microphone_fallback(self): + from ovos_plugin_manager.microphone import OVOSMicrophoneFactory + real_get_class = OVOSMicrophoneFactory.get_class + mock_class = Mock() + call_args = None + bad_call_args = None + + def _copy_args(*args): + nonlocal call_args, bad_call_args + if args[0]["module"] == "bad": + bad_call_args = deepcopy(args) + return None + call_args = deepcopy(args) + return mock_class + + mock_get_class = Mock(side_effect=_copy_args) + OVOSMicrophoneFactory.get_class = mock_get_class + + OVOSMicrophoneFactory.create(config=_FALLBACK_CONFIG) + mock_get_class.assert_called() + self.assertEqual(call_args[0]["module"], 'dummy') + self.assertEqual(bad_call_args[0]["module"], 'bad') + mock_class.assert_called_once_with(**_FALLBACK_CONFIG['microphone']['dummy']) + OVOSMicrophoneFactory.get_class = real_get_class + @patch("ovos_plugin_manager.utils.load_plugin") def test_get_class(self, load_plugin): mock = Mock() diff --git a/test/unittests/test_vad.py b/test/unittests/test_vad.py index e2664a00..7545fd38 100644 --- a/test/unittests/test_vad.py +++ b/test/unittests/test_vad.py @@ -1,6 +1,7 @@ import unittest -from unittest.mock import patch, Mock from copy import copy, deepcopy +from unittest.mock import patch, Mock + from ovos_plugin_manager.utils import PluginTypes, PluginConfigTypes _TEST_CONFIG = { @@ -17,6 +18,13 @@ } } } +_FALLBACK_CONFIG = { + "VAD": { + "module": "bad", + "bad": {"fallback_module": "good"}, + "good": {"a": "b"} + } +} class TestVADTemplate(unittest.TestCase): @@ -74,10 +82,8 @@ def test_create(self): OVOSVADFactory.get_class = mock_get_class OVOSVADFactory.create(config=_TEST_CONFIG) - mock_get_class.assert_called_once_with( - {**_TEST_CONFIG['listener']['VAD']['dummy'], **{"module": "dummy"}}) - mock_class.assert_called_once_with( - _TEST_CONFIG['listener']["VAD"]['dummy']) + mock_get_class.assert_called_once_with(_TEST_CONFIG['listener']['VAD']) + mock_class.assert_called_once_with(_TEST_CONFIG['listener']["VAD"]['dummy']) # Test invalid config with self.assertRaises(ValueError): @@ -85,6 +91,31 @@ def test_create(self): OVOSVADFactory.get_class = real_get_class + def test_create_fallback(self): + from ovos_plugin_manager.vad import OVOSVADFactory + real_get_class = OVOSVADFactory.get_class + mock_class = Mock() + call_args = None + bad_call_args = None + + def _copy_args(*args): + nonlocal call_args, bad_call_args + if args[0]["module"] == "bad": + bad_call_args = deepcopy(args) + return None + call_args = deepcopy(args) + return mock_class + + mock_get_class = Mock(side_effect=_copy_args) + OVOSVADFactory.get_class = mock_get_class + + OVOSVADFactory.create(config=_FALLBACK_CONFIG) + mock_get_class.assert_called() + self.assertEqual(call_args[0]["module"], 'good') + self.assertEqual(bad_call_args[0]["module"], 'bad') + mock_class.assert_called_once_with(_FALLBACK_CONFIG['VAD']['good']) + OVOSVADFactory.get_class = real_get_class + @patch("ovos_plugin_manager.utils.load_plugin") def test_get_class(self, load_plugin): mock = Mock() @@ -121,7 +152,7 @@ def test_get_vad_config(self): webrtc_config = get_vad_config(config) self.assertEqual(webrtc_config, {**_TEST_CONFIG['listener']['VAD'] - ['ovos-vad-plugin-webrtcvad'], + ['ovos-vad-plugin-webrtcvad'], **{'module': 'ovos-vad-plugin-webrtcvad'}}) config = copy(_TEST_CONFIG) config['VAD'] = {'module': 'fake'} diff --git a/test/unittests/test_wakewords.py b/test/unittests/test_wakewords.py index a07eb2c8..ca54aa5d 100644 --- a/test/unittests/test_wakewords.py +++ b/test/unittests/test_wakewords.py @@ -11,7 +11,7 @@ "active": True }, "hey_mycroft": { - "module": "precise", + "module": "ovos-ww-plugin-precise", "listen": True, "active": True } @@ -103,7 +103,7 @@ def test_create_hotword(self): OVOSWakeWordFactory.load_module = mock_load OVOSWakeWordFactory.create_hotword(config=_TEST_CONFIG) - mock_load.assert_called_once_with("precise", "hey_mycroft", + mock_load.assert_called_once_with("ovos-ww-plugin-precise", "hey_mycroft", _TEST_CONFIG["hotwords"] ['hey_mycroft'], "en-us", None) @@ -148,7 +148,7 @@ def test_load_module(self): mock_return = Mock() mock_get_class.return_value = mock_return module = OVOSWakeWordFactory.load_module( - "precise", "hey_mycroft", _TEST_CONFIG['hotwords']['hey_mycroft'], + "ovos-ww-plugin-precise", "hey_mycroft", _TEST_CONFIG['hotwords']['hey_mycroft'], 'en-us') mock_get_class.assert_called_once_with( "hey_mycroft", {"lang": "en-us", "hotwords": { From 221687db88d1983d8f108234518c35f910fb949d Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 11 Sep 2024 21:23:31 +0000 Subject: [PATCH 5/6] Increment Version to 0.3.0a1 --- ovos_plugin_manager/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_plugin_manager/version.py b/ovos_plugin_manager/version.py index 59c1ec1b..00ce5ca6 100644 --- a/ovos_plugin_manager/version.py +++ b/ovos_plugin_manager/version.py @@ -1,6 +1,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 -VERSION_MINOR = 2 +VERSION_MINOR = 3 VERSION_BUILD = 0 VERSION_ALPHA = 1 # END_VERSION_BLOCK \ No newline at end of file From 433e1d12ab9a79ab05e5fb6fc9cc1203f9978ecb Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 11 Sep 2024 21:24:05 +0000 Subject: [PATCH 6/6] Update Changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59ea4379..3964b469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## [0.2.0a1](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/0.2.0a1) (2024-09-10) +## [0.3.0a1](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/0.3.0a1) (2024-09-11) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/0.2.0...0.2.0a1) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/0.2.0...0.3.0a1) **Merged pull requests:** +- feat:fallback\_plugins [\#263](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/263) ([JarbasAl](https://github.com/JarbasAl)) - chore:semver\_versioning [\#262](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/262) ([JarbasAl](https://github.com/JarbasAl))