Skip to content

Commit

Permalink
feat/legacy_audio_api (#456)
Browse files Browse the repository at this point in the history
* feat/legacy_audio_api

* feat/legacy_audio_api

* allow some media types

* allow some media types

* ocp tests

* install new test skill

* utils version

* test legacy api

* ensure OCP loaded for tests

* ensure OCP loaded for tests

* legacy audio state tracking

* test more ocp intents

* fix config key

* more tests

* untangle config
  • Loading branch information
JarbasAl authored May 10, 2024
1 parent d0f88b8 commit 68d63a6
Show file tree
Hide file tree
Showing 10 changed files with 1,175 additions and 55 deletions.
1 change: 1 addition & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
17 changes: 9 additions & 8 deletions ovos_core/intent_services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("intents", {})
if "padatious" not in self.config:
self.config["padatious"] = Configuration().get("padatious", {})

# Dictionary for translating a skill id to a name
self.skill_names = {}
Expand All @@ -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

Expand Down Expand Up @@ -110,8 +112,7 @@ def __init__(self, bus):

def _load_ocp_pipeline(self):
"""EXPERIMENTAL: this feature is not yet ready for end users"""
audio_enabled = Configuration().get("enable_old_audioservice", True)
if not audio_enabled:
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
Expand Down
138 changes: 102 additions & 36 deletions ovos_core/intent_services/ocp_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
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_plugin_manager.ocp import load_stream_extractors
from ovos_utils import classproperty
from ovos_utils.log import LOG
from ovos_utils.messagebus import FakeBus
Expand Down Expand Up @@ -104,6 +106,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()
Expand Down Expand Up @@ -171,6 +174,12 @@ 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.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):
Expand Down Expand Up @@ -526,7 +535,10 @@ 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:
self.legacy_play(results, query)
else:
self.ocp_api.play(results, query)

def handle_open_intent(self, message: Message):
LOG.info("Requesting OCP homescreen")
Expand All @@ -539,49 +551,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()

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})
if self.use_legacy_audio:
LOG.info("Requesting Legacy AudioService to stop")
self.legacy_api.stop()
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
self.ocp_api.play(results, phrase)
LOG.info("Requesting OCP to stop")
self.ocp_api.stop()

# NLP
@staticmethod
Expand Down Expand Up @@ -751,11 +768,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"
Expand Down Expand Up @@ -853,3 +871,51 @@ 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_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
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
7 changes: 3 additions & 4 deletions ovos_core/transformers.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion requirements/tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ ovos-gui~=0.0, >=0.0.2
ovos-messagebus~=0.0

# Support OCP tests
ovos_bus_client>=0.0.9a15
ovos_bus_client>=0.0.9a15
ovos-utils>=0.1.0a16
12 changes: 6 additions & 6 deletions test/end2end/minicroft.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 68d63a6

Please sign in to comment.