Somewhat fancy voice command recognition software
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

kaylee.py 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  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 sys
  7. import subprocess
  8. import signal
  9. import os.path
  10. from gi.repository import GObject, GLib
  11. from kayleevc.recognizer import Recognizer
  12. from kayleevc.util import *
  13. from kayleevc.numbers import NumberParser
  14. import kayleevc.plugins
  15. class Kaylee:
  16. def __init__(self):
  17. self.ui = None
  18. self.options = {}
  19. self.continuous_listen = False
  20. # Load configuration
  21. self.config = Config()
  22. self.options = vars(self.config.options)
  23. # Make sure some plugins are configured to be loaded
  24. if self.config.plugins is None:
  25. print("error: no plugins configured", file=sys.stderr)
  26. sys.exit(1)
  27. # Load plugins
  28. self.plugins = []
  29. for plugin in self.config.plugins.keys():
  30. pmod = importlib.import_module(plugin, 'kayleevc.plugins')
  31. self.plugins.append(pmod.Plugin(self.config, plugin))
  32. # Create a hasher
  33. self.hasher = Hasher(self.config)
  34. # Create the strings file
  35. self.update_voice_commands_if_changed()
  36. if self.options['interface']:
  37. if self.options['interface'] == "g":
  38. from kayleevc.gui import GTKInterface as UI
  39. elif self.options['interface'] == "gt":
  40. from kayleevc.gui import GTKTrayInterface 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 all the plugins
  62. for plugin in self.plugins:
  63. self.recognizer.connect('finished', plugin.recognizer_finished)
  64. plugin.connect('processed', self.plugin_processed)
  65. plugin.connect('tts', self.plugin_tts)
  66. self.recognizer.connect('finished', self.recognizer_finished)
  67. def update_voice_commands_if_changed(self):
  68. """Use hashes to test if the voice commands have changed"""
  69. stored_hash = self.hasher['voice_commands']
  70. # Calculate the hash the voice commands have right now
  71. hasher = self.hasher.get_hash_object()
  72. for plugin in self.plugins:
  73. for string in sorted(plugin.corpus_strings):
  74. hasher.update(string.encode('utf-8'))
  75. # Add a separator to avoid odd behavior
  76. hasher.update('\n'.encode('utf-8'))
  77. new_hash = hasher.hexdigest()
  78. if new_hash != stored_hash:
  79. self.create_strings_file()
  80. self.hasher['voice_commands'] = new_hash
  81. self.hasher.store()
  82. def create_strings_file(self):
  83. # Open the strings file
  84. with open(self.config.strings_file, 'w') as strings:
  85. # Add command words to the corpus
  86. # FIXME: Doing this twice is a silly thing
  87. for plugin in self.plugins:
  88. for string in sorted(plugin.corpus_strings):
  89. strings.write(string + "\n")
  90. # Add number words to the corpus
  91. if NumberParser.number_words is not None:
  92. for word in NumberParser.number_words:
  93. strings.write(word + " ")
  94. strings.write("\n")
  95. def plugin_processed(self, plugin, text):
  96. """Callback for ``processed`` signal from plugins
  97. Runs the valid_sentence_command and logs the recognized sentence to the
  98. history file.
  99. """
  100. # Run the valid_sentence_command if it's set
  101. if self.options['valid_sentence_command']:
  102. subprocess.call(self.options['valid_sentence_command'], shell=True)
  103. # Log the command to the history file
  104. if self.options['history']:
  105. self.history.append("{}: {}".format(plugin.name, text))
  106. if len(self.history) > self.options['history']:
  107. # Pop off the first item
  108. self.history.pop(0)
  109. # Open and truncate the history file
  110. with open(self.config.history_file, 'w') as hfile:
  111. for line in self.history:
  112. hfile.write(line + '\n')
  113. self._stop_ui(text)
  114. def recognizer_finished(self, recognizer, text):
  115. # No loaded plugin wanted the text, so run the invalid_sentence_command
  116. # if it's set
  117. if self.options['invalid_sentence_command']:
  118. subprocess.call(self.options['invalid_sentence_command'],
  119. shell=True)
  120. print("no matching command {0}".format(text))
  121. self._stop_ui(text)
  122. def plugin_tts(self, plugin, text):
  123. # Stop listening
  124. self.recognizer.pause()
  125. # Speak
  126. try:
  127. subprocess.call(self.options['tts'] + [text])
  128. except KeyError:
  129. print('TTS:', text)
  130. # Resume listening
  131. self.recognizer.listen()
  132. def _stop_ui(self, text):
  133. # If there is a UI and we are not continuous listen
  134. if self.ui:
  135. if not self.continuous_listen:
  136. # Stop listening
  137. self.recognizer.pause()
  138. # Let the UI know that there is a finish
  139. self.ui.finished(text)
  140. def run(self):
  141. if self.ui:
  142. self.ui.run()
  143. else:
  144. self.recognizer.listen()
  145. def quit(self):
  146. sys.exit()
  147. def process_command(self, UI, command):
  148. print(command)
  149. if command == "listen":
  150. self.recognizer.listen()
  151. elif command == "stop":
  152. self.continuous_listen = False
  153. self.recognizer.pause()
  154. elif command == "continuous_listen":
  155. self.continuous_listen = True
  156. self.recognizer.listen()
  157. elif command == "quit":
  158. self.quit()
  159. def load_resource(self, string):
  160. # TODO: Use the Config object for this path management
  161. local_data = os.path.join(os.path.dirname(__file__), '..', 'data')
  162. paths = ["/usr/share/kaylee/", "/usr/local/share/kaylee", local_data]
  163. for path in paths:
  164. resource = os.path.join(path, string)
  165. if os.path.exists(resource):
  166. return resource
  167. # If we get this far, no resource was found
  168. return False
  169. def run():
  170. # Make our kaylee object
  171. kaylee = Kaylee()
  172. # Init gobject threads
  173. GObject.threads_init()
  174. # We want a main loop
  175. main_loop = GObject.MainLoop()
  176. # Handle sigint
  177. signal.signal(signal.SIGINT, signal.SIG_DFL)
  178. # Run the kaylee
  179. kaylee.run()
  180. # Start the main loop
  181. try:
  182. main_loop.run()
  183. except:
  184. main_loop.quit()
  185. sys.exit()