|
@@ -1,8 +1,9 @@
|
|
|
import logging
|
|
|
-import os
|
|
|
import random
|
|
|
+from time import sleep
|
|
|
|
|
|
from flask import Flask
|
|
|
+from transitions import Machine
|
|
|
|
|
|
from kalliope.core import Utils
|
|
|
from kalliope.core.ConfigurationManager import SettingLoader
|
|
@@ -21,36 +22,151 @@ class MainController:
|
|
|
"""
|
|
|
This Class is the global controller of the application.
|
|
|
"""
|
|
|
+ states = ['init',
|
|
|
+ 'starting_trigger',
|
|
|
+ 'unpausing_trigger',
|
|
|
+ 'playing_ready_sound',
|
|
|
+ 'waiting_for_trigger_callback',
|
|
|
+ 'start_order_listener',
|
|
|
+ 'playing_wake_up_answer',
|
|
|
+ 'waiting_for_order_listener_callback',
|
|
|
+ 'analysing_order']
|
|
|
+
|
|
|
def __init__(self, brain=None):
|
|
|
self.brain = brain
|
|
|
# get global configuration
|
|
|
sl = SettingLoader()
|
|
|
self.settings = sl.settings
|
|
|
+ # keep in memory the order to process
|
|
|
+ self.order_to_process = None
|
|
|
|
|
|
- # run the api if the user want it
|
|
|
- if self.settings.rest_api.active:
|
|
|
- Utils.print_info("Starting REST API Listening port: %s" % self.settings.rest_api.port)
|
|
|
- app = Flask(__name__)
|
|
|
- flask_api = FlaskAPI(app, port=self.settings.rest_api.port, brain=self.brain)
|
|
|
- flask_api.daemon = True
|
|
|
- flask_api.start()
|
|
|
+ # Starting the rest API
|
|
|
+ self._start_rest_api()
|
|
|
|
|
|
- # create an order listener object. This last will the trigger callback before starting
|
|
|
- self.order_listener = OrderListener(self.analyse_order)
|
|
|
- # Wait that the kalliope trigger is pronounced by the user
|
|
|
+ # save an instance of the trigger
|
|
|
+ self.trigger_instance = None
|
|
|
+ self.trigger_callback_called = False
|
|
|
+
|
|
|
+ # save the current order listener
|
|
|
+ self.order_listener = None
|
|
|
+ self.order_listener_callback_called = False
|
|
|
+
|
|
|
+ # boolean used to know id we played the on ready notification at least one time
|
|
|
+ self.on_ready_notification_played_once = False
|
|
|
+
|
|
|
+ # Initialize the state machine
|
|
|
+ self.machine = Machine(model=self, states=MainController.states, initial='init')
|
|
|
+
|
|
|
+ # define transitions
|
|
|
+ self.machine.add_transition('start_trigger', 'init', 'starting_trigger')
|
|
|
+ self.machine.add_transition('unpause_trigger', ['starting_trigger', 'analysing_order'], 'unpausing_trigger')
|
|
|
+ self.machine.add_transition('play_ready_sound', 'unpausing_trigger', 'playing_ready_sound')
|
|
|
+ self.machine.add_transition('wait_trigger_callback', 'playing_ready_sound', 'waiting_for_trigger_callback')
|
|
|
+ self.machine.add_transition('play_wake_up_answer', 'waiting_for_trigger_callback', 'playing_wake_up_answer')
|
|
|
+ self.machine.add_transition('wait_for_order', 'playing_wake_up_answer', 'waiting_for_order_listener_callback')
|
|
|
+ self.machine.add_transition('analyse_order', 'waiting_for_order_listener_callback', 'analysing_order')
|
|
|
+
|
|
|
+ self.machine.add_ordered_transitions()
|
|
|
+
|
|
|
+ # add method which are called when changing state
|
|
|
+ self.machine.on_enter_starting_trigger('start_trigger_process')
|
|
|
+ self.machine.on_enter_playing_ready_sound('play_ready_sound_process')
|
|
|
+ self.machine.on_enter_waiting_for_trigger_callback('waiting_for_trigger_callback_thread')
|
|
|
+ self.machine.on_enter_playing_wake_up_answer('play_wake_up_answer_thread')
|
|
|
+ self.machine.on_enter_start_order_listener('start_order_listener_thread')
|
|
|
+ self.machine.on_enter_waiting_for_order_listener_callback('waiting_for_order_listener_callback_thread')
|
|
|
+ self.machine.on_enter_analysing_order('analysing_order_thread')
|
|
|
+ self.machine.on_enter_unpausing_trigger('unpausing_trigger_process')
|
|
|
+
|
|
|
+ self.start_trigger()
|
|
|
+
|
|
|
+ def start_trigger_process(self):
|
|
|
+ """
|
|
|
+ This function will start the trigger thread that listen for the hotword
|
|
|
+ """
|
|
|
+ logger.debug("Entering state: %s" % self.state)
|
|
|
self.trigger_instance = self._get_default_trigger()
|
|
|
+ self.trigger_instance.daemon = True
|
|
|
+ # Wait that the kalliope trigger is pronounced by the user
|
|
|
self.trigger_instance.start()
|
|
|
+ self.next_state()
|
|
|
+
|
|
|
+ def unpausing_trigger_process(self):
|
|
|
+ """
|
|
|
+ If the trigger was in pause, this method will unpause it to listen again for the hotword
|
|
|
+ """
|
|
|
+ logger.debug("Entering state: %s" % self.state)
|
|
|
+ self.trigger_instance.unpause()
|
|
|
+ self.trigger_callback_called = False
|
|
|
Utils.print_info("Waiting for trigger detection")
|
|
|
+ self.next_state()
|
|
|
+
|
|
|
+ def play_ready_sound_process(self):
|
|
|
+ """
|
|
|
+ Play a sound when Kalliope is ready to be awaken at the first start
|
|
|
+ """
|
|
|
+ logger.debug("Entering state: %s" % self.state)
|
|
|
+ if (not self.on_ready_notification_played_once and self.settings.play_on_ready_notification == "once") or \
|
|
|
+ self.settings.play_on_ready_notification == "always":
|
|
|
+ # we remember that we played the notification one time
|
|
|
+ self.on_ready_notification_played_once = True
|
|
|
+ # here we tell the user that we are listening
|
|
|
+ if self.settings.on_ready_answers is not None:
|
|
|
+ Say(message=self.settings.on_ready_answers)
|
|
|
+ elif self.settings.on_ready_sounds is not None:
|
|
|
+ random_sound_to_play = self._get_random_sound(self.settings.on_ready_sounds)
|
|
|
+ Mplayer.play(random_sound_to_play)
|
|
|
+ self.next_state()
|
|
|
+
|
|
|
+ def waiting_for_trigger_callback_thread(self):
|
|
|
+ """
|
|
|
+ Method to print in debug that the main process is waiting for a trigger detection
|
|
|
+ """
|
|
|
+ logger.debug("Entering state: %s" % self.state)
|
|
|
+ # this loop is used to keep the main thread alive
|
|
|
+ while not self.trigger_callback_called:
|
|
|
+ sleep(0.1)
|
|
|
+ self.next_state()
|
|
|
|
|
|
- def callback(self):
|
|
|
+ def waiting_for_order_listener_callback_thread(self):
|
|
|
+ """
|
|
|
+ Method to print in debug that the main process is waiting for an order to analyse
|
|
|
+ """
|
|
|
+ logger.debug("Entering state: %s" % self.state)
|
|
|
+ # this loop is used to keep the main thread alive
|
|
|
+ while not self.order_listener_callback_called:
|
|
|
+ sleep(0.1)
|
|
|
+ self.next_state()
|
|
|
+
|
|
|
+ def trigger_callback(self):
|
|
|
"""
|
|
|
we have detected the hotword, we can now pause the Trigger for a while
|
|
|
The user can speak out loud his order during this time.
|
|
|
"""
|
|
|
+ logger.debug("Trigger callback called, switching to the next state")
|
|
|
+ # self.next_state()
|
|
|
+ self.trigger_callback_called = True
|
|
|
+
|
|
|
+ def start_order_listener_thread(self):
|
|
|
+ """
|
|
|
+ Start the STT engine thread
|
|
|
+ """
|
|
|
+ logger.debug("Entering state: %s" % self.state)
|
|
|
# pause the trigger process
|
|
|
self.trigger_instance.pause()
|
|
|
# start listening for an order
|
|
|
+ self.order_listener_callback_called = False
|
|
|
+ self.order_listener = OrderListener(callback=self.order_listener_callback)
|
|
|
+ self.order_listener.daemon = True
|
|
|
self.order_listener.start()
|
|
|
+ self.next_state()
|
|
|
+
|
|
|
+ def play_wake_up_answer_thread(self):
|
|
|
+ """
|
|
|
+ Play a sound or make Kalliope say something to notify the user that she has been awaken and now
|
|
|
+ waiting for order
|
|
|
+ """
|
|
|
+ logger.debug("Entering state: %s" % self.state)
|
|
|
# if random wake answer sentence are present, we play this
|
|
|
if self.settings.random_wake_up_answers is not None:
|
|
|
Say(message=self.settings.random_wake_up_answers)
|
|
@@ -58,21 +174,29 @@ class MainController:
|
|
|
random_sound_to_play = self._get_random_sound(self.settings.random_wake_up_sounds)
|
|
|
Mplayer.play(random_sound_to_play)
|
|
|
|
|
|
- def analyse_order(self, order):
|
|
|
+ self.next_state()
|
|
|
+
|
|
|
+ def order_listener_callback(self, order):
|
|
|
"""
|
|
|
Receive an order, try to retrieve it in the brain.yml to launch to attached plugins
|
|
|
:param order: the sentence received
|
|
|
:type order: str
|
|
|
"""
|
|
|
- if order is not None: # maybe we have received a null audio from STT engine
|
|
|
- order_analyser = OrderAnalyser(order, brain=self.brain)
|
|
|
- order_analyser.start()
|
|
|
+ logger.debug("order listener callback called. Order to process: %s" % order)
|
|
|
+ self.order_to_process = order
|
|
|
+ self.order_listener_callback_called = True
|
|
|
|
|
|
- # restart the trigger when the order analyser has finish his job
|
|
|
- Utils.print_info("Waiting for trigger detection")
|
|
|
- self.trigger_instance.unpause()
|
|
|
- # create a new order listener that will wait for start
|
|
|
- self.order_listener = OrderListener(self.analyse_order)
|
|
|
+ def analysing_order_thread(self):
|
|
|
+ """
|
|
|
+ Start the order analyser with the caught order to process
|
|
|
+ :param order: the text order to analyse
|
|
|
+ """
|
|
|
+ logger.debug("order in analysing_order_thread %s" % self.order_to_process)
|
|
|
+ if self.order_to_process is not None: # maybe we have received a null audio from STT engine
|
|
|
+ order_analyser = OrderAnalyser(self.order_to_process, brain=self.brain)
|
|
|
+ order_analyser.start()
|
|
|
+ # return to the state "unpausing_trigger"
|
|
|
+ self.unpause_trigger()
|
|
|
|
|
|
def _get_default_trigger(self):
|
|
|
"""
|
|
@@ -81,7 +205,7 @@ class MainController:
|
|
|
"""
|
|
|
for trigger in self.settings.triggers:
|
|
|
if trigger.name == self.settings.default_trigger_name:
|
|
|
- return TriggerLauncher.get_trigger(trigger, callback=self.callback)
|
|
|
+ return TriggerLauncher.get_trigger(trigger, callback=self.trigger_callback)
|
|
|
|
|
|
@staticmethod
|
|
|
def _get_random_sound(random_wake_up_sounds):
|
|
@@ -96,3 +220,18 @@ class MainController:
|
|
|
random_path = random.choice(random_wake_up_sounds)
|
|
|
logger.debug("Selected sound: %s" % random_path)
|
|
|
return Utils.get_real_file_path(random_path)
|
|
|
+
|
|
|
+ def _start_rest_api(self):
|
|
|
+ """
|
|
|
+ Start the Rest API if asked in the user settings
|
|
|
+ """
|
|
|
+ # run the api if the user want it
|
|
|
+ if self.settings.rest_api.active:
|
|
|
+ Utils.print_info("Starting REST API Listening port: %s" % self.settings.rest_api.port)
|
|
|
+ app = Flask(__name__)
|
|
|
+ flask_api = FlaskAPI(app=app,
|
|
|
+ port=self.settings.rest_api.port,
|
|
|
+ brain=self.brain,
|
|
|
+ allowed_cors_origin=self.settings.rest_api.allowed_cors_origin)
|
|
|
+ flask_api.daemon = True
|
|
|
+ flask_api.start()
|