def execute(self, sentence, ident=None): """ Convert sentence to speech, preprocessing out unsupported ssml The method caches results if possible using the hash of the sentence. Args: sentence: Sentence to be spoken ident: Id reference to current interaction """ sentence = self.validate_ssml(sentence) create_signal("isSpeaking") if self.phonetic_spelling: for word in re.findall(r"[\w']+", sentence): if word.lower() in self.spellings: sentence = sentence.replace(word, self.spellings[word.lower()]) key = str(hashlib.md5(sentence.encode('utf-8', 'ignore')).hexdigest()) wav_file = os.path.join(owo.util.get_cache_directory("tts"), key + '.' + self.audio_ext) if os.path.exists(wav_file): LOG.debug("TTS cache hit") phonemes = self.load_phonemes(key) else: wav_file, phonemes = self.get_tts(sentence, wav_file) if phonemes: self.save_phonemes(key, phonemes) vis = self.visime(phonemes) self.queue.put((self.audio_ext, wav_file, vis, ident))
def schedule_repeating_event(self, handler, when, frequency, data=None, name=None): """ Schedule a repeating event. Args: handler: method to be called when (datetime): time for calling the handler or None to initially trigger <frequency> seconds from now frequency (float/int): time in seconds between calls data (dict, optional): data to send along to the handler name (str, optional): friendly name parameter """ # Do not schedule if this event is already scheduled by the skill if name not in self.scheduled_repeats: data = data or {} if not when: when = datetime.now() + timedelta(seconds=frequency) self._schedule_event(handler, when, data, name, frequency) else: LOG.debug('The event is already scheduled, cancel previous ' 'event if this scheduling should replace the last.')
def transcribe(self, audio): try: # Invoke the STT engine on the audio clip text = self.stt.execute(audio).lower().strip() LOG.debug("STT: " + text) return text except sr.RequestError as e: LOG.error("Could not request Speech Recognition {0}".format(e)) except ConnectionError as e: LOG.error("Connection Error: {0}".format(e)) self.emitter.emit("recognizer_loop:no_internet") except HTTPError as e: if e.response.status_code == 401: LOG.warning("Access Denied at owo.ai") return "pair my device" # phrase to start the pairing process else: LOG.error(e.__class__.__name__ + ': ' + str(e)) except RequestException as e: LOG.error(e.__class__.__name__ + ': ' + str(e)) except Exception as e: self.emitter.emit('recognizer_loop:speech.recognition.unknown') if isinstance(e, IndexError): LOG.info('no words were transcribed') else: LOG.error(e) LOG.error("Speech Recognition could not understand audio") return None if connected(): dialog_name = 'backend.down' else: dialog_name = 'not connected to the internet' self.emitter.emit('speak', {'utterance': dialog.get(dialog_name)})
def enable_intent(self, intent_name): """ (Re)Enable a registered intent if it belongs to this skill Args: intent_name: name of the intent to be enabled Returns: bool: True if enabled, False if it wasn't registered """ names = [intent[0] for intent in self.registered_intents] intents = [intent[1] for intent in self.registered_intents] if intent_name in names: intent = intents[names.index(intent_name)] self.registered_intents.remove((intent_name, intent)) if ".intent" in intent_name: self.register_intent_file(intent_name, None) else: intent.name = intent_name self.register_intent(intent, None) LOG.debug('Enabling intent ' + intent_name) return True LOG.error('Could not enable ' + intent_name + ', it hasn\'t been ' 'registered.') return False
def get(phrase, lang=None, context=None): """ Looks up a resource file for the given phrase. If no file is found, the requested phrase is returned as the string. This will use the default language for translations. Args: phrase (str): resource phrase to retrieve/translate lang (str): the language to use context (dict): values to be inserted into the string Returns: str: a randomized and/or translated version of the phrase """ if not lang: from owo.configuration import Configuration lang = Configuration.get().get("lang") filename = "text/" + lang.lower() + "/" + phrase + ".dialog" template = resolve_resource_file(filename) if not template: LOG.debug("Resource file not found: " + filename) return phrase stache = MustacheDialogRenderer() stache.load_template_file("template", template) if not context: context = {} return stache.render("template", context)
def load_vocab_files(self, root_directory): vocab_dir = join(root_directory, 'vocab', self.lang) if exists(vocab_dir): load_vocabulary(vocab_dir, self.bus, self.skill_id) elif exists(join(root_directory, 'locale', self.lang)): load_vocabulary(join(root_directory, 'locale', self.lang), self.bus, self.skill_id) else: LOG.debug('No vocab loaded')
def init_dialog(self, root_directory): # If "<skill>/dialog/<lang>" exists, load from there. Otherwise # load dialog from "<skill>/locale/<lang>" dialog_dir = join(root_directory, 'dialog', self.lang) if exists(dialog_dir): self.dialog_renderer = DialogLoader().load(dialog_dir) elif exists(join(root_directory, 'locale', self.lang)): locale_path = join(root_directory, 'locale', self.lang) self.dialog_renderer = DialogLoader().load(locale_path) else: LOG.debug('No dialog loaded')
def play(self): """ Start playback. """ self.cast.quit_app() track = self.tracklist[0] # Report start of playback to audioservice if self._track_start_callback: self._track_start_callback(track) LOG.debug('track: {}, type: {}'.format(track, guess_type(track))) mime = guess_type(track)[0] or 'audio/mp3' self.cast.play_media(track, mime)
def run(self): self.start_async() while self.state.running: try: time.sleep(1) if self._config_hash != hash(str(Configuration().get())): LOG.debug('Config has changed, reloading...') self.reload() except KeyboardInterrupt as e: LOG.error(e) self.stop() raise # Re-raise KeyboardInterrupt
def _save_uuid(self, uuid): """ Saves uuid. Args: uuid (str): uuid, unique id of new settingsmeta """ LOG.debug("saving uuid {}".format(str(uuid))) directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) uuid_file = join(directory, 'uuid') with open(uuid_file, 'w') as f: f.write(str(uuid))
def _save_hash(self, hashed_meta): """ Saves hashed_meta to settings directory. Args: hashed_meta (str): hash of new settingsmeta """ LOG.debug("saving hash {}".format(hashed_meta)) directory = self.config.get("skills")["directory"] directory = join(directory, self.name) directory = expanduser(directory) hash_file = join(directory, 'hash') with open(hash_file, 'w') as f: f.write(hashed_meta)
def _register_object(self, message, object_name, register_func): file_name = message.data['file_name'] name = message.data['name'] LOG.debug('Registering Padatious ' + object_name + ': ' + name) if not isfile(file_name): LOG.warning('Could not find file ' + file_name) return register_func(name, file_name) self.train_time = get_time() + self.train_delay self.wait_and_train()
def _load_settings_meta(self): """ Loads settings metadata from skills path. """ if isfile(self._meta_path): try: with open(self._meta_path) as f: data = json.load(f) return data except Exception as e: LOG.error("Failed to load setting file: " + self._meta_path) LOG.error(repr(e)) return None else: LOG.debug("settingemeta.json does not exist") return None
def _restore_volume(self, message): """ Is triggered when OwO is done speaking and restores the volume Args: message: message bus message, not used but required """ if self.current: LOG.debug('restoring volume') self.volume_is_low = False time.sleep(2) if not self.volume_is_low: self.current.restore_volume() if self.pulse_restore: self.pulse_restore()
def initialize_remote_settings(self): """ initializes the remote settings to the server """ # if settingsmeta.json exists (and is valid) # this block of code is a control flow for # different scenarios that may arises with settingsmeta self.load_skill_settings_from_file() # loads existing settings.json settings_meta = self._load_settings_meta() if not settings_meta: return if not is_paired(): return self._device_identity = self.api.identity.uuid self._api_path = "/" + self._device_identity + "/skill" self._user_identity = self.api.get()['user']['uuid'] LOG.debug("settingsmeta.json exist for {}".format(self.name)) hashed_meta = self._get_meta_hash(settings_meta) skill_settings = self._request_other_settings(hashed_meta) # if hash is new then there is a diff version of settingsmeta if self._is_new_hash(hashed_meta): # first look at all other devices on user account to see # if the settings exist. if it does then sync with device if skill_settings: # not_owner flags that this settings is loaded from # another device. If a skill settings doesn't have # not_owner, then the skill is created from that device self['not_owner'] = True self.save_skill_settings(skill_settings) else: # upload skill settings if uuid = self._load_uuid() if uuid is not None: self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta) else: # hash is not new if skill_settings is not None: self['not_owner'] = True self.save_skill_settings(skill_settings) else: settings = self._request_my_settings(hashed_meta) if settings is None: LOG.debug("seems like it got deleted from home... " "sending settingsmeta.json for " "{}".format(self.name)) self._upload_meta(settings_meta, hashed_meta) else: self.save_skill_settings(settings) self._complete_intialization = True
def _lower_volume(self, message=None): """ Is triggered when OwO starts to speak and reduces the volume. Args: message: message bus message, not used but required """ if self.current: LOG.debug('lowering volume') self.current.lower_volume() self.volume_is_low = True try: if self.pulse_quiet: self.pulse_quiet() except Exception as exc: LOG.error(exc)
def _stop(self, message=None): """ Handler for owo.stop. Stops any playing service. Args: message: message bus message, not used but required """ LOG.debug('stopping all playing services') with self.service_lock: if self.current: name = self.current.name if self.current.stop(): self.bus.emit( Message("owo.stop.handled", {"by": "audio:" + name})) self.current = None
def _delete_metadata(self, uuid): """ Deletes the current skill metadata Args: uuid (str): unique id of the skill """ try: LOG.debug("deleting metadata") self.api.request({ "method": "DELETE", "path": self._api_path + "/{}".format(uuid) }) except Exception as e: LOG.error(e) LOG.error("cannot delete metadata because this" "device is not original uploader of skill")
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 load_phonemes(self, key): """ Load phonemes from cache file. Args: Key: Key identifying phoneme cache """ pho_file = os.path.join(owo.util.get_cache_directory("tts"), key + ".pho") if os.path.exists(pho_file): try: with open(pho_file, "r") as cachefile: phonemes = cachefile.read().strip() return phonemes except: LOG.debug("Failed to read .PHO from cache") return None
def handle_fallback(self, message): if not self.finished_training_event.is_set(): LOG.debug('Waiting for Padatious training to finish...') return False utt = message.data.get('utterance') LOG.debug("Padatious fallback attempt: " + utt) data = self.calc_intent(utt) if data.conf < 0.5: return False data.matches['utterance'] = utt self.service.add_active_skill(data.name.split(':')[0]) self.bus.emit(message.reply(data.name, data=data.matches)) return True
def _upload_meta(self, settings_meta, hashed_meta): """ uploads the new meta data to settings with settings migration Args: settings_meta (dict): settingsmeta.json hashed_meta (str): {skill-folder}-settinsmeta.json """ LOG.debug("sending settingsmeta.json for {}".format(self.name) + " to servers") meta = self._migrate_settings(settings_meta) meta['identifier'] = str(hashed_meta) response = self._send_settings_meta(meta) if response and 'uuid' in response: self._save_uuid(response['uuid']) if 'not_owner' in self: del self['not_owner'] self._save_hash(hashed_meta)
def flush(self): publisher = MetricsPublisher() payload = { 'counters': self._counters, 'timers': self._timers, 'levels': self._levels, 'attributes': self._attributes } self.clear() count = (len(payload['counters']) + len(payload['timers']) + len(payload['levels'])) if count > 0: LOG.debug(json.dumps(payload)) def publish(): publisher.publish(payload) threading.Thread(target=publish).start()
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 _skip_wake_word(self): # Check if told programatically to skip the wake word, like # when we are in a dialog with the user. if check_for_signal('startListening'): return True # Pressing the Mark 1 button can start recording (unless # it is being used to mean 'stop' instead) if check_for_signal('buttonPress', 1): # give other processes time to consume this signal if # it was meant to be a 'stop' sleep(0.25) if check_for_signal('buttonPress'): # Signal is still here, assume it was intended to # begin recording LOG.debug("Button Pressed, wakeword not needed") return True return False
def _connect(self, message): """ Callback method to connect to mopidy if server is not available at startup. """ url = 'http://localhost:6680' if self.config is not None: url = self.config.get('url', url) try: self.mopidy = Mopidy(url) except: if self.connection_attempts < 1: LOG.debug('Could not connect to server, will retry quietly') self.connection_attempts += 1 time.sleep(10) self.bus.emit(Message('MopidyServiceConnect')) return LOG.info('Connected to mopidy server')
def download_subscriber_voices(selected_voice): """ Function to download all premium voices, starting with the currently selected if applicable """ def make_executable(dest): """ Call back function to make the downloaded file executable. """ LOG.info('Make executable') # make executable st = os.stat(dest) os.chmod(dest, st.st_mode | stat.S_IEXEC) # First download the selected voice if needed voice_file = SUBSCRIBER_VOICES.get(selected_voice) if voice_file is not None and not exists(voice_file): LOG.info('voice doesn\'t exist, downloading') url = DeviceApi().get_subscriber_voice_url(selected_voice) # Check we got an url if url: dl = download(url, voice_file, make_executable) # Wait for completion while not dl.done: sleep(1) else: LOG.debug('{} is not available for this architecture' .format(selected_voice)) # Download the rest of the subsciber voices as needed for voice in SUBSCRIBER_VOICES: voice_file = SUBSCRIBER_VOICES[voice] if not exists(voice_file): url = DeviceApi().get_subscriber_voice_url(voice) # Check we got an url if url: dl = download(url, voice_file, make_executable) # Wait for completion while not dl.done: sleep(1) else: LOG.debug('{} is not available for this architecture' .format(voice))
def store(self, force=False): """ Store dictionary to file if a change has occured. Args: force: Force write despite no change """ if force or not self._is_stored: with open(self._settings_path, 'w') as f: json.dump(self, f) self.loaded_hash = hash(json.dumps(self, sort_keys=True)) if self._should_upload_from_change: settings_meta = self._load_settings_meta() hashed_meta = self._get_meta_hash(settings_meta) uuid = self._load_uuid() if uuid is not None: LOG.debug("deleting meta data for {}".format(self.name)) self._delete_metadata(uuid) self._upload_meta(settings_meta, hashed_meta)
def disable_intent(self, intent_name): """ Disable a registered intent if it belongs to this skill Args: intent_name (string): name of the intent to be disabled Returns: bool: True if disabled, False if it wasn't registered """ names = [intent_tuple[0] for intent_tuple in self.registered_intents] if intent_name in names: LOG.debug('Disabling intent ' + intent_name) name = str(self.skill_id) + ':' + intent_name self.bus.emit(Message("detach_intent", {"intent_name": name})) return True LOG.error('Could not disable ' + intent_name + ', it hasn\'t been registered.') return False
def _request_other_settings(self, identifier): """ Retrieves user skill from other devices by identifier (hashed_meta) Args: identifier (str): identifier for this skill Returns: settings (dict or None): returns the settings if true else None """ LOG.debug("syncing settings with other devices " "from server for {}".format(self.name)) path = \ "/" + self._device_identity + "/userSkill?identifier=" + identifier user_skill = self.api.request({"method": "GET", "path": path}) if len(user_skill) == 0: return None else: settings = self._type_cast(user_skill[0], to_platform='core') return settings