Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:fallback_plugins #263

Merged
merged 4 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions ovos_plugin_manager/g2p.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -115,13 +107,20 @@ def create(config=None):
"module": <engine_name>
}
"""
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
76 changes: 20 additions & 56 deletions ovos_plugin_manager/language.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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

Expand All @@ -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):
Expand All @@ -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

Expand All @@ -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
13 changes: 11 additions & 2 deletions ovos_plugin_manager/microphone.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -60,18 +60,27 @@ def create(config=None):
"module": <engine_name>
}
"""
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
# since they are dataclasses and don't have the same init signature
# 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
33 changes: 17 additions & 16 deletions ovos_plugin_manager/vad.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -99,16 +92,24 @@ def create(config=None):
"module": <engine_name>
}
"""
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
10 changes: 0 additions & 10 deletions ovos_plugin_manager/wakewords.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
71 changes: 67 additions & 4 deletions test/unittests/test_g2p.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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
Loading
Loading