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.

blather.py 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. #!/usr/bin/env python3
  2. # This is part of Kaylee
  3. # -- this code is licensed GPLv3
  4. # Copyright 2013 Jezra
  5. # Copyright 2015 Clayton G. Hobbs
  6. from __future__ import print_function
  7. import sys
  8. import signal
  9. import hashlib
  10. import os.path
  11. import subprocess
  12. from argparse import ArgumentParser
  13. from gi.repository import GObject, GLib
  14. import json
  15. from recognizer import Recognizer
  16. # Where are the files?
  17. conf_dir = os.path.expanduser(os.path.join(GLib.get_user_config_dir(),
  18. "blather"))
  19. lang_dir = os.path.join(conf_dir, "language")
  20. command_file = os.path.join(conf_dir, "commands.conf")
  21. strings_file = os.path.join(conf_dir, "sentences.corpus")
  22. history_file = os.path.join(conf_dir, "blather.history")
  23. opt_file = os.path.join(conf_dir, "options.json")
  24. hash_file = os.path.join(conf_dir, "hash.json")
  25. lang_file = os.path.join(lang_dir, 'lm')
  26. dic_file = os.path.join(lang_dir, 'dic')
  27. # Make the lang_dir if it doesn't exist
  28. if not os.path.exists(lang_dir):
  29. os.makedirs(lang_dir)
  30. class Blather:
  31. def __init__(self, opts):
  32. self.ui = None
  33. self.options = {}
  34. ui_continuous_listen = False
  35. self.continuous_listen = False
  36. self.commands = {}
  37. # Read the commands
  38. self.read_commands()
  39. # Load the options file
  40. self.load_options()
  41. print(opts)
  42. # Merge the options with the ones provided by command-line arguments
  43. for k, v in vars(opts).items():
  44. if v is not None:
  45. self.options[k] = v
  46. if self.options['interface'] != None:
  47. if self.options['interface'] == "g":
  48. from gtkui import UI
  49. elif self.options['interface'] == "gt":
  50. from gtktrayui import UI
  51. else:
  52. print("no GUI defined")
  53. sys.exit()
  54. self.ui = UI(args, self.options['continuous'])
  55. self.ui.connect("command", self.process_command)
  56. # Can we load the icon resource?
  57. icon = self.load_resource("icon.png")
  58. if icon:
  59. self.ui.set_icon_active_asset(icon)
  60. # Can we load the icon_inactive resource?
  61. icon_inactive = self.load_resource("icon_inactive.png")
  62. if icon_inactive:
  63. self.ui.set_icon_inactive_asset(icon_inactive)
  64. if self.options['history']:
  65. self.history = []
  66. # Update the language if necessary
  67. self.update_language()
  68. # Create the recognizer
  69. try:
  70. self.recognizer = Recognizer(lang_file, dic_file, self.options['microphone'])
  71. except Exception as e:
  72. # No recognizer? bummer
  73. print('error making recognizer')
  74. sys.exit()
  75. self.recognizer.connect('finished', self.recognizer_finished)
  76. print("Using Options: ", self.options)
  77. def read_commands(self):
  78. # Read the commands file
  79. file_lines = open(command_file)
  80. strings = open(strings_file, "w")
  81. for line in file_lines:
  82. print(line)
  83. # Trim the white spaces
  84. line = line.strip()
  85. # If the line has length and the first char isn't a hash
  86. if len(line) and line[0]!="#":
  87. # This is a parsible line
  88. (key, value) = line.split(":", 1)
  89. print(key, value)
  90. self.commands[key.strip().lower()] = value.strip()
  91. strings.write(key.strip() + "\n")
  92. # Close the strings file
  93. strings.close()
  94. def load_options(self):
  95. """Load options from the options.json file"""
  96. # Is there an opt file?
  97. with open(opt_file, 'r') as f:
  98. self.options = json.load(f)
  99. print(self.options)
  100. def log_history(self, text):
  101. if self.options['history']:
  102. self.history.append(text)
  103. if len(self.history) > self.options['history']:
  104. # Pop off the first item
  105. self.history.pop(0)
  106. # Open and truncate the blather history file
  107. hfile = open(history_file, "w")
  108. for line in self.history:
  109. hfile.write(line + "\n")
  110. # Close the file
  111. hfile.close()
  112. def update_language(self):
  113. """Update the language if its hash has changed"""
  114. # Load the stored hash from the hash file
  115. try:
  116. with open(hash_file, 'r') as f:
  117. hashes = json.load(f)
  118. stored_hash = hashes['language']
  119. except (IOError, KeyError, TypeError):
  120. # No stored hash
  121. stored_hash = ''
  122. # Calculate the hash the language file has right now
  123. hasher = hashlib.sha256()
  124. with open(strings_file, 'rb') as sfile:
  125. buf = sfile.read()
  126. hasher.update(buf)
  127. new_hash = hasher.hexdigest()
  128. # If the hashes differ
  129. if stored_hash != new_hash:
  130. # Update the language
  131. # FIXME: Do this with Python, not Bash
  132. self.run_command('./language_updater.sh')
  133. # Store the new hash
  134. new_hashes = {'language': new_hash}
  135. with open(hash_file, 'w') as f:
  136. json.dump(new_hashes, f)
  137. def run_command(self, cmd):
  138. """Print the command, then run it"""
  139. print(cmd)
  140. subprocess.call(cmd, shell=True)
  141. def recognizer_finished(self, recognizer, text):
  142. t = text.lower()
  143. # Is there a matching command?
  144. if t in self.commands:
  145. # Run the valid_sentence_command if there is a valid sentence command
  146. if self.options['valid_sentence_command']:
  147. subprocess.call(self.options['valid_sentence_command'], shell=True)
  148. cmd = self.commands[t]
  149. # Should we be passing words?
  150. if self.options['pass_words']:
  151. cmd += " " + t
  152. self.run_command(cmd)
  153. else:
  154. self.run_command(cmd)
  155. self.log_history(text)
  156. else:
  157. # Run the invalid_sentence_command if there is an invalid sentence command
  158. if self.options['invalid_sentence_command']:
  159. subprocess.call(self.options['invalid_sentence_command'], shell=True)
  160. print("no matching command {0}".format(t))
  161. # If there is a UI and we are not continuous listen
  162. if self.ui:
  163. if not self.continuous_listen:
  164. # Stop listening
  165. self.recognizer.pause()
  166. # Let the UI know that there is a finish
  167. self.ui.finished(t)
  168. def run(self):
  169. if self.ui:
  170. self.ui.run()
  171. else:
  172. blather.recognizer.listen()
  173. def quit(self):
  174. sys.exit()
  175. def process_command(self, UI, command):
  176. print(command)
  177. if command == "listen":
  178. self.recognizer.listen()
  179. elif command == "stop":
  180. self.recognizer.pause()
  181. elif command == "continuous_listen":
  182. self.continuous_listen = True
  183. self.recognizer.listen()
  184. elif command == "continuous_stop":
  185. self.continuous_listen = False
  186. self.recognizer.pause()
  187. elif command == "quit":
  188. self.quit()
  189. def load_resource(self, string):
  190. local_data = os.path.join(os.path.dirname(__file__), 'data')
  191. paths = ["/usr/share/blather/", "/usr/local/share/blather", local_data]
  192. for path in paths:
  193. resource = os.path.join(path, string)
  194. if os.path.exists(resource):
  195. return resource
  196. # If we get this far, no resource was found
  197. return False
  198. if __name__ == "__main__":
  199. parser = ArgumentParser()
  200. parser.add_argument("-i", "--interface", type=str, dest="interface",
  201. action='store',
  202. help="Interface to use (if any). 'g' for GTK or 'gt' for GTK system tray icon")
  203. parser.add_argument("-c", "--continuous",
  204. action="store_true", dest="continuous", default=False,
  205. help="starts interface with 'continuous' listen enabled")
  206. parser.add_argument("-p", "--pass-words",
  207. action="store_true", dest="pass_words", default=False,
  208. help="passes the recognized words as arguments to the shell command")
  209. parser.add_argument("-H", "--history", type=int,
  210. action="store", dest="history",
  211. help="number of commands to store in history file")
  212. parser.add_argument("-m", "--microphone", type=int,
  213. action="store", dest="microphone", default=None,
  214. help="Audio input card to use (if other than system default)")
  215. parser.add_argument("--valid-sentence-command", type=str, dest="valid_sentence_command",
  216. action='store',
  217. help="command to run when a valid sentence is detected")
  218. parser.add_argument("--invalid-sentence-command", type=str, dest="invalid_sentence_command",
  219. action='store',
  220. help="command to run when an invalid sentence is detected")
  221. args = parser.parse_args()
  222. # Make our blather object
  223. blather = Blather(args)
  224. # Init gobject threads
  225. GObject.threads_init()
  226. # We want a main loop
  227. main_loop = GObject.MainLoop()
  228. # Handle sigint
  229. signal.signal(signal.SIGINT, signal.SIG_DFL)
  230. # Run the blather
  231. blather.run()
  232. # Start the main loop
  233. try:
  234. main_loop.run()
  235. except:
  236. print("time to quit")
  237. main_loop.quit()
  238. sys.exit()