scripts/mycroft/util/__init__.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 from __future__ import absolute_import
16 import re
17 import socket
18 import subprocess
19 import pyaudio
20 
21 from os.path import join, expanduser, splitext
22 
23 from threading import Thread
24 from time import sleep
25 
26 import json
27 import os.path
28 import psutil
29 from stat import S_ISREG, ST_MTIME, ST_MODE, ST_SIZE
30 import requests
31 import logging
32 
33 import signal as sig
34 
35 import mycroft.audio
37 from mycroft.util.format import nice_number
38 # Officially exported methods from this file:
39 # play_wav, play_mp3, play_ogg, get_cache_directory,
40 # resolve_resource_file, wait_while_speaking
41 from mycroft.util.log import LOG
42 from mycroft.util.parse import extract_datetime, extract_number, normalize
43 from mycroft.util.signal import *
44 
45 
46 def resolve_resource_file(res_name):
47  """Convert a resource into an absolute filename.
48 
49  Resource names are in the form: 'filename.ext'
50  or 'path/filename.ext'
51 
52  The system wil look for ~/.mycroft/res_name first, and
53  if not found will look at /opt/mycroft/res_name,
54  then finally it will look for res_name in the 'mycroft/res'
55  folder of the source code package.
56 
57  Example:
58  With mycroft running as the user 'bob', if you called
59  resolve_resource_file('snd/beep.wav')
60  it would return either '/home/bob/.mycroft/snd/beep.wav' or
61  '/opt/mycroft/snd/beep.wav' or '.../mycroft/res/snd/beep.wav',
62  where the '...' is replaced by the path where the package has
63  been installed.
64 
65  Args:
66  res_name (str): a resource path/name
67  Returns:
68  str: path to resource or None if no resource found
69  """
70  config = mycroft.configuration.Configuration.get()
71 
72  # First look for fully qualified file (e.g. a user setting)
73  if os.path.isfile(res_name):
74  return res_name
75 
76  # Now look for ~/.mycroft/res_name (in user folder)
77  filename = os.path.expanduser("~/.mycroft/" + res_name)
78  if os.path.isfile(filename):
79  return filename
80 
81  # Next look for /opt/mycroft/res/res_name
82  data_dir = expanduser(config['data_dir'])
83  filename = os.path.expanduser(join(data_dir, res_name))
84  if os.path.isfile(filename):
85  return filename
86 
87  # Finally look for it in the source package
88  filename = os.path.join(os.path.dirname(__file__), '..', 'res', res_name)
89  filename = os.path.abspath(os.path.normpath(filename))
90  if os.path.isfile(filename):
91  return filename
92 
93  return None # Resource cannot be resolved
94 
95 
96 def play_audio_file(uri: str):
97  """ Play an audio file.
98 
99  This wraps the other play_* functions, choosing the correct one based on
100  the file extension. The function will return directly and play the file
101  in the background.
102 
103  Arguments:
104  uri: uri to play
105 
106  Returns: subprocess.Popen object. None if the format is not supported or
107  an error occurs playing the file.
108 
109  """
110  extension_to_function = {
111  '.wav': play_wav,
112  '.mp3': play_mp3,
113  '.ogg': play_ogg
114  }
115  _, extension = splitext(uri)
116  play_function = extension_to_function.get(extension.lower())
117  if play_function:
118  return play_function(uri)
119  else:
120  LOG.error("Could not find a function capable of playing {uri}."
121  " Supported formats are {keys}."
122  .format(uri=uri, keys=list(extension_to_function.keys())))
123  return None
124 
125 
126 def play_wav(uri):
127  """ Play a wav-file.
128 
129  This will use the application specified in the mycroft config
130  and play the uri passed as argument. The function will return directly
131  and play the file in the background.
132 
133  Arguments:
134  uri: uri to play
135 
136  Returns: subprocess.Popen object
137  """
138  config = mycroft.configuration.Configuration.get()
139  play_cmd = config.get("play_wav_cmdline")
140  play_wav_cmd = str(play_cmd).split(" ")
141  for index, cmd in enumerate(play_wav_cmd):
142  if cmd == "%1":
143  play_wav_cmd[index] = (get_http(uri))
144  try:
145  return subprocess.Popen(play_wav_cmd)
146  except Exception as e:
147  LOG.error("Failed to launch WAV: {}".format(play_wav_cmd))
148  LOG.debug("Error: {}".format(repr(e)), exc_info=True)
149  return None
150 
151 
152 def play_mp3(uri):
153  """ Play a mp3-file.
154 
155  This will use the application specified in the mycroft config
156  and play the uri passed as argument. The function will return directly
157  and play the file in the background.
158 
159  Arguments:
160  uri: uri to play
161 
162  Returns: subprocess.Popen object
163  """
164  config = mycroft.configuration.Configuration.get()
165  play_cmd = config.get("play_mp3_cmdline")
166  play_mp3_cmd = str(play_cmd).split(" ")
167  for index, cmd in enumerate(play_mp3_cmd):
168  if cmd == "%1":
169  play_mp3_cmd[index] = (get_http(uri))
170  try:
171  return subprocess.Popen(play_mp3_cmd)
172  except Exception as e:
173  LOG.error("Failed to launch MP3: {}".format(play_mp3_cmd))
174  LOG.debug("Error: {}".format(repr(e)), exc_info=True)
175  return None
176 
177 
178 def play_ogg(uri):
179  """ Play a ogg-file.
180 
181  This will use the application specified in the mycroft config
182  and play the uri passed as argument. The function will return directly
183  and play the file in the background.
184 
185  Arguments:
186  uri: uri to play
187 
188  Returns: subprocess.Popen object
189  """
190  config = mycroft.configuration.Configuration.get()
191  play_cmd = config.get("play_ogg_cmdline")
192  play_ogg_cmd = str(play_cmd).split(" ")
193  for index, cmd in enumerate(play_ogg_cmd):
194  if cmd == "%1":
195  play_ogg_cmd[index] = (get_http(uri))
196  try:
197  return subprocess.Popen(play_ogg_cmd)
198  except Exception as e:
199  LOG.error("Failed to launch OGG: {}".format(play_ogg_cmd))
200  LOG.debug("Error: {}".format(repr(e)), exc_info=True)
201  return None
202 
203 
204 def record(file_path, duration, rate, channels):
205  if duration > 0:
206  return subprocess.Popen(
207  ["arecord", "-r", str(rate), "-c", str(channels), "-d",
208  str(duration), file_path])
209  else:
210  return subprocess.Popen(
211  ["arecord", "-r", str(rate), "-c", str(channels), file_path])
212 
213 
214 def find_input_device(device_name):
215  """ Find audio input device by name.
216 
217  Arguments:
218  device_name: device name or regex pattern to match
219 
220  Returns: device_index (int) or None if device wasn't found
221  """
222  LOG.info('Searching for input device: {}'.format(device_name))
223  LOG.debug('Devices: ')
224  pa = pyaudio.PyAudio()
225  pattern = re.compile(device_name)
226  for device_index in range(pa.get_device_count()):
227  dev = pa.get_device_info_by_index(device_index)
228  LOG.debug(' {}'.format(dev['name']))
229  if dev['maxInputChannels'] > 0 and pattern.match(dev['name']):
230  LOG.debug(' ^-- matched')
231  return device_index
232  return None
233 
234 
235 def get_http(uri):
236  return uri.replace("https://", "http://")
237 
238 
240  if url and url.endswith('/'):
241  url = url[:-1]
242  return url
243 
244 
245 def read_stripped_lines(filename):
246  with open(filename, 'r') as f:
247  return [line.strip() for line in f]
248 
249 
250 def read_dict(filename, div='='):
251  d = {}
252  with open(filename, 'r') as f:
253  for line in f:
254  (key, val) = line.split(div)
255  d[key.strip()] = val.strip()
256  return d
257 
258 
259 def connected():
260  """ Check connection by connecting to 8.8.8.8, if this is
261  blocked/fails, Microsoft NCSI is used as a backup
262 
263  Returns:
264  True if internet connection can be detected
265  """
266  return connected_dns() or connected_ncsi()
267 
268 
270  """ Check internet connection by retrieving the Microsoft NCSI endpoint.
271 
272  Returns:
273  True if internet connection can be detected
274  """
275  try:
276  r = requests.get('http://www.msftncsi.com/ncsi.txt')
277  if r.text == u'Microsoft NCSI':
278  return True
279  except Exception:
280  pass
281  return False
282 
283 
284 def connected_dns(host="8.8.8.8", port=53, timeout=3):
285  """ Check internet connection by connecting to DNS servers
286 
287  Returns:
288  True if internet connection can be detected
289  """
290  # Thanks to 7h3rAm on
291  # Host: 8.8.8.8 (google-public-dns-a.google.com)
292  # OpenPort: 53/tcp
293  # Service: domain (DNS/TCP)
294  try:
295  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
296  s.settimeout(timeout)
297  s.connect((host, port))
298  return True
299  except IOError:
300  try:
301  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
302  s.settimeout(timeout)
303  s.connect(("8.8.4.4", port))
304  return True
305  except IOError:
306  return False
307 
308 
309 def curate_cache(directory, min_free_percent=5.0, min_free_disk=50):
310  """Clear out the directory if needed
311 
312  This assumes all the files in the directory can be deleted as freely
313 
314  Args:
315  directory (str): directory path that holds cached files
316  min_free_percent (float): percentage (0.0-100.0) of drive to keep free,
317  default is 5% if not specified.
318  min_free_disk (float): minimum allowed disk space in MB, default
319  value is 50 MB if not specified.
320  """
321 
322  # Simpleminded implementation -- keep a certain percentage of the
323  # disk available.
324  # TODO: Would be easy to add more options, like whitelisted files, etc.
325  space = psutil.disk_usage(directory)
326 
327  # convert from MB to bytes
328  min_free_disk *= 1024 * 1024
329  # space.percent = space.used/space.total*100.0
330  percent_free = 100.0 - space.percent
331  if percent_free < min_free_percent and space.free < min_free_disk:
332  LOG.info('Low diskspace detected, cleaning cache')
333  # calculate how many bytes we need to delete
334  bytes_needed = (min_free_percent - percent_free) / 100.0 * space.total
335  bytes_needed = int(bytes_needed + 1.0)
336 
337  # get all entries in the directory w/ stats
338  entries = (os.path.join(directory, fn) for fn in os.listdir(directory))
339  entries = ((os.stat(path), path) for path in entries)
340 
341  # leave only regular files, insert modification date
342  entries = ((stat[ST_MTIME], stat[ST_SIZE], path)
343  for stat, path in entries if S_ISREG(stat[ST_MODE]))
344 
345  # delete files with oldest modification date until space is freed
346  space_freed = 0
347  for moddate, fsize, path in sorted(entries):
348  try:
349  os.remove(path)
350  space_freed += fsize
351  except Exception:
352  pass
353 
354  if space_freed > bytes_needed:
355  return # deleted enough!
356 
357 
358 def get_cache_directory(domain=None):
359  """Get a directory for caching data
360 
361  This directory can be used to hold temporary caches of data to
362  speed up performance. This directory will likely be part of a
363  small RAM disk and may be cleared at any time. So code that
364  uses these cached files must be able to fallback and regenerate
365  the file.
366 
367  Args:
368  domain (str): The cache domain. Basically just a subdirectory.
369 
370  Return:
371  str: a path to the directory where you can cache data
372  """
373  config = mycroft.configuration.Configuration.get()
374  dir = config.get("cache_path")
375  if not dir:
376  # If not defined, use /tmp/mycroft/cache
377  dir = os.path.join(tempfile.gettempdir(), "mycroft", "cache")
378  return ensure_directory_exists(dir, domain)
379 
380 
381 def validate_param(value, name):
382  if not value:
383  raise ValueError("Missing or empty %s in mycroft.conf " % name)
384 
385 
387  """Determine if Text to Speech is occurring
388 
389  Returns:
390  bool: True while still speaking
391  """
392  LOG.info("mycroft.utils.is_speaking() is depreciated, use "
393  "mycroft.audio.is_speaking() instead.")
395 
396 
398  """Pause as long as Text to Speech is still happening
399 
400  Pause while Text to Speech is still happening. This always pauses
401  briefly to ensure that any preceeding request to speak has time to
402  begin.
403  """
404  LOG.info("mycroft.utils.wait_while_speaking() is depreciated, use "
405  "mycroft.audio.wait_while_speaking() instead.")
407 
408 
410  # TODO: Less hacky approach to this once Audio Manager is implemented
411  # Skills should only be able to stop speech they've initiated
412  LOG.info("mycroft.utils.stop_speaking() is depreciated, use "
413  "mycroft.audio.stop_speaking() instead.")
415 
416 
417 def get_arch():
418  """ Get architecture string of system. """
419  return os.uname()[4]
420 
421 
423  """
424  Reset the sigint handler to the default. This fixes KeyboardInterrupt
425  not getting raised when started via start-mycroft.sh
426  """
427  sig.signal(sig.SIGINT, sig.default_int_handler)
428 
429 
430 def create_daemon(target, args=(), kwargs=None):
431  """Helper to quickly create and start a thread with daemon = True"""
432  t = Thread(target=target, args=args, kwargs=kwargs)
433  t.daemon = True
434  t.start()
435  return t
436 
437 
439  """Blocks until KeyboardInterrupt is received"""
440  try:
441  while True:
442  sleep(100)
443  except KeyboardInterrupt:
444  pass
445 
446 
447 _log_all_bus_messages = False
448 
449 
450 def create_echo_function(name, whitelist=None):
451  """ Standard logging mechanism for Mycroft processes.
452 
453  This handles the setup of the basic logging for all Mycroft
454  messagebus-based processes.
455 
456  Args:
457  name (str): Reference name of the process
458  whitelist (list, optional): List of "type" strings. If defined, only
459  messages in this list will be logged.
460 
461  Returns:
462  func: The echo function
463  """
464 
465  from mycroft.configuration import Configuration
466  blacklist = Configuration.get().get("ignore_logs")
467 
468  # Make sure whitelisting doesn't remove the log level setting command
469  if whitelist:
470  whitelist.append('mycroft.debug.log')
471 
472  def echo(message):
473  global _log_all_bus_messages
474  try:
475  msg = json.loads(message)
476  msg_type = msg.get("type", "")
477  # Whitelist match beginning of message
478  # i.e 'mycroft.audio.service' will allow the message
479  # 'mycroft.audio.service.play' for example
480  if whitelist and not any([msg_type.startswith(e)
481  for e in whitelist]):
482  return
483 
484  if blacklist and msg_type in blacklist:
485  return
486 
487  if msg_type == "mycroft.debug.log":
488  # Respond to requests to adjust the logger settings
489  lvl = msg["data"].get("level", "").upper()
490  if lvl in ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]:
491  LOG.level = lvl
492  LOG(name).info("Changing log level to: {}".format(lvl))
493  try:
494  logging.getLogger().setLevel(lvl)
495  logging.getLogger('urllib3').setLevel(lvl)
496  except Exception:
497  pass # We don't really care about if this fails...
498  else:
499  LOG(name).info("Invalid level provided: {}".format(lvl))
500 
501  # Allow enable/disable of messagebus traffic
502  log_bus = msg["data"].get("bus", None)
503  if log_bus is not None:
504  LOG(name).info("Bus logging: {}".format(log_bus))
505  _log_all_bus_messages = log_bus
506  elif msg_type == "registration":
507  # do not log tokens from registration messages
508  msg["data"]["token"] = None
509  message = json.dumps(msg)
510  except Exception as e:
511  LOG.info("Error: {}".format(repr(e)), exc_info=True)
512 
513  if _log_all_bus_messages:
514  # Listen for messages and echo them for logging
515  LOG(name).info("BUS: {}".format(message))
516  return echo
517 
518 
519 def camel_case_split(identifier: str) -> str:
520  """Split camel case string"""
521  regex = '.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)'
522  matches = re.finditer(regex, identifier)
523  return ' '.join([m.group(0) for m in matches])
def create_daemon(target, args=(), kwargs=None)
def resolve_resource_file(res_name)
def find_input_device(device_name)
def read_dict(filename, div='=')
def validate_param(value, name)
def create_echo_function(name, whitelist=None)
def curate_cache(directory, min_free_percent=5.0, min_free_disk=50)
def record(file_path, duration, rate, channels)
def ensure_directory_exists(directory, domain=None)
Definition: signal.py:46
def get_cache_directory(domain=None)
def get(phrase, lang=None, context=None)
def connected_dns(host="8.8.8.8", port=53, timeout=3)


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