def handle(self): """ @see Handler.handle() """ try: # Make the change, capping at min and max cur = get_volume() new = max(MIN_VOLUME, min(MAX_VOLUME, cur + self._delta)) # Any change? if cur != new: # Acknowledge that it happened set_volume(new) direction = "Up" if self._delta > 0 else "Down" return Result(self, direction, False, True) else: # Nothing to do return None except Exception: LOG.error("Problem setting changing the volume by %s:\n%s" % (self._delta, traceback.format_exc())) return Result(self, "Sorry, there was a problem changing the volume", False, True)
def handle(self): ''' @see Handler.handle() ''' try: LOG.info("Querying Wikipedia for '%s'" % (self._thing, )) summary = wikipedia.summary(self._thing) except Exception as e: LOG.error("Failed to query Wikipedia about '%s': %s" % (self._thing, e)) return Result( self, "Sorry, there was a problem asking Wikipedia about %s" % (self._thing, ), False, True) # Anything? if summary is None or len(summary.strip()) == 0: return None # Strip the summary down a little, some of these can be pretty # long. First just grab the first paragraph. Next, stop after about # 400 chars. shortened = summary.split('\n')[0] if '. ' in shortened and len(shortened) > 400: index = shortened.index('. ') shortened = shortened[:index + 1] # And give it back. We use a period after "says" here so that the speech # output will pause appropriately. It's not good gramma though. Since we # got back a result then we mark ourselves as exclusive; there is # probably not a lot of point in having others also return information. return Result(self, "Wikipedia says.\n%s" % shortened, False, True)
def handle(self): """ @see Handler.handle() """ try: value = parse_number(self._volume) LOG.info("Got value of %s from %s" % (value, self._volume)) if value < MIN_VOLUME or value > MAX_VOLUME: # Bad value return Result( self, "Sorry, volume needs to be between %d and %d" % (MIN_VOLUME, MAX_VOLUME), False, True) else: # Acknowledge that it happened set_volume(value) return Result(self, "Okay, volume now %s" % (value, ), False, True) except Exception: LOG.error("Problem parsing volume '%s':\n%s" % (self._volume, traceback.format_exc())) return Result( self, "Sorry, I don't know how to set the volume to %s" % (self._volume, ), False, True)
def handle(self): ''' @see Handler.handle() ''' try: value = parse_number(self._volume) LOG.info("Got value of %s from %s" % (value, self._volume)) if value < 0 or value > 11: # Bad value return Result( self, "Sorry, volume needs to be between zero and eleven", False, True) else: # Acknowledge that it happened set_volume(value) return Result(self, "Okay, volume now %s" % (value, ), False, True) except Exception: LOG.error("Problem parsing volume '%s':\n%s" % (self._volume, traceback.format_exc())) return Result( self, "Sorry, I don't know how to set the volume to %s" % (self._volume, ), False, True)
def _run(self): """ The actual worker thread. """ # Festival is a little hokey and so we need to start it in the same # thread that we use it. Otherwise we get an message saying: # SIOD ERROR: the currently assigned stack limit has been exceeded # As such we need to do everything here and communicate back success or # failure to the _start() method via member variables. Lovely. try: import festival festival.execCommand(self._voice) self._boot_strapped = True except Exception as e: self._start_error = e return # Keep going until we're told to stop while self.is_running: if len(self._queue) == 0: time.sleep(0.1) continue # Else we have something to say try: # Get the text, make sure that '"'s in it won't confuse things start = time.time() text = self._queue.pop() text = text.replace('"', '') # Festvial pauses for too long with commas so just ignore them text = text.replace(',', '') # Ignore empty strings if not text: LOG.info("Nothing to say...") continue # We're about to say something, clear any interrupted flag ready # for any new one self._interrupted = False # We're talking so mark ourselves as active accordingly self._notify(Notifier.WORKING) # Break up the text into bits and say them so that we can # interrupt the output. Then say each non-empty part. We break # on natural pauses in the speech. for part in re.split(r'[\.,;:]', text): if self._interrupted: break if part: festival.sayText(part) except Exception as e: LOG.error("Failed to say '%s': %s" % (text, e)) finally: self._notify(Notifier.IDLE)
def _run(self): """ The actual worker thread. """ # Keep going until we're told to stop while self.is_running: if len(self._queue) == 0: time.sleep(0.1) continue # Else we have something to say try: # Get the text, make sure that '"'s in it won't confuse things start = time.time() text = self._queue.pop() text = text.replace('"', '') # Festvial pauses for too long with commas so just ignore them text = text.replace(',', '') # Ignore empty strings if not text: LOG.info("Nothing to say...") continue # We're about to say something, clear any interrupted flag ready # for any new one self._interrupted = False # I've got something to say (it's better to burn out, than to # fade away...) command = '(SayText "%s")\n' % text LOG.info("Sending: %s" % command.strip()) self._notify(Notifier.WORKING) self._subproc.stdin.write(command) self._subproc.stdin.flush() # Wait for an approximate amount of time that we think it will # take to say what we were told to. Yes, this isn't great but we # don't have a good way to tell when festival has done talking # owing to the fact it buffers its output when it's piping. while (not self._interrupted and time.time() - start < len(text) * 0.04): time.sleep(0.1) except Exception as e: LOG.error("Failed to say '%s': %s" % (text, e)) finally: self._notify(Notifier.IDLE) # Kill off the child try: if self._subproc is not None: self._subproc.terminate() self._subproc.communicate() except: pass
def stop(self): ''' Stop all the notifiers. ''' for notifier in self._notifiers: try: LOG.info("Stopping %s" % notifier) notifier.stop() except Exception as e: LOG.error("Failed to stop %s: %s" % (notifier, e))
def start(self): ''' Start all the notifiers going. ''' for notifier in self._notifiers: try: LOG.info("Starting %s" % notifier) notifier.start() except Exception as e: LOG.error("Failed to start %s: %s" % (notifier, e))
def run(self): ''' The main worker. ''' LOG.info("Starting the system") self._start() LOG.info("Entering main loop") while self._running: try: # What time is love? now = time.time() # Handle any events. First check to see if any time events are # pending and need to be scheduled. while len(self._timer_events) > 0 and \ self._timer_events[0].schedule_time <= now: self._events.put(heapq.heappop(self._timer_events)) # Now handle the actual events while not self._events.empty(): event = self._events.get() try: result = event.invoke() if result is not None: self._events.put(result) except Exception as e: LOG.error("Event %s raised exception: %s", event, e) # Loop over all the inputs and see if they have anything pending for input in self._inputs: # Attempt a read, this will return None if there's nothing # available tokens = input.read() if tokens is not None: # Okay, we read something, attempt to handle it LOG.info("Read from %s: %s" % (input, [str(t) for t in tokens])) result = self._handle(tokens) # If we got something back then give it back to the user if result is not None: self._respond(result) # Wait for a bit before going around again time.sleep(0.1) except KeyboardInterrupt: LOG.warning("KeyboardInterrupt received") break # We're out of the main loop, shut things down LOG.info("Stopping the system") self._stop()
def evaluate(self, tokens): """ @see Service.evaluate() """ # Render to lower-case, for matching purposes. words = self._words(tokens) # Look for these types of queston prefices = (('what', 'is', 'a'), ('what', 'is', 'the'), ('what', 'is'), ('who', 'is', 'the'), ('who', 'is')) match = None for prefix in prefices: try: # Look for the prefix in the words (start, end, score) = fuzzy_list_range(words, prefix) LOG.debug("%s matches %s with from %d to %d with score %d", prefix, words, start, end, score) if start == 0 and (match is None or match[2] < score): match = (start, end, score) except ValueError: pass # If we got a good match then use it if match: (start, end, score) = match thing = ' '.join(words[end:]).strip().lower() # Let's look to see if Wikipedia returns anything when we search # for this thing best = None try: self._notify(Notifier.ACTIVE) for result in wikipedia.search(thing): if result is None or len(result) == 0: continue score = fuzz.ratio(thing, result.lower()) LOG.debug("'%s' matches '%s' with a score of %d", result, thing, score) if best is None or best[1] < score: best = (result, score) except Exception as e: LOG.error("Failed to query Wikipedia for '%s': %s" % (thing, e)) finally: self._notify(Notifier.IDLE) # Turn the words into a string for the handler if best is not None: return _Handler(self, tokens, best[1] / 100, best[0]) # If we got here then it didn't look like a query for us return None
def _stop(self): ''' Stop the system. ''' # Stop the notifiers self._state.stop() # And the components for component in self._inputs + self._outputs + self._services: # Best effort, since we're likely shutting down try: LOG.info("Stopping %s" % (component, )) component.stop() except Exception as e: LOG.error("Failed to stop %s: %$s" % (component, e))
def evaluate(self, tokens): ''' @see Service.evaluate() ''' # Render to lower-case, for matching purposes. words = self._words(tokens) # Look for these types of queston prefices = (('what', 'is'), ('who', 'is')) for prefix in prefices: try: # Look for the prefix in the words index = list_index(words, prefix) # If we got here then we found the prefix. Strip that from the # query which we are about to make to Wikipedia. thing = ' '.join(words[index + len(prefix):]).strip() # Let's look to see if Wikipedia returns anything when we search # for this thing. belief = 0.5 try: self._notify(Notifier.ACTIVE) results = [ result.lower().strip() for result in wikipedia.search(thing) if result is not None and len(result) > 0 ] if thing in results: belief = 1.0 except Exception as e: LOG.error("Failed to query Wikipedia for '%s': %s" % (thing, e)) finally: self._notify(Notifier.IDLE) # Turn the words into a string for the handler. return _Handler(self, tokens, belief, thing) except Exception as e: LOG.info("%s not in %s: %s" % (prefix, words, e)) # If we got here then it didn't look like a query for us. return None
def _get_handler_for(self, tokens, platform_match, genre, artist, song_or_album): """ @see MusicService._get_handler_for() """ # Do nothing if we have no name if song_or_album is None or len(song_or_album) == 0: return None # Normalise to strings name = ' '.join(song_or_album) if artist is None or len(artist) == 0: artist = None else: artist = ' '.join(artist) # Construct the search string search = name if artist is not None: search += " by " + artist # Do the search LOG.error("Looking for '%s'", search) result = self._pandora.search(search) LOG.error("Got: %s", result) # See if we got something back if len(result.songs) > 0: # Pick the best song = sorted(result.songs, key=lambda item: item.score, reverse=True)[0] # Grab what the handler needs what = "%s by %s" % (song.song_name, song.artist) score = song.score / 100.0 token = song.token # And give back the handler to play it return _PandoraServicePlayHandler(self, tokens, what, token, score) else: # We got nothing return None
def _handler(self): """ Pulls values from the decoder queue and handles them appropriately. Runs in its own thread. """ LOG.info("Started decoding handler") while True: try: # Get a handle on the queue. This will be nulled out when we're # done. queue = self._decode_queue if queue is None: break # Anything? if len(queue) > 0: item = queue.popleft() if item is None: # A None denotes the end of the data so we look to # decode what we've been given LOG.info("Decoding audio") self._notify(Notifier.WORKING) self._output.append(self._decode()) self._notify(Notifier.IDLE) elif isinstance(item, bytes): # Something to feed the decoder LOG.debug("Feeding %d bytes" % len(item)) self._feed_raw(item) else: LOG.warning("Ignoring junk on decode queue: %r" % (item,)) # Go around again continue except Exception as e: # Be robust but log it LOG.error("Got an error in the decoder queue: %s" % (e,)) # Don't busy-wait time.sleep(0.001) # And we're done! LOG.info("Stopped decoding handler")
def _respond(self, response): ''' Given back the response to the user via the outputs. @type response: str @param response: The text to send off to the user in the real world. ''' # Give back nothing if we have no response if response is None: return None # Simply hand it to all the outputs for output in self._outputs: try: output.write(response) except: LOG.error("Failed to respond with %s:\n%s" % (output, traceback.format_exc()))
def _handle(self, sckt): """ Handle reading from a socket """ LOG.info("Started new socket handler") # We'll build these up tokens = [] cur = b'' # Loop until they go away while True: c = sckt.recv(1) if c is None or len(c) == 0: LOG.info("Peer closed connection") return if len(cur) == 0 and ord(c) == 4: LOG.info("Got EOT") try: sckt.close() except: pass return if c in b' \t\n': if len(cur.strip()) > 0: try: tokens.append(Token(cur.strip().decode(), 1.0, True)) except Exception as e: LOG.error("Error handling '%s': %s", cur, e) cur = b'' if c == b'\n': if len(tokens) > 0: if self._prefix: tokens = self._prefix + tokens self._output.append(tokens) tokens = [] else: cur += c
def _run(self): ''' The actual worker thread. ''' # Keep going until we're told to stop while self.is_running: if len(self._queue) == 0: time.sleep(0.1) continue # Else we have something to say try: # Get the text, make sure that '"'s in it won't confuse things start = time.time() text = self._queue.pop() command = '(SayText "%s")\n' % text.replace('"', '') LOG.info("Sending: %s" % command.strip()) self._notify(Notifier.WORKING) self._subproc.stdin.write(command) self._subproc.stdin.flush() # Wait for a bit, since festival can sometimes return with a # response right away while time.time() - start < len(text) * 0.05: time.sleep(0.1) # And read in the result, which should mean it's done for line in self._readlines(): LOG.info("Received: %s" % line) except Exception as e: LOG.error("Failed to say '%s': %s" % (text, e)) finally: self._notify(Notifier.IDLE) # Kill off the child try: self._subproc.terminate() self._subproc.communicate() except: pass
def play(self, token): """ Set the song(s) to play. """ # Out with the old if self._station is not None: try: self._player.end_station() except: pass self._pandora.delete_station(self._station.token) # In with the new self._station = self._pandora.create_station(search_token=token) LOG.error("Playing station %s", self._station) # And play it, in a new thread since it is blocking thread = Thread(target=self._play_station) thread.daemon = True thread.start()
def update_status(self, component, status): ''' @see Notifier.update_status() ''' # See if this is a speaker, if so then we have to account for that if component.is_speech: if status == Notifier.IDLE: if component in self._speakers: self._speakers.remove(component) LOG.info("%s is no longer speaking" % (component, )) else: self._speakers.add(component) LOG.info("%s is speaking" % (component, )) # And tell the notifiers for notifier in self._notifiers: try: notifier.update_status(component, status) except Exception as e: LOG.error("Failed to update %s with (%s,%s): %s" % (notifier, component, status, e))
def create_index(): try: self._media_index = MusicIndex(dirname) except Exception as e: LOG.error("Failed to create music index: %s", e)
def handle(self): """ @see Handler.handle() """ # If we got nothing then grumble in a vaguely (un)helpful way if len(self._times) == 0: return Result(self, "I'm sorry, I didn't catch that", False, True) # Look for the times in the words try: # Try to find the various units of time in something like: # seven hours and fifteen days indices = [] for (words, seconds) in _PERIODS: for word in words: if word in self._times: index = self._times.index(word) LOG.info("Found '%s' at index %d" % (word, index)) indices.append((index, seconds)) # Anything? if len(indices) == 0: raise ValueError("Found no units in %s" % ' '.join(self._times)) # Put them in order and look for the numbers, accumulating into the # total total = 0 indices = sorted(indices, key=lambda pair: pair[0]) prev = 0 for (index, seconds) in indices: # Get the words leading up to the time unit, these should be the # number (possibly with other bits) value = self._times[prev:index] # Strip off any "and"s while value[0] == "and": value = value[1:] # Now try to parse it LOG.info("Parsing %s" % (value, )) amount = parse_number(' '.join(value)) # Accumulate total = amount * seconds LOG.info("Got value of %s seconds from %s" % (total, self._times)) # Handle accordingly if total > 0: # That's a valid timer value. Set the timer and say we did it. self.service.add_timer(total) return Result( self, "Okay, timer set for %s" % (' '.join(self._times), ), False, True) else: # We can't have an alarm in the past return Result( self, "%s was not a time in the future" % (' '.join(self._times), ), False, True) except Exception: LOG.error("Problem parsing timer '%s':\n%s" % (self._times, traceback.format_exc())) return Result( self, "Sorry, I don't know how to set the timer for %s" % (' '.join(self._times), ), False, True)
def input(self, cmd, song): LOG.error("Got command '%s' for song '%s'", cmd, song)
def _handle(self, tokens): """ Handle a list of L{Token}s from the input. :type tokens: list(L{Token}) :param tokens: The tokens to handle. :rtype: str :return: The textual response, if any. """ # Give back nothing if we have no tokens if tokens is None: return None # Get the words from this text words = [ to_letters(token.element).lower() for token in tokens if token.verbal ] LOG.info("Handling: \"%s\"" % ' '.join(words)) # Anything? if len(words) == 0: LOG.info("Nothing to do") return None # See if the key-phrase is in the tokens and use it to determine the # offset of the command. offset = None for key_phrase in self._key_phrases: try: offset = (list_index(words, key_phrase) + len(key_phrase)) LOG.info("Found key-phrase %s at offset %d in %s" % (key_phrase, offset - len(key_phrase), words)) except ValueError: pass # If we don't have an offset then we haven't found a key-phrase. Try a # fuzzy match, but only if we are not waiting for someone to say # something after priming with the keypharse. now = time.time() if now - self._last_keyphrase_only < Dexter._KEY_PHRASE_ONLY_TIMEOUT: # Okay, treat what we got as the command, so we have no keypharse # and so the offset is zero. LOG.info("Treating %s as a command", (words, )) offset = 0 elif offset is None: # Not found, but if we got something which sounded like the key # phrase then set the offset this way too. This allows someone to # just say the keyphrase and we can handle it by waiting for them to # then say something else. LOG.info("Key pharses %s not found in %s" % (self._key_phrases, words)) # Check for it being _almost_ the key phrase ratio = 75 what = ' '.join(words) for key_phrase in self._key_phrases: if fuzz.ratio(' '.join(key_phrase), what) > ratio: offset = len(key_phrase) LOG.info("Fuzzy-matched key-pharse %s in %s" % (key_phrase, words)) # Anything? if offset is None: return None # If we have the keyphrase and no more then we just got a priming # command, we should be ready for the rest of it to follow if offset == len(words): # Remember that we were primed LOG.info("Just got keyphrase") self._last_keyphrase_only = now # To drop the volume down so that we can hear what's coming. We'll # need a capturing function to do this. def make_fn(v): def fn(): set_volume(v) return None return fn try: # Get the current volume volume = get_volume() # If the volume is too high then we will need to lower it if volume > Dexter._LISTENING_VOLUME: # And schedule an event to bump it back up in a little while LOG.info( "Lowering volume from %d to %d while we wait for more", volume, Dexter._LISTENING_VOLUME) set_volume(Dexter._LISTENING_VOLUME) heapq.heappush( self._timer_events, TimerEvent(now + Dexter._KEY_PHRASE_ONLY_TIMEOUT, runnable=make_fn(volume))) except Exception as e: LOG.warning("Failed to set listening volume: %s", e) # Nothing more to do until we hear the rest of the command return None # Special handling if we have active outputs and someone said "stop" if offset == len(words) - 1 and words[-1] == "stop": stopped = False for output in self._outputs: # If this output is busy doing something then we tell it to stop # doing that thing with interrupt(). (stop() means shutdown.) if output.status in (Notifier.ACTIVE, Notifier.WORKING): try: # Best effort LOG.info("Interrupting %s", output) output.interrupt() stopped = True except: pass # If we managed to stop one of the output components then we're # done. We don't want another component to pick up this command. if stopped: return None # See which services want them handlers = [] for service in self._services: try: # This service is being woken to so update the status self._state.update_status(service, Notifier.ACTIVE) # Get any handler from the service for the given tokens handler = service.evaluate(tokens[offset:]) if handler is not None: handlers.append(handler) except: LOG.error( "Failed to evaluate %s with %s:\n%s" % ([str(token) for token in tokens], service, traceback.format_exc())) return "Sorry, there was a problem" finally: # This service is done working now self._state.update_status(service, Notifier.IDLE) # Anything? if len(handlers) == 0: return "I'm sorry, I don't know how to help with that" # Okay, put the handlers into order of belief and try them. Notice that # we want the higher beliefs first so we reverse the sign in the key. handlers = sorted(handlers, key=lambda h: -h.belief) # If any of the handlers have marked themselves as exclusive then we # restrict the list to just them. Hopefully there will be just one but, # if there are more, then they should be in the order of belief so we'll # pick the "best" exclusive one first. if True in [h.exclusive for h in handlers]: handlers = [h for h in handlers if h.exclusive] # Now try each of the handlers response = [] error_service = None for handler in handlers: try: # Update the status of this handler's service to "working" while # we call it self._state.update_status(handler.service, Notifier.WORKING) # Invoked the handler and see what we get back result = handler.handle() if result is None: continue # Accumulate into the resultant text if result.text is not None: response.append(result.text) # Stop here? if handler.exclusive or result.exclusive: break except: error_service = handler.service LOG.error( "Handler %s with tokens %s for service %s yielded:\n%s" % (handler, [str(t) for t in handler.tokens ], handler.service, traceback.format_exc())) finally: # This service is done working now self._state.update_status(handler.service, Notifier.IDLE) # Give back whatever we had, if anything if error_service: return "Sorry, there was a problem with %s" % (error_service, ) elif len(response) > 0: return '\n'.join(response) else: return None
def _handle(self, tokens): ''' Handle a list of L{Token}s from the input. @type tokens: list(L{Token}) @param tokens: The tokens to handle. @rtype: str @return: The textual response, if any. ''' # Give back nothing if we have no tokens if tokens is None: return None # See if the key-phrase is in the tokens and use it to determine the # offset of the command. words = [to_letters(token.element).lower() for token in tokens] offset = None for key_phrase in self._key_phrases: try: offset = (list_index(words, key_phrase) + len(key_phrase)) LOG.info("Found key-pharse %s at offset %d in %s" % (key_phrase, offset - len(key_phrase), words)) except ValueError: pass # If we don't have an offset then we haven't found a key-phrase. Try a # fuzzy match, but only if we are not waiting for someone to say # something after priming with the keypharse. now = time.time() if now - self._last_keyphrase_only < Dexter._KEY_PHRASE_ONLY_TIMEOUT: # Okay, treat what we got as the command, so we have no keypharse # and so the offset is zero. LOG.info("Treating %s as a command", (words, )) offset = 0 elif offset is None: # Not found, but if we got something which sounded like the key # phrase then set the offset this way too. This allows someone to # just say the keyphrase and we can handle it by waiting for them to # then say something else. LOG.info("Key pharses %s not found in %s" % (self._key_phrases, words)) # Check for it being _almost_ the key phrase ratio = 75 what = ' '.join(words) for key_phrase in self._key_phrases: if fuzz.ratio(' '.join(key_phrase), what) > ratio: offset = len(key_phrase) LOG.info("Fuzzy-matched key-pharse %s in %s" % (key_phrase, words)) # Anything? if offset is None: return None # If we have the keyphrase and no more then we just got a priming # command, we should be ready for the rest of it to follow if offset == len(words): # Remember that we were primed self._last_keyphrase_only = now # Drop the volume down on any services so that we can hear what's # coming. We'll need a capturing function to do this. def make_fn(s, v): def fn(): s.set_volume(v) return None # Look at all the services and see which have volume controls for service in self._services: if not (hasattr(service, 'get_volume') and hasattr(service, 'set_volume')): continue # Get the current volume try: volume = service.get_volume() except Exception: continue # If it's too loud then drop it down, and schedule an event to # bump it back up in a little bit, if volume > Dexter._LISTENING_VOLUME: service.set_volume(Dexter._LISTENING_VOLUME) heapq.heappush( self._timer_events.put, TimerEvent(now + Dexter._KEY_PHRASE_ONLY_TIMEOUT, make_fn(service, volume))) # Nothing more to do until we hear the rest of the command return None # See which services want them handlers = [] for service in self._services: try: # This service is being woken to so update the status self._state.update_status(service, Notifier.ACTIVE) # Get any handler from the service for the given tokens handler = service.evaluate(tokens[offset:]) if handler is not None: handlers.append(handler) except: LOG.error( "Failed to evaluate %s with %s:\n%s" % ([str(token) for token in tokens], service, traceback.format_exc())) return "Sorry, there was a problem" finally: # This service is done working now self._state.update_status(service, Notifier.IDLE) # Anything? if len(handlers) == 0: return "I'm sorry, I don't know how to help with that" # Okay, put the handlers into order of belief and try them. Notice that # we want the higher beliefs first so we reverse the sign in the key. handlers = sorted(handlers, key=lambda h: -h.belief) # If any of the handlers have marked themselves as exclusive then we # restrict the list to just them. Hopefully there will be just one but, # if there are more, then they should be in the order of belief so we'll # pick the "best" exclusive one first. if True in [h.exclusive for h in handlers]: handlers = [h for h in handlers if h.exclusive] # Now try each of the handlers response = [] error = False for handler in handlers: try: # Update the status of this handler's service to "working" while # we call it self._state.update_status(handler.service, Notifier.WORKING) # Invoked the handler and see what we get back result = handler.handle() if result is None: continue # Accumulate into the resultant text if result.text is not None: response.append(result.text) # Stop here? if handler.exclusive or result.exclusive: break except: error = True LOG.error( "Handler %s with tokens %s for service %s yielded:\n%s" % (handler, [str(t) for t in handler.tokens ], handler.service, traceback.format_exc())) finally: # This service is done working now self._state.update_status(handler.service, Notifier.IDLE) # Give back whatever we had, if anything if error: return "Sorry, there was a problem" elif len(response) > 0: return '\n'.join(response) else: return None
def _get_handler_for(self, tokens, platform_match, genre, artist, song_or_album): """ @see MusicService._get_handler_for() """ # Do nothing if we have no name if song_or_album is None or len(song_or_album) == 0: return None # Normalise to strings name = ' '.join(song_or_album).lower() if artist is None or len(artist) == 0: artist = None else: artist = ' '.join(artist).lower() # We will put all the track URIs in here uris = [] # Search by track name then album name, these are essentially the same # logic for which in ('track', 'album'): LOG.info("Looking for '%s'%s as a %s", name, " by '%s'" % artist if artist else '', which) # This is the key in the results plural = which + 's' # Try using the song_or_album as the name result = self._spotify.search(name, type=which) if not result: LOG.info("No results") continue # Did we get back any tracks if plural not in result: LOG.error("%s was not in result keys: %s", plural, result.keys()) continue # We got some results back, let's assign scores to them all results = result[plural] matches = [] for item in results.get('items', []): # It must have a uri if 'uri' not in item and item['uri']: LOG.error("No URI in %s", item) # Look at all the candidate entries if 'name' in item: # See if this is better than any existing match name_score = fuzz.ratio(name, item['name'].lower()) LOG.debug("'%s' matches '%s' with score %d", item['name'], name, name_score) # Check to make sure that we have an artist match as well if artist is None: # Treat as a wildcard artist_score = 100 else: artist_score = 0 for entry in item.get('artists', []): score = fuzz.ratio(artist, entry.get('name', '').lower()) LOG.debug("Artist match score for '%s' was %d", entry.get('name', ''), score) if score > artist_score: artist_score = score LOG.debug("Artist match score was %d", artist_score) # Only consider cases where the scores look "good enough" if name_score > 75 and artist_score > 75: LOG.debug("Adding match") matches.append((item, name_score, artist_score)) # Anything? if len(matches) > 0: LOG.debug("Got %d matches", len(matches)) # Order them accordingly matches.sort(key=lambda e: (e[1], e[2])) # Now, pick the top one best = matches[0] item = best[0] LOG.debug("Best match was: %s", item) # Extract the info item_name = item.get('name', None) or name artists = item.get('artists', []) artist_name = (artists[0].get('name', None) if len(artists) > 0 else None) or artist # Description of what we are playing what = item_name if item_name else name if artist_name: what += " by " + artist_name what += " on Spotify" # The score is the geometric value of the two score = sqrt(best[1] * best[1] + best[2] * best[2]) / 100.0 # The should be here assert 'uri' in item, "Missing URI in %s" % (item, ) uri = item['uri'] # If we are an album then grab the track URIs if which == 'album': tracks = self._spotify.album_tracks(uri) if tracks and 'items' in tracks: uris = [track['uri'] for track in tracks['items']] else: # Just the track uris = [uri] # And we're done break # Otherwise assume that it's an artist if len(uris) == 0 and artist is None: LOG.info("Looking for '%s' as an artist", name) result = self._spotify.search(name, type='artist') LOG.debug("Got: %s", result) if result and 'artists' in result and 'items' in result['artists']: items = sorted(result['artists']['items'], key=lambda entry: fuzz.ratio( name, entry.get('name', '').lower()), reverse=True) # Look at the best one, if any LOG.debug("Got %d matches", len(items)) if len(items) > 0: match = items[0] who = match['name'] what = "%s on Spotify" % (who, ) score = fuzz.ratio(who.lower(), name) # Find all their albums if 'uri' in match: LOG.debug("Got match: %s", match['uri']) artist_albums = self._spotify.artist_albums( match['uri']) for album in artist_albums.get('items', []): # Append all the tracks LOG.debug("Looking at album: %s", album) if 'uri' in album: tracks = self._spotify.album_tracks( album['uri']) if tracks and 'items' in tracks: LOG.debug( "Adding tracks: %s", ' '.join(track['name'] for track in tracks['items'])) uris.extend([ track['uri'] for track in tracks['items'] ]) # And now we can give it back, if we had something if len(uris) > 0: return _SpotifyServicePlayHandler(self, tokens, what, uris, score) else: # We got nothing return None
def parse_number(words): ''' Turn a set of words into a number. These might be complex ("One thousand four hundred and eleven") or simple ("Seven"). >>> parse_number('one') 1 >>> parse_number('one point eight') 1.8 >>> parse_number('minus six') -6 >>> parse_number('minus four point seven eight nine') -4.789 @type words: str @parse words: The string words to parse. E.g. C{'twenty seven'}. ''' # Sanity if words is None: return None # Make sure it's a string words = str(words) # Trim surrounding whitespace words = words.strip() # Not a lot we can do if we have no words if len(words) == 0: return None # First try to parse the string as an integer or a float directly if not re.search(r'\s', words): try: return int(words) except: pass try: return float(words) except: pass # Sanitise it since we're now going to attempt to parse it. Collapse # multiple spaces to one and strip out non-letters words = ' '.join(to_letters(s) for s in re.split(r'\s+', words)) LOG.debug("Parsing '%s'" % (words, )) # Recheck for empty if words == '': return None # See if we have to negate the result mult = 1 for neg in ("minus ", "negative "): if words.startswith(neg): words = words[len(neg):] mult = -1 break # Look for "point" in the words since it might be "six point two" or # something if ' point ' in words: # Determine the integer and decimal portions (integer, decimal) = words.split(' point ', 1) LOG.debug("'%s' becomes '%s' and '%s'" % (words, integer, decimal)) # Parsing the whole number is easy enough whole = parse_number(integer) if whole is None: return None # S;plit up the digits to parse them. digits = numpy.array( [parse_number(digit) for digit in decimal.split(' ')]) if None in digits or \ numpy.any(digits < 0) or \ numpy.any(digits > 9): LOG.error("'%s' was not a valid decimal" % (words, )) return None # Okay, use some cheese to parse into a float return mult * float('%d.%s' % (whole, ''.join(str(d) for d in digits))) else: # No ' point ' in it, parse directly try: return mult * _WORDS_TO_NUMBERS.parse(words) except Exception as e: LOG.error("Failed to parse '%s': %s" % (words, e)) return None
def _handler(self): """ Pulls values from the decoder queue and handles them appropriately. Runs in its own thread. """ # Whether we are skipping the current input gobble = False LOG.info("Started decoding handler") while True: try: # Get a handle on the queue. This will be nulled out when we're # done. queue = self._decode_queue if queue is None: break # Anything? if len(queue) > 0: item = queue.popleft() if item is None: # A None denotes the end of the data so we look to # decode what we've been given if we're not throwing it # away. if gobble: LOG.info("Dropped audio") else: LOG.info("Decoding audio") self._notify(Notifier.WORKING) self._output.append(self._decode()) self._notify(Notifier.IDLE) elif isinstance(item, float): # This is the timestamp of the clip. If it's too old # then we throw it away. age = time.time() - item if int(age) > 0: LOG.info("Upcoming audio clip is %0.2fs old" % (age, )) gobble = age > self._GOBBLE_LIMIT elif isinstance(item, bytes): # Something to feed the decoder if gobble: LOG.debug("Ignoring %d bytes" % len(item)) else: LOG.debug("Feeding %d bytes" % len(item)) self._feed_raw(item) else: LOG.warning("Ignoring junk on decode queue: %r" % (item, )) # Go around again continue except Exception as e: # Be robust but log it LOG.error("Got an error in the decoder queue: %s" % (e, )) # Don't busy-wait time.sleep(0.001) # And we're done! LOG.info("Stopped decoding handler")