From b07d830f55ba9156842e0b8faf965be8baa4fb69 Mon Sep 17 00:00:00 2001 From: miro Date: Thu, 9 May 2024 04:56:49 +0100 Subject: [PATCH 01/15] feat/legacy_audio_api --- ovos_core/intent_services/ocp_service.py | 81 +++++++++++++++++++----- 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/ovos_core/intent_services/ocp_service.py b/ovos_core/intent_services/ocp_service.py index f8c78222fa88..84849f6c3a84 100644 --- a/ovos_core/intent_services/ocp_service.py +++ b/ovos_core/intent_services/ocp_service.py @@ -4,14 +4,15 @@ from threading import RLock from typing import List, Tuple, Optional +from ovos_classifiers.skovos.classifier import SklearnOVOSClassifier +from ovos_classifiers.skovos.features import ClassifierProbaVectorizer, KeywordFeaturesVectorizer from padacioso import IntentContainer from sklearn.pipeline import FeatureUnion import ovos_core.intent_services -from ovos_bus_client.apis.ocp import OCPInterface, OCPQuery +from ovos_bus_client.apis.ocp import OCPInterface, OCPQuery, ClassicAudioServiceInterface from ovos_bus_client.message import Message -from ovos_classifiers.skovos.classifier import SklearnOVOSClassifier -from ovos_classifiers.skovos.features import ClassifierProbaVectorizer, KeywordFeaturesVectorizer +from ovos_config import Configuration from ovos_utils import classproperty from ovos_utils.log import LOG from ovos_utils.messagebus import FakeBus @@ -104,6 +105,7 @@ def __init__(self, bus=None, config=None): resources_dir=f"{dirname(__file__)}") self.ocp_api = OCPInterface(self.bus) + self.legacy_api = ClassicAudioServiceInterface(self.bus) self.config = config or {} self.search_lock = RLock() @@ -123,6 +125,15 @@ def __init__(self, bus=None, config=None): # request available Stream extractor plugins from OCP self.bus.emit(Message("ovos.common_play.SEI.get")) + @property + def use_legacy_audio(self): + """when neither ovos-media nor old OCP are available""" + if self.config.get("legacy"): + # explicitly set in pipeline config + return True + cfg = Configuration() + return cfg.get("disable_ocp") and cfg.get("enable_old_audioservice") + def load_classifiers(self): # warm up the featurizer so intent matches faster (lazy loaded) @@ -526,7 +537,13 @@ def handle_play_intent(self, message: Message): 'origin': OCP_ID})) # ovos-PHAL-plugin-mk1 will display music icon in response to play message - self.ocp_api.play(results, query) + if self.use_legacy_audio: + # TODO - we need to extract streams here with ocp extractors + # some uris arent valid + results = [r.uri for r in results] + self.legacy_api.play(results) + else: + self.ocp_api.play(results, query) def handle_open_intent(self, message: Message): LOG.info("Requesting OCP homescreen") @@ -539,30 +556,54 @@ def handle_like_intent(self, message: Message): self.bus.emit(message.forward("ovos.common_play.like")) def handle_stop_intent(self, message: Message): - LOG.info("Requesting OCP to go to stop") - self.ocp_api.stop() + if self.use_legacy_audio: + LOG.info("Requesting Legacy AudioService to stop") + self.legacy_api.stop() + else: + LOG.info("Requesting OCP to stop") + self.ocp_api.stop() def handle_next_intent(self, message: Message): - LOG.info("Requesting OCP to go to next track") - self.ocp_api.next() + if self.use_legacy_audio: + LOG.info("Requesting Legacy AudioService to go to next track") + self.legacy_api.next() + else: + LOG.info("Requesting OCP to go to next track") + self.ocp_api.next() def handle_prev_intent(self, message: Message): - LOG.info("Requesting OCP to go to prev track") - self.ocp_api.prev() + if self.use_legacy_audio: + LOG.info("Requesting Legacy AudioService to go to prev track") + self.legacy_api.prev() + else: + LOG.info("Requesting OCP to go to prev track") + self.ocp_api.prev() def handle_pause_intent(self, message: Message): - LOG.info("Requesting OCP to go to pause") - self.ocp_api.pause() + if self.use_legacy_audio: + LOG.info("Requesting Legacy AudioService to pause") + self.legacy_api.pause() + else: + LOG.info("Requesting OCP to go to pause") + self.ocp_api.pause() def handle_resume_intent(self, message: Message): - LOG.info("Requesting OCP to go to resume") - self.ocp_api.resume() + if self.use_legacy_audio: + LOG.info("Requesting Legacy AudioService to resume") + self.legacy_api.resume() + else: + LOG.info("Requesting OCP to go to resume") + self.ocp_api.resume() def handle_search_error_intent(self, message: Message): self.bus.emit(message.forward("mycroft.audio.play_sound", {"uri": "snd/error.mp3"})) - LOG.info("Requesting OCP to stop") - self.ocp_api.stop() + if self.use_legacy_audio: + LOG.info("Requesting Legacy AudioService to stop") + self.legacy_api.stop() + else: + LOG.info("Requesting OCP to stop") + self.ocp_api.stop() def _do_play(self, phrase: str, results, media_type=MediaType.GENERIC): self.bus.emit(Message('ovos.common_play.reset')) @@ -581,7 +622,13 @@ def _do_play(self, phrase: str, results, media_type=MediaType.GENERIC): 'origin': OCP_ID})) # ovos-PHAL-plugin-mk1 will display music icon in response to play message - self.ocp_api.play(results, phrase) + if self.use_legacy_audio: + # TODO - we need to extract streams here with ocp extractors + # some uris arent valid + results = [r.uri for r in results] + self.legacy_api.play(results) + else: + self.ocp_api.play(results, phrase) # NLP @staticmethod From 5d14fdb194c3c08d81d7450202c5ca1f99af5563 Mon Sep 17 00:00:00 2001 From: miro Date: Thu, 9 May 2024 05:07:25 +0100 Subject: [PATCH 02/15] feat/legacy_audio_api --- ovos_core/intent_services/ocp_service.py | 25 ++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/ovos_core/intent_services/ocp_service.py b/ovos_core/intent_services/ocp_service.py index 84849f6c3a84..d1c8e37b12d2 100644 --- a/ovos_core/intent_services/ocp_service.py +++ b/ovos_core/intent_services/ocp_service.py @@ -13,6 +13,7 @@ from ovos_bus_client.apis.ocp import OCPInterface, OCPQuery, ClassicAudioServiceInterface from ovos_bus_client.message import Message from ovos_config import Configuration +from ovos_plugin_manager.ocp import load_stream_extractors from ovos_utils import classproperty from ovos_utils.log import LOG from ovos_utils.messagebus import FakeBus @@ -538,10 +539,7 @@ def handle_play_intent(self, message: Message): # ovos-PHAL-plugin-mk1 will display music icon in response to play message if self.use_legacy_audio: - # TODO - we need to extract streams here with ocp extractors - # some uris arent valid - results = [r.uri for r in results] - self.legacy_api.play(results) + self.legacy_play(results, query) else: self.ocp_api.play(results, query) @@ -623,13 +621,19 @@ def _do_play(self, phrase: str, results, media_type=MediaType.GENERIC): # ovos-PHAL-plugin-mk1 will display music icon in response to play message if self.use_legacy_audio: - # TODO - we need to extract streams here with ocp extractors - # some uris arent valid - results = [r.uri for r in results] - self.legacy_api.play(results) + self.legacy_play(results, phrase) else: self.ocp_api.play(results, phrase) + def legacy_play(self, results: List[MediaEntry], phrase=""): + xtract = load_stream_extractors() + # for legacy audio service we need to do stream extraction here + # we also need to filter video results + results = [xtract.extract_stream(r.uri, video=False)["uri"] + for r in results + if r.playback in [PlaybackType.AUDIO, PlaybackType.AUDIO_SERVICE]] + self.legacy_api.play(results, utterance=phrase) + # NLP @staticmethod def label2media(label: str) -> MediaType: @@ -798,11 +802,12 @@ def filter_results(self, results: list, phrase: str, lang: str, video_only = True # check if user said "play XXX audio only" - if audio_only: + if audio_only or self.use_legacy_audio: l1 = len(results) # TODO - also check inside playlists results = [r for r in results - if isinstance(r, Playlist) or r.playback == PlaybackType.AUDIO] + if (isinstance(r, Playlist) and not self.use_legacy_audio) + or r.playback == PlaybackType.AUDIO] LOG.debug(f"filtered {l1 - len(results)} non-audio results") # check if user said "play XXX video only" From bb0072e1d2f7ad775c121312b37ce74372d94815 Mon Sep 17 00:00:00 2001 From: miro Date: Thu, 9 May 2024 05:34:30 +0100 Subject: [PATCH 03/15] allow some media types --- ovos_core/intent_services/ocp_service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ovos_core/intent_services/ocp_service.py b/ovos_core/intent_services/ocp_service.py index d1c8e37b12d2..f50ffc9c2724 100644 --- a/ovos_core/intent_services/ocp_service.py +++ b/ovos_core/intent_services/ocp_service.py @@ -631,7 +631,8 @@ def legacy_play(self, results: List[MediaEntry], phrase=""): # we also need to filter video results results = [xtract.extract_stream(r.uri, video=False)["uri"] for r in results - if r.playback in [PlaybackType.AUDIO, PlaybackType.AUDIO_SERVICE]] + if r.playback in [PlaybackType.AUDIO, PlaybackType.AUDIO_SERVICE] + or r.media_type in OCPQuery.cast2audio] self.legacy_api.play(results, utterance=phrase) # NLP @@ -807,7 +808,7 @@ def filter_results(self, results: list, phrase: str, lang: str, # TODO - also check inside playlists results = [r for r in results if (isinstance(r, Playlist) and not self.use_legacy_audio) - or r.playback == PlaybackType.AUDIO] + or r.playback in [PlaybackType.AUDIO, PlaybackType.AUDIO_SERVICE]] LOG.debug(f"filtered {l1 - len(results)} non-audio results") # check if user said "play XXX video only" From 265f51e378c17e37b023d2b060c23f00e5b4de5d Mon Sep 17 00:00:00 2001 From: miro Date: Thu, 9 May 2024 05:37:05 +0100 Subject: [PATCH 04/15] allow some media types --- ovos_core/intent_services/ocp_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_core/intent_services/ocp_service.py b/ovos_core/intent_services/ocp_service.py index f50ffc9c2724..4f94be7b6e10 100644 --- a/ovos_core/intent_services/ocp_service.py +++ b/ovos_core/intent_services/ocp_service.py @@ -631,7 +631,7 @@ def legacy_play(self, results: List[MediaEntry], phrase=""): # we also need to filter video results results = [xtract.extract_stream(r.uri, video=False)["uri"] for r in results - if r.playback in [PlaybackType.AUDIO, PlaybackType.AUDIO_SERVICE] + if r.playback == PlaybackType.AUDIO or r.media_type in OCPQuery.cast2audio] self.legacy_api.play(results, utterance=phrase) @@ -808,7 +808,7 @@ def filter_results(self, results: list, phrase: str, lang: str, # TODO - also check inside playlists results = [r for r in results if (isinstance(r, Playlist) and not self.use_legacy_audio) - or r.playback in [PlaybackType.AUDIO, PlaybackType.AUDIO_SERVICE]] + or r.playback == PlaybackType.AUDIO] LOG.debug(f"filtered {l1 - len(results)} non-audio results") # check if user said "play XXX video only" From b205c9ad1811a526308c032f9ded7547adb9284d Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 10 May 2024 01:49:43 +0100 Subject: [PATCH 05/15] ocp tests --- test/end2end/session/test_ocp.py | 289 +++++++++++++++++++++++++ test/end2end/skill-fake-fm/__init__.py | 39 ++++ test/end2end/skill-fake-fm/setup.py | 46 ++++ 3 files changed, 374 insertions(+) create mode 100644 test/end2end/session/test_ocp.py create mode 100644 test/end2end/skill-fake-fm/__init__.py create mode 100755 test/end2end/skill-fake-fm/setup.py diff --git a/test/end2end/session/test_ocp.py b/test/end2end/session/test_ocp.py new file mode 100644 index 000000000000..2c1621e35b2a --- /dev/null +++ b/test/end2end/session/test_ocp.py @@ -0,0 +1,289 @@ +import time +from time import sleep +from unittest import TestCase + +from ovos_bus_client.message import Message +from ovos_bus_client.session import SessionManager, Session + +from ..minicroft import get_minicroft + + +class TestOCPPipeline(TestCase): + + def setUp(self): + self.skill_id = "skill-fake-fm.openvoiceos" + self.core = get_minicroft(self.skill_id) + + def tearDown(self) -> None: + self.core.stop() + + def test_no_match(self): + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["play unknown thing"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "enclosure.active_skill", + "speak", + "ocp:play", + "ovos.common_play.search.start", + "enclosure.mouth.think", + "ovos.common_play.search.stop", # any ongoing previous search + "ovos.common_play.query", + # skill searching (generic) + "ovos.common_play.skill.search_start", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.skill.search_end", + "ovos.common_play.search.end", + # no good results + "ovos.common_play.reset", + "enclosure.active_skill", + "speak" # error + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + def test_radio_media_match(self): + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["play some radio station"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "enclosure.active_skill", + "speak", + "ocp:play", + "ovos.common_play.search.start", + "enclosure.mouth.think", + "ovos.common_play.search.stop", # any ongoing previous search + "ovos.common_play.query", # media type radio + # skill searching (radio) + "ovos.common_play.skill.search_start", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.skill.search_end", + "ovos.common_play.search.end", + # good results because of radio media type + "ovos.common_play.reset", + "add_context", # NowPlaying context + "ovos.common_play.play" # OCP api + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + def test_unk_media_match(self): + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["play the alien movie"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "enclosure.active_skill", + "speak", + "ocp:play", + "ovos.common_play.search.start", + "enclosure.mouth.think", + "ovos.common_play.search.stop", # any ongoing previous search + "ovos.common_play.query", # movie media type search + # no skills want to search + "ovos.common_play.query", # generic media type fallback + # skill searching (generic) + "ovos.common_play.skill.search_start", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.skill.search_end", + "ovos.common_play.search.end", + # no good results + "ovos.common_play.reset", + "enclosure.active_skill", + "speak" # error + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + def test_skill_name_match(self): + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["play Fake FM"]}, # auto derived from skill class name in this case + {"session": sess.serialize(), + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "enclosure.active_skill", + "speak", + "ocp:play", + "ovos.common_play.search.start", + "enclosure.mouth.think", + "ovos.common_play.search.stop", # any ongoing previous search + f"ovos.common_play.query.{self.skill_id}", # explicitly search skill + # skill searching (explicit) + "ovos.common_play.skill.search_start", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.skill.search_end", + "ovos.common_play.search.end", + # good results + "ovos.common_play.reset", + "add_context", # NowPlaying context + "ovos.common_play.play" # OCP api + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + diff --git a/test/end2end/skill-fake-fm/__init__.py b/test/end2end/skill-fake-fm/__init__.py new file mode 100644 index 000000000000..880b9205941a --- /dev/null +++ b/test/end2end/skill-fake-fm/__init__.py @@ -0,0 +1,39 @@ +from os.path import join, dirname + +from ovos_utils.ocp import MediaType, PlaybackType +from ovos_workshop.decorators.ocp import ocp_search +from ovos_workshop.skills.common_play import OVOSCommonPlaybackSkill + + +class FakeFMSkill(OVOSCommonPlaybackSkill): + + def __init__(self, *args, **kwargs): + super().__init__(supported_media = [MediaType.RADIO, + MediaType.GENERIC], + skill_icon=join(dirname(__file__), "ui", "fakefm.png"), + *args, **kwargs) + + @ocp_search() + def search_fakefm(self, phrase, media_type): + score = 50 + if "fake" in phrase: + score += 35 + if media_type == MediaType.RADIO: + score += 20 + else: + score -= 30 + + for i in range(5): + score = score + i + yield { + "match_confidence": score, + "media_type": MediaType.RADIO, + "uri": f"https://fake_{i}.mp3", + "playback": PlaybackType.AUDIO, + "image": f"https://fake_{i}.png", + "bg_image": f"https://fake_{i}.png", + "skill_icon": f"https://fakefm.png", + "title": f"fake station {i}", + "author": "FakeFM", + "length": 0 + } \ No newline at end of file diff --git a/test/end2end/skill-fake-fm/setup.py b/test/end2end/skill-fake-fm/setup.py new file mode 100755 index 000000000000..e8e602d24b74 --- /dev/null +++ b/test/end2end/skill-fake-fm/setup.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +from os import walk, path + +from setuptools import setup + +URL = "https://github.com/OpenVoiceOS/skill-fake-fm" +SKILL_CLAZZ = "FakeFMSkill" # needs to match __init__.py class name + +# below derived from github url to ensure standard skill_id +SKILL_AUTHOR, SKILL_NAME = URL.split(".com/")[-1].split("/") +SKILL_PKG = SKILL_NAME.lower().replace('-', '_') +PLUGIN_ENTRY_POINT = f'{SKILL_NAME.lower()}.{SKILL_AUTHOR.lower()}={SKILL_PKG}:{SKILL_CLAZZ}' + + +# skill_id=package_name:SkillClass + + +def find_resource_files(): + resource_base_dirs = ("locale", "ui", "vocab", "dialog", "regex", "skill") + base_dir = path.dirname(__file__) + package_data = ["*.json"] + for res in resource_base_dirs: + if path.isdir(path.join(base_dir, res)): + for (directory, _, files) in walk(path.join(base_dir, res)): + if files: + package_data.append( + path.join(directory.replace(base_dir, "").lstrip('/'), + '*')) + return package_data + + +setup( + name="skill-fake-fm", + version="0.0.0", + long_description="test", + description='OVOS test plugin', + author_email='jarbasai@mailfence.com', + license='Apache-2.0', + package_dir={SKILL_PKG: ""}, + package_data={SKILL_PKG: find_resource_files()}, + packages=[SKILL_PKG], + include_package_data=True, + install_requires=["ovos-workshop>=0.0.16a8"], + keywords='ovos skill plugin', + entry_points={'ovos.plugin.skill': PLUGIN_ENTRY_POINT} +) From 58691f32dda55e4a197624bfc6dcc23101c0a2b5 Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 10 May 2024 01:53:54 +0100 Subject: [PATCH 06/15] install new test skill --- .github/workflows/coverage.yml | 1 + .github/workflows/unit_tests.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 801b48f649af..db3a2c016824 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -32,6 +32,7 @@ jobs: pip install ./test/end2end/skill-ovos-schedule pip install ./test/end2end/skill-new-stop pip install ./test/end2end/skill-old-stop + pip install ./test/end2end/skill-fake-fm - name: Install core repo run: | pip install -e .[mycroft,deprecated] diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index d5955a771011..4f01dab4407c 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -59,6 +59,7 @@ jobs: pip install ./test/end2end/skill-ovos-schedule pip install ./test/end2end/skill-new-stop pip install ./test/end2end/skill-old-stop + pip install ./test/end2end/skill-fake-fm - name: Install core repo run: | pip install -e .[mycroft,deprecated] From 87e1015fef569a00a3693cdab4fd0463b6866107 Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 10 May 2024 02:04:26 +0100 Subject: [PATCH 07/15] utils version --- requirements/tests.txt | 3 ++- test/end2end/session/test_ocp.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements/tests.txt b/requirements/tests.txt index 9d56ec15ee6e..e17156486922 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -20,4 +20,5 @@ ovos-gui~=0.0, >=0.0.2 ovos-messagebus~=0.0 # Support OCP tests -ovos_bus_client>=0.0.9a15 \ No newline at end of file +ovos_bus_client>=0.0.9a15 +ovos-utils>=0.1.0a16 \ No newline at end of file diff --git a/test/end2end/session/test_ocp.py b/test/end2end/session/test_ocp.py index 2c1621e35b2a..929b4c3d42e8 100644 --- a/test/end2end/session/test_ocp.py +++ b/test/end2end/session/test_ocp.py @@ -18,6 +18,7 @@ def tearDown(self) -> None: self.core.stop() def test_no_match(self): + self.assertIsNotNone(self.core.intent_service.ocp) messages = [] def new_msg(msg): @@ -85,6 +86,7 @@ def wait_for_n_messages(n): self.assertEqual(m.msg_type, expected_messages[idx]) def test_radio_media_match(self): + self.assertIsNotNone(self.core.intent_service.ocp) messages = [] def new_msg(msg): @@ -152,6 +154,7 @@ def wait_for_n_messages(n): self.assertEqual(m.msg_type, expected_messages[idx]) def test_unk_media_match(self): + self.assertIsNotNone(self.core.intent_service.ocp) messages = [] def new_msg(msg): @@ -221,6 +224,7 @@ def wait_for_n_messages(n): self.assertEqual(m.msg_type, expected_messages[idx]) def test_skill_name_match(self): + self.assertIsNotNone(self.core.intent_service.ocp) messages = [] def new_msg(msg): From 3b75295bf262b29f0f309b7288879650ce174b0d Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 10 May 2024 02:06:55 +0100 Subject: [PATCH 08/15] test legacy api --- test/end2end/session/test_ocp.py | 73 ++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/test/end2end/session/test_ocp.py b/test/end2end/session/test_ocp.py index 929b4c3d42e8..a8d4137f5aa4 100644 --- a/test/end2end/session/test_ocp.py +++ b/test/end2end/session/test_ocp.py @@ -19,6 +19,7 @@ def tearDown(self) -> None: def test_no_match(self): self.assertIsNotNone(self.core.intent_service.ocp) + self.assertFalse(self.core.intent_service.ocp.use_legacy_audio) messages = [] def new_msg(msg): @@ -87,6 +88,7 @@ def wait_for_n_messages(n): def test_radio_media_match(self): self.assertIsNotNone(self.core.intent_service.ocp) + self.assertFalse(self.core.intent_service.ocp.use_legacy_audio) messages = [] def new_msg(msg): @@ -155,6 +157,7 @@ def wait_for_n_messages(n): def test_unk_media_match(self): self.assertIsNotNone(self.core.intent_service.ocp) + self.assertFalse(self.core.intent_service.ocp.use_legacy_audio) messages = [] def new_msg(msg): @@ -224,6 +227,7 @@ def wait_for_n_messages(n): self.assertEqual(m.msg_type, expected_messages[idx]) def test_skill_name_match(self): + self.assertFalse(self.core.intent_service.ocp.use_legacy_audio) self.assertIsNotNone(self.core.intent_service.ocp) messages = [] @@ -291,3 +295,72 @@ def wait_for_n_messages(n): for idx, m in enumerate(messages): self.assertEqual(m.msg_type, expected_messages[idx]) + def test_legacy_match(self): + self.assertIsNotNone(self.core.intent_service.ocp) + self.core.intent_service.ocp.config = {"legacy": True} + self.assertTrue(self.core.intent_service.ocp.use_legacy_audio) + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["play some radio station"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "enclosure.active_skill", + "speak", + "ocp:play", + "ovos.common_play.search.start", + "enclosure.mouth.think", + "ovos.common_play.search.stop", # any ongoing previous search + "ovos.common_play.query", # media type radio + # skill searching (radio) + "ovos.common_play.skill.search_start", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.query.response", + "ovos.common_play.skill.search_end", + "ovos.common_play.search.end", + # good results because of radio media type + "ovos.common_play.reset", + "add_context", # NowPlaying context + 'mycroft.audio.service.play' # LEGACY api + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) From e8f3d933919eda7c1f3593a5acc3e9a8e4983ae5 Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 10 May 2024 02:26:32 +0100 Subject: [PATCH 09/15] ensure OCP loaded for tests --- ovos_core/intent_services/__init__.py | 6 +++--- test/end2end/session/test_ocp.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ovos_core/intent_services/__init__.py b/ovos_core/intent_services/__init__.py index 4faec8fe92ad..67d3c6e568bc 100644 --- a/ovos_core/intent_services/__init__.py +++ b/ovos_core/intent_services/__init__.py @@ -108,10 +108,10 @@ def __init__(self, bus): self.bus.on('intent.service.padatious.entities.manifest.get', self.handle_entity_manifest) - def _load_ocp_pipeline(self): + def _load_ocp_pipeline(self, force=False): """EXPERIMENTAL: this feature is not yet ready for end users""" - audio_enabled = Configuration().get("enable_old_audioservice", True) - if not audio_enabled: + disable_ocp = Configuration().get("disable_ocp") + if disable_ocp or force: LOG.warning("EXPERIMENTAL: the OCP pipeline is enabled!") try: from ovos_core.intent_services.ocp_service import OCPPipelineMatcher diff --git a/test/end2end/session/test_ocp.py b/test/end2end/session/test_ocp.py index a8d4137f5aa4..8a0e30402dc9 100644 --- a/test/end2end/session/test_ocp.py +++ b/test/end2end/session/test_ocp.py @@ -13,6 +13,8 @@ class TestOCPPipeline(TestCase): def setUp(self): self.skill_id = "skill-fake-fm.openvoiceos" self.core = get_minicroft(self.skill_id) + if self.core.intent_service.ocp is None: + self.core.intent_service._load_ocp_pipeline(force=True) def tearDown(self) -> None: self.core.stop() From 5ba3d06e198cd974e95d3fa4193820890fcd2b81 Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 10 May 2024 03:01:03 +0100 Subject: [PATCH 10/15] ensure OCP loaded for tests --- ovos_core/intent_services/__init__.py | 18 ++++++++++-------- ovos_core/transformers.py | 7 +++---- test/end2end/minicroft.py | 12 ++++++------ test/end2end/session/test_ocp.py | 4 +--- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/ovos_core/intent_services/__init__.py b/ovos_core/intent_services/__init__.py index 67d3c6e568bc..a422ddcaec5b 100644 --- a/ovos_core/intent_services/__init__.py +++ b/ovos_core/intent_services/__init__.py @@ -49,9 +49,11 @@ class IntentService: querying the intent service. """ - def __init__(self, bus): + def __init__(self, bus, config=None): self.bus = bus - config = Configuration() + self.config = config or Configuration().get("intent_service", {}) + if "padatious" not in self.config: + self.config["padatious"] = Configuration().get("padatious", {}) # Dictionary for translating a skill id to a name self.skill_names = {} @@ -60,18 +62,18 @@ def __init__(self, bus): self.adapt_service = AdaptService() try: from ovos_core.intent_services.padatious_service import PadatiousService - self.padatious_service = PadatiousService(bus, config['padatious']) + self.padatious_service = PadatiousService(bus, self.config["padatious"]) except ImportError: LOG.error(f'Failed to create padatious intent handlers, padatious not installed') self.padatious_service = None - self.padacioso_service = PadaciosoService(bus, config['padatious']) + self.padacioso_service = PadaciosoService(bus, self.config["padatious"]) self.fallback = FallbackService(bus) self.converse = ConverseService(bus) self.common_qa = CommonQAService(bus) self.stop = StopService(bus) self.ocp = None - self.utterance_plugins = UtteranceTransformersService(bus, config=config) - self.metadata_plugins = MetadataTransformersService(bus, config=config) + self.utterance_plugins = UtteranceTransformersService(bus) + self.metadata_plugins = MetadataTransformersService(bus) self._load_ocp_pipeline() # TODO - enable by default once stable @@ -108,10 +110,10 @@ def __init__(self, bus): self.bus.on('intent.service.padatious.entities.manifest.get', self.handle_entity_manifest) - def _load_ocp_pipeline(self, force=False): + def _load_ocp_pipeline(self): """EXPERIMENTAL: this feature is not yet ready for end users""" disable_ocp = Configuration().get("disable_ocp") - if disable_ocp or force: + if disable_ocp or self.config.get("experimental_ocp_pipeline", False): LOG.warning("EXPERIMENTAL: the OCP pipeline is enabled!") try: from ovos_core.intent_services.ocp_service import OCPPipelineMatcher diff --git a/ovos_core/transformers.py b/ovos_core/transformers.py index 168a1417cd63..f287aabcfe1d 100644 --- a/ovos_core/transformers.py +++ b/ovos_core/transformers.py @@ -1,7 +1,6 @@ from typing import Optional, List - +from ovos_config import Configuration from ovos_plugin_manager.metadata_transformers import find_metadata_transformer_plugins -from ovos_plugin_manager.templates.transformers import UtteranceTransformer from ovos_plugin_manager.text_transformers import find_utterance_transformer_plugins from ovos_utils.json_helper import merge_dict @@ -11,7 +10,7 @@ class UtteranceTransformersService: def __init__(self, bus, config=None): - self.config_core = config or {} + self.config_core = config or Configuration() self.loaded_plugins = {} self.has_loaded = False self.bus = bus @@ -68,7 +67,7 @@ def transform(self, utterances: List[str], context: Optional[dict] = None): class MetadataTransformersService: def __init__(self, bus, config=None): - self.config_core = config or {} + self.config_core = config or Configuration() self.loaded_plugins = {} self.has_loaded = False self.bus = bus diff --git a/test/end2end/minicroft.py b/test/end2end/minicroft.py index 237227f7c734..863bdb41eb96 100644 --- a/test/end2end/minicroft.py +++ b/test/end2end/minicroft.py @@ -12,20 +12,20 @@ class MiniCroft(SkillManager): - def __init__(self, skill_ids, *args, **kwargs): + def __init__(self, skill_ids, ocp=False, *args, **kwargs): bus = FakeBus() super().__init__(bus, *args, **kwargs) self.skill_ids = skill_ids - self.intent_service = self._register_intent_services() + self.intent_service = self._register_intent_services(ocp=ocp) self.scheduler = EventScheduler(bus, schedule_file="/tmp/schetest.json") - def _register_intent_services(self): + def _register_intent_services(self, ocp=False): """Start up the all intent services and connect them as needed. Args: bus: messagebus client to register the services on """ - service = IntentService(self.bus) + service = IntentService(self.bus, config={"experimental_ocp_pipeline": ocp}) # Register handler to trigger fallback system self.bus.on( 'mycroft.skills.fallback', @@ -61,11 +61,11 @@ def stop(self): SessionManager.default_session = SessionManager.sessions["default"] = Session("default") -def get_minicroft(skill_id): +def get_minicroft(skill_id, ocp=False): if isinstance(skill_id, str): skill_id = [skill_id] assert isinstance(skill_id, list) - croft1 = MiniCroft(skill_id) + croft1 = MiniCroft(skill_id, ocp=ocp) croft1.start() while croft1.status.state != ProcessState.READY: sleep(0.2) diff --git a/test/end2end/session/test_ocp.py b/test/end2end/session/test_ocp.py index 8a0e30402dc9..057f53894146 100644 --- a/test/end2end/session/test_ocp.py +++ b/test/end2end/session/test_ocp.py @@ -12,9 +12,7 @@ class TestOCPPipeline(TestCase): def setUp(self): self.skill_id = "skill-fake-fm.openvoiceos" - self.core = get_minicroft(self.skill_id) - if self.core.intent_service.ocp is None: - self.core.intent_service._load_ocp_pipeline(force=True) + self.core = get_minicroft(self.skill_id, ocp=True) def tearDown(self) -> None: self.core.stop() From 319e48b524772dd734543191fc9ce84d1a019c01 Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 10 May 2024 03:32:18 +0100 Subject: [PATCH 11/15] legacy audio state tracking --- ovos_core/intent_services/ocp_service.py | 89 +++++++++++++----------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/ovos_core/intent_services/ocp_service.py b/ovos_core/intent_services/ocp_service.py index 4f94be7b6e10..9e427f4ff008 100644 --- a/ovos_core/intent_services/ocp_service.py +++ b/ovos_core/intent_services/ocp_service.py @@ -126,15 +126,6 @@ def __init__(self, bus=None, config=None): # request available Stream extractor plugins from OCP self.bus.emit(Message("ovos.common_play.SEI.get")) - @property - def use_legacy_audio(self): - """when neither ovos-media nor old OCP are available""" - if self.config.get("legacy"): - # explicitly set in pipeline config - return True - cfg = Configuration() - return cfg.get("disable_ocp") and cfg.get("enable_old_audioservice") - def load_classifiers(self): # warm up the featurizer so intent matches faster (lazy loaded) @@ -183,6 +174,11 @@ def register_ocp_api_events(self): self.bus.on('ovos.common_play.register_keyword', self.handle_skill_keyword_register) self.bus.on('ovos.common_play.deregister_keyword', self.handle_skill_keyword_deregister) self.bus.on('ovos.common_play.announce', self.handle_skill_register) + + self.bus.on("mycroft.audio.playing_track", self._handle_legacy_audio_start) + self.bus.on("mycroft.audio.queue_end", self._handle_legacy_audio_end) + self.bus.on("mycroft.audio.service.pause", self._handle_legacy_audio_pause) + self.bus.on("mycroft.audio.service.resume", self._handle_legacy_audio_resume) self.bus.emit(Message("ovos.common_play.status")) # sync on launch def register_ocp_intents(self): @@ -603,38 +599,6 @@ def handle_search_error_intent(self, message: Message): LOG.info("Requesting OCP to stop") self.ocp_api.stop() - def _do_play(self, phrase: str, results, media_type=MediaType.GENERIC): - self.bus.emit(Message('ovos.common_play.reset')) - LOG.debug(f"Playing {len(results)} results for: {phrase}") - if not results: - self.speak_dialog("cant.play", - data={"phrase": phrase, - "media_type": media_type}) - else: - best = self.select_best(results) - results = [r for r in results if r.uri != best.uri] - results.insert(0, best) - self.bus.emit(Message('add_context', - {'context': "Playing", - 'word': "", - 'origin': OCP_ID})) - - # ovos-PHAL-plugin-mk1 will display music icon in response to play message - if self.use_legacy_audio: - self.legacy_play(results, phrase) - else: - self.ocp_api.play(results, phrase) - - def legacy_play(self, results: List[MediaEntry], phrase=""): - xtract = load_stream_extractors() - # for legacy audio service we need to do stream extraction here - # we also need to filter video results - results = [xtract.extract_stream(r.uri, video=False)["uri"] - for r in results - if r.playback == PlaybackType.AUDIO - or r.media_type in OCPQuery.cast2audio] - self.legacy_api.play(results, utterance=phrase) - # NLP @staticmethod def label2media(label: str) -> MediaType: @@ -906,3 +870,46 @@ def select_best(self, results: list) -> MediaEntry: LOG.info(f"OVOSCommonPlay selected: {selected.skill_id} - {selected.match_confidence}") LOG.debug(str(selected)) return selected + + ################## + # Legacy Audio subsystem API + @property + def use_legacy_audio(self): + """when neither ovos-media nor old OCP are available""" + if self.config.get("legacy"): + # explicitly set in pipeline config + return True + cfg = Configuration() + return cfg.get("disable_ocp") and cfg.get("enable_old_audioservice") + + def legacy_play(self, results: List[MediaEntry], phrase=""): + xtract = load_stream_extractors() + # for legacy audio service we need to do stream extraction here + # we also need to filter video results + results = [xtract.extract_stream(r.uri, video=False)["uri"] + for r in results + if r.playback == PlaybackType.AUDIO + or r.media_type in OCPQuery.cast2audio] + self.player_state = PlayerState.PLAYING + self.media_state = MediaState.LOADING_MEDIA + self.legacy_api.play(results, utterance=phrase) + + def _handle_legacy_audio_pause(self, message: Message): + if self.use_legacy_audio and self.player_state == PlayerState.PLAYING: + self.player_state = PlayerState.PAUSED + self.media_state = MediaState.LOADED_MEDIA + + def _handle_legacy_audio_resume(self, message: Message): + if self.use_legacy_audio and self.player_state == PlayerState.PAUSED: + self.player_state = PlayerState.PLAYING + self.media_state = MediaState.LOADED_MEDIA + + def _handle_legacy_audio_start(self, message: Message): + if self.use_legacy_audio: + self.player_state = PlayerState.PLAYING + self.media_state = MediaState.LOADED_MEDIA + + def _handle_legacy_audio_end(self, message: Message): + if self.use_legacy_audio: + self.player_state = PlayerState.STOPPED + self.media_state = MediaState.END_OF_MEDIA From 666991f84590f4031a8b2896a84338f8cfcc5799 Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 10 May 2024 03:46:31 +0100 Subject: [PATCH 12/15] test more ocp intents --- ovos_core/intent_services/ocp_service.py | 6 + test/end2end/session/test_ocp.py | 181 ++++++++++++++++++++++- 2 files changed, 183 insertions(+), 4 deletions(-) diff --git a/ovos_core/intent_services/ocp_service.py b/ovos_core/intent_services/ocp_service.py index 9e427f4ff008..d1bc068a0ccc 100644 --- a/ovos_core/intent_services/ocp_service.py +++ b/ovos_core/intent_services/ocp_service.py @@ -179,6 +179,7 @@ def register_ocp_api_events(self): self.bus.on("mycroft.audio.queue_end", self._handle_legacy_audio_end) self.bus.on("mycroft.audio.service.pause", self._handle_legacy_audio_pause) self.bus.on("mycroft.audio.service.resume", self._handle_legacy_audio_resume) + self.bus.on("mycroft.audio.service.stop", self._handle_legacy_audio_stop) self.bus.emit(Message("ovos.common_play.status")) # sync on launch def register_ocp_intents(self): @@ -894,6 +895,11 @@ def legacy_play(self, results: List[MediaEntry], phrase=""): self.media_state = MediaState.LOADING_MEDIA self.legacy_api.play(results, utterance=phrase) + def _handle_legacy_audio_stop(self, message: Message): + if self.use_legacy_audio: + self.player_state = PlayerState.STOPPED + self.media_state = MediaState.NO_MEDIA + def _handle_legacy_audio_pause(self, message: Message): if self.use_legacy_audio and self.player_state == PlayerState.PLAYING: self.player_state = PlayerState.PAUSED diff --git a/test/end2end/session/test_ocp.py b/test/end2end/session/test_ocp.py index 057f53894146..02c29f512bd6 100644 --- a/test/end2end/session/test_ocp.py +++ b/test/end2end/session/test_ocp.py @@ -3,8 +3,8 @@ from unittest import TestCase from ovos_bus_client.message import Message -from ovos_bus_client.session import SessionManager, Session - +from ovos_bus_client.session import Session +from ovos_utils.ocp import PlayerState, MediaState from ..minicroft import get_minicroft @@ -133,7 +133,7 @@ def wait_for_n_messages(n): "ovos.common_play.search.start", "enclosure.mouth.think", "ovos.common_play.search.stop", # any ongoing previous search - "ovos.common_play.query", # media type radio + "ovos.common_play.query", # media type radio # skill searching (radio) "ovos.common_play.skill.search_start", "ovos.common_play.query.response", @@ -298,6 +298,8 @@ def wait_for_n_messages(n): def test_legacy_match(self): self.assertIsNotNone(self.core.intent_service.ocp) self.core.intent_service.ocp.config = {"legacy": True} + self.core.intent_service.ocp.player_state = PlayerState.STOPPED + self.core.intent_service.ocp.media_state = MediaState.NO_MEDIA self.assertTrue(self.core.intent_service.ocp.use_legacy_audio) messages = [] @@ -343,7 +345,7 @@ def wait_for_n_messages(n): "ovos.common_play.search.start", "enclosure.mouth.think", "ovos.common_play.search.stop", # any ongoing previous search - "ovos.common_play.query", # media type radio + "ovos.common_play.query", # media type radio # skill searching (radio) "ovos.common_play.skill.search_start", "ovos.common_play.query.response", @@ -364,3 +366,174 @@ def wait_for_n_messages(n): for idx, m in enumerate(messages): self.assertEqual(m.msg_type, expected_messages[idx]) + + self.assertEqual(self.core.intent_service.ocp.player_state, PlayerState.PLAYING) + self.assertEqual(self.core.intent_service.ocp.media_state, MediaState.LOADING_MEDIA) + + def test_legacy_pause(self): + self.assertIsNotNone(self.core.intent_service.ocp) + self.core.intent_service.ocp.config = {"legacy": True} + self.core.intent_service.ocp.player_state = PlayerState.PLAYING + self.core.intent_service.ocp.media_state = MediaState.LOADED_MEDIA + self.assertTrue(self.core.intent_service.ocp.use_legacy_audio) + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["pause"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "ocp:pause", + 'mycroft.audio.service.pause' # LEGACY api + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + self.assertEqual(self.core.intent_service.ocp.player_state, PlayerState.PAUSED) + + def test_legacy_resume(self): + self.assertIsNotNone(self.core.intent_service.ocp) + self.core.intent_service.ocp.config = {"legacy": True} + self.core.intent_service.ocp.player_state = PlayerState.PAUSED + self.core.intent_service.ocp.media_state = MediaState.LOADED_MEDIA + self.assertTrue(self.core.intent_service.ocp.use_legacy_audio) + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["resume"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "ocp:resume", + 'mycroft.audio.service.resume' # LEGACY api + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + self.assertEqual(self.core.intent_service.ocp.player_state, PlayerState.PLAYING) + + def test_legacy_stop(self): + self.assertIsNotNone(self.core.intent_service.ocp) + self.core.intent_service.ocp.config = {"legacy": True} + self.core.intent_service.ocp.player_state = PlayerState.PLAYING + self.core.intent_service.ocp.media_state = MediaState.LOADED_MEDIA + self.assertTrue(self.core.intent_service.ocp.use_legacy_audio) + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["stop"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "ocp:media_stop", + 'mycroft.audio.service.stop' # LEGACY api + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + self.assertEqual(self.core.intent_service.ocp.player_state, PlayerState.STOPPED) From 7dc77a034e3b278237cbdf43ef136b72577676ad Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 10 May 2024 03:50:45 +0100 Subject: [PATCH 13/15] fix config key --- ovos_core/intent_services/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_core/intent_services/__init__.py b/ovos_core/intent_services/__init__.py index a422ddcaec5b..8bc8cdaa6653 100644 --- a/ovos_core/intent_services/__init__.py +++ b/ovos_core/intent_services/__init__.py @@ -51,7 +51,7 @@ class IntentService: def __init__(self, bus, config=None): self.bus = bus - self.config = config or Configuration().get("intent_service", {}) + self.config = config or Configuration().get("intents", {}) if "padatious" not in self.config: self.config["padatious"] = Configuration().get("padatious", {}) From 083fb4a4709fc399c48903f8edcaa2f641f38194 Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 10 May 2024 04:58:15 +0100 Subject: [PATCH 14/15] more tests --- test/end2end/session/test_ocp.py | 427 +++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) diff --git a/test/end2end/session/test_ocp.py b/test/end2end/session/test_ocp.py index 02c29f512bd6..58d0f6637398 100644 --- a/test/end2end/session/test_ocp.py +++ b/test/end2end/session/test_ocp.py @@ -537,3 +537,430 @@ def wait_for_n_messages(n): self.assertEqual(m.msg_type, expected_messages[idx]) self.assertEqual(self.core.intent_service.ocp.player_state, PlayerState.STOPPED) + + def test_legacy_next(self): + self.assertIsNotNone(self.core.intent_service.ocp) + self.core.intent_service.ocp.config = {"legacy": True} + self.core.intent_service.ocp.player_state = PlayerState.PLAYING + self.core.intent_service.ocp.media_state = MediaState.LOADED_MEDIA + self.assertTrue(self.core.intent_service.ocp.use_legacy_audio) + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["next"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "ocp:next", + 'mycroft.audio.service.next' # LEGACY api + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + def test_legacy_prev(self): + self.assertIsNotNone(self.core.intent_service.ocp) + self.core.intent_service.ocp.config = {"legacy": True} + self.core.intent_service.ocp.player_state = PlayerState.PLAYING + self.core.intent_service.ocp.media_state = MediaState.LOADED_MEDIA + self.assertTrue(self.core.intent_service.ocp.use_legacy_audio) + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["previous"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "ocp:prev", + 'mycroft.audio.service.prev' # LEGACY api + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + def test_pause(self): + self.assertIsNotNone(self.core.intent_service.ocp) + self.core.intent_service.ocp.player_state = PlayerState.PLAYING + self.core.intent_service.ocp.media_state = MediaState.LOADED_MEDIA + self.assertFalse(self.core.intent_service.ocp.use_legacy_audio) + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["pause"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "ocp:pause", + 'ovos.common_play.pause' + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + def test_resume(self): + self.assertIsNotNone(self.core.intent_service.ocp) + self.core.intent_service.ocp.player_state = PlayerState.PAUSED + self.core.intent_service.ocp.media_state = MediaState.LOADED_MEDIA + self.assertFalse(self.core.intent_service.ocp.use_legacy_audio) + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["resume"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "ocp:resume", + 'ovos.common_play.resume' + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + def test_stop(self): + self.assertIsNotNone(self.core.intent_service.ocp) + self.core.intent_service.ocp.player_state = PlayerState.PLAYING + self.core.intent_service.ocp.media_state = MediaState.LOADED_MEDIA + self.assertFalse(self.core.intent_service.ocp.use_legacy_audio) + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["stop"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "ocp:media_stop", + 'ovos.common_play.stop', + "ovos.common_play.stop.response" + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + def test_next(self): + self.assertIsNotNone(self.core.intent_service.ocp) + self.core.intent_service.ocp.player_state = PlayerState.PLAYING + self.core.intent_service.ocp.media_state = MediaState.LOADED_MEDIA + self.assertFalse(self.core.intent_service.ocp.use_legacy_audio) + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["next"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "ocp:next", + 'ovos.common_play.next' + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + def test_prev(self): + self.assertIsNotNone(self.core.intent_service.ocp) + self.core.intent_service.ocp.player_state = PlayerState.PLAYING + self.core.intent_service.ocp.media_state = MediaState.LOADED_MEDIA + self.assertFalse(self.core.intent_service.ocp.use_legacy_audio) + messages = [] + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + utt = Message("recognizer_loop:utterance", + {"utterances": ["previous"]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "intent.service.skills.activate", + "intent.service.skills.activated", + "ovos.common_play.activate", + "ocp:prev", + 'ovos.common_play.previous' + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) + + def test_status_matches_not_playing(self): + self.assertIsNotNone(self.core.intent_service.ocp) + self.core.intent_service.ocp.player_state = PlayerState.STOPPED + self.core.intent_service.ocp.media_state = MediaState.NO_MEDIA + + def new_msg(msg): + nonlocal messages + m = Message.deserialize(msg) + if m.msg_type in ["ovos.skills.settings_changed", "gui.status.request"]: + return # skip these, only happen in 1st run + messages.append(m) + print(len(messages), msg) + + def wait_for_n_messages(n): + nonlocal messages + t = time.time() + while len(messages) < n: + sleep(0.1) + if time.time() - t > 10: + raise RuntimeError("did not get the number of expected messages under 10 seconds") + + self.core.bus.on("message", new_msg) + + sess = Session("test-session", + pipeline=[ + "converse", + "ocp_high" + ]) + + # wont match unless PlayerState.Playing + for t in ["pause", "resume", "stop", "next", "previous"]: + messages = [] + + utt = Message("recognizer_loop:utterance", + {"utterances": [t]}, + {"session": sess.serialize(), # explicit + }) + self.core.bus.emit(utt) + + # confirm all expected messages are sent + expected_messages = [ + "recognizer_loop:utterance", + "ovos.common_play.status", + "mycroft.audio.play_sound", + "complete_intent_failure" + ] + wait_for_n_messages(len(expected_messages)) + + self.assertEqual(len(expected_messages), len(messages)) + + for idx, m in enumerate(messages): + self.assertEqual(m.msg_type, expected_messages[idx]) From 2887c06a5201e12f111dd876015ef536e1e4606b Mon Sep 17 00:00:00 2001 From: miro Date: Fri, 10 May 2024 04:59:40 +0100 Subject: [PATCH 15/15] untangle config --- ovos_core/intent_services/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ovos_core/intent_services/__init__.py b/ovos_core/intent_services/__init__.py index 8bc8cdaa6653..0d9b690e1650 100644 --- a/ovos_core/intent_services/__init__.py +++ b/ovos_core/intent_services/__init__.py @@ -112,8 +112,7 @@ def __init__(self, bus, config=None): def _load_ocp_pipeline(self): """EXPERIMENTAL: this feature is not yet ready for end users""" - disable_ocp = Configuration().get("disable_ocp") - if disable_ocp or self.config.get("experimental_ocp_pipeline", False): + if self.config.get("experimental_ocp_pipeline", False): LOG.warning("EXPERIMENTAL: the OCP pipeline is enabled!") try: from ovos_core.intent_services.ocp_service import OCPPipelineMatcher