def validate_connection(self): try: subprocess.call([BIN, '--version']) except: LOG.info("Failed to find mimic at: " + BIN) raise Exception( 'Mimic was not found. Run install-mimic.sh to install it.')
def during_download(self, first_run=False): LOG.info('Still downloading executable...') if first_run: # TODO: Localize self._snd_msg('mouth.text=Updating listener...') if not self.download_complete: self.show_download_progress = Timer(30, self.during_download) self.show_download_progress.start()
def get_services(services_folder): """ Load and initialize services from all subfolders. Args: services_folder: base folder to look for services in. Returns: Sorted list of audio services. """ LOG.info("Loading services from " + services_folder) services = [] possible_services = listdir(services_folder) for i in possible_services: location = join(services_folder, i) if (isdir(location) and not MAINMODULE + ".py" in listdir(location)): for j in listdir(location): name = join(location, j) if (not isdir(name) or not MAINMODULE + ".py" in listdir(name)): continue try: services.append(create_service_descriptor(name)) except Exception: LOG.error('Failed to create service from ' + name, exc_info=True) if (not isdir(location) or not MAINMODULE + ".py" in listdir(location)): continue try: services.append(create_service_descriptor(location)) except Exception: LOG.error('Failed to create service from ' + location, exc_info=True) return sorted(services, key=lambda p: p.get('name'))
def load_module(module, hotword, config, lang, loop): LOG.info('Loading "{}" wake word via {}'.format(hotword, module)) instance = None complete = Event() 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() Thread(target=initialize, daemon=True).start() if not complete.wait(INIT_TIMEOUT): LOG.info('{} is taking too long to load'.format(module)) complete.set() return instance
def handle_utterance(event): LOG.info("Utterance: " + str(event['utterances'])) context = {'client_name': 'OwO_listener'} if 'ident' in event: ident = event.pop('ident') context['ident'] = ident bus.emit(Message('recognizer_loop:utterance', event, context))
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 supported_uris(self): """ Return supported uris of chromecast. """ LOG.info("Chromecasts found: " + str(self.cast)) if self.cast: return ['http', 'https'] else: return []
def _do_net_check(self): # TODO: This should live in the derived Enclosure, e.g. Enclosure_Mark1 LOG.info("Checking internet connection") if not connected(): # and self.conn_monitor is None: if has_been_paired(): # TODO: Enclosure/localization self.speak("This unit is not connected to the Internet. " "Either plug in a network cable or hold the " "button on top for two seconds, then select " "wifi from the menu") else: # Begin the unit startup process, this is the first time it # is being run with factory defaults. # TODO: This logic should be in Enclosure_Mark1 # TODO: Enclosure/localization # Don't listen to mic during this out-of-box experience self.bus.emit(Message("owo.mic.mute")) # Setup handler to unmute mic at the end of on boarding # i.e. after pairing is complete self.bus.once('owo.paired', self._handle_pairing_complete) self.speak(owo.dialog.get('owo.intro')) wait_while_speaking() time.sleep(2) # a pause sounds better than just jumping in # Kick off wifi-setup automatically data = {'allow_timeout': False, 'lang': self.lang} self.bus.emit(Message('system.wifi.setup', data))
def mute_and_speak(utterance, ident): """ Mute mic and start speaking the utterance using selected tts backend. Args: utterance: The sentence to be spoken ident: Ident tying the utterance to the source query """ global tts_hash # update TTS object if configuration has changed if tts_hash != hash(str(config.get('tts', ''))): global tts # Stop tts playback thread tts.playback.stop() tts.playback.join() # Create new tts instance tts = TTSFactory.create() tts.init(bus) tts_hash = hash(str(config.get('tts', ''))) LOG.info("Speak: " + utterance) try: tts.execute(utterance, ident) except Exception as e: LOG.error('TTS execution failed ({})'.format(repr(e)))
def is_speaking(): """Determine if Text to Speech is occurring Returns: bool: True while still speaking """ LOG.info("owo.utils.is_speaking() is depreciated, use " "owo.audio.is_speaking() instead.") return owo.audio.is_speaking()
def wait_while_speaking(): """Pause as long as Text to Speech is still happening Pause while Text to Speech is still happening. This always pauses briefly to ensure that any preceeding request to speak has time to begin. """ LOG.info("owo.utils.wait_while_speaking() is depreciated, use " "owo.audio.wait_while_speaking() instead.") return owo.audio.wait_while_speaking()
def listen(self, source, emitter): """Listens for chunks of audio that OwO should perform STT on. This will listen continuously for a wake-up-word, then return the audio chunk containing the spoken phrase that comes immediately afterwards. Args: source (AudioSource): Source producing the audio chunks emitter (EventEmitter): Emitter for notifications of when recording begins and ends. Returns: AudioData: audio with the user's utterance, minus the wake-up-word """ assert isinstance(source, AudioSource), "Source must be an AudioSource" # bytes_per_sec = source.SAMPLE_RATE * source.SAMPLE_WIDTH sec_per_buffer = float(source.CHUNK) / source.SAMPLE_RATE # Every time a new 'listen()' request begins, reset the threshold # used for silence detection. This is as good of a reset point as # any, as we expect the user and OwO to not be talking. # NOTE: adjust_for_ambient_noise() doc claims it will stop early if # speech is detected, but there is no code to actually do that. self.adjust_for_ambient_noise(source, 1.0) LOG.debug("Waiting for wake word...") self._wait_until_wake_word(source, sec_per_buffer) if self._stop_signaled: return LOG.debug("Recording...") emitter.emit("recognizer_loop:record_begin") # If enabled, play a wave file with a short sound to audibly # indicate recording has begun. if self.config.get('confirm_listening'): file = resolve_resource_file( self.config.get('sounds').get('start_listening')) if file: play_wav(file) frame_data = self._record_phrase(source, sec_per_buffer) audio_data = self._create_audio_data(frame_data, source) emitter.emit("recognizer_loop:record_end") if self.save_utterances: LOG.info("Recording utterance") stamp = str(datetime.datetime.now()) filename = "/tmp/OwO_utterance%s.wav" % stamp with open(filename, 'wb') as filea: filea.write(audio_data.get_wav_data()) LOG.debug("Thinking...") return audio_data
def on_download(self): LOG.info('Downloading Precise executable...') if isdir(join(self.folder, 'precise-stream')): rmtree(join(self.folder, 'precise-stream')) for old_package in glob(join(self.folder, 'precise-engine_*.tar.gz')): os.remove(old_package) self.download_complete = False self.show_download_progress = Timer(5, self.during_download, args=[True]) self.show_download_progress.start()
def stop(self): """ Stop vlc playback. """ LOG.info('VLCService Stop') if self.player.is_playing(): # Restore volume if lowered self.restore_volume() self.clear_list() self.list_player.stop() return True else: return False
def autodetect(config, bus): """ Autodetect chromecasts on the network and create backends for each """ casts = pychromecast.get_chromecasts(timeout=5, tries=2, retry_wait=2) ret = [] for c in casts: LOG.info(c.name + " found.") ret.append(ChromecastService(config, bus, c.name.lower(), c)) return ret
def __init_serial(self): try: self.port = self.config.get("port") self.rate = self.config.get("rate") self.timeout = self.config.get("timeout") self.serial = serial.serial_for_url( url=self.port, baudrate=self.rate, timeout=self.timeout) LOG.info("Connected to: %s rate: %s timeout: %s" % (self.port, self.rate, self.timeout)) except: LOG.error("Impossible to connect to serial port: "+str(self.port)) raise
def train(self, message=None): if message is None: single_thread = False else: single_thread = message.data.get('single_thread', False) self.finished_training_event.clear() LOG.info('Training... (single_thread={})'.format(single_thread)) self.container.train(single_thread=single_thread) LOG.info('Training complete.') self.finished_training_event.set() self.finished_initial_train = True
def deactivate_except(self, message): """ Deactivate all skills except the provided. """ try: skill_to_keep = message.data['skill'] LOG.info('DEACTIVATING ALL SKILLS EXCEPT {}'.format(skill_to_keep)) if skill_to_keep in [basename(i) for i in self.loaded_skills]: for skill in self.loaded_skills: if basename(skill) != skill_to_keep: self.__deactivate_skill(skill) else: LOG.info('Couldn\'t find skill') except Exception as e: LOG.error('Error during skill removal, {}'.format(repr(e)))
def _play(self, message=None): """ Implementation specific async method to handle playback. This allows mpg123 service to use the "next method as well as basic play/stop. """ LOG.info('SimpleAudioService._play') self._is_playing = True if isinstance(self.tracks[self.index], list): track = self.tracks[self.index][0] mime = self.tracks[self.index][1] mime = mime.split('/') else: # Assume string track = self.tracks[self.index] mime = find_mime(track) # Indicate to audio service which track is being played if self._track_start_callback: self._track_start_callback(track) # Replace file:// uri's with normal paths track = track.replace('file://', '') try: if 'mpeg' in mime[1]: self.process = play_mp3(track) elif 'ogg' in mime[1]: self.process = play_ogg(track) elif 'wav' in mime[1]: self.process = play_wav(track) else: # If no mime info could be determined guess mp3 self.process = play_mp3(track) except FileNotFoundError as e: LOG.error('Couldn\'t play audio, {}'.format(repr(e))) self.process = None # Wait for completion or stop request while (self.process and self.process.poll() is None and not self._stop_signal): sleep(0.25) if self._stop_signal: self.process.terminate() self.process = None self._is_playing = False return self.index += 1 # if there are more tracks available play next if self.index < len(self.tracks): self.bus.emit(Message('SimpleAudioServicePlay')) else: self._is_playing = False
def _hack_check_for_duplicates(self): # TEMPORARY HACK: Look for multiple instance of the # OwO-speech-client and/or OwO-skills services, which could # happen when upgrading a shipping Mark 1 from release 0.8.17 or # before. When found, force the unit to reboot. import psutil LOG.info("Hack to check for duplicate service instances") count_instances = 0 needs_reboot = False for process in psutil.process_iter(): if process.cmdline() == ['python2.7', '/usr/local/bin/OwO-speech-client']: count_instances += 1 if (count_instances > 1): LOG.info("Duplicate OwO-speech-client found") needs_reboot = True count_instances = 0 for process in psutil.process_iter(): if process.cmdline() == ['python2.7', '/usr/local/bin/OwO-skills']: count_instances += 1 if (count_instances > 1): LOG.info("Duplicate OwO-skills found") needs_reboot = True if needs_reboot: LOG.info("Hack reboot...") self.reader.process("unit.reboot") self.bus.emit(Message("enclosure.eyes.spin")) self.bus.emit(Message("enclosure.mouth.reset"))
def install_model(self, url: str, wake_word: str) -> str: model_url = url.format(wake_word=wake_word) model_file = join(self.folder, posixpath.basename(model_url)) try: install_package( model_url, self.folder, on_download=lambda: LOG.info('Updated precise model')) except HTTPError: if isfile(model_file): LOG.info("Couldn't find remote model. Using local file") else: raise RuntimeError('Failed to download model:', model_url) return model_file
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 get(): """ get the active session. :return: An active session """ config = Configuration.get().get('session') with SessionManager.__lock: if (not SessionManager.__current_session or SessionManager.__current_session.expired()): SessionManager.__current_session = Session( str(uuid4()), expiration_seconds=config.get('ttl', 180)) LOG.info("New Session Start: " + SessionManager.__current_session.session_id) return SessionManager.__current_session
def curate_cache(directory, min_free_percent=5.0, min_free_disk=50): """Clear out the directory if needed This assumes all the files in the directory can be deleted as freely Args: directory (str): directory path that holds cached files min_free_percent (float): percentage (0.0-100.0) of drive to keep free, default is 5% if not specified. min_free_disk (float): minimum allowed disk space in MB, default value is 50 MB if not specified. """ # Simpleminded implementation -- keep a certain percentage of the # disk available. # TODO: Would be easy to add more options, like whitelisted files, etc. space = psutil.disk_usage(directory) # convert from MB to bytes min_free_disk *= 1024 * 1024 # space.percent = space.used/space.total*100.0 percent_free = 100.0 - space.percent if percent_free < min_free_percent and space.free < min_free_disk: LOG.info('Low diskspace detected, cleaning cache') # calculate how many bytes we need to delete bytes_needed = (min_free_percent - percent_free) / 100.0 * space.total bytes_needed = int(bytes_needed + 1.0) # get all entries in the directory w/ stats entries = (os.path.join(directory, fn) for fn in os.listdir(directory)) entries = ((os.stat(path), path) for path in entries) # leave only regular files, insert modification date entries = ((stat[ST_MTIME], stat[ST_SIZE], path) for stat, path in entries if S_ISREG(stat[ST_MODE])) # delete files with oldest modification date until space is freed space_freed = 0 for moddate, fsize, path in sorted(entries): try: os.remove(path) space_freed += fsize except: pass if space_freed > bytes_needed: return # deleted enough!
def main(): """ Main function. Run when file is invoked. """ reset_sigint_handler() check_for_signal("isSpeaking") bus = WebsocketClient() # Connect to the OwO Messagebus Configuration.init(bus) speech.init(bus) LOG.info("Starting Audio Services") bus.on('message', create_echo_function('AUDIO', ['owo.audio.service'])) audio = AudioService(bus) # Connect audio service instance to message bus create_daemon(bus.run_forever) wait_for_exit_signal() speech.shutdown() audio.shutdown()
def _connect(self, message): LOG.info('Trying to connect to chromecast') casts = pychromecast.get_chromecasts() if self.config is None or 'identifier' not in self.config: LOG.error("Chromecast identifier not found!") return # Can't connect since no id is specified else: identifier = self.config['identifier'] for c in casts: if c.name == identifier: self.cast = c break else: LOG.info('Couldn\'t find chromecast ' + identifier) self.connection_attempts += 1 time.sleep(10) self.bus.emit(Message('ChromecastServiceConnect')) return
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 play(self, tracks, prefered_service): """ play starts playing the audio on the prefered service if it supports the uri. If not the next best backend is found. Args: tracks: list of tracks to play. prefered_service: indecates the service the user prefer to play the tracks. """ self._stop() if isinstance(tracks[0], str): uri_type = tracks[0].split(':')[0] else: uri_type = tracks[0][0].split(':')[0] # check if user requested a particular service if prefered_service and uri_type in prefered_service.supported_uris(): selected_service = prefered_service # check if default supports the uri elif self.default and uri_type in self.default.supported_uris(): LOG.debug("Usando backend por defecto({})".format( self.default.name)) selected_service = self.default else: # Check if any other service can play the media LOG.debug("Buscando los servicios") for s in self.service: if uri_type in s.supported_uris(): LOG.debug("El servicio {} soporta la URI {}".format( s, uri_type)) selected_service = s break else: LOG.info('No se encuentran servicios para uri_type: ' + uri_type) return if not selected_service.supports_mime_hints: tracks = [t[0] if isinstance(t, list) else t for t in tracks] selected_service.clear_list() selected_service.add_list(tracks) selected_service.play() self.current = selected_service
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))