common_play_skill.py
Go to the documentation of this file.
1 # Copyright 2018 Mycroft AI Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 import re
16 from enum import Enum
17 from abc import ABC, abstractmethod
18 from mycroft import MycroftSkill
19 from mycroft.skills.audioservice import AudioService
20 from mycroft.messagebus.message import Message
21 
22 
23 class CPSMatchLevel(Enum):
24  EXACT = 1
25  MULTI_KEY = 2
26  TITLE = 3
27  ARTIST = 4
28  CATEGORY = 5
29  GENERIC = 6
30 
31 
32 class CommonPlaySkill(MycroftSkill, ABC):
33  """ To integrate with the common play infrastructure of Mycroft
34  skills should use this base class and override the two methods
35  `CPS_match_query_phrase` (for checking if the skill can play the
36  utterance) and `CPS_start` for launching the media.
37 
38  The class makes the skill available to queries from the
39  mycroft-playback-control skill and no special vocab for starting playback
40  is needed.
41  """
42  def __init__(self, name=None, bus=None):
43  super().__init__(name, bus)
44  self.audioservice = None
45  self.play_service_string = None
46 
47  # "MusicServiceSkill" -> "Music Service"
48  spoken = name or self.__class__.__name__
49  self.spoken_name = re.sub(r"([a-z])([A-Z])", r"\g<1> \g<2>",
50  spoken.replace("Skill", ""))
51  # NOTE: Derived skills will likely want to override self.spoken_name
52  # with a translatable name in their initialize() method.
53 
54  def bind(self, bus):
55  """ Overrides the normal bind method.
56  Adds handlers for play:query and play:start messages allowing
57  interaction with the playback control skill.
58 
59  This is called automatically during setup, and
60  need not otherwise be used.
61  """
62  if bus:
63  super().bind(bus)
64  self.audioservice = AudioService(self.bus)
65  self.add_event('play:query', self.__handle_play_query)
66  self.add_event('play:start', self.__handle_play_start)
67 
68  def __handle_play_query(self, message):
69  search_phrase = message.data["phrase"]
70 
71  # First, notify the requestor that we are attempting to handle
72  # (this extends a timeout while this skill looks for a match)
73  self.bus.emit(message.response({"phrase": search_phrase,
74  "skill_id": self.skill_id,
75  "searching": True}))
76 
77  # Now invoke the CPS handler to let the skill perform its search
78  result = self.CPS_match_query_phrase(search_phrase)
79 
80  if result:
81  match = result[0]
82  level = result[1]
83  callback = result[2] if len(result) > 2 else None
84  confidence = self.__calc_confidence(match, search_phrase, level)
85  self.bus.emit(message.response({"phrase": search_phrase,
86  "skill_id": self.skill_id,
87  "callback_data": callback,
88  "service_name": self.spoken_name,
89  "conf": confidence}))
90  else:
91  # Signal we are done (can't handle it)
92  self.bus.emit(message.response({"phrase": search_phrase,
93  "skill_id": self.skill_id,
94  "searching": False}))
95 
96  def __calc_confidence(self, match, phrase, level):
97  # "play pandora"
98  # "play pandora is my girlfriend"
99  # "play tom waits on pandora"
100 
101  # Assume the more of the words that get consumed, the better the match
102  consumed_pct = len(match.split()) / len(phrase.split())
103  if consumed_pct > 1.0:
104  consumed_pct = 1.0 / consumed_pct # deal with over/under-matching
105 
106  # We'll use this to modify the level, but don't want it to allow a
107  # match to jump to the next match level. So bonus is 0 - 0.05 (1/20)
108  bonus = consumed_pct / 20.0
109 
110  if level == CPSMatchLevel.EXACT:
111  return 1.0
112  elif level == CPSMatchLevel.MULTI_KEY:
113  return 0.9 + bonus
114  elif level == CPSMatchLevel.TITLE:
115  return 0.8 + bonus
116  elif level == CPSMatchLevel.ARTIST:
117  return 0.7 + bonus
118  elif level == CPSMatchLevel.CATEGORY:
119  return 0.6 + bonus
120  elif level == CPSMatchLevel.GENERIC:
121  return 0.5 + bonus
122  else:
123  return 0.0 # should never happen
124 
125  def __handle_play_start(self, message):
126  if message.data["skill_id"] != self.skill_id:
127  # Not for this skill!
128  return
129  phrase = message.data["phrase"]
130  data = message.data.get("callback_data")
131 
132  # Stop any currently playing audio
133  if self.audioservice.is_playing:
134  self.audioservice.stop()
135  self.bus.emit(Message("mycroft.stop"))
136 
137  # Save for CPS_play() later, e.g. if phrase includes modifiers like
138  # "... on the chromecast"
139  self.play_service_string = phrase
140 
141  # Invoke derived class to provide playback data
142  self.CPS_start(phrase, data)
143 
144  def CPS_play(self, *args, **kwargs):
145  """
146  Begin playback of a media file or stream
147 
148  Normally this method will be invoked with somthing like:
149  self.CPS_play(url)
150  Advanced use can also include keyword arguments, such as:
151  self.CPS_play(url, repeat=True)
152 
153  Args:
154  same as the Audioservice.play method
155  """
156  # Inject the user's utterance in case the audio backend wants to
157  # interpret it. E.g. "play some rock at full volume on the stereo"
158  if 'utterance' not in kwargs:
159  kwargs['utterance'] = self.play_service_string
160  self.audioservice.play(*args, **kwargs)
161 
162  def stop(self):
163  if self.audioservice.is_playing:
164  self.audioservice.stop()
165  return True
166  else:
167  return False
168 
169  ######################################################################
170  # Abstract methods
171  # All of the following must be implemented by a skill that wants to
172  # act as a CommonPlay Skill
173  @abstractmethod
174  def CPS_match_query_phrase(self, phrase):
175  """
176  Analyze phrase to see if it is a play-able phrase with this
177  skill.
178 
179  Args:
180  phrase (str): User phrase uttered after "Play", e.g. "some music"
181 
182  Returns:
183  (match, CPSMatchLevel[, callback_data]) or None: Tuple containing
184  a string with the appropriate matching phrase, the PlayMatch
185  type, and optionally data to return in the callback if the
186  match is selected.
187  """
188  # Derived classes must implement this, e.g.
189  #
190  # if phrase in ["Zoosh"]:
191  # return ("Zoosh", CPSMatchLevel.Generic, {"hint": "music"})
192  # or:
193  # zoosh_song = find_zoosh(phrase)
194  # if zoosh_song and "Zoosh" in phrase:
195  # # "play Happy Birthday in Zoosh"
196  # return ("Zoosh", CPSMatchLevel.MULTI_KEY, {"song": zoosh_song})
197  # elif zoosh_song:
198  # # "play Happy Birthday"
199  # return ("Zoosh", CPSMatchLevel.TITLE, {"song": zoosh_song})
200  # elif "Zoosh" in phrase
201  # # "play Zoosh"
202  # return ("Zoosh", CPSMatchLevel.GENERIC, {"cmd": "random"})
203  return None
204 
205  @abstractmethod
206  def CPS_start(self, phrase, data):
207  """
208  Begin playing whatever is specified in 'phrase'
209 
210  Args:
211  phrase (str): User phrase uttered after "Play", e.g. "some music"
212  data (dict): Callback data specified in match_query_phrase()
213  """
214  # Derived classes must implement this, e.g.
215  # self.CPS_play("http://zoosh.com/stream_music")
216  pass
def CPS_match_query_phrase(self, phrase)
Abstract methods All of the following must be implemented by a skill that wants to act as a CommonPla...
def __calc_confidence(self, match, phrase, level)


mycroft_ros
Author(s):
autogenerated on Mon Apr 26 2021 02:35:40