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.

kaylee.py 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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 importlib
  6. import queue
  7. import sys
  8. import subprocess
  9. import signal
  10. import os.path
  11. from gi.repository import GObject, GLib
  12. from .recognizer import Recognizer
  13. from .util import *
  14. from .numbers import NumberParser
  15. import kayleevc.plugins
  16. class Kaylee:
  17. def __init__(self):
  18. self.ui = None
  19. self.options = {}
  20. self.continuous_listen = False
  21. # Load configuration
  22. self.config = Config()
  23. self.options = vars(self.config.options)
  24. # Make sure some plugins are configured to be loaded
  25. if self.config.plugins is None:
  26. print("error: no plugins configured", file=sys.stderr)
  27. sys.exit(1)
  28. # Load plugins
  29. self.plugins = []
  30. for plugin in self.config.plugins.keys():
  31. pmod = importlib.import_module(plugin, 'kayleevc.plugins')
  32. pobj = pmod.Plugin(self.config, plugin)
  33. self.plugins.append(pobj)
  34. # Create a hasher
  35. self.hasher = Hasher(self.config)
  36. # Create the strings file
  37. self.update_voice_commands_if_changed()
  38. if self.options['interface']:
  39. if self.options['interface'] == "g":
  40. from kayleevc.gui import GTKInterface as UI
  41. else:
  42. print("no GUI defined")
  43. sys.exit()
  44. self.ui = UI(self.options, self.options['continuous'])
  45. self.ui.connect("command", self.process_command)
  46. # Can we load the icon resource?
  47. icon = self.load_resource("icon_small.png")
  48. if icon:
  49. self.ui.set_icon_active_asset(icon)
  50. # Can we load the icon_inactive resource?
  51. icon_inactive = self.load_resource("icon_inactive_small.png")
  52. if icon_inactive:
  53. self.ui.set_icon_inactive_asset(icon_inactive)
  54. if self.options['history']:
  55. self.history = []
  56. # Update the language if necessary
  57. self.language_updater = LanguageUpdater(self.config)
  58. self.language_updater.update_language_if_changed()
  59. # Create the recognizer
  60. self.recognizer = Recognizer(self.config)
  61. # Connect the recognizer's finished signal to the appropriate method
  62. self.recognizer.connect('finished', self.recognizer_finished)
  63. def update_voice_commands_if_changed(self):
  64. """Use hashes to test if the voice commands have changed"""
  65. stored_hash = self.hasher['voice_commands']
  66. # Calculate the hash the voice commands have right now
  67. hasher = self.hasher.get_hash_object()
  68. for plugin in self.plugins:
  69. for string in sorted(plugin.corpus_strings):
  70. hasher.update(string.encode('utf-8'))
  71. # Add a separator to avoid odd behavior
  72. hasher.update('\n'.encode('utf-8'))
  73. new_hash = hasher.hexdigest()
  74. if new_hash != stored_hash:
  75. self.create_strings_file()
  76. self.hasher['voice_commands'] = new_hash
  77. self.hasher.store()
  78. def create_strings_file(self):
  79. # Open the strings file
  80. with open(self.config.strings_file, 'w') as strings:
  81. # Add command words to the corpus
  82. # FIXME: Doing this twice is a silly thing
  83. for plugin in self.plugins:
  84. for string in sorted(plugin.corpus_strings):
  85. strings.write(string + "\n")
  86. # Add number words to the corpus
  87. if NumberParser.number_words is not None:
  88. for word in NumberParser.number_words:
  89. strings.write(word + " ")
  90. strings.write("\n")
  91. def _log_history(self, plugin, text):
  92. """Log the recognized sentence to the history file"""
  93. if self.options['history']:
  94. self.history.append(f"{plugin.name}: {text}")
  95. if len(self.history) > self.options['history']:
  96. # Pop off the first item
  97. self.history.pop(0)
  98. # Open and truncate the history file
  99. with open(self.config.history_file, 'w') as hfile:
  100. for line in self.history:
  101. hfile.write(line + '\n')
  102. self._stop_ui(text)
  103. def _valid_cmd(self):
  104. """Run the valid_sentence_command if it's set"""
  105. if self.options['valid_sentence_command']:
  106. subprocess.call(self.options['valid_sentence_command'], shell=True)
  107. def _invalid_cmd(self, text):
  108. """Run the invalid_sentence_command if it's set"""
  109. if self.options['invalid_sentence_command']:
  110. subprocess.call(self.options['invalid_sentence_command'],
  111. shell=True)
  112. print(f"no matching command {text}")
  113. def recognizer_finished(self, recognizer, text):
  114. confidence_heap = queue.PriorityQueue()
  115. min_confidence = self.options['minimum_confidence']
  116. # Add handlers to the heap
  117. for index, plugin in enumerate(self.plugins):
  118. # Get a handler
  119. handler = plugin.get_handler(text)
  120. # If the handler meets minimum confidence
  121. if ((handler is not None)
  122. and ((min_confidence is None)
  123. or (min_confidence is not None
  124. and handler.confidence >= min_confidence))):
  125. # Add a triple to the heap, so handlers are sorted first by
  126. # their confidence, then by index to break ties
  127. confidence_heap.put_nowait((handler, index, plugin))
  128. # Run valid or invalid sentence command
  129. if confidence_heap.empty():
  130. self._invalid_cmd(text)
  131. else:
  132. self._valid_cmd()
  133. # Give the command to the handlers that want it
  134. while True:
  135. # Get information about the handler
  136. try:
  137. info = confidence_heap.get_nowait()
  138. except queue.Empty:
  139. # If the queue is empty, we have nothing else to try, so break
  140. break
  141. handler = info[0]
  142. plugin = info[2]
  143. # Handle the command
  144. try:
  145. handler(self.plugin_tts)
  146. except kayleevc.plugins.HandlerFailure:
  147. # If the handler failed, keep trying
  148. continue
  149. else:
  150. # If the handler succeeded, log it and break
  151. self._log_history(plugin, text)
  152. break
  153. self._stop_ui(text)
  154. def plugin_tts(self, text):
  155. # Stop listening
  156. self.recognizer.pause()
  157. # Speak
  158. try:
  159. subprocess.call(self.options['tts'] + [text])
  160. except KeyError:
  161. print('TTS:', text)
  162. # Resume listening
  163. self.recognizer.listen()
  164. def _stop_ui(self, text):
  165. # If there is a UI and we are not continuous listen
  166. if self.ui:
  167. if not self.continuous_listen:
  168. # Stop listening
  169. self.recognizer.pause()
  170. # Let the UI know that there is a finish
  171. self.ui.finished(text)
  172. def run(self):
  173. if self.ui:
  174. self.ui.run()
  175. else:
  176. self.recognizer.listen()
  177. def quit(self):
  178. sys.exit()
  179. def process_command(self, UI, command):
  180. print(command)
  181. if command == "listen":
  182. self.recognizer.listen()
  183. elif command == "stop":
  184. self.continuous_listen = False
  185. self.recognizer.pause()
  186. elif command == "continuous_listen":
  187. self.continuous_listen = True
  188. self.recognizer.listen()
  189. elif command == "quit":
  190. self.quit()
  191. def load_resource(self, string):
  192. # TODO: Use the Config object for this path management
  193. local_data = os.path.join(os.path.dirname(__file__), '..', 'data')
  194. paths = ["/usr/share/kaylee/", "/usr/local/share/kaylee", local_data]
  195. for path in paths:
  196. resource = os.path.join(path, string)
  197. if os.path.exists(resource):
  198. return resource
  199. # If we get this far, no resource was found
  200. return False
  201. def run():
  202. # Make our kaylee object
  203. kaylee = Kaylee()
  204. # Init gobject threads
  205. GObject.threads_init()
  206. # We want a main loop
  207. main_loop = GObject.MainLoop()
  208. # Handle sigint
  209. signal.signal(signal.SIGINT, signal.SIG_DFL)
  210. # Run the kaylee
  211. kaylee.run()
  212. # Start the main loop
  213. try:
  214. main_loop.run()
  215. except:
  216. main_loop.quit()
  217. sys.exit()