MainController.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  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.OrderAnalyser import OrderAnalyser
  9. from kalliope.core.OrderListener import OrderListener
  10. from kalliope.core.Players import Mplayer
  11. from kalliope.core.RestAPI.FlaskAPI import FlaskAPI
  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. 'unpausing_trigger',
  23. 'playing_ready_sound',
  24. 'waiting_for_trigger_callback',
  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')
  48. # define transitions
  49. self.machine.add_transition('start_trigger', 'init', 'starting_trigger')
  50. self.machine.add_transition('unpause_trigger', ['starting_trigger', 'analysing_order'], 'unpausing_trigger')
  51. self.machine.add_transition('play_ready_sound', 'unpausing_trigger', 'playing_ready_sound')
  52. self.machine.add_transition('wait_trigger_callback', 'playing_ready_sound', 'waiting_for_trigger_callback')
  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_start_order_listener('start_order_listener_thread')
  63. self.machine.on_enter_waiting_for_order_listener_callback('waiting_for_order_listener_callback_thread')
  64. self.machine.on_enter_analysing_order('analysing_order_thread')
  65. self.machine.on_enter_unpausing_trigger('unpausing_trigger_process')
  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_instance.daemon = True
  74. # Wait that the kalliope trigger is pronounced by the user
  75. self.trigger_instance.start()
  76. self.next_state()
  77. def unpausing_trigger_process(self):
  78. """
  79. If the trigger was in pause, this method will unpause it to listen again for the hotword
  80. """
  81. logger.debug("Entering state: %s" % self.state)
  82. self.trigger_instance.unpause()
  83. self.trigger_callback_called = False
  84. Utils.print_info("Waiting for trigger detection")
  85. self.next_state()
  86. def play_ready_sound_process(self):
  87. """
  88. Play a sound when Kalliope is ready to be awaken at the first start
  89. """
  90. logger.debug("Entering state: %s" % self.state)
  91. if (not self.on_ready_notification_played_once and self.settings.play_on_ready_notification == "once") or \
  92. self.settings.play_on_ready_notification == "always":
  93. # we remember that we played the notification one time
  94. self.on_ready_notification_played_once = True
  95. # here we tell the user that we are listening
  96. if self.settings.on_ready_answers is not None:
  97. Say(message=self.settings.on_ready_answers)
  98. elif self.settings.on_ready_sounds is not None:
  99. random_sound_to_play = self._get_random_sound(self.settings.on_ready_sounds)
  100. Mplayer.play(random_sound_to_play)
  101. self.next_state()
  102. def waiting_for_trigger_callback_thread(self):
  103. """
  104. Method to print in debug that the main process is waiting for a trigger detection
  105. """
  106. logger.debug("Entering state: %s" % self.state)
  107. # this loop is used to keep the main thread alive
  108. while not self.trigger_callback_called:
  109. sleep(0.1)
  110. self.next_state()
  111. def waiting_for_order_listener_callback_thread(self):
  112. """
  113. Method to print in debug that the main process is waiting for an order to analyse
  114. """
  115. logger.debug("Entering state: %s" % self.state)
  116. # this loop is used to keep the main thread alive
  117. while not self.order_listener_callback_called:
  118. sleep(0.1)
  119. self.next_state()
  120. def trigger_callback(self):
  121. """
  122. we have detected the hotword, we can now pause the Trigger for a while
  123. The user can speak out loud his order during this time.
  124. """
  125. logger.debug("Trigger callback called, switching to the next state")
  126. # self.next_state()
  127. self.trigger_callback_called = True
  128. def start_order_listener_thread(self):
  129. """
  130. Start the STT engine thread
  131. """
  132. logger.debug("Entering state: %s" % self.state)
  133. # pause the trigger process
  134. self.trigger_instance.pause()
  135. # start listening for an order
  136. self.order_listener_callback_called = False
  137. self.order_listener = OrderListener(callback=self.order_listener_callback)
  138. self.order_listener.daemon = True
  139. self.order_listener.start()
  140. self.next_state()
  141. def play_wake_up_answer_thread(self):
  142. """
  143. Play a sound or make Kalliope say something to notify the user that she has been awaken and now
  144. waiting for order
  145. """
  146. logger.debug("Entering state: %s" % self.state)
  147. # if random wake answer sentence are present, we play this
  148. if self.settings.random_wake_up_answers is not None:
  149. Say(message=self.settings.random_wake_up_answers)
  150. else:
  151. random_sound_to_play = self._get_random_sound(self.settings.random_wake_up_sounds)
  152. Mplayer.play(random_sound_to_play)
  153. self.next_state()
  154. def order_listener_callback(self, order):
  155. """
  156. Receive an order, try to retrieve it in the brain.yml to launch to attached plugins
  157. :param order: the sentence received
  158. :type order: str
  159. """
  160. logger.debug("order listener callback called. Order to process: %s" % order)
  161. self.order_to_process = order
  162. self.order_listener_callback_called = True
  163. def analysing_order_thread(self):
  164. """
  165. Start the order analyser with the caught order to process
  166. :param order: the text order to analyse
  167. """
  168. logger.debug("order in analysing_order_thread %s" % self.order_to_process)
  169. if self.order_to_process is not None: # maybe we have received a null audio from STT engine
  170. order_analyser = OrderAnalyser(self.order_to_process, brain=self.brain)
  171. order_analyser.start()
  172. # return to the state "unpausing_trigger"
  173. self.unpause_trigger()
  174. def _get_default_trigger(self):
  175. """
  176. Return an instance of the default trigger
  177. :return: Trigger
  178. """
  179. for trigger in self.settings.triggers:
  180. if trigger.name == self.settings.default_trigger_name:
  181. return TriggerLauncher.get_trigger(trigger, callback=self.trigger_callback)
  182. @staticmethod
  183. def _get_random_sound(random_wake_up_sounds):
  184. """
  185. Return a path of a sound to play
  186. If the path is absolute, test if file exist
  187. If the path is relative, we check if the file exist in the sound folder
  188. :param random_wake_up_sounds: List of wake_up sounds
  189. :return: path of a sound to play
  190. """
  191. # take first randomly a path
  192. random_path = random.choice(random_wake_up_sounds)
  193. logger.debug("Selected sound: %s" % random_path)
  194. return Utils.get_real_file_path(random_path)
  195. def _start_rest_api(self):
  196. """
  197. Start the Rest API if asked in the user settings
  198. """
  199. # run the api if the user want it
  200. if self.settings.rest_api.active:
  201. Utils.print_info("Starting REST API Listening port: %s" % self.settings.rest_api.port)
  202. app = Flask(__name__)
  203. flask_api = FlaskAPI(app=app,
  204. port=self.settings.rest_api.port,
  205. brain=self.brain,
  206. allowed_cors_origin=self.settings.rest_api.allowed_cors_origin)
  207. flask_api.daemon = True
  208. flask_api.start()