Somewhat fancy voice command recognition software
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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