def simple_cli(): global bus global bSimple bSimple = True bus = WebsocketClient() # OwO messagebus connection event_thread = Thread(target=connect) event_thread.setDaemon(True) event_thread.start() bus.on('speak', handle_speak) try: while True: # Sleep for a while so all the output that results # from the previous command finishes before we print. time.sleep(1.5) print("Input (Ctrl+C to quit):") line = sys.stdin.readline() bus.emit( Message("recognizer_loop:utterance", {'utterances': [line.strip()]})) except KeyboardInterrupt as e: # User hit Ctrl+C to quit print("") except KeyboardInterrupt as e: LOG.exception(e) event_thread.exit() sys.exit()
def _poll_skill_settings(self): """ If identifier exists for this skill poll to backend to request settings and store it if it changes TODO: implement as websocket """ original = hash(str(self)) try: if not is_paired(): pass elif not self._complete_intialization: self.initialize_remote_settings() if not self._complete_intialization: return # unable to do remote sync else: self.update_remote() except Exception as e: LOG.exception('Failed to fetch skill settings: {}'.format(repr(e))) finally: # Call callback for updated settings if self.changed_callback and hash(str(self)) != original: self.changed_callback() if self._poll_timer: self._poll_timer.cancel() if not self._is_alive: return # continues to poll settings every minute self._poll_timer = Timer(60, self._poll_skill_settings) self._poll_timer.daemon = True self._poll_timer.start()
def _adapt_intent_match(self, utterances, lang): """ Run the Adapt engine to search for an matching intent Args: utterances (list): list of utterances lang (string): 4 letter ISO language code Returns: Intent structure, or None if no match was found. """ best_intent = None for utterance in utterances: try: # normalize() changes "it's a boy" to "it is boy", etc. best_intent = next( self.engine.determine_intent( normalize(utterance, lang), 100, include_tags=True, context_manager=self.context_manager)) # TODO - Should Adapt handle this? best_intent['utterance'] = utterance except StopIteration: # don't show error in log continue except Exception as e: LOG.exception(e) continue if best_intent and best_intent.get('confidence', 0.0) > 0.0: self.update_context(best_intent) # update active skills skill_id = best_intent['intent_type'].split(":")[0] self.add_active_skill(skill_id) return best_intent
def load_skill(skill_descriptor, bus, skill_id, BLACKLISTED_SKILLS=None): """ Load skill from skill descriptor. Args: skill_descriptor: descriptor of skill to load bus: OwO messagebus connection skill_id: id number for skill Returns: OwOSkill: the loaded skill or None on failure """ BLACKLISTED_SKILLS = BLACKLISTED_SKILLS or [] path = skill_descriptor["path"] name = basename(path) LOG.info("ATTEMPTING TO LOAD SKILL: {} with ID {}".format(name, skill_id)) if name in BLACKLISTED_SKILLS: LOG.info("SKILL IS BLACKLISTED " + name) return None main_file = join(path, MainModule + '.py') try: with open(main_file, 'rb') as fp: skill_module = imp.load_module(name.replace('.', '_'), fp, main_file, ('.py', 'rb', imp.PY_SOURCE)) if (hasattr(skill_module, 'create_skill') and callable(skill_module.create_skill)): # v2 skills framework skill = skill_module.create_skill() skill.settings.allow_overwrite = True skill.settings.load_skill_settings_from_file() skill.bind(bus) try: skill.skill_id = skill_id skill.load_data_files(path) # Set up intent handlers skill._register_decorated() skill.initialize() except Exception as e: # If an exception occurs, make sure to clean up the skill skill.default_shutdown() raise e LOG.info("Loaded " + name) # The very first time a skill is run, speak the intro first_run = skill.settings.get("__OwO_skill_firstrun", True) if first_run: LOG.info("First run of " + name) skill.settings["__OwO_skill_firstrun"] = False skill.settings.store() intro = skill.get_intro_message() if intro: skill.speak(intro) return skill else: LOG.warning("Module {} does not appear to be skill".format(name)) except Exception: LOG.exception("Failed to load skill: " + name) return None
def handle_utterance(self, message): """ Main entrypoint for handling user utterances with OwO skills Monitor the messagebus for 'recognizer_loop:utterance', typically generated by a spoken interaction but potentially also from a CLI or other method of injecting a 'user utterance' into the system. Utterances then work through this sequence to be handled: 1) Active skills attempt to handle using converse() 2) Adapt intent handlers 3) Padatious intent handlers 4) Other fallbacks Args: message (Message): The messagebus data """ try: # Get language of the utterance lang = message.data.get('lang', "en-us") utterances = message.data.get('utterances', '') stopwatch = Stopwatch() with stopwatch: # Give active skills an opportunity to handle the utterance converse = self._converse(utterances, lang) if not converse: # No conversation, use intent system to handle utterance intent = self._adapt_intent_match(utterances, lang) padatious_intent = PadatiousService.instance.calc_intent( utterances[0]) if converse: # Report that converse handled the intent and return ident = message.context['ident'] if message.context else None report_timing(ident, 'intent_service', stopwatch, {'intent_type': 'converse'}) return elif intent and not (padatious_intent and padatious_intent.conf >= 0.95): # Send the message to the Adapt intent's handler unless # Padatious is REALLY sure it was directed at it instead. reply = message.reply(intent.get('intent_type'), intent) else: # Allow fallback system to handle utterance # NOTE: Padatious intents are handled this way, too reply = message.reply('intent_failure', { 'utterance': utterances[0], 'lang': lang }) self.bus.emit(reply) self.send_metrics(intent, message.context, stopwatch) except Exception as e: LOG.exception(e)
def load_priority(self): skills = {skill.name: skill for skill in self.msm.list()} for skill_name in PRIORITY_SKILLS: skill = skills[skill_name] if not skill.is_local: try: skill.install() except Exception: LOG.exception('Downloading priority skill:' + skill.name) if not skill.is_local: continue self._load_or_reload_skill(skill.path)
def stop(self): """ Tell the manager to shutdown """ self._stop_event.set() # Do a clean shutdown of all skills for name, skill_info in self.loaded_skills.items(): instance = skill_info.get('instance') if instance: try: instance.default_shutdown() except Exception: LOG.exception('Shutting down skill: ' + name)
def initialize(): nonlocal instance, complete try: clazz = HotWordFactory.CLASSES[module] instance = clazz(hotword, config, lang=lang) except TriggerReload: complete.set() sleep(0.5) loop.reload() except Exception: LOG.exception( 'Could not create hotword. Falling back to default.') instance = None complete.set()
def load_spellings(self): """Load phonetic spellings of words as dictionary""" path = join('text', self.lang, 'phonetic_spellings.txt') spellings_file = resolve_resource_file(path) if not spellings_file: return {} try: with open(spellings_file) as f: lines = filter(bool, f.read().split('\n')) lines = [i.split(':') for i in lines] return {key.strip(): value.strip() for key, value in lines} except ValueError: LOG.exception('Failed to load phonetic spellings.') return {}
def on_message(self, message): LOG.debug(message) try: deserialized_message = Message.deserialize(message) except: return try: self.emitter.emit(deserialized_message.type, deserialized_message) except Exception as e: LOG.exception(e) traceback.print_exc(file=sys.stdout) pass for client in client_connections: client.write_message(message)
def send_skill_list(self, message=None): """ Send list of loaded skills. """ try: info = {} for s in self.loaded_skills: is_active = (self.loaded_skills[s].get('active', True) and self.loaded_skills[s].get('instance') is not None) info[basename(s)] = { 'active': is_active, 'id': self.loaded_skills[s]['id'] } self.bus.emit(Message('owo.skills.list', data=info)) except Exception as e: LOG.exception(e)
def save_phonemes(self, key, phonemes): """ Cache phonemes Args: key: Hash key for the sentence phonemes: phoneme string to save """ cache_dir = owo.util.get_cache_directory("tts") pho_file = os.path.join(cache_dir, key + ".pho") try: with open(pho_file, "w") as cachefile: cachefile.write(phonemes) except Exception: LOG.exception("Failed to write {} to cache".format(pho_file)) pass
def _unload_removed(self, paths): """ Shutdown removed skills. Arguments: paths: list of current directories in the skills folder """ paths = [p.rstrip('/') for p in paths] skills = self.loaded_skills # Find loaded skills that doesn't exist on disk removed_skills = [str(s) for s in skills.keys() if str(s) not in paths] for s in removed_skills: LOG.info('removing {}'.format(s)) try: LOG.debug('Removing: {}'.format(skills[s])) skills[s]['instance'].default_shutdown() except Exception as e: LOG.exception(e) self.loaded_skills.pop(s)
def _normalized_numbers(self, sentence): """normalized numbers to word equivalent. Args: sentence (str): setence to speak Returns: stf: normalized sentences to speak """ try: numbers = re.findall(r'\d+', sentence) normalized_num = [(num, pronounce_number(int(num))) for num in numbers] for num, norm_num in normalized_num: sentence = sentence.replace(num, norm_num, 1) except TypeError: LOG.exception("type error in mimic2_tts.py _normalized_numbers()") return sentence
def wrapper(message): skill_data = {'name': get_handler_name(handler)} stopwatch = Stopwatch() try: message = unmunge_message(message, self.skill_id) # Indicate that the skill handler is starting if handler_info: # Indicate that the skill handler is starting if requested msg_type = handler_info + '.start' self.bus.emit(message.reply(msg_type, skill_data)) if once: # Remove registered one-time handler before invoking, # allowing them to re-schedule themselves. self.remove_event(name) with stopwatch: if len(signature(handler).parameters) == 0: handler() else: handler(message) self.settings.store() # Store settings if they've changed except Exception as e: # Convert "MyFancySkill" to "My Fancy Skill" for speaking handler_name = camel_case_split(self.name) msg_data = {'skill': handler_name} msg = dialog.get('skill.error', self.lang, msg_data) self.speak(msg) LOG.exception(msg) # append exception information in message skill_data['exception'] = repr(e) finally: # Indicate that the skill handler has completed if handler_info: msg_type = handler_info + '.complete' self.bus.emit(message.reply(msg_type, skill_data)) # Send timing metrics context = message.context if context and 'ident' in context: report_timing(context['ident'], 'skill_handler', stopwatch, {'handler': handler.__name__})
def handler(message): # indicate fallback handling start bus.emit( message.reply("owo.skill.handler.start", data={'handler': "fallback"})) stopwatch = Stopwatch() handler_name = None with stopwatch: for _, handler in sorted(cls.fallback_handlers.items(), key=operator.itemgetter(0)): try: if handler(message): # indicate completion handler_name = get_handler_name(handler) bus.emit( message.reply('owo.skill.handler.complete', data={ 'handler': "fallback", "fallback_handler": handler_name })) break except Exception: LOG.exception('Exception in fallback.') else: # No fallback could handle the utterance bus.emit(message.reply('complete_intent_failure')) warning = "No fallback could handle intent." LOG.warning(warning) # indicate completion with exception bus.emit( message.reply('owo.skill.handler.complete', data={ 'handler': "fallback", 'exception': warning })) # Send timing metric if message.context and message.context['ident']: ident = message.context['ident'] report_timing(ident, 'fallback_handler', stopwatch, {'handler': handler_name})
def run(self): """ Thread main loop. get audio and visime data from queue and play. """ while not self._terminated: try: snd_type, data, visimes, ident = self.queue.get(timeout=2) self.blink(0.5) if not self._processing_queue: self._processing_queue = True self.tts.begin_audio() stopwatch = Stopwatch() with stopwatch: if snd_type == 'wav': self.p = play_wav(data) elif snd_type == 'mp3': self.p = play_mp3(data) if visimes: if self.show_visimes(visimes): self.clear_queue() else: self.p.communicate() self.p.wait() send_playback_metric(stopwatch, ident) if self.queue.empty(): self.tts.end_audio() self._processing_queue = False self._clear_visimes = False self.blink(0.2) except Empty: pass except Exception as e: LOG.exception(e) if self._processing_queue: self.tts.end_audio() self._processing_queue = False
def handle_converse_request(self, message): """ Check if the targeted skill id can handle conversation If supported, the conversation is invoked. """ skill_id = message.data["skill_id"] utterances = message.data["utterances"] lang = message.data["lang"] # loop trough skills list and call converse for skill with skill_id for skill in self.loaded_skills: if (self.loaded_skills[skill]["instance"] and self.loaded_skills[skill]["id"] == skill_id): try: instance = self.loaded_skills[skill]["instance"] except BaseException: LOG.error("converse requested but skill not loaded") self.bus.emit( message.reply("skill.converse.response", { "skill_id": 0, "result": False })) return try: result = instance.converse(utterances, lang) self.bus.emit( message.reply("skill.converse.response", { "skill_id": skill_id, "result": result })) return except BaseException: LOG.exception("Error in converse method for skill " + str(skill_id)) self.bus.emit( message.reply("skill.converse.response", { "skill_id": 0, "result": False }))
def on_error(self, ws, error): """ On error start trying to reconnect to the websocket. """ if isinstance(error, WebSocketConnectionClosedException): LOG.warning('Could not send message because connection has closed') else: LOG.exception('=== ' + repr(error) + ' ===') try: self.emitter.emit('error', error) if self.client.keep_running: self.client.close() except Exception as e: LOG.error('Exception closing websocket: ' + repr(e)) LOG.warning("WS Client will reconnect in %d seconds." % self.retry) time.sleep(self.retry) self.retry = min(self.retry * 2, 60) try: self.client = self.create_client() self.run_forever() except WebSocketException: pass
def _load_or_reload_skill(self, skill_path): """ Check if unloaded skill or changed skill needs reloading and perform loading if necessary. Returns True if the skill was loaded/reloaded """ skill_path = skill_path.rstrip('/') skill = self.loaded_skills.setdefault(skill_path, {}) skill.update({"id": basename(skill_path), "path": skill_path}) # check if folder is a skill (must have __init__.py) if not MainModule + ".py" in os.listdir(skill_path): return False # getting the newest modified date of skill modified = _get_last_modified_date(skill_path) last_mod = skill.get("last_modified", 0) # checking if skill is loaded and hasn't been modified on disk if skill.get("loaded") and modified <= last_mod: return False # Nothing to do! # check if skill was modified elif skill.get("instance") and modified > last_mod: # check if skill has been blocked from reloading if (not skill["instance"].reload_skill or not skill.get('active', True)): return False LOG.debug("Reloading Skill: " + basename(skill_path)) # removing listeners and stopping threads try: skill["instance"].default_shutdown() except Exception: LOG.exception("An error occured while shutting down {}".format( skill["instance"].name)) if DEBUG: gc.collect() # Collect garbage to remove false references # Remove two local references that are known refs = sys.getrefcount(skill["instance"]) - 2 if refs > 0: msg = ("After shutdown of {} there are still " "{} references remaining. The skill " "won't be cleaned from memory.") LOG.warning(msg.format(skill['instance'].name, refs)) del skill["instance"] self.bus.emit( Message("owo.skills.shutdown", { "path": skill_path, "id": skill["id"] })) skill["loaded"] = True desc = create_skill_descriptor(skill_path) skill["instance"] = load_skill(desc, self.bus, skill["id"], BLACKLISTED_SKILLS) skill["last_modified"] = modified if skill['instance'] is not None: self.bus.emit( Message( 'owo.skills.loaded', { 'path': skill_path, 'id': skill['id'], 'name': skill['instance'].name, 'modified': modified })) return True else: self.bus.emit( Message('owo.skills.loading_failure', { 'path': skill_path, 'id': skill['id'] })) return False