Somewhat fancy voice command recognition software
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. # This is part of Kaylee
  2. # -- this code is licensed GPLv3
  3. # Copyright 2015-2017 Clayton G. Hobbs
  4. # Portions Copyright 2013 Jezra
  5. import re
  6. import json
  7. import hashlib
  8. import os
  9. from argparse import ArgumentParser, Namespace
  10. from collections import OrderedDict
  11. import pkg_resources
  12. import requests
  13. from gi.repository import GLib
  14. class Config:
  15. """Keep track of the configuration of Kaylee"""
  16. # Name of the program, for later use
  17. program_name = "kaylee"
  18. # Directories
  19. conf_dir = os.path.join(GLib.get_user_config_dir(), program_name)
  20. cache_dir = os.path.join(GLib.get_user_cache_dir(), program_name)
  21. data_dir = os.path.join(GLib.get_user_data_dir(), program_name)
  22. # Configuration files
  23. default_opt_file = pkg_resources.resource_filename('kayleevc',
  24. 'conf/options.json')
  25. local_opt_file = os.path.join(conf_dir, "options.json")
  26. plugins_file = os.path.join(conf_dir, "plugins.json")
  27. # Cache files
  28. history_file = os.path.join(cache_dir, program_name + "history")
  29. hash_file = os.path.join(cache_dir, "hash.json")
  30. # Data files
  31. strings_file = os.path.join(data_dir, "sentences.corpus")
  32. lang_file = os.path.join(data_dir, 'lm')
  33. dic_file = os.path.join(data_dir, 'dic')
  34. def __init__(self):
  35. # Ensure necessary directories exist
  36. self._make_dir(self.conf_dir)
  37. self._make_dir(self.cache_dir)
  38. self._make_dir(self.data_dir)
  39. # Set up the argument parser
  40. self._parser = ArgumentParser()
  41. self._parser.add_argument("-i", "--interface", type=str,
  42. dest="interface", action='store',
  43. help="Interface to use (if any). 'g' for GTK or 'gt' for GTK" +
  44. " system tray icon")
  45. self._parser.add_argument("-c", "--continuous",
  46. action="store_true", dest="continuous", default=False,
  47. help="Start interface with 'continuous' listen enabled")
  48. self._parser.add_argument("-H", "--history", type=int,
  49. action="store", dest="history",
  50. help="Number of commands to store in history file")
  51. self._parser.add_argument("-m", "--microphone", type=int,
  52. action="store", dest="microphone", default=None,
  53. help="Audio input card to use (if other than system default)")
  54. self._parser.add_argument("--valid-sentence-command", type=str,
  55. dest="valid_sentence_command", action='store',
  56. help="Command to run when a valid sentence is detected")
  57. self._parser.add_argument("--invalid-sentence-command", type=str,
  58. dest="invalid_sentence_command", action='store',
  59. help="Command to run when an invalid sentence is detected")
  60. # Read the configuration files
  61. self.options = self._read_json_file(self.default_opt_file)
  62. self.options.update(self._read_json_file(self.local_opt_file))
  63. self.options = Namespace(**self.options)
  64. # Parse command-line arguments, overriding config file as appropriate
  65. self._parser.parse_args(namespace=self.options)
  66. # Read the plugins file
  67. self.plugins = self._read_json_file(self.plugins_file)
  68. def _make_dir(self, directory):
  69. if not os.path.exists(directory):
  70. os.makedirs(directory)
  71. def _read_json_file(self, filename):
  72. try:
  73. with open(filename, 'r') as f:
  74. return json.load(f, object_pairs_hook=OrderedDict)
  75. except FileNotFoundError:
  76. return {}
  77. class Hasher:
  78. """Keep track of hashes for Kaylee"""
  79. def __init__(self, config):
  80. self.config = config
  81. try:
  82. with open(self.config.hash_file, 'r') as f:
  83. self.hashes = json.load(f)
  84. except IOError:
  85. # No stored hash
  86. self.hashes = {}
  87. def __getitem__(self, hashname):
  88. try:
  89. return self.hashes[hashname]
  90. except (KeyError, TypeError):
  91. return None
  92. def __setitem__(self, hashname, value):
  93. self.hashes[hashname] = value
  94. def get_hash_object(self):
  95. """Returns an object to compute a new hash"""
  96. return hashlib.sha256()
  97. def store(self):
  98. """Store the current hashes into a the hash file"""
  99. with open(self.config.hash_file, 'w') as f:
  100. json.dump(self.hashes, f)
  101. class LanguageUpdater:
  102. """
  103. Handles updating the language using the online lmtool.
  104. This class provides methods to check if the corpus has changed, and to
  105. update the language to match the new corpus using the lmtool. This allows
  106. us to automatically update the language if the corpus has changed, saving
  107. the user from having to do this manually.
  108. """
  109. def __init__(self, config):
  110. self.config = config
  111. self.hasher = Hasher(config)
  112. def update_language_if_changed(self):
  113. """Test if the language has changed, and if it has, update it"""
  114. if self.language_has_changed():
  115. self.update_language()
  116. self.save_language_hash()
  117. def language_has_changed(self):
  118. """Use hashes to test if the language has changed"""
  119. self.stored_hash = self.hasher['language']
  120. # Calculate the hash the language file has right now
  121. hasher = self.hasher.get_hash_object()
  122. with open(self.config.strings_file, 'rb') as sfile:
  123. buf = sfile.read()
  124. hasher.update(buf)
  125. self.new_hash = hasher.hexdigest()
  126. return self.new_hash != self.stored_hash
  127. def update_language(self):
  128. """Update the language using the online lmtool"""
  129. print('Updating language using online lmtool')
  130. host = 'http://www.speech.cs.cmu.edu'
  131. url = host + '/cgi-bin/tools/lmtool/run'
  132. # Submit the corpus to the lmtool
  133. response_text = ""
  134. with open(self.config.strings_file, 'rb') as corpus:
  135. files = {'corpus': corpus}
  136. values = {'formtype': 'simple'}
  137. r = requests.post(url, files=files, data=values)
  138. response_text = r.text
  139. # Parse response to get URLs of the files we need
  140. path_re = r'.*<title>Index of (.*?)</title>.*'
  141. number_re = r'.*TAR([0-9]*?)\.tgz.*'
  142. for line in response_text.split('\n'):
  143. # If we found the directory, keep it and don't break
  144. if re.search(path_re, line):
  145. path = host + re.sub(path_re, r'\1', line)
  146. # If we found the number, keep it and break
  147. elif re.search(number_re, line):
  148. number = re.sub(number_re, r'\1', line)
  149. break
  150. lm_url = path + '/' + number + '.lm'
  151. dic_url = path + '/' + number + '.dic'
  152. self._download_file(lm_url, self.config.lang_file)
  153. self._download_file(dic_url, self.config.dic_file)
  154. def save_language_hash(self):
  155. self.hasher['language'] = self.new_hash
  156. self.hasher.store()
  157. def _download_file(self, url, path):
  158. r = requests.get(url, stream=True)
  159. if r.status_code == 200:
  160. with open(path, 'wb') as f:
  161. for chunk in r:
  162. f.write(chunk)