MainController.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import logging
  2. import random
  3. from time import sleep
  4. from flask import Flask
  5. from transitions import Machine
  6. from kalliope.core import Utils
  7. from kalliope.core.ConfigurationManager import SettingLoader
  8. from kalliope.core.OrderListener import OrderListener
  9. from kalliope.core.Players import Mplayer
  10. from kalliope.core.RestAPI.FlaskAPI import FlaskAPI
  11. from kalliope.core.SynapseLauncher import SynapseLauncher
  12. from kalliope.core.TriggerLauncher import TriggerLauncher
  13. from kalliope.neurons.say.say import Say
  14. logging.basicConfig()
  15. logger = logging.getLogger("kalliope")
  16. class MainController:
  17. """
  18. This Class is the global controller of the application.
  19. """
  20. states = ['init',
  21. 'starting_trigger',
  22. 'playing_ready_sound',
  23. 'waiting_for_trigger_callback',
  24. 'stopping_trigger',
  25. 'start_order_listener',
  26. 'playing_wake_up_answer',
  27. 'waiting_for_order_listener_callback',
  28. 'analysing_order']
  29. def __init__(self, brain=None):
  30. self.brain = brain
  31. # get global configuration
  32. sl = SettingLoader()
  33. self.settings = sl.settings
  34. # keep in memory the order to process
  35. self.order_to_process = None
  36. # Starting the rest API
  37. self._start_rest_api()
  38. # save an instance of the trigger
  39. self.trigger_instance = None
  40. self.trigger_callback_called = False
  41. # save the current order listener
  42. self.order_listener = None
  43. self.order_listener_callback_called = False
  44. # boolean used to know id we played the on ready notification at least one time
  45. self.on_ready_notification_played_once = False
  46. # Initialize the state machine
  47. self.machine = Machine(model=self, states=MainController.states, initial='init', queued=True)
  48. # define transitions
  49. self.machine.add_transition('start_trigger', ['init', 'analysing_order'], 'starting_trigger')
  50. self.machine.add_transition('play_ready_sound', 'starting_trigger', 'playing_ready_sound')
  51. self.machine.add_transition('wait_trigger_callback', 'playing_ready_sound', 'waiting_for_trigger_callback')
  52. self.machine.add_transition('stop_trigger', 'waiting_for_trigger_callback', 'stopping_trigger')
  53. self.machine.add_transition('play_wake_up_answer', 'waiting_for_trigger_callback', 'playing_wake_up_answer')
  54. self.machine.add_transition('wait_for_order', 'playing_wake_up_answer', 'waiting_for_order_listener_callback')
  55. self.machine.add_transition('analyse_order', 'waiting_for_order_listener_callback', 'analysing_order')
  56. self.machine.add_ordered_transitions()
  57. # add method which are called when changing state
  58. self.machine.on_enter_starting_trigger('start_trigger_process')
  59. self.machine.on_enter_playing_ready_sound('play_ready_sound_process')
  60. self.machine.on_enter_waiting_for_trigger_callback('waiting_for_trigger_callback_thread')
  61. self.machine.on_enter_playing_wake_up_answer('play_wake_up_answer_thread')
  62. self.machine.on_enter_stopping_trigger('stop_trigger_process')
  63. self.machine.on_enter_start_order_listener('start_order_listener_thread')
  64. self.machine.on_enter_waiting_for_order_listener_callback('waiting_for_order_listener_callback_thread')
  65. self.machine.on_enter_analysing_order('analysing_order_thread')
  66. self.start_trigger()
  67. def start_trigger_process(self):
  68. """
  69. This function will start the trigger thread that listen for the hotword
  70. """
  71. logger.debug("Entering state: %s" % self.state)
  72. self.trigger_instance = self._get_default_trigger()
  73. self.trigger_callback_called = False
  74. self.trigger_instance.daemon = True
  75. # Wait that the kalliope trigger is pronounced by the user
  76. self.trigger_instance.start()
  77. self.next_state()
  78. def play_ready_sound_process(self):
  79. """
  80. Play a sound when Kalliope is ready to be awaken at the first start
  81. """
  82. logger.debug("Entering state: %s" % self.state)
  83. if (not self.on_ready_notification_played_once and self.settings.play_on_ready_notification == "once") or \
  84. self.settings.play_on_ready_notification == "always":
  85. # we remember that we played the notification one time
  86. self.on_ready_notification_played_once = True
  87. # here we tell the user that we are listening
  88. if self.settings.on_ready_answers is not None:
  89. Say(message=self.settings.on_ready_answers)
  90. elif self.settings.on_ready_sounds is not None:
  91. random_sound_to_play = self._get_random_sound(self.settings.on_ready_sounds)
  92. Mplayer.play(random_sound_to_play)
  93. self.next_state()
  94. def waiting_for_trigger_callback_thread(self):
  95. """
  96. Method to print in debug that the main process is waiting for a trigger detection
  97. """
  98. logger.debug("Entering state: %s" % self.state)
  99. Utils.print_info("Waiting for trigger detection")
  100. # this loop is used to keep the main thread alive
  101. while not self.trigger_callback_called:
  102. sleep(0.1)
  103. self.next_state()
  104. def waiting_for_order_listener_callback_thread(self):
  105. """
  106. Method to print in debug that the main process is waiting for an order to analyse
  107. """
  108. logger.debug("Entering state: %s" % self.state)
  109. # this loop is used to keep the main thread alive
  110. while not self.order_listener_callback_called:
  111. sleep(0.1)
  112. self.next_state()
  113. def trigger_callback(self):
  114. """
  115. we have detected the hotword, we can now pause the Trigger for a while
  116. The user can speak out loud his order during this time.
  117. """
  118. logger.debug("Trigger callback called, switching to the next state")
  119. self.trigger_callback_called = True
  120. def stop_trigger_process(self):
  121. """
  122. The trigger has been awaken, we don't needed it anymore
  123. :return:
  124. """
  125. logger.debug("Entering state: %s" % self.state)
  126. self.trigger_instance.stop()
  127. self.next_state()
  128. def start_order_listener_thread(self):
  129. """
  130. Start the STT engine thread
  131. """
  132. logger.debug("Entering state: %s" % self.state)
  133. # start listening for an order
  134. self.order_listener_callback_called = False
  135. self.order_listener = OrderListener(callback=self.order_listener_callback)
  136. self.order_listener.daemon = True
  137. self.order_listener.start()
  138. self.next_state()
  139. def play_wake_up_answer_thread(self):
  140. """
  141. Play a sound or make Kalliope say something to notify the user that she has been awaken and now
  142. waiting for order
  143. """
  144. logger.debug("Entering state: %s" % self.state)
  145. # if random wake answer sentence are present, we play this
  146. if self.settings.random_wake_up_answers is not None:
  147. Say(message=self.settings.random_wake_up_answers)
  148. else:
  149. random_sound_to_play = self._get_random_sound(self.settings.random_wake_up_sounds)
  150. Mplayer.play(random_sound_to_play)
  151. self.next_state()
  152. def order_listener_callback(self, order):
  153. """
  154. Receive an order, try to retrieve it in the brain.yml to launch to attached plugins
  155. :param order: the sentence received
  156. :type order: str
  157. """
  158. logger.debug("order listener callback called. Order to process: %s" % order)
  159. self.order_to_process = order
  160. self.order_listener_callback_called = True
  161. def analysing_order_thread(self):
  162. """
  163. Start the order analyser with the caught order to process
  164. """
  165. logger.debug("order in analysing_order_thread %s" % self.order_to_process)
  166. SynapseLauncher.run_matching_synapse_from_order(self.order_to_process,
  167. self.brain,
  168. self.settings,
  169. is_api_call=False)
  170. # return to the state "unpausing_trigger"
  171. self.start_trigger()
  172. def _get_default_trigger(self):
  173. """
  174. Return an instance of the default trigger
  175. :return: Trigger
  176. """
  177. for trigger in self.settings.triggers:
  178. if trigger.name == self.settings.default_trigger_name:
  179. return TriggerLauncher.get_trigger(trigger, callback=self.trigger_callback)
  180. @staticmethod
  181. def _get_random_sound(random_wake_up_sounds):
  182. """
  183. Return a path of a sound to play
  184. If the path is absolute, test if file exist
  185. If the path is relative, we check if the file exist in the sound folder
  186. :param random_wake_up_sounds: List of wake_up sounds
  187. :return: path of a sound to play
  188. """
  189. # take first randomly a path
  190. random_path = random.choice(random_wake_up_sounds)
  191. logger.debug("Selected sound: %s" % random_path)
  192. return Utils.get_real_file_path(random_path)
  193. def _start_rest_api(self):
  194. """
  195. Start the Rest API if asked in the user settings
  196. """
  197. # run the api if the user want it
  198. if self.settings.rest_api.active:
  199. Utils.print_info("Starting REST API Listening port: %s" % self.settings.rest_api.port)
  200. app = Flask(__name__)
  201. flask_api = FlaskAPI(app=app,
  202. port=self.settings.rest_api.port,
  203. brain=self.brain,
  204. allowed_cors_origin=self.settings.rest_api.allowed_cors_origin)
  205. flask_api.daemon = True
  206. flask_api.start()