def _build_from_dirname(self, dirname): ''' Build an index based on the given directory root. @type dirname: str @param dirname: The directory name to build from. ''' # Walk the tree for (subdir, subdirs, files) in os.walk(dirname): LOG.info("Indexing %s", subdir) # Handle all the files which we can find for filename in files: try: # Use mutagen to grab details path = os.path.join(subdir, filename) info = mutagen.File(path) if isinstance(info, mutagen.mp3.MP3): self._add_entry(AudioEntry.from_mp3(info)) elif isinstance(info, mutagen.flac.FLAC): self._add_entry(AudioEntry.from_flac(info)) else: LOG.debug("Ignoring %s", path) except Exception as e: LOG.warning("Failed to index %s: %s", path, e)
def _decode(self): """ @see AudioInput._decode() """ if self._sckt is None: # No context means no tokens LOG.warning("Had no stream context to close") return [] try: # Send the EOD token self._sckt.sendall(struct.pack('!q', -1)) # Get back the result: # 8 bytes for the length # data... LOG.info("Waiting for result...") length = b'' while len(length) < 8: got = self._sckt.recv(8 - len(length)) if len(got) == 0: raise IOError("EOF in recv()") length += got (count, ) = struct.unpack("!q", length) # Read in the string LOG.info("Reading %d chars" % (count, )) result = b'' while len(result) < count: got = self._sckt.recv(count - len(result)) if len(got) == 0: raise IOError("EOF in recv()") result += got result = result.decode() LOG.info("Result is: '%s'" % (result, )) # Convert to tokens tokens = [ Token(word.strip(), 1.0, True) for word in result.split(' ') if word.strip() != '' ] return tokens except Exception as e: # Again, just grumble on exceptions LOG.info("Failed to do remote processing: %s" % e) return [] finally: # Close it out, best effort try: LOG.info("Closing connection") self._sckt.shutdown(socket.SHUT_RDWR) self._sckt.close() except: pass finally: self._sckt = None
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 _start(self): """ @see Startable._start() """ # This is what we need to be able to do scope = ','.join(('user-library-read', 'user-read-playback-state', 'user-modify-playback-state')) # Create the authorization manager, and then use that to create the # client auth_manager = SpotifyOAuth(client_id=self._client_id, client_secret=self._client_secret, redirect_uri=self._redirect_uri, scope=scope) self._spotify = Spotify(auth_manager=auth_manager) # See what devices we have to hand try: if self._device_name is not None: LOG.info("Looking for device named '%s'", self._device_name) devices = self._spotify.devices() for device in devices['devices']: # Say what we see name = device['name'] id_ = device['id'] type_ = device['type'] active = device['is_active'] vol = device['volume_percent'] LOG.info("Found %sactive %s device: '%s'", '' if active else 'in', type_, name) # See if we're looking for a specific device, if not just snoop # the volume from the first active one if self._device_name is not None: if name == self._device_name: LOG.info("Matched '%s' to ID '%s'", name, id_) self._device_id = id_ self._volume = vol / 100.0 * MAX_VOLUME else: if active and self._volume is None: self._volume = vol / 100.0 * MAX_VOLUME except Exception as e: LOG.warning("Unable to determine active Spoify devices: %s", e) # If we were looking for a specific device then make sure that we found # it in the list if self._device_name is not None and self._device_id is None: raise ValueError("Failed to find device with name '%s'" % (self._device_name, ))
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 _decode(self): """ @see AudioInput._decode() """ if self._context is None: # No context means no tokens LOG.warning("Had no stream context to close") tokens = [] else: # Finish up by finishing the decoding words = self._context.finishStream() LOG.info("Got: %s" % (words, )) self._context = None # And tokenize tokens = [ Token(word.strip(), 1.0, True) for word in words.split(' ') if len(word.strip()) > 0 ] return tokens
def _controller(self): """ The main controller thread. This handles keeping things going in the background. It does not do much heavy lifting however. """ while True: # Tum-ti-tum time.sleep(0.1) # Do nothing while the song is playing. if get_pygame().mixer.music.get_busy(): continue # Get the next song to play, this will block song = self._queue.get() LOG.info("Playing %s", song) # Attempt to play it try: get_pygame().mixer.music.load(song) get_pygame().mixer.music.play() except Exception as e: LOG.warning("Failed to play %s: %s", song, e)
def sound_alarm(self, timer): """ Ring the alarm for the given timer. """ # Let the terminal know LOG.info("DING DING DING!!! Timer '%s' has expired..." % (timer, )) # Play any sound... if self._timer_audio is not None: LOG.info("Playing timer sound") # ...for about 5 seconds end = time.time() + 5 while time.time() < end: try: self._timer_audio.play() except Exception as e: LOG.warning("Failed to play timer sound: %s", e) # And remove the timer (this should not fail but...) try: self._timers.remove(timer) except: pass
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 _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")