hotword_factory.py
Go to the documentation of this file.
1 # Copyright 2017 Mycroft AI Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15 import time
16 from time import sleep
17 
18 import os
19 import platform
20 import posixpath
21 import tempfile
22 import requests
23 from contextlib import suppress
24 from glob import glob
25 from os.path import dirname, exists, join, abspath, expanduser, isfile, isdir
26 from petact import install_package
27 from shutil import rmtree
28 from threading import Timer, Event, Thread
29 from urllib.error import HTTPError
30 
31 from mycroft.configuration import Configuration, LocalConf, USER_CONFIG
32 from mycroft.util.log import LOG
33 
34 RECOGNIZER_DIR = join(abspath(dirname(__file__)), "recognizer")
35 INIT_TIMEOUT = 10 # In seconds
36 
37 
38 class TriggerReload(Exception):
39  pass
40 
41 
42 class NoModelAvailable(Exception):
43  pass
44 
45 
47  def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"):
48  self.key_phrase = str(key_phrase).lower()
49  # rough estimate 1 phoneme per 2 chars
50  self.num_phonemes = len(key_phrase) / 2 + 1
51  if config is None:
52  config = Configuration.get().get("hot_words", {})
53  config = config.get(self.key_phrase, {})
54  self.config = config
55  self.listener_config = Configuration.get().get("listener", {})
56  self.lang = str(self.config.get("lang", lang)).lower()
57 
58  def found_wake_word(self, frame_data):
59  return False
60 
61  def update(self, chunk):
62  pass
63 
64  def stop(self):
65  """ Perform any actions needed to shut down the hot word engine.
66 
67  This may include things such as unload loaded data or shutdown
68  external processess.
69  """
70  pass
71 
72 
73 class PocketsphinxHotWord(HotWordEngine):
74  def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"):
75  super(PocketsphinxHotWord, self).__init__(key_phrase, config, lang)
76  # Hotword module imports
77  from pocketsphinx import Decoder
78  # Hotword module params
79  self.phonemes = self.config.get("phonemes", "HH EY . M AY K R AO F T")
80  self.num_phonemes = len(self.phonemes.split())
81  self.threshold = self.config.get("threshold", 1e-90)
82  self.sample_rate = self.listener_config.get("sample_rate", 1600)
83  dict_name = self.create_dict(self.key_phrase, self.phonemes)
84  config = self.create_config(dict_name, Decoder.default_config())
85  self.decoder = Decoder(config)
86 
87  def create_dict(self, key_phrase, phonemes):
88  (fd, file_name) = tempfile.mkstemp()
89  words = key_phrase.split()
90  phoneme_groups = phonemes.split('.')
91  with os.fdopen(fd, 'w') as f:
92  for word, phoneme in zip(words, phoneme_groups):
93  f.write(word + ' ' + phoneme + '\n')
94  return file_name
95 
96  def create_config(self, dict_name, config):
97  model_file = join(RECOGNIZER_DIR, 'model', self.lang, 'hmm')
98  if not exists(model_file):
99  LOG.error('PocketSphinx model not found at ' + str(model_file))
100  config.set_string('-hmm', model_file)
101  config.set_string('-dict', dict_name)
102  config.set_string('-keyphrase', self.key_phrase)
103  config.set_float('-kws_threshold', float(self.threshold))
104  config.set_float('-samprate', self.sample_rate)
105  config.set_int('-nfft', 2048)
106  config.set_string('-logfn', '/dev/null')
107  return config
108 
109  def transcribe(self, byte_data, metrics=None):
110  start = time.time()
111  self.decoder.start_utt()
112  self.decoder.process_raw(byte_data, False, False)
113  self.decoder.end_utt()
114  if metrics:
115  metrics.timer("mycroft.stt.local.time_s", time.time() - start)
116  return self.decoder.hyp()
117 
118  def found_wake_word(self, frame_data):
119  hyp = self.transcribe(frame_data)
120  return hyp and self.key_phrase in hyp.hypstr.lower()
121 
122 
124  def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"):
125  super(PreciseHotword, self).__init__(key_phrase, config, lang)
126  from precise_runner import (
127  PreciseRunner, PreciseEngine, ReadWriteStream
128  )
129  local_conf = LocalConf(USER_CONFIG)
130  if local_conf.get('precise', {}).get('dist_url') == \
131  'http://bootstrap.mycroft.ai/artifacts/static/daily/':
132  del local_conf['precise']['dist_url']
133  local_conf.store()
134  Configuration.updated(None)
135 
136  self.download_complete = True
137 
138  self.show_download_progress = Timer(0, lambda: None)
139  precise_config = Configuration.get()['precise']
140  precise_exe = self.install_exe(precise_config['dist_url'])
141 
142  local_model = self.config.get('local_model_file')
143  if local_model:
144  self.precise_model = expanduser(local_model)
145  else:
146  self.precise_model = self.install_model(
147  precise_config['model_url'], key_phrase.replace(' ', '-')
148  ).replace('.tar.gz', '.pb')
149 
150  self.has_found = False
151  self.stream = ReadWriteStream()
152 
153  def on_activation():
154  self.has_found = True
155 
156  trigger_level = self.config.get('trigger_level', 3)
157  sensitivity = self.config.get('sensitivity', 0.5)
158 
159  self.runner = PreciseRunner(
160  PreciseEngine(precise_exe, self.precise_model),
161  trigger_level, sensitivity,
162  stream=self.stream, on_activation=on_activation,
163  )
164  self.runner.start()
165 
166  @property
167  def folder(self):
168  return join(expanduser('~'), '.mycroft', 'precise')
169 
170  def install_exe(self, url: str) -> str:
171  url = url.format(arch=platform.machine())
172  if not url.endswith('.tar.gz'):
173  url = requests.get(url).text.strip()
174  if install_package(
175  url, self.folder,
176  on_download=self.on_download, on_complete=self.on_complete
177  ):
178  raise TriggerReload
179  return join(self.folder, 'precise-engine', 'precise-engine')
180 
181  def install_model(self, url: str, wake_word: str) -> str:
182  model_url = url.format(wake_word=wake_word)
183  model_file = join(self.folder, posixpath.basename(model_url))
184  try:
185  install_package(
186  model_url, self.folder,
187  on_download=lambda: LOG.info('Updated precise model')
188  )
189  except (HTTPError, ValueError):
190  if isfile(model_file):
191  LOG.info("Couldn't find remote model. Using local file")
192  else:
193  raise NoModelAvailable('Failed to download model:', model_url)
194  return model_file
195 
196  @staticmethod
197  def _snd_msg(cmd):
198  with suppress(OSError):
199  with open('/dev/ttyAMA0', 'w') as f:
200  print(cmd, file=f)
201 
202  def on_download(self):
203  LOG.info('Downloading Precise executable...')
204  if isdir(join(self.folder, 'precise-stream')):
205  rmtree(join(self.folder, 'precise-stream'))
206  for old_package in glob(join(self.folder, 'precise-engine_*.tar.gz')):
207  os.remove(old_package)
208  self.download_complete = False
209  self.show_download_progress = Timer(
210  5, self.during_download, args=[True]
211  )
212  self.show_download_progress.start()
213 
214  def during_download(self, first_run=False):
215  LOG.info('Still downloading executable...')
216  if first_run: # TODO: Localize
217  self._snd_msg('mouth.text=Updating listener...')
218  if not self.download_complete:
219  self.show_download_progress = Timer(30, self.during_download)
220  self.show_download_progress.start()
221 
222  def on_complete(self):
223  LOG.info('Precise download complete!')
224  self.download_complete = True
225  self.show_download_progress.cancel()
226  self._snd_msg('mouth.reset')
227 
228  def update(self, chunk):
229  self.stream.write(chunk)
230 
231  def found_wake_word(self, frame_data):
232  if self.has_found:
233  self.has_found = False
234  return True
235  return False
236 
237  def stop(self):
238  if self.runner:
239  self.runner.stop()
240 
241 
243  def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us"):
244  super(SnowboyHotWord, self).__init__(key_phrase, config, lang)
245  # Hotword module imports
246  from snowboydecoder import HotwordDetector
247  # Hotword module config
248  module = self.config.get("module")
249  if module != "snowboy":
250  LOG.warning(module + " module does not match with Hotword class "
251  "snowboy")
252  # Hotword params
253  models = self.config.get("models", {})
254  paths = []
255  for key in models:
256  paths.append(models[key])
257  sensitivity = self.config.get("sensitivity", 0.5)
258  self.snowboy = HotwordDetector(paths,
259  sensitivity=[sensitivity] * len(paths))
260  self.lang = str(lang).lower()
261  self.key_phrase = str(key_phrase).lower()
262 
263  def found_wake_word(self, frame_data):
264  wake_word = self.snowboy.detector.RunDetection(frame_data)
265  return wake_word == 1
266 
267 
269  CLASSES = {
270  "pocketsphinx": PocketsphinxHotWord,
271  "precise": PreciseHotword,
272  "snowboy": SnowboyHotWord
273  }
274 
275  @staticmethod
276  def load_module(module, hotword, config, lang, loop):
277  LOG.info('Loading "{}" wake word via {}'.format(hotword, module))
278  instance = None
279  complete = Event()
280 
281  def initialize():
282  nonlocal instance, complete
283  try:
284  clazz = HotWordFactory.CLASSES[module]
285  instance = clazz(hotword, config, lang=lang)
286  except TriggerReload:
287  complete.set()
288  sleep(0.5)
289  loop.reload()
290  except NoModelAvailable:
291  LOG.warning('Could not found find model for {} on {}.'.format(
292  hotword, module
293  ))
294  instance = None
295  except Exception:
296  LOG.exception(
297  'Could not create hotword. Falling back to default.')
298  instance = None
299  complete.set()
300 
301  Thread(target=initialize, daemon=True).start()
302  if not complete.wait(INIT_TIMEOUT):
303  LOG.info('{} is taking too long to load'.format(module))
304  complete.set()
305  return instance
306 
307  @classmethod
308  def create_hotword(cls, hotword="hey mycroft", config=None,
309  lang="en-us", loop=None):
310  if not config:
311  config = Configuration.get()['hotwords']
312  config = config[hotword]
313 
314  module = config.get("module", "precise")
315  return cls.load_module(module, hotword, config, lang, loop) or \
316  cls.load_module('pocketsphinx', hotword, config, lang, loop) or \
317  cls.CLASSES['pocketsphinx']()
def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us")
def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us")
def load_module(module, hotword, config, lang, loop)
def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us")
def __init__(self, key_phrase="hey mycroft", config=None, lang="en-us")
def get(phrase, lang=None, context=None)
def create_hotword(cls, hotword="hey mycroft", config=None, lang="en-us", loop=None)


mycroft_ros
Author(s):
autogenerated on Mon Apr 26 2021 02:35:40