diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 472b5daf..62da0901 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -5,7 +5,6 @@ on: - dev paths-ignore: - 'ovos_plugin_manager/version.py' - - 'requirements/**' - 'examples/**' - '.github/**' - '.gitignore' @@ -19,7 +18,6 @@ on: - master paths-ignore: - 'ovos_plugin_manager/version.py' - - 'requirements/**' - 'examples/**' - '.github/**' - '.gitignore' @@ -53,7 +51,7 @@ jobs: pip install . - name: Install test dependencies run: | - pip install pytest pytest-timeout pytest-cov neon-lang-plugin-libretranslate + pip install -r requirements/test.txt - name: Run unittests run: | pytest --cov=ovos_plugin_manager --cov-report xml test/unittests diff --git a/.gitignore b/.gitignore index 9595be50..c8ece8cf 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ dist # Created by unit tests .pytest_cache/ +/.gtm/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 5408ffec..321608e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,230 +1,162 @@ # Changelog -## [0.0.23](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/0.0.23) (2023-06-03) +## [V0.0.24a18](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a18) (2023-10-25) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a27...0.0.23) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a17...V0.0.24a18) **Fixed bugs:** -- Automation fixes [\#164](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/164) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.23a27](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a27) (2023-06-02) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a26...V0.0.23a27) - -**Fixed bugs:** - -- Update `get_plugin_config` and tests for GUI support [\#163](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/163) ([NeonDaniel](https://github.com/NeonDaniel)) +- STT class init with wrong config [\#132](https://github.com/OpenVoiceOS/ovos-plugin-manager/issues/132) +- Fix STT and TTS configuration handling [\#187](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/187) ([NeonDaniel](https://github.com/NeonDaniel)) -## [V0.0.23a26](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a26) (2023-06-02) +## [V0.0.24a17](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a17) (2023-10-25) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a25...V0.0.23a26) - -**Fixed bugs:** - -- Logged exception when cache directory doesn't exist [\#133](https://github.com/OpenVoiceOS/ovos-plugin-manager/issues/133) -- Add error handling and tests to cache curation [\#162](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/162) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.23a25](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a25) (2023-06-02) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a24...V0.0.23a25) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a16...V0.0.24a17) **Merged pull requests:** -- Troubleshoot default STT config handling [\#161](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/161) ([NeonDaniel](https://github.com/NeonDaniel)) -- Add trivial test case to `test_ocp` [\#160](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/160) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.23a24](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a24) (2023-05-31) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a23...V0.0.23a24) - -**Implemented enhancements:** +- Update test dependencies [\#190](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/190) ([NeonDaniel](https://github.com/NeonDaniel)) -- Add persona plugins [\#159](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/159) ([NeonDaniel](https://github.com/NeonDaniel)) +## [V0.0.24a16](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a16) (2023-10-24) -## [V0.0.23a23](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a23) (2023-05-31) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a15...V0.0.24a16) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a22...V0.0.23a23) - -**Merged pull requests:** - -- Release Automation and Stable Dependencies [\#142](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/142) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.23a22](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a22) (2023-05-26) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a21...V0.0.23a22) - -**Implemented enhancements:** +**Fixed bugs:** -- Outline Unit Tests [\#158](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/158) ([NeonDaniel](https://github.com/NeonDaniel)) +- remove log spam [\#188](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/188) ([JarbasAl](https://github.com/JarbasAl)) -## [V0.0.23a21](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a21) (2023-05-26) +**Closed issues:** -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a20...V0.0.23a21) +- support ovos-translate-server [\#182](https://github.com/OpenVoiceOS/ovos-plugin-manager/issues/182) -**Implemented enhancements:** +## [V0.0.24a15](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a15) (2023-10-12) -- Refactor to consolidate common logic [\#157](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/157) ([NeonDaniel](https://github.com/NeonDaniel)) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a14...V0.0.24a15) -## [V0.0.23a20](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a20) (2023-05-24) +**Fixed bugs:** -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a19...V0.0.23a20) +- Language Module Factory Tests/Fixes [\#184](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/184) ([NeonDaniel](https://github.com/NeonDaniel)) -**Merged pull requests:** +## [V0.0.24a14](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a14) (2023-10-11) -- Add VAD module tests with bugfixes and doc updates [\#156](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/156) ([NeonDaniel](https://github.com/NeonDaniel)) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a13...V0.0.24a14) -## [V0.0.23a19](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a19) (2023-05-22) +**Fixed bugs:** -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a18...V0.0.23a19) +- No module named 'mycroft\_bus\_client' [\#146](https://github.com/OpenVoiceOS/ovos-plugin-manager/issues/146) +- Circular import while importing `StreamHandler` [\#130](https://github.com/OpenVoiceOS/ovos-plugin-manager/issues/130) **Merged pull requests:** -- Fix MicrophoneFactory config handling [\#155](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/155) ([NeonDaniel](https://github.com/NeonDaniel)) +- Add license notice and link to plugins from neon\_solvers package [\#185](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/185) ([NeonDaniel](https://github.com/NeonDaniel)) -## [V0.0.23a18](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a18) (2023-05-20) +## [V0.0.24a13](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a13) (2023-10-10) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a17...V0.0.23a18) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a12...V0.0.24a13) **Implemented enhancements:** -- Dinkum listener compat fixes with tests [\#154](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/154) ([NeonDaniel](https://github.com/NeonDaniel)) +- feat/translate\_plug\_as\_arg [\#183](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/183) ([JarbasAl](https://github.com/JarbasAl)) -## [V0.0.23a17](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a17) (2023-05-18) +## [V0.0.24a12](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a12) (2023-10-08) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a16...V0.0.23a17) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a11...V0.0.24a12) **Fixed bugs:** -- Fix Hotword Plugin Load Compat [\#153](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/153) ([NeonDaniel](https://github.com/NeonDaniel)) +- fix/playback\_thread startup [\#181](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/181) ([JarbasAl](https://github.com/JarbasAl)) -## [V0.0.23a16](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a16) (2023-05-17) +## [V0.0.24a11](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a11) (2023-10-08) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a15...V0.0.23a16) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a10...V0.0.24a11) -**Implemented enhancements:** +**Fixed bugs:** -- feat/microphone\_plugs [\#152](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/152) ([JarbasAl](https://github.com/JarbasAl)) +- fix/playback\_thread startup [\#180](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/180) ([JarbasAl](https://github.com/JarbasAl)) -## [V0.0.23a15](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a15) (2023-05-16) +## [V0.0.24a10](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a10) (2023-10-07) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a14...V0.0.23a15) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a9...V0.0.24a10) **Implemented enhancements:** -- :tada: - GUI plugin [\#151](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/151) ([JarbasAl](https://github.com/JarbasAl)) +- feat/ovos\_dialog\_tts\_transformers [\#179](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/179) ([JarbasAl](https://github.com/JarbasAl)) -## [V0.0.23a14](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a14) (2023-05-12) +## [V0.0.24a9](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a9) (2023-09-22) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a13...V0.0.23a14) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a8...V0.0.24a9) **Fixed bugs:** -- fix/ww\_json [\#150](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/150) ([JarbasAl](https://github.com/JarbasAl)) +- fix/deprecation\_warnings [\#178](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/178) ([JarbasAl](https://github.com/JarbasAl)) -## [V0.0.23a13](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a13) (2023-05-12) +## [V0.0.24a8](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a8) (2023-09-17) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a12...V0.0.23a13) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a6...V0.0.24a8) **Merged pull requests:** -- refactor/solvers\_init [\#149](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/149) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.23a12](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a12) (2023-05-12) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a11...V0.0.23a12) - -**Implemented enhancements:** - -- feat/more\_solvers [\#148](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/148) ([JarbasAl](https://github.com/JarbasAl)) +- Don't swallow plugin load errors [\#176](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/176) ([strugee](https://github.com/strugee)) -## [V0.0.23a11](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a11) (2023-05-06) +## [V0.0.24a6](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a6) (2023-09-08) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a10...V0.0.23a11) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a5...V0.0.24a6) **Implemented enhancements:** -- feat/audio2ipa [\#147](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/147) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.23a10](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a10) (2023-04-29) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a9...V0.0.23a10) +- feat/extract\_speech [\#139](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/139) ([JarbasAl](https://github.com/JarbasAl)) **Fixed bugs:** -- better translate fallback module handling [\#145](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/145) ([emphasize](https://github.com/emphasize)) - -**Closed issues:** - -- translator: api key from configuration [\#143](https://github.com/OpenVoiceOS/ovos-plugin-manager/issues/143) - -## [V0.0.23a9](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a9) (2023-04-13) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a8...V0.0.23a9) - -**Implemented enhancements:** +- fix/audio\_config [\#174](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/174) ([JarbasAl](https://github.com/JarbasAl)) -- feat/neon\_transformers [\#141](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/141) ([JarbasAl](https://github.com/JarbasAl)) +## [V0.0.24a5](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a5) (2023-07-07) -## [V0.0.23a8](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a8) (2023-04-13) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a4...V0.0.24a5) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a7...V0.0.23a8) - -**Implemented enhancements:** - -- feat/question\_solvers [\#140](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/140) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.23a7](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a7) (2023-04-09) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a6...V0.0.23a7) - -**Fixed bugs:** +**Merged pull requests:** -- fix plugin requirements conflicts [\#138](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/138) ([JarbasAl](https://github.com/JarbasAl)) +- Updates logging around language plugin errors [\#171](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/171) ([NeonDaniel](https://github.com/NeonDaniel)) -## [V0.0.23a6](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a6) (2023-04-07) +## [V0.0.24a4](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a4) (2023-07-07) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a5...V0.0.23a6) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a3...V0.0.24a4) **Merged pull requests:** -- migrate to ovos-bus-client [\#136](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/136) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.23a5](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a5) (2023-04-05) +- Replace `sleep` with `Event.wait` [\#170](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/170) ([NeonDaniel](https://github.com/NeonDaniel)) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a4...V0.0.23a5) +## [V0.0.24a3](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a3) (2023-07-04) -## [V0.0.23a4](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a4) (2023-03-31) - -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a3...V0.0.23a4) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a2...V0.0.24a3) **Fixed bugs:** -- more missing imports [\#135](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/135) ([builderjer](https://github.com/builderjer)) +- feat/optional\_g2p [\#169](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/169) ([JarbasAl](https://github.com/JarbasAl)) -## [V0.0.23a3](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a3) (2023-03-31) +## [V0.0.24a2](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a2) (2023-07-04) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a2...V0.0.23a3) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.24a1...V0.0.24a2) **Fixed bugs:** -- fix/missing\_imports md5 [\#134](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/134) ([JarbasAl](https://github.com/JarbasAl)) +- fix/on\_mouth\_viseme\_list [\#168](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/168) ([JarbasAl](https://github.com/JarbasAl)) -## [V0.0.23a2](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a2) (2023-03-31) +## [V0.0.24a1](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.24a1) (2023-06-21) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23a1...V0.0.23a2) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.23...V0.0.24a1) **Implemented enhancements:** -- feat/voice\_configs [\#131](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/131) ([JarbasAl](https://github.com/JarbasAl)) +- feat/tts\_session\_context [\#167](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/167) ([JarbasAl](https://github.com/JarbasAl)) -## [V0.0.23a1](https://github.com/OpenVoiceOS/ovos-plugin-manager/tree/V0.0.23a1) (2023-03-10) +**Fixed bugs:** -[Full Changelog](https://github.com/OpenVoiceOS/ovos-plugin-manager/compare/V0.0.22...V0.0.23a1) +- Automation fixes [\#164](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/164) ([NeonDaniel](https://github.com/NeonDaniel)) -**Fixed bugs:** +**Closed issues:** -- fix/dummy\_stt\_plugin [\#129](https://github.com/OpenVoiceOS/ovos-plugin-manager/pull/129) ([JarbasAl](https://github.com/JarbasAl)) +- ImportError: Wake Word marvin with module ovos-ww-plugin-precise-lite failed to load [\#166](https://github.com/OpenVoiceOS/ovos-plugin-manager/issues/166) diff --git a/ovos_plugin_manager/audio.py b/ovos_plugin_manager/audio.py index 7f1eacba..04d582a3 100644 --- a/ovos_plugin_manager/audio.py +++ b/ovos_plugin_manager/audio.py @@ -45,13 +45,14 @@ def setup_audio_service(service_module, config=None, bus=None): Arguments: service_module: Python module to run - config (dict): Mycroft configuration dict + config (dict): OpenVoiceOS configuration dict bus (MessageBusClient): Messagebus interface Returns: (list) List of created services. """ - config = config or Configuration() + config = config or Configuration().get("Audio", {}) bus = bus or get_mycroft_bus() + if (hasattr(service_module, 'autodetect') and callable(service_module.autodetect)): try: @@ -71,8 +72,8 @@ def load_audio_service_plugins(config=None, bus=None): """Load installed audioservice plugins. Arguments: - config: Mycroft core configuration - bus: Mycroft messagebus + config: OpenVoiceOS core configuration + bus: OpenVoiceOS messagebus Returns: List of started services diff --git a/ovos_plugin_manager/audio_transformers.py b/ovos_plugin_manager/audio_transformers.py index 877a43f7..3e9d6db0 100644 --- a/ovos_plugin_manager/audio_transformers.py +++ b/ovos_plugin_manager/audio_transformers.py @@ -32,7 +32,7 @@ def load_audio_transformer_plugin(module_name: str) -> type(AudioTransformer): """Wrapper function for loading audio_transformer plugin. Arguments: - (str) Mycroft audio_transformer module name from config + (str) OpenVoiceOS audio_transformer module name from config Returns: class: found audio_transformer plugin class """ diff --git a/ovos_plugin_manager/dialog_transformers.py b/ovos_plugin_manager/dialog_transformers.py new file mode 100644 index 00000000..ece91fdc --- /dev/null +++ b/ovos_plugin_manager/dialog_transformers.py @@ -0,0 +1,41 @@ +from ovos_plugin_manager.templates.transformers import DialogTransformer, TTSTransformer +from ovos_plugin_manager.utils import PluginTypes +from ovos_plugin_manager.utils import load_plugin, find_plugins + + +def find_dialog_transformer_plugins() -> dict: + """ + Find all installed plugins + @return: dict plugin names to entrypoints + """ + return find_plugins(PluginTypes.DIALOG_TRANSFORMER) + + +def load_dialog_transformer_plugin(module_name: str) -> type(DialogTransformer): + """Wrapper function for loading dialog_transformer plugin. + + Arguments: + (str) OpenVoiceOS dialog_transformer module name from config + Returns: + class: found dialog_transformer plugin class + """ + return load_plugin(module_name, PluginTypes.DIALOG_TRANSFORMER) + + +def find_tts_transformer_plugins() -> dict: + """ + Find all installed plugins + @return: dict plugin names to entrypoints + """ + return find_plugins(PluginTypes.TTS_TRANSFORMER) + + +def load_tts_transformer_plugin(module_name: str) -> type(TTSTransformer): + """Wrapper function for loading dialog_transformer plugin. + + Arguments: + (str) OpenVoiceOS dialog_transformer module name from config + Returns: + class: found dialog_transformer plugin class + """ + return load_plugin(module_name, PluginTypes.TTS_TRANSFORMER) diff --git a/ovos_plugin_manager/hardware/led/animations.py b/ovos_plugin_manager/hardware/led/animations.py index 48e7e1a6..3893ec8f 100644 --- a/ovos_plugin_manager/hardware/led/animations.py +++ b/ovos_plugin_manager/hardware/led/animations.py @@ -2,7 +2,7 @@ from threading import Event from ovos_utils.log import LOG -from time import time, sleep +from time import time from typing import Optional from ovos_plugin_manager.hardware.led import AbstractLed, Color @@ -11,6 +11,7 @@ class LedAnimation: def __init__(self, leds: AbstractLed, **kwargs): self.leds = leds + self._delay = Event() @abstractmethod def start(self, timeout: Optional[int] = None, one_shot: bool = False): @@ -56,7 +57,7 @@ def start(self, timeout=None, one_shot=False): brightness += step self.leds.fill(tuple(brightness * part for part in self.color.as_rgb_tuple())) - sleep(self.step_delay) + self._delay.wait(self.step_delay) if one_shot and brightness >= 1: ending = True elif ending and brightness <= 0: @@ -94,7 +95,7 @@ def start(self, timeout=None, one_shot=False): while not self.stopping.is_set(): for led in range(0, self.leds.num_leds): self.leds.set_led(led, self.foreground_color.as_rgb_tuple()) - sleep(self.step_delay) + self._delay.wait(self.step_delay) self.leds.set_led(led, self.background_color.as_rgb_tuple()) if one_shot: self.stopping.set() @@ -129,7 +130,7 @@ def start(self, timeout=None, one_shot=True): leds.reverse() for led in leds: self.leds.set_led(led, self.fill_color.as_rgb_tuple()) - sleep(self.step_delay) + self._delay.wait(self.step_delay) def stop(self): pass @@ -227,17 +228,17 @@ def start(self, timeout=None, one_shot=False): end_time = time() + timeout if timeout else None self.leds.fill(Color.BLACK.as_rgb_tuple()) - sleep(0.5) + self._delay.wait(0.5) while not self.stopping.is_set(): for i in range(self.num_blinks): self.leds.fill(self.color.as_rgb_tuple()) - sleep(0.25) + self._delay.wait(0.25) self.leds.fill(Color.BLACK.as_rgb_tuple()) - sleep(0.5) + self._delay.wait(0.5) if one_shot: self.stopping.set() elif self.repeat: - sleep(2) + self._delay.wait(2) else: self.stopping.set() if end_time and time() > end_time: @@ -273,7 +274,7 @@ def start(self, timeout: Optional[int] = None, one_shot: bool = False): else: self.leds.set_led(led, Color.BLACK.as_rgb_tuple(), False) self.leds.show() - sleep(self.delay) + self._delay.wait(self.delay) evens = not evens if one_shot and evens: # We did one animation self.stopping.set() diff --git a/ovos_plugin_manager/language.py b/ovos_plugin_manager/language.py index 586b7f2d..ed9f8fec 100644 --- a/ovos_plugin_manager/language.py +++ b/ovos_plugin_manager/language.py @@ -99,8 +99,14 @@ 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""" + """ + replicates the base neon class, but uses only OPM enabled plugins + """ MAPPINGS = { "libretranslate": "libretranslate_detection_plug", "google": "googletranslate_detection_plug", @@ -112,41 +118,63 @@ class OVOSLangDetectionFactory: "lingua_podre": "lingua_podre_plug" } - # TODO - get_class method + @staticmethod + def get_class(config=None): + """ + Factory method to get a Language Detector class based on configuration. + + Configuration contains a `language` section with + the name of a LangDetection module to be read by this method. + + "language": { + "detection_module": + } + """ + config = config or Configuration() + if "language" in config: + config = config["language"] + 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): - """Factory method to create a LangDetection engine based on configuration. + def create(config=None) -> LanguageDetector: + """ + Factory method to create a LangDetection engine based on configuration - The configuration file ``mycroft.conf`` contains a ``language`` section with + Configuration contains a `language` section with the name of a LangDetection module to be read by this method. - "language": { + "language": { "detection_module": - } + } """ + config = config or Configuration() + if "language" in config: + config = config["language"] + lang_module = config.get("detection_module", config.get("module")) try: - config = config or Configuration() - if "language" in config: - config = config["language"] - lang_module = config.get("detection_module", "libretranslate_detection_plug") - if lang_module in OVOSLangDetectionFactory.MAPPINGS: - lang_module = OVOSLangDetectionFactory.MAPPINGS[lang_module] - - clazz = load_lang_detect_plugin(lang_module) + clazz = OVOSLangDetectionFactory.get_class(config) if clazz is None: - raise ValueError + raise ValueError(f"Failed to load module: {lang_module}") LOG.info(f'Loaded the Language Detection plugin {lang_module}') - return clazz(config=get_plugin_config(config, "language", 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 != "libretranslate_detection_plug": - lang_module = "libretranslate_detection_plug" - LOG.error(f'Language Translation plugin {lang_module} not found\n' - f'Falling back to libretranslate plugin') - clazz = load_tx_plugin("libretranslate_detection_plug") + 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)) + return clazz(config=get_plugin_config(config, "language", + lang_module)) raise @@ -160,39 +188,62 @@ class OVOSLangTranslationFactory: "apertium": "apertium_plug" } - # TODO - get_class method @staticmethod - def create(config=None): - """Factory method to create a LangTranslation engine based on configuration. + def get_class(config=None): + """ + Factory method to get a Language Translator class based on configuration. - The configuration file ``mycroft.conf`` contains a ``language`` section with - the name of a LangDetection module to be read by this method. + Configuration contains a `language` section with + the name of a Translation module to be read by this method. + "language": { + "translation_module": + } + """ + config = config or Configuration() + if "language" in config: + config = config["language"] + 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) - "language": { + @staticmethod + def create(config=None) -> LanguageTranslator: + """ + Factory method to create a LangTranslation engine based on configuration + + Configuration contains a `language` section with + the name of a Translation module to be read by this method. + + "language": { "translation_module": - } + } """ + config = config or Configuration() + if "language" in config: + config = config["language"] + lang_module = config.get("translation_module", config.get("module")) try: - config = config or Configuration() - if "language" in config: - config = config["language"] - lang_module = config.get("translation_module", "libretranslate_plug") - if lang_module in OVOSLangTranslationFactory.MAPPINGS: - lang_module = OVOSLangTranslationFactory.MAPPINGS[lang_module] - clazz = load_tx_plugin(lang_module) + clazz = OVOSLangTranslationFactory.get_class(config) if clazz is None: - raise ValueError + raise ValueError(f"Failed to load module: {lang_module}") LOG.info(f'Loaded the Language Translation plugin {lang_module}') - return clazz(config=get_plugin_config(config, "language", 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 Detection backend failed to start, fall back if appropriate. - if lang_module != "libretranslate_plug": - lang_module = "libretranslate_plug" - LOG.error(f'Language Translation plugin {lang_module} not found\n' - f'Falling back to libretranslate plugin') - clazz = load_tx_plugin("libretranslate_plug") + # 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)) + return clazz(config=get_plugin_config(config, "language", + lang_module)) raise diff --git a/ovos_plugin_manager/stt.py b/ovos_plugin_manager/stt.py index 16782eee..dd92ff0c 100644 --- a/ovos_plugin_manager/stt.py +++ b/ovos_plugin_manager/stt.py @@ -2,8 +2,8 @@ PluginTypes, PluginConfigTypes from ovos_config import Configuration from ovos_plugin_manager.utils.config import get_valid_plugin_configs, \ - sort_plugin_configs -from ovos_utils.log import LOG + sort_plugin_configs, get_plugin_config +from ovos_utils.log import LOG, log_deprecation from ovos_plugin_manager.templates.stt import STT, StreamingSTT, StreamThread @@ -88,14 +88,15 @@ def get_stt_supported_langs() -> dict: return get_plugin_supported_languages(PluginTypes.STT) -def get_stt_config(config: dict = None) -> dict: +def get_stt_config(config: dict = None, module: str = None) -> dict: """ Get relevant configuration for factory methods @param config: global Configuration OR plugin class-specific configuration + @param module: STT module to get configuration for @return: plugin class-specific configuration """ from ovos_plugin_manager.utils.config import get_plugin_config - stt_config = get_plugin_config(config, "stt") + stt_config = get_plugin_config(config, "stt", module) assert stt_config.get('lang') is not None, "expected lang but got None" return stt_config @@ -133,7 +134,7 @@ def get_class(config=None): "module": } """ - config = config or get_stt_config() + config = get_stt_config(config) stt_module = config["module"] if stt_module in OVOSSTTFactory.MAPPINGS: stt_module = OVOSSTTFactory.MAPPINGS[stt_module] @@ -150,12 +151,15 @@ def create(config=None): "module": } """ - config = get_stt_config(config) - plugin = config["module"] - plugin_config = config.get(plugin) or {} + stt_config = get_stt_config(config) + plugin = stt_config.get("module", "dummy") + if plugin in OVOSSTTFactory.MAPPINGS: + log_deprecation("Module mappings will be deprecated", "0.1.0") + plugin = OVOSSTTFactory.MAPPINGS[plugin] + stt_config = get_stt_config(config, plugin) try: - clazz = OVOSSTTFactory.get_class(config) - return clazz(plugin_config) + clazz = OVOSSTTFactory.get_class(stt_config) + return clazz(stt_config) except Exception: LOG.exception('The selected STT plugin could not be loaded!') raise diff --git a/ovos_plugin_manager/templates/audio.py b/ovos_plugin_manager/templates/audio.py index 6156fde2..3bd0d2b1 100644 --- a/ovos_plugin_manager/templates/audio.py +++ b/ovos_plugin_manager/templates/audio.py @@ -1,7 +1,7 @@ """Definition of the audio service backends base classes. These classes can be used to create an Audioservice plugin extending -Mycroft's media playback options. +OpenVoiceOS's media playback options. """ from abc import ABCMeta, abstractmethod @@ -15,7 +15,7 @@ class AudioBackend(metaclass=ABCMeta): Arguments: config (dict): configuration dict for the instance - bus (MessageBusClient): Mycroft messagebus emitter + bus (MessageBusClient): OpenVoiceOS messagebus emitter """ def __init__(self, config=None, bus=None): @@ -134,7 +134,7 @@ def lower_volume(self): """Lower volume. This method is used to implement audio ducking. It will be called when - Mycroft is listening or speaking to make sure the media playing isn't + OpenVoiceOS is listening or speaking to make sure the media playing isn't interfering. """ @@ -142,7 +142,7 @@ def restore_volume(self): """Restore normal volume. Called when to restore the playback volume to previous level after - Mycroft has lowered it using lower_volume(). + OpenVoiceOS has lowered it using lower_volume(). """ def get_track_length(self): diff --git a/ovos_plugin_manager/templates/gui.py b/ovos_plugin_manager/templates/gui.py index 44800ea2..fad98e19 100644 --- a/ovos_plugin_manager/templates/gui.py +++ b/ovos_plugin_manager/templates/gui.py @@ -2,6 +2,7 @@ from ovos_bus_client import MessageBusClient from ovos_utils.gui import GUIInterface from ovos_utils.log import LOG +from ovos_config import Configuration class GUIExtension: @@ -27,7 +28,8 @@ def __init__(self, config, bus=None, gui=None, bus.run_in_thread() bus.connected_event.wait() self.bus = bus - self.gui = gui or GUIInterface("ovos.shell", bus=self.bus) + self.gui = gui or GUIInterface("ovos.shell", bus=self.bus, + config=Configuration().get("gui", {})) self.preload_gui = preload_gui self.permanent = permanent self.config = config diff --git a/ovos_plugin_manager/templates/phal.py b/ovos_plugin_manager/templates/phal.py index 52557607..aed65f3a 100644 --- a/ovos_plugin_manager/templates/phal.py +++ b/ovos_plugin_manager/templates/phal.py @@ -27,7 +27,7 @@ def validate(config: dict = None) -> bool: class PHALPlugin(Thread): """ This base class is intended to be used to interface with the hardware - that is running Mycroft. It exposes all possible commands which + that is running OpenVoiceOS. It exposes all possible commands which are expected be sent to a PHAL plugin. All of the handlers are optional and for convenience only """ @@ -45,9 +45,27 @@ def __init__(self, bus=None, name="", config=None): self.log = LOG self.name = name + self.register_enclosure_namespace() + self.register_core_events() + + self._activate_mouth_events() + self.start() + + def register_core_events(self): + # audio events + self.bus.on('recognizer_loop:record_begin', self.on_record_begin) + self.bus.on('recognizer_loop:record_end', self.on_record_end) + self.bus.on("recognizer_loop:sleep", self.on_sleep) + self.bus.on('recognizer_loop:audio_output_start', self.on_audio_output_start) + self.bus.on('recognizer_loop:audio_output_end', self.on_audio_output_end) + self.bus.on("mycroft.awoken", self.on_awake) + self.bus.on("speak", self.on_speak) + + def register_enclosure_namespace(self): + self.bus.on("enclosure.notify.no_internet", self.on_no_internet) self.bus.on("enclosure.reset", self.on_reset) - # enclosure commands for Mycroft's Hardware. + # enclosure commands for OpenVoiceOS's Hardware. self.bus.on("enclosure.system.reset", self.on_system_reset) self.bus.on("enclosure.system.mute", self.on_system_mute) self.bus.on("enclosure.system.unmute", self.on_system_unmute) @@ -76,6 +94,7 @@ def __init__(self, bus=None, name="", config=None): self.bus.on("enclosure.mouth.listen", self._on_mouth_listen) self.bus.on("enclosure.mouth.smile", self._on_mouth_smile) self.bus.on("enclosure.mouth.viseme", self._on_mouth_viseme) + self.bus.on("enclosure.mouth.viseme_list", self._on_mouth_viseme_list) # mouth/matrix display self.bus.on("enclosure.mouth.reset", self.on_display_reset) @@ -83,20 +102,6 @@ def __init__(self, bus=None, name="", config=None): self.bus.on("enclosure.mouth.display", self.on_display) self.bus.on("enclosure.weather.display", self.on_weather_display) - # audio events - self.bus.on('recognizer_loop:record_begin', self.on_record_begin) - self.bus.on('recognizer_loop:record_end', self.on_record_end) - self.bus.on("recognizer_loop:sleep", self.on_sleep) - self.bus.on('recognizer_loop:audio_output_start', self.on_audio_output_start) - self.bus.on('recognizer_loop:audio_output_end', self.on_audio_output_end) - - # other events - self.bus.on("mycroft.awoken", self.on_awake) - self.bus.on("speak", self.on_speak) - self.bus.on("enclosure.notify.no_internet", self.on_no_internet) - - self._activate_mouth_events() - self.start() @classproperty def runtime_requirements(self): @@ -156,7 +161,8 @@ def shutdown(self): self.bus.remove("enclosure.eyes.timedspin", self.on_eyes_timed_spin) self.bus.remove("enclosure.eyes.volume", self.on_eyes_volume) self.bus.remove("enclosure.eyes.spin", self.on_eyes_spin) - self.bus.remove("enclosure.eyes.set_pixel", self.on_eyes_set_pixel) + self.bus.remove("enclosure.eyes.setpixel", self.on_eyes_set_pixel) + self.bus.remove('enclosure.eyes.fill', self.on_eyes_fill) self.bus.remove("enclosure.mouth.reset", self.on_display_reset) self.bus.remove("enclosure.mouth.talk", self.on_talk) @@ -164,6 +170,7 @@ def shutdown(self): self.bus.remove("enclosure.mouth.listen", self.on_listen) self.bus.remove("enclosure.mouth.smile", self.on_smile) self.bus.remove("enclosure.mouth.viseme", self.on_viseme) + self.bus.remove("enclosure.mouth.viseme_list", self._on_mouth_viseme_list) self.bus.remove("enclosure.mouth.text", self.on_text) self.bus.remove("enclosure.mouth.display", self.on_display) self.bus.remove("enclosure.mouth.events.activate", self._activate_mouth_events) @@ -372,6 +379,26 @@ def _on_mouth_viseme(self, message=None): if self.mouth_events_active: self.on_viseme(message) + def _on_mouth_viseme_list(self, message=None): + """mouth visemes as a list in a single message. + + Args: + start (int): Timestamp for start of speech + viseme_pairs: Pairs of viseme id and cumulative end times + (code, end time) + + codes: + 0 = shape for sounds like 'y' or 'aa' + 1 = shape for sounds like 'aw' + 2 = shape for sounds like 'uh' or 'r' + 3 = shape for sounds like 'th' or 'sh' + 4 = neutral shape for no sound + 5 = shape for sounds like 'f' or 'v' + 6 = shape for sounds like 'oy' or 'ao' + """ + if self.mouth_events_active: + self.on_viseme_list(message) + def _on_mouth_text(self, message=None): """Display text (scrolling as needed) Args: @@ -418,6 +445,25 @@ def on_viseme(self, message=None): """ pass + def on_viseme_list(self, message=None): + """ Send mouth visemes as a list in a single message. + + Args: + start (int): Timestamp for start of speech + viseme_pairs: Pairs of viseme id and cumulative end times + (code, end time) + + codes: + 0 = shape for sounds like 'y' or 'aa' + 1 = shape for sounds like 'aw' + 2 = shape for sounds like 'uh' or 'r' + 3 = shape for sounds like 'th' or 'sh' + 4 = neutral shape for no sound + 5 = shape for sounds like 'f' or 'v' + 6 = shape for sounds like 'oy' or 'ao' + """ + pass + def on_text(self, message=None): """Display text (scrolling as needed) Args: diff --git a/ovos_plugin_manager/templates/solvers.py b/ovos_plugin_manager/templates/solvers.py index 2afc6ab9..a517a1b1 100644 --- a/ovos_plugin_manager/templates/solvers.py +++ b/ovos_plugin_manager/templates/solvers.py @@ -1,3 +1,33 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Solver service can be found at: https://github.com/Neongeckocom/neon_solvers + from json_database import JsonStorageXDG from ovos_utils.xdg_utils import xdg_cache_home from quebra_frases import sentence_tokenize @@ -11,9 +41,9 @@ class AbstractSolver: enable_tx = False enable_cache = False - def __init__(self, config=None, *args, **kwargs): + def __init__(self, config=None, translator=None, *args, **kwargs): if args or kwargs: - LOG.warning("solver plugins init signature changed, please update to accept a single config kwarg. " + LOG.warning("solver plugins init signature changed, please update to accept config=None, translator=None. " "an exception will be raised in next stable release") for arg in args: if isinstance(arg, str): @@ -31,7 +61,7 @@ def __init__(self, config=None, *args, **kwargs): self.default_lang = self.config.get("lang", "en") if self.default_lang not in self.supported_langs: self.supported_langs.insert(0, self.default_lang) - self.translator = OVOSLangTranslationFactory.create() + self.translator = translator or OVOSLangTranslationFactory.create() @staticmethod def sentence_split(text, max_sentences=25): @@ -73,8 +103,8 @@ class QuestionSolver(AbstractSolver): """free form unscontrained spoken question solver handling automatic translation back and forth as needed""" - def __init__(self, config=None, *args, **kwargs): - super().__init__(config, *args, **kwargs) + def __init__(self, config=None, translator=None, *args, **kwargs): + super().__init__(config, translator, *args, **kwargs) name = kwargs.get("name") or self.__class__.__name__ if self.enable_cache: # cache contains raw data diff --git a/ovos_plugin_manager/templates/transformers.py b/ovos_plugin_manager/templates/transformers.py index 363ff614..32a8f65f 100644 --- a/ovos_plugin_manager/templates/transformers.py +++ b/ovos_plugin_manager/templates/transformers.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Tuple from ovos_config.config import Configuration from ovos_utils.messagebus import get_mycroft_bus @@ -7,6 +7,7 @@ class MetadataTransformer: + """ runs after utterance transformers and before intent service""" def __init__(self, name, priority=50, config=None): self.name = name @@ -41,6 +42,7 @@ def default_shutdown(self): class UtteranceTransformer: + """ runs before metadata transformers and intent service""" def __init__(self, name, priority=50, config=None): self.name = name @@ -165,3 +167,69 @@ def transform(self, audio_data): def default_shutdown(self): """ perform any shutdown actions """ pass + + +class DialogTransformer: + """ runs before TTS stage""" + + def __init__(self, name, priority=50, config=None): + self.name = name + self.bus = None + self.priority = priority + if not config: + config_core = dict(Configuration()) + config = config_core.get("dialog_transformers", {}).get(self.name) + self.config = config or {} + + def bind(self, bus=None): + """ attach messagebus """ + self.bus = bus or get_mycroft_bus() + + def initialize(self): + """ perform any initialization actions """ + pass + + def transform(self, dialog: str, context: dict = None) -> Tuple[str, dict]: + """ + Optionally transform passed dialog and/or return additional context + :param dialog: str utterance to mutate before TTS + :returns: str mutated dialog + """ + return dialog, context + + def default_shutdown(self): + """ perform any shutdown actions """ + pass + + +class TTSTransformer: + """ runs after TTS stage but before playback""" + + def __init__(self, name, priority=50, config=None): + self.name = name + self.bus = None + self.priority = priority + if not config: + config_core = dict(Configuration()) + config = config_core.get("dialog_transformers", {}).get(self.name) + self.config = config or {} + + def bind(self, bus=None): + """ attach messagebus """ + self.bus = bus or get_mycroft_bus() + + def initialize(self): + """ perform any initialization actions """ + pass + + def transform(self, wav_file: str, context: dict = None) -> Tuple[str, dict]: + """ + Optionally transform passed wav_file and return path to transformed file + :param wav_file: path to wav file generated in TTS stage + :returns: path to transformed wav file for playback + """ + return wav_file, context + + def default_shutdown(self): + """ perform any shutdown actions """ + pass diff --git a/ovos_plugin_manager/templates/tts.py b/ovos_plugin_manager/templates/tts.py index 8e7ec607..78aa4094 100644 --- a/ovos_plugin_manager/templates/tts.py +++ b/ovos_plugin_manager/templates/tts.py @@ -25,16 +25,17 @@ import random import re import subprocess -import threading -from os.path import isfile, join, splitext +from os.path import isfile, join from pathlib import Path -from queue import Queue, Empty +from queue import Queue from threading import Thread -from time import time, sleep - import requests from ovos_bus_client.message import Message, dig_for_message from ovos_config import Configuration +from ovos_plugin_manager.g2p import OVOSG2PFactory, find_g2p_plugins +from ovos_plugin_manager.templates.g2p import OutOfVocabulary +from ovos_plugin_manager.utils.config import get_plugin_config +from ovos_plugin_manager.utils.tts_cache import TextToSpeechCache, hash_sentence from ovos_utils import classproperty from ovos_utils import resolve_resource_file from ovos_utils.enclosure.api import EnclosureAPI @@ -44,269 +45,26 @@ from ovos_utils.messagebus import FakeBus as BUS from ovos_utils.metrics import Stopwatch from ovos_utils.process_utils import RuntimeRequirements -from ovos_utils.signal import check_for_signal, create_signal -from ovos_utils.sound import play_audio - -from ovos_plugin_manager.g2p import OVOSG2PFactory, find_g2p_plugins -from ovos_plugin_manager.templates.g2p import OutOfVocabulary -from ovos_plugin_manager.utils.config import get_plugin_config -from ovos_plugin_manager.utils.tts_cache import TextToSpeechCache, hash_sentence EMPTY_PLAYBACK_QUEUE_TUPLE = (None, None, None, None, None) SSML_TAGS = re.compile(r'<[^>]*>') class PlaybackThread(Thread): - """Thread class for playing back tts audio and sending - viseme data to enclosure. - """ - - def __init__(self, queue): - super(PlaybackThread, self).__init__() - self.queue = queue - self._terminated = False - self._processing_queue = False - self._paused = False - self.enclosure = None - self.p = None - self._tts = [] - self.bus = None - self._now_playing = None - self.active_tts = None - self._started = threading.Event() - - @property - def is_running(self): - return self._started.is_set() and not self._terminated - - def activate_tts(self, tts_id): - self.active_tts = tts_id - tts = self.get_attached_tts() - if tts: - tts.begin_audio() - - def deactivate_tts(self): - if self.active_tts: - tts = self.get_attached_tts() - if tts: - tts.end_audio() - self.active_tts = None - - def init(self, tts): - """DEPRECATED! Init the TTS Playback thread.""" - self.attach_tts(tts) - self.set_bus(tts.bus) - - def set_bus(self, bus): - """Provide bus instance to the TTS Playback thread. - Args: - bus (MycroftBusClient): bus client - """ - self.bus = bus + """ PlaybackThread moved to ovos_audio.playback + standalone plugin usage should rely on self.get_tts + ovos-audio relies on self.execute and needs this class - @property - def tts(self): - tts = self.get_attached_tts() - if not tts and self._tts: - return self._tts[0] - return tts - - @tts.setter - def tts(self, val): - self.attach_tts(val) - - @property - def attached_tts(self): - return self._tts - - def attach_tts(self, tts): - """Add TTS to be cache checked.""" - if tts not in self.attached_tts: - self.attached_tts.append(tts) - - def detach_tts(self, tts): - """Remove TTS from cache check.""" - if tts in self.attached_tts: - self.attached_tts.remove(tts) - - def get_attached_tts(self, tts_id=None): - tts_id = tts_id or self.active_tts - if not tts_id: - return - for tts in self.attached_tts: - if hasattr(tts, "tts_id"): - # opm plugin - if tts.tts_id == tts_id: - return tts - - for tts in self.attached_tts: - if not hasattr(tts, "tts_id"): - # non-opm plugin - if tts.tts_name == tts_id: - return tts - - def clear_queue(self): - """Remove all pending playbacks.""" - while not self.queue.empty(): - self.queue.get() - try: - self.p.terminate() - except Exception: - pass - - def begin_audio(self): - """Perform beginning of speech actions.""" - # This check will clear the "signal", in case it is still there for some reasons - check_for_signal("isSpeaking") - # this will create it again - create_signal("isSpeaking") - # Create signals informing start of speech - if self.bus: - self.bus.emit(Message("recognizer_loop:audio_output_start")) - else: - LOG.warning("Speech started before bus was attached.") + this class was only in ovos-plugin-manager in order to + patch usage of our plugins in mycroft-core""" - def end_audio(self, listen): - """Perform end of speech output actions. - Will inform the system that speech has ended and trigger the TTS's - cache checks. Listening will be triggered if requested. - Args: - listen (bool): True if listening event should be emitted - """ - if self.bus: - # Send end of speech signals to the system - self.bus.emit(Message("recognizer_loop:audio_output_end")) - if listen: - self.bus.emit(Message('mycroft.mic.listen')) - else: - LOG.warning("Speech started before bus was attached.") - - # This check will clear the filesystem IPC "signal" - check_for_signal("isSpeaking") - - def on_start(self): - self.blink(0.5) - if not self._processing_queue: - self._processing_queue = True - self.begin_audio() - - def on_end(self, listen=False): - if self._processing_queue: - self.end_audio(listen) - self._processing_queue = False - - # Clear cache for all attached tts objects - # This is basically the only safe time - for tts in self.attached_tts: - tts.cache.curate() - self.blink(0.2) - - def _play(self): - listen = False - tts_id = None + def __new__(self, *args, **kwargs): + LOG.warning("PlaybackThread moved to ovos_audio.playback") try: - if len(self._now_playing) == 6: - # opm style with tts_id - snd_type, data, visemes, ident, listen, tts_id = self._now_playing - elif len(self._now_playing) == 5: - # new mycroft style - snd_type, data, visemes, ident, listen = self._now_playing - else: - # old mycroft style TODO can this be deprecated? its very old - snd_type, data, visemes, ident = self._now_playing - - self.activate_tts(tts_id) - self.on_start() - self.p = play_audio(data) - if visemes: - self.show_visemes(visemes) - if self.p: - self.p.communicate() - self.p.wait() - self.deactivate_tts() - if self.queue.empty(): - self.on_end(listen) - except Empty: - pass - except Exception as e: - LOG.exception(e) - if self._processing_queue: - self.on_end(listen) - self._now_playing = None - - def run(self, cb=None): - """Thread main loop. Get audio and extra data from queue and play. - - The queue messages is a tuple containing - snd_type: 'mp3' or 'wav' telling the loop what format the data is in - data: path to temporary audio data - videmes: list of visemes to display while playing - listen: if listening should be triggered at the end of the sentence. - - Playback of audio is started and the visemes are sent over the bus - the loop then wait for the playback process to finish before starting - checking the next position in queue. - - If the queue is empty the tts.end_audio() is called possibly triggering - listening. - """ - self._paused = False - self._started.set() - while not self._terminated: - while self._paused: - sleep(0.2) - try: - self._now_playing = self.queue.get(timeout=2) - self._play() - except Exception as e: - pass - - def show_visemes(self, pairs): - """Send viseme data to enclosure - - Args: - pairs (list): Visime and timing pair - - Returns: - bool: True if button has been pressed. - """ - if self.enclosure: - self.enclosure.mouth_viseme(time(), pairs) - - def pause(self): - """pause thread""" - self._paused = True - if self.p: - self.p.terminate() - - def resume(self): - """resume thread""" - if self._now_playing: - self._play() - self._paused = False - - def clear(self): - """Clear all pending actions for the TTS playback thread.""" - self.clear_queue() - - def blink(self, rate=1.0): - """Blink mycroft's eyes""" - if self.enclosure and random.random() < rate: - self.enclosure.eyes_blink("b") - - def stop(self): - """Stop thread""" - self._now_playing = None - self._terminated = True - self.clear_queue() - - def shutdown(self): - self.stop() - for tts in self.attached_tts: - self.detach_tts(tts) - - def __del__(self): - self.shutdown() + from ovos_audio.playback import PlaybackThread + return PlaybackThread(*args, **kwargs) + except ImportError: + raise ImportError("please install ovos-audio for playback handling") class TTSContext: @@ -438,7 +196,13 @@ def __init__(self, lang="en-us", config=None, validator=None, cfg["g2p"]["module"] = g2pm else: LOG.warning(f"TTS selected {g2pm}, but it is not available!") - self.g2p = OVOSG2PFactory.create(cfg) + + try: + self.g2p = OVOSG2PFactory.create(cfg) + except: + LOG.exception("G2P plugin not loaded, there will be no mouth movements") + self.g2p = None + self.cache.curate() self.add_metric({"metric_type": "tts.init"}) @@ -481,7 +245,7 @@ def tts_id(self): @property def cache(self): return self.caches.get(self.tts_id) or \ - self.get_cache() + self.get_cache() @cache.setter def cache(self, val): @@ -514,7 +278,7 @@ def load_spellings(self, config=None): """Load phonetic spellings of words as dictionary.""" path = join('text', self.lang.lower(), 'phonetic_spellings.txt') try: - spellings_file = resolve_resource_file(path, config=config) + spellings_file = resolve_resource_file(path, config=config or Configuration()) except: LOG.debug('Failed to locate phonetic spellings resouce file.') return {} @@ -542,27 +306,32 @@ def end_audio(self, listen=False): self.add_metric({"metric_type": "tts.end"}) self.stopwatch.stop() - def init(self, bus=None): + def init(self, bus=None, playback=None): """ Performs intial setup of TTS object. Arguments: - bus: Mycroft messagebus connection + bus: OpenVoiceOS messagebus connection """ self.bus = bus or BUS() - self._init_playback() + if playback is None: + LOG.warning("PlaybackThread should be inited by ovos-audio, initing via plugin has been deprecated, " + "please pass playback=PlaybackThread() to TTS.init") + if TTS.playback: + playback.shutdown() + playback = PlaybackThread(TTS.queue, self.bus) # compat + playback.start() + self._init_playback(playback) self.add_metric({"metric_type": "tts.setup"}) - def _init_playback(self): - # shutdown any previous thread - if TTS.playback: - TTS.playback.shutdown() - - TTS.playback = PlaybackThread(TTS.queue) + def _init_playback(self, playback): + TTS.playback = playback TTS.playback.set_bus(self.bus) TTS.playback.attach_tts(self) if not TTS.playback.enclosure: TTS.playback.enclosure = EnclosureAPI(self.bus) - TTS.playback.start() + + if not TTS.playback.is_running: + TTS.playback.start() @property def enclosure(self): @@ -706,13 +475,12 @@ def execute(self, sentence, ident=None, listen=False, **kwargs): Arguments: sentence: (str) Sentence to be spoken - ident: (str) Id reference to current interaction + ident: (str) session_id from Message listen: (bool) True if listen should be triggered at the end of the utterance. """ sentence = self.validate_ssml(sentence) self.add_metric({"metric_type": "tts.ssml.validated"}) - create_signal("isSpeaking") self._execute(sentence, ident, listen, **kwargs) def _replace_phonetic_spellings(self, sentence): @@ -742,10 +510,10 @@ def _execute(self, sentence, ident, listen, **kwargs): audio_file, phonemes = self.synth(sentence, **kwargs) # get visemes/mouth movements + viseme = [] if phonemes: viseme = self.viseme(phonemes) - else: - viseme = [] + elif self.g2p is not None: try: viseme = self.g2p.utterance2visemes(sentence, lang) except OutOfVocabulary: @@ -754,28 +522,18 @@ def _execute(self, sentence, ident, listen, **kwargs): # this one is unplanned, let devs know all the info so they can fix it LOG.exception(f"Unexpected failure in G2P plugin: {self.g2p}") - audio_ext = self._determine_ext(audio_file) - if not viseme: # Debug level because this is expected in default installs LOG.debug(f"no mouth movements available! unknown visemes for {sentence}") + message = kwargs.get("message") or \ + dig_for_message() or \ + Message("speak", context={"session": {"session_id": ident}}) TTS.queue.put( - (audio_ext, str(audio_file), viseme, ident, l, tts_id) + (str(audio_file), viseme, l, tts_id, message) ) self.add_metric({"metric_type": "tts.queued"}) - def _determine_ext(self, audio_file): - # determine audio_ext on the fly - # do not use the ext defined in the plugin since it might not match - # some plugins support multiple extensions - # or have caches in different extensions - try: - _, audio_ext = splitext(str(audio_file)) - return audio_ext[1:] or self.audio_ext - except Exception as e: - return self.audio_ext - def synth(self, sentence, **kwargs): """ This method wraps get_tts several optional keyword arguments are supported @@ -819,7 +577,7 @@ def synth(self, sentence, **kwargs): def _cache_phonemes(self, sentence, phonemes=None, sentence_hash=None): sentence_hash = sentence_hash or hash_sentence(sentence) - if not phonemes: + if not phonemes and self.g2p is not None: try: phonemes = self.g2p.utterance2arpa(sentence, self.lang) self.add_metric({"metric_type": "tts.phonemes.g2p"}) @@ -861,7 +619,7 @@ def get_voice(self, gender, lang=None): def viseme(self, phonemes): """Create visemes from phonemes. - May be implemented to convert TTS phonemes into Mycroft mouth + May be implemented to convert TTS phonemes into OpenVoiceOS mouth visuals. Arguments: diff --git a/ovos_plugin_manager/templates/vad.py b/ovos_plugin_manager/templates/vad.py index 55f096d5..33a21e7a 100644 --- a/ovos_plugin_manager/templates/vad.py +++ b/ovos_plugin_manager/templates/vad.py @@ -1,8 +1,20 @@ +import abc +import collections + from ovos_config import Configuration from ovos_utils import classproperty from ovos_utils.process_utils import RuntimeRequirements +class AudioFrame: + """Represents a "frame" of audio data.""" + + def __init__(self, audio: bytes, timestamp: float, duration: int): + self.bytes = audio + self.timestamp = timestamp + self.duration = duration + + class VADEngine: def __init__(self, config=None, sample_rate=None): self.config_core = Configuration() @@ -10,6 +22,11 @@ def __init__(self, config=None, sample_rate=None): self.sample_rate = sample_rate or \ self.config_core.get("listener", {}).get("sample_rate", 16000) + self.padding_duration_ms = self.config.get("padding_duration_ms", 300) + self.frame_duration_ms = self.config.get("frame_duration_ms", 30) + self.thresh = self.config.get("thresh", 0.8) + self.num_padding_frames = int(self.padding_duration_ms / self.frame_duration_ms) + @classproperty def runtime_requirements(self): """ skill developers should override this if they do not require connectivity @@ -45,6 +62,65 @@ def runtime_requirements(self): no_internet_fallback=True, no_network_fallback=True) + def _frame_generator(self, audio: bytes): + """Generates audio frames from PCM audio data. + Takes the desired frame duration in milliseconds, the PCM data, and + the sample rate. + Yields Frames of the requested duration. + """ + n = int(self.sample_rate * (self.frame_duration_ms / 1000.0) * 2) + offset = 0 + timestamp = 0.0 + duration = (float(n) / self.sample_rate) / 2.0 + + while offset + n <= len(audio): + yield AudioFrame(audio[offset:offset + n], timestamp, duration) + timestamp += duration + offset += n + + def extract_speech(self, audio: bytes): + """returns the audio data with speech only, removing all noise before and after speech""" + # We use a deque for our sliding window/ring buffer. + ring_buffer = collections.deque(maxlen=self.num_padding_frames) + triggered = False + is_speech = False + voiced_frames = [] + + for frame in self._frame_generator(audio): + + is_speech = not self.is_silence(frame.bytes) + + if not triggered: + ring_buffer.append((frame, is_speech)) + num_voiced = len([f for f, speech in ring_buffer if speech]) + # If we're NOTTRIGGERED and more than 90% of the frames in + # the ring buffer are voiced frames, then enter the + # TRIGGERED state. + if num_voiced > self.thresh * ring_buffer.maxlen: + triggered = True + # We want to yield all the audio we see from now until + # we are NOTTRIGGERED, but we have to start with the + # audio that's already in the ring buffer. + for f, s in ring_buffer: + voiced_frames.append(f) + ring_buffer.clear() + else: + # We're in the TRIGGERED state, so collect the audio data + # and add it to the ring buffer. + voiced_frames.append(frame) + ring_buffer.append((frame, is_speech)) + num_unvoiced = len([f for f, speech in ring_buffer if not speech]) + + # If more than 90% of the frames in the ring buffer are + # unvoiced, then enter NOTTRIGGERED and yield whatever + # audio we've collected. + if num_unvoiced > self.thresh * ring_buffer.maxlen: + return b''.join([f.bytes for f in voiced_frames]) + + @abc.abstractmethod def is_silence(self, chunk): # return True or False return False + + def reset(self): + pass diff --git a/ovos_plugin_manager/tts.py b/ovos_plugin_manager/tts.py index 33871b89..ba1983a2 100644 --- a/ovos_plugin_manager/tts.py +++ b/ovos_plugin_manager/tts.py @@ -5,8 +5,8 @@ from ovos_plugin_manager.utils import PluginTypes, normalize_lang, \ PluginConfigTypes from ovos_plugin_manager.utils.config import get_valid_plugin_configs, \ - sort_plugin_configs -from ovos_utils.log import LOG + sort_plugin_configs, get_plugin_config +from ovos_utils.log import LOG, log_deprecation from ovos_utils.xdg_utils import xdg_data_home from hashlib import md5 @@ -90,18 +90,20 @@ def get_tts_supported_langs(): return get_plugin_supported_languages(PluginTypes.TTS) -def get_tts_config(config: dict = None) -> dict: +def get_tts_config(config: dict = None, module: str = None) -> dict: """ Get relevant configuration for factory methods @param config: global Configuration OR plugin class-specific configuration + @param module: TTS module to get configuration for @return: plugin class-specific configuration """ from ovos_plugin_manager.utils.config import get_plugin_config - return get_plugin_config(config, 'tts') + return get_plugin_config(config, 'tts', module) def get_voice_id(plugin_name, lang, tts_config): - tts_hash = md5(json.dumps(tts_config, sort_keys=True).encode("utf-8")).hexdigest() + tts_hash = md5(json.dumps(tts_config, + sort_keys=True).encode("utf-8")).hexdigest() return f"{plugin_name}_{lang}_{tts_hash}" @@ -110,7 +112,8 @@ def scan_voices(): for lang in get_tts_supported_langs(): VOICES_FOLDER = f"{xdg_data_home()}/OPM/voice_configs/{lang}" os.makedirs(VOICES_FOLDER, exist_ok=True) - for plug, voices in get_tts_lang_configs(lang, include_dialects=True).items(): + for plug, voices in get_tts_lang_configs(lang, + include_dialects=True).items(): for voice in voices: voiceid = get_voice_id(plug, lang, voice) if "meta" not in voice: @@ -189,20 +192,27 @@ def create(config=None): } """ tts_config = get_tts_config(config) - tts_lang = tts_config["lang"] tts_module = tts_config.get('module', 'dummy') + if tts_module in OVOSTTSFactory.MAPPINGS: + # The configured module maps to a valid plugin; get configuration + # again to make sure any module-specific config/overrides are loaded + log_deprecation("Module mappings will be deprecated", "0.1.0") + tts_module = OVOSTTSFactory.MAPPINGS[tts_module] + tts_config = get_tts_config(config, tts_module) try: clazz = OVOSTTSFactory.get_class(tts_config) if clazz: LOG.info(f'Found plugin {tts_module}') - tts = clazz(tts_lang, tts_config) + tts = clazz(lang=None, # explicitly read lang from config + config=tts_config) tts.validator.validate() LOG.info(f'Loaded plugin {tts_module}') else: - raise FileNotFoundError("unknown plugin") + raise RuntimeError(f"unknown plugin: {tts_module}") except Exception: plugins = find_tts_plugins() modules = ",".join(plugins.keys()) - LOG.exception(f'The TTS plugin "{tts_module}" could not be loaded.\nAvailable modules: {modules}') + LOG.exception(f'The TTS plugin "{tts_module}" could not be loaded.' + f'\nAvailable modules: {modules}') raise return tts diff --git a/ovos_plugin_manager/utils/__init__.py b/ovos_plugin_manager/utils/__init__.py index b89580e3..26f3bd51 100644 --- a/ovos_plugin_manager/utils/__init__.py +++ b/ovos_plugin_manager/utils/__init__.py @@ -11,14 +11,13 @@ # limitations under the License. # """Common functions for loading plugins.""" -from typing import Optional - import time from enum import Enum from threading import Event +from typing import Optional import pkg_resources -from langcodes import standardize_tag as _normalize_lang + from ovos_utils.log import LOG @@ -40,6 +39,8 @@ class PluginTypes(str, Enum): UTTERANCE_TRANSFORMER = "neon.plugin.text" METADATA_TRANSFORMER = "neon.plugin.metadata" AUDIO_TRANSFORMER = "neon.plugin.audio" + DIALOG_TRANSFORMER = "opm.transformer.dialog" + TTS_TRANSFORMER = "opm.transformer.tts" QUESTION_SOLVER = "neon.plugin.solver" TLDR_SOLVER = "opm.solver.summarization" ENTAILMENT_SOLVER = "opm.solver.entailment" @@ -72,6 +73,8 @@ class PluginConfigTypes(str, Enum): UTTERANCE_TRANSFORMER = "neon.plugin.text.config" METADATA_TRANSFORMER = "neon.plugin.metadata.config" AUDIO_TRANSFORMER = "neon.plugin.audio.config" + DIALOG_TRANSFORMER = "opm.transformer.dialog.config" + TTS_TRANSFORMER = "opm.transformer.tts.config" QUESTION_SOLVER = "neon.plugin.solver.config" TLDR_SOLVER = "opm.solver.summarization.config" ENTAILMENT_SOLVER = "opm.solver.entailment.config" @@ -109,11 +112,17 @@ def find_plugins(plug_type: PluginTypes = None) -> dict: if entry_point.name not in entrypoints: LOG.debug(f"Loaded plugin entry point {entry_point.name}") except Exception as e: - LOG.debug(f"Failed to load plugin entry point {entry_point}: " - f"{e}") + if entry_point not in find_plugins._errored: + find_plugins._errored.append(entry_point) + # NOTE: this runs in a loop inside skills manager, this would endlessly spam logs + LOG.error(f"Failed to load plugin entry point {entry_point}: " + f"{e}") return entrypoints +find_plugins._errored = [] + + def _iter_entrypoints(plug_type: Optional[str]): """ Return an iterator containing all entrypoints of the requested type @@ -149,15 +158,17 @@ def load_plugin(plug_name: str, plug_type: Optional[PluginTypes] = None): def normalize_lang(lang): # TODO consider moving to LF or ovos_utils + # special handling, the parse sometimes messes this up + # eg, uk-ua gets normalized to uk-gb + # this also makes lookup easier as we + # often get duplicate entries with both variants + if "-" in lang: + pieces = lang.split("-") + if len(pieces) == 2 and pieces[0] == pieces[1]: + lang = pieces[0] + try: - # special handling, the parse sometimes messes this up - # eg, uk-uk gets normalized to uk-gb - # this also makes lookup easier as we - # often get duplicate entries with both variants - if "-" in lang: - pieces = lang.split("-") - if len(pieces) == 2 and pieces[0] == pieces[1]: - lang = pieces[0] + from langcodes import standardize_tag as _normalize_lang lang = _normalize_lang(lang, macro=True) except ValueError: # this lang code is apparently not valid ? diff --git a/ovos_plugin_manager/utils/config.py b/ovos_plugin_manager/utils/config.py index f13f7799..dd1186ae 100644 --- a/ovos_plugin_manager/utils/config.py +++ b/ovos_plugin_manager/utils/config.py @@ -27,10 +27,12 @@ def get_plugin_config(config: Optional[dict] = None, section: str = None, module_config = dict(config.get(module) or dict()) module_config.setdefault('module', module) for key, val in config.items(): - if key == "module": + # Configured module name is not part of that module's config + if key in ("module", "translation_module", "detection_module"): continue elif isinstance(val, dict): continue + # Use section-scoped config as defaults (i.e. TTS.lang) module_config.setdefault(key, val) config = module_config if section not in ["hotwords", "VAD", "listener", "gui"]: diff --git a/ovos_plugin_manager/version.py b/ovos_plugin_manager/version.py index 89461772..aacf6ad5 100644 --- a/ovos_plugin_manager/version.py +++ b/ovos_plugin_manager/version.py @@ -2,6 +2,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 VERSION_MINOR = 0 -VERSION_BUILD = 23 +VERSION_BUILD = 24 VERSION_ALPHA = 0 # END_VERSION_BLOCK diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 00000000..48548151 --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,4 @@ +pytest +pytest-timeout +pytest-cov +ovos-translate-server-plugin \ No newline at end of file diff --git a/test/unittests/test_language.py b/test/unittests/test_language.py index 4b4b331e..ed46e1e3 100644 --- a/test/unittests/test_language.py +++ b/test/unittests/test_language.py @@ -1,6 +1,6 @@ import unittest -from unittest.mock import patch +from unittest.mock import patch, Mock from ovos_plugin_manager.utils import PluginTypes, PluginConfigTypes @@ -82,11 +82,168 @@ def test_get_module_configs(self, load_plugin_configs): self.CONFIG_TYPE) -class TestLangTranslationFactory(unittest.TestCase): - from ovos_plugin_manager.language import OVOSLangTranslationFactory - # TODO +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): + 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") + + # Test invalid module config + conf = {"language": {}} + with self.assertRaises(ValueError): + OVOSLangDetectionFactory.get_class(conf) + + @patch("ovos_plugin_manager.language.load_lang_detect_plugin") + @patch("ovos_plugin_manager.language.Configuration") + 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 + 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"}) + 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"}) + 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 -class TestLangDetectionFactory(unittest.TestCase): - from ovos_plugin_manager.language import OVOSLangDetectionFactory - # TODO +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): + 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") + + # Test invalid module config + conf = {"language": {}} + with self.assertRaises(ValueError): + OVOSLangTranslationFactory.get_class(conf) + + @patch("ovos_plugin_manager.language.load_tx_plugin") + @patch("ovos_plugin_manager.language.Configuration") + 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 + 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"}) + 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) + self.assertEquals(plug_instance, plug) + + # TODO: Test exception handling fallback to libretranslate diff --git a/test/unittests/test_stt.py b/test/unittests/test_stt.py index bd4b27e9..a1705dd3 100644 --- a/test/unittests/test_stt.py +++ b/test/unittests/test_stt.py @@ -1,7 +1,7 @@ import unittest from copy import copy -from unittest.mock import patch +from unittest.mock import patch, Mock from ovos_plugin_manager.utils import PluginTypes, PluginConfigTypes @@ -81,15 +81,76 @@ def test_get_supported_langs(self, get_supported_languages): get_supported_languages.assert_called_once_with(self.PLUGIN_TYPE) @patch("ovos_plugin_manager.utils.config.get_plugin_config") - def test_get_config(self, get_config): + def test_get_stt_config(self, get_config): from ovos_plugin_manager.stt import get_stt_config config = copy(self.TEST_CONFIG) get_stt_config(self.TEST_CONFIG) get_config.assert_called_once_with(self.TEST_CONFIG, - self.CONFIG_SECTION) + self.CONFIG_SECTION, None) self.assertEqual(config, self.TEST_CONFIG) class TestSTTFactory(unittest.TestCase): - from ovos_plugin_manager.stt import OVOSSTTFactory - # TODO + def test_mappings(self): + from ovos_plugin_manager.stt import OVOSSTTFactory + self.assertIsInstance(OVOSSTTFactory.MAPPINGS, dict) + for key in OVOSSTTFactory.MAPPINGS: + self.assertIsInstance(key, str) + self.assertIsInstance(OVOSSTTFactory.MAPPINGS[key], str) + self.assertNotEqual(key, OVOSSTTFactory.MAPPINGS[key]) + + @patch("ovos_plugin_manager.stt.load_stt_plugin") + def test_get_class(self, load_plugin): + from ovos_plugin_manager.stt import OVOSSTTFactory + global_config = {"stt": {"module": "dummy"}} + tts_config = {"module": "test-stt-plugin-test"} + + # Test load plugin mapped global config + OVOSSTTFactory.get_class(global_config) + load_plugin.assert_called_with("ovos-stt-plugin-dummy") + + # Test load plugin explicit STT config + OVOSSTTFactory.get_class(tts_config) + load_plugin.assert_called_with("test-stt-plugin-test") + + @patch("ovos_plugin_manager.stt.OVOSSTTFactory.get_class") + def test_create(self, get_class): + from ovos_plugin_manager.stt import OVOSSTTFactory + plugin_class = Mock() + get_class.return_value = plugin_class + + global_config = {"lang": "en-gb", + "stt": {"module": "dummy", + "ovos-stt-plugin-dummy": {"config": True, + "lang": "en-ca"}}} + stt_config = {"lang": "es-es", + "module": "test-stt-plugin-test"} + + stt_config_2 = {"lang": "es-es", + "module": "test-stt-plugin-test", + "test-stt-plugin-test": {"config": True, + "lang": "es-mx"}} + + # Test create with global config and lang override + plugin = OVOSSTTFactory.create(global_config) + expected_config = {"module": "ovos-stt-plugin-dummy", + "config": True, + "lang": "en-ca"} + get_class.assert_called_once_with(expected_config) + plugin_class.assert_called_once_with(expected_config) + self.assertEqual(plugin, plugin_class()) + + # Test create with STT config and no module config + plugin = OVOSSTTFactory.create(stt_config) + get_class.assert_called_with(stt_config) + plugin_class.assert_called_with(stt_config) + self.assertEqual(plugin, plugin_class()) + + # Test create with STT config with module-specific config + plugin = OVOSSTTFactory.create(stt_config_2) + expected_config = {"module": "test-stt-plugin-test", + "config": True, "lang": "es-mx"} + get_class.assert_called_with(expected_config) + plugin_class.assert_called_with(expected_config) + self.assertEqual(plugin, plugin_class()) + diff --git a/test/unittests/test_tts.py b/test/unittests/test_tts.py index fde4f0a2..27fe6e3d 100644 --- a/test/unittests/test_tts.py +++ b/test/unittests/test_tts.py @@ -1,5 +1,5 @@ import unittest -from unittest.mock import patch +from unittest.mock import patch, Mock from ovos_plugin_manager.utils import PluginTypes, PluginConfigTypes from ovos_plugin_manager.templates.tts import TTS @@ -180,11 +180,11 @@ def test_get_supported_langs(self, get_supported_languages): get_supported_languages.assert_called_once_with(self.PLUGIN_TYPE) @patch("ovos_plugin_manager.utils.config.get_plugin_config") - def test_get_config(self, get_config): + def test_get_tts_config(self, get_config): from ovos_plugin_manager.tts import get_tts_config get_tts_config(self.TEST_CONFIG) get_config.assert_called_once_with(self.TEST_CONFIG, - self.CONFIG_SECTION) + self.CONFIG_SECTION, None) def test_get_voice_id(self): from ovos_plugin_manager.tts import get_voice_id @@ -200,6 +200,65 @@ def test_get_voices(self): class TestTTSFactory(unittest.TestCase): - from ovos_plugin_manager.tts import OVOSTTSFactory - # TODO - + def test_mappings(self): + from ovos_plugin_manager.tts import OVOSTTSFactory + self.assertIsInstance(OVOSTTSFactory.MAPPINGS, dict) + for key in OVOSTTSFactory.MAPPINGS: + self.assertIsInstance(key, str) + self.assertIsInstance(OVOSTTSFactory.MAPPINGS[key], str) + self.assertNotEqual(key, OVOSTTSFactory.MAPPINGS[key]) + + @patch("ovos_plugin_manager.tts.load_tts_plugin") + def test_get_class(self, load_plugin): + from ovos_plugin_manager.tts import OVOSTTSFactory + global_config = {"tts": {"module": "dummy"}} + tts_config = {"module": "test-tts-plugin-test"} + + # Test load plugin mapped global config + OVOSTTSFactory.get_class(global_config) + load_plugin.assert_called_with("ovos-tts-plugin-dummy") + + # Test load plugin explicit TTS config + OVOSTTSFactory.get_class(tts_config) + load_plugin.assert_called_with("test-tts-plugin-test") + + @patch("ovos_plugin_manager.tts.OVOSTTSFactory.get_class") + def test_create(self, get_class): + from ovos_plugin_manager.tts import OVOSTTSFactory + plugin_class = Mock() + get_class.return_value = plugin_class + + global_config = {"lang": "en-gb", + "tts": {"module": "dummy", + "ovos-tts-plugin-dummy": {"config": True, + "lang": "en-ca"}}} + tts_config = {"lang": "es-es", + "module": "test-tts-plugin-test"} + + tts_config_2 = {"lang": "es-es", + "module": "test-tts-plugin-test", + "test-tts-plugin-test": {"config": True, + "lang": "es-mx"}} + + # Test create with global config and lang override + plugin = OVOSTTSFactory.create(global_config) + expected_config = {"module": "ovos-tts-plugin-dummy", + "config": True, + "lang": "en-ca"} + get_class.assert_called_once_with(expected_config) + plugin_class.assert_called_once_with(lang=None, config=expected_config) + self.assertEqual(plugin, plugin_class()) + + # Test create with TTS config and no module config + plugin = OVOSTTSFactory.create(tts_config) + get_class.assert_called_with(tts_config) + plugin_class.assert_called_with(lang=None, config=tts_config) + self.assertEqual(plugin, plugin_class()) + + # Test create with TTS config with module-specific config + plugin = OVOSTTSFactory.create(tts_config_2) + expected_config = {"module": "test-tts-plugin-test", + "config": True, "lang": "es-mx"} + get_class.assert_called_with(expected_config) + plugin_class.assert_called_with(lang=None, config=expected_config) + self.assertEqual(plugin, plugin_class()) diff --git a/test/unittests/test_utils.py b/test/unittests/test_utils.py index 69780297..ec0b3590 100644 --- a/test/unittests/test_utils.py +++ b/test/unittests/test_utils.py @@ -4,7 +4,7 @@ from os.path import join, dirname, isfile from copy import deepcopy, copy -from unittest.mock import patch +from unittest.mock import patch, Mock _MOCK_CONFIG = { "lang": "global", @@ -524,16 +524,43 @@ def test_plugin_types(self): self.assertIsInstance(plug_type, str) # Handle plugins without associated config entrypoint if plug_type not in (PluginTypes.PERSONA,): - self.assertIsInstance(PluginConfigTypes(f"{plug_type}.config"), + self.assertIsInstance(PluginConfigTypes(f"{plug_type.value}.config"), PluginConfigTypes) for cfg_type in PluginConfigTypes: self.assertIsInstance(cfg_type, PluginConfigTypes) self.assertIsInstance(cfg_type, str) self.assertTrue(cfg_type.value.endswith('.config')) - def test_find_plugins(self): + @patch("ovos_plugin_manager.utils.LOG.error") + @patch("ovos_plugin_manager.utils._iter_entrypoints") + def test_find_plugins(self, iter_entrypoints, log_error): from ovos_plugin_manager.utils import find_plugins - # TODO + good_plugin = Mock(name="working_plugin") + bad_plugin = Mock(name="failing_plugin") + bad_plugin.load = Mock( + side_effect=Exception("This plugin doesn't load")) + + # Test load valid plugin + iter_entrypoints.return_value = [good_plugin] + valid_loaded = find_plugins() + self.assertEqual(len(valid_loaded), 1) + self.assertEqual(list(valid_loaded.keys())[0], good_plugin.name) + self.assertEqual(list(valid_loaded.values())[0], good_plugin.load()) + log_error.assert_not_called() + + # Test load with invalid plugin + iter_entrypoints.return_value.append(bad_plugin) + with_invalid_loaded = find_plugins() + self.assertEqual(with_invalid_loaded.keys(), valid_loaded.keys()) + log_error.assert_called_once() + + # Test error not re-logged + with_invalid_reloaded = find_plugins() + self.assertEqual(with_invalid_reloaded.keys(), + with_invalid_loaded.keys()) + log_error.assert_called_once() + + # TODO: Test loading by plugin type def test_load_plugin(self): from ovos_plugin_manager.utils import load_plugin @@ -602,6 +629,26 @@ def test_get_plugin_config(self, config): self.assertEqual(seg_config, get_plugin_config(section="segmentation")) self.assertEqual(gui_config, get_plugin_config(section="gui")) + # Test TTS config with plugin `lang` override + config = { + "lang": "en-us", + "tts": { + "module": "ovos_tts_plugin_espeakng", + "ovos_tts_plugin_espeakng": { + "lang": "de-de", + "voice": "german-mbrola-5", + "speed": "135", + "amplitude": "80", + "pitch": "20" + } + } + } + tts_config = get_plugin_config(config, "tts") + self.assertEqual(tts_config['lang'], 'de-de') + self.assertEqual(tts_config['module'], 'ovos_tts_plugin_espeakng') + self.assertEqual(tts_config['voice'], 'german-mbrola-5') + self.assertNotIn("ovos_tts_plugin_espeakng", tts_config) + self.assertEqual(_MOCK_CONFIG, start_config) def test_get_valid_plugin_configs(self):