Explorar o código

refactor STT management

nico %!s(int64=8) %!d(string=hai) anos
pai
achega
6c9d781452

+ 14 - 9
Docs/contributing/contribute_stt.md

@@ -34,30 +34,35 @@ Creating a new STT must follow some rules:
 The constructor has an incoming callback to call once we get the text.
 The constructor has a __**kwargs argument__ which is corresponding to the Dict of incoming variables:values defined either in the settings file.
 1. The STT must init itself first.
-1. Attach the incoming callback to the self.attribute.
+1. Attach the incoming callback to the self.main_controller_callback attribute. This callback come from the main controller and will receive the text at the end of the process
 1. Obtain audio from the microphone in the constructor. (Note : we mostly use the [speech_recognition library](https://pypi.python.org/pypi/SpeechRecognition/))
-1. Use self.start_listening(self.my_callback) from the mother class to get an audio and pass it to the callback of your choice.
-1. The callback methode must implement two arguments: recognizer and audio. The audio argument contains the stream caught by the microphone
-1. Once you get the text back, let give it to the callback method received in the constructor
+1. Set your callback method into the mother class with `self.set_callback(self.google_callback)`. This callback is the one which will process the audio into a text.
+1. Use self.start_listening() from the mother class to get an audio. Once caught, the mother class will give the audio stream to the callback you've set before.
+1. The callback method must implement two arguments: recognizer and audio. The audio argument contains the stream caught by the microphone
+1. Do magic stuff with the audio in order to get a string that contains the translated text
+1. Once you get the text, let give it to the main_controller_callback method received in the constructor by calling it with the text string as argument `self.main_controller_callback(audio_to_text)`
 
     ```python
     def __init__(self, callback=None, **kwargs):
         OrderListener.__init__(self)
-        self.callback = callback
+        # here is the main controller callback. We will return the text at the end of the process
+        self.main_controller_callback = callback
         
         self.argument_from_settings = kwargs.get('argument_from_settings', None)
         
-        # start the microphone to capture an audio, give to the function a callback        
-        self.stop_listening = self.start_listening(self.my_callback)
+        #  give our callback   
+        self.set_callback(self.my_callback)
+        # start the microphone to capture an audio
+        self.start_listening()
         
         def my_callback((self, recognizer, audio):
             # ---------------------------------------------
-            # do amazing code
+            # do amazing code to translate the audio stream into text
             # 'audio' contain stream caught by the microphone
             # ---------------------------------------------
             
             # at the end of the process, send the text into the received callback method
-            self.callback(audio_to_text)
+            self.main_controller_callback(audio_to_text)
     ```
 
 

+ 1 - 0
install/files/python_requirements.txt

@@ -18,3 +18,4 @@ Flask-Testing==0.6.1
 apscheduler==3.3.0
 GitPython==2.1.1
 packaging>=16.8
+transitions>=0.4.3

+ 1 - 1
kalliope/__init__.py

@@ -88,7 +88,7 @@ def main():
             Utils.print_info("Press Ctrl+C for stopping")
             # catch signal for killing on Ctrl+C pressed
             signal.signal(signal.SIGINT, signal_handler)
-            # start the main controller
+            # start the state machine
             MainController(brain=brain)
 
     if args.action == "gui":

+ 147 - 23
kalliope/core/MainController.py

@@ -1,7 +1,9 @@
 import logging
 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
@@ -20,44 +22,143 @@ 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
 
-        # 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,
-                                 allowed_cors_origin=self.settings.rest_api.allowed_cors_origin)
-            flask_api.daemon = True
-            flask_api.start()
+        # Starting the rest API
+        self._start_rest_api()
+
+        # 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
+
+        # 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()
 
-        # create an order listener object. This last will the trigger callback before starting
-        self.order_listener = OrderListener(self.analyse_order)
+    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
+        """
+        # TODO place a settings to play the sound every time kalliope is waiting for a wake up
+        logger.debug("Entering state: %s" % self.state)
+        # here we tell the user that we are listening
         if self.settings.random_on_ready_answers is not None:
             Say(message=self.settings.random_on_ready_answers)
         elif self.settings.random_on_ready_sounds is not None:
             random_sound_to_play = self._get_random_sound(self.settings.random_on_ready_sounds)
             Mplayer.play(random_sound_to_play)
+        self.next_state()
 
-        # Wait that the kalliope trigger is pronounced by the user
-        self.trigger_instance = self._get_default_trigger()
-        self.trigger_instance.start()
-        Utils.print_info("Waiting for trigger detection")
+    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 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 callback(self):
+    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)
@@ -65,21 +166,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
         """
+        logger.debug("order listener callback called. Order to process: %s" % order)
+        self.order_listener_callback_called = False
+        self.next_state(order)
+
+    def analysing_order_thread(self, order):
+        """
+        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" % order)
         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()
-
-        # 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)
+        # return to the state "unpausing_trigger"
+        self.unpause_trigger()
 
     def _get_default_trigger(self):
         """
@@ -88,7 +197,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):
@@ -103,3 +212,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()

+ 6 - 5
kalliope/core/OrderListener.py

@@ -41,12 +41,13 @@ class OrderListener(Thread):
         self.callback = callback
         sl = SettingLoader()
         self.settings = sl.settings
+        self.stt_instance = None
 
     def run(self):
         """
         Start thread
         """
-        self.load_stt_plugin()
+        self.stt_instance = self.load_stt_plugin()
 
     def load_stt_plugin(self):
         if self.stt is None:
@@ -59,10 +60,10 @@ class OrderListener(Thread):
                 stt_folder = None
                 if self.settings.resources:
                     stt_folder = self.settings.resources.stt_folder
-                Utils.get_dynamic_class_instantiation(package_name='stt',
-                                                      module_name=stt_object.name.capitalize(),
-                                                      parameters=stt_object.parameters,
-                                                      resources_dir=stt_folder)
+                return Utils.get_dynamic_class_instantiation(package_name='stt',
+                                                             module_name=stt_object.name.capitalize(),
+                                                             parameters=stt_object.parameters,
+                                                             resources_dir=stt_folder)
 
     @staticmethod
     def _ignore_stderr():

+ 0 - 30
kalliope/core/TriggerModule.py

@@ -1,30 +0,0 @@
-import logging
-
-from kalliope.core.Utils import Utils
-
-logging.basicConfig()
-logger = logging.getLogger("kalliope")
-
-
-class TriggerModule(object):
-    """
-    Mother class of a trigger object
-    """
-
-    def __init__(self):
-        super(TriggerModule, self).__init__()
-
-    @staticmethod
-    def get_file_from_path(file_path):
-        """
-        Trigger can be based on a model file, or other file.
-        If a file is precised in settings, the path can be relative or absolute.
-        If the path is absolute, there is no problem when can try to load it directly
-        If the path is relative, we need to test the get the full path of the file in the following order:
-            - from the current directory where kalliope has been called. Eg: /home/me/Documents/kalliope_config
-            - from /etc/kalliope
-            - from the root of the project. Eg: /usr/local/lib/python2.7/dist-packages/kalliope-version/kalliope/<file_path>
-
-        :return: absolute path
-        """
-        return Utils.get_real_file_path(file_path)

+ 1 - 1
kalliope/settings.yml

@@ -15,7 +15,7 @@ default_trigger: "snowboy"
 # - snowboy
 triggers:
   - snowboy:
-      pmdl_file: "trigger/snowboy/resources/kalliope-FR-20samples.pmdl"
+      pmdl_file: "trigger/snowboy/resources/kalliope-FR-13samples.pmdl"
 
 
 # ---------------------------

+ 39 - 5
kalliope/stt/Utils.py

@@ -1,21 +1,55 @@
+from threading import Thread
+from time import sleep
+
+import logging
 import speech_recognition as sr
 
 from kalliope import Utils
 
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
 
-class SpeechRecognition(object):
+class SpeechRecognition(Thread):
 
     def __init__(self):
+        """
+        Thread used to caught n audio from the microphone and pass it to a callback method
+        """
+        super(SpeechRecognition, self).__init__()
         self.recognizer = sr.Recognizer()
         self.microphone = sr.Microphone()
+        self.callback = None
         self.stop_listening = None
+        self.kill_yourself = False
         with self.microphone as source:
             # we only need to calibrate once, before we start listening
             self.recognizer.adjust_for_ambient_noise(source)
 
-    def start_listening(self, callback):
+    def run(self):
+        """
+        Start the thread that listen the microphone and then give the audio to the callback method
+        """
         Utils.print_info("Say something!")
-        self.recognizer.listen_in_background(self.microphone, callback=callback)
-
-    def interrupt(self):
+        self.stop_listening = self.recognizer.listen_in_background(self.microphone, self.callback)
+        while not self.kill_yourself:
+            sleep(0.1)
+        logger.debug("kill the speech recognition process")
         self.stop_listening()
+
+    def start_listening(self):
+        """
+        A method to start the thread
+        """
+        self.start()
+
+    def stop_listening(self):
+        self.kill_yourself = True
+
+    def set_callback(self, callback):
+        """
+        set the callback method that will receive the audio stream caught by the microphone
+        :param callback: callback method
+        :return:
+        """
+        self.callback = callback

+ 12 - 8
kalliope/stt/apiai/apiai.py

@@ -15,14 +15,15 @@ class Apiai(SpeechRecognition):
         SpeechRecognition.__init__(self)
 
         # callback function to call after the translation speech/tex
-        self.callback = callback
+        self.main_controller_callback = callback
         self.key = kwargs.get('key', None)
         self.language = kwargs.get('language', "en")
         self.session_id = kwargs.get('session_id', None)
         self.show_all = kwargs.get('show_all', False)
 
         # start listening in the background
-        self.stop_listening = self.start_listening(self.apiai_callback)
+        self.set_callback(self.apiai_callback)
+        self.start_listening()
 
     def apiai_callback(self, recognizer, audio):
         """
@@ -43,16 +44,19 @@ class Apiai(SpeechRecognition):
         except sr.UnknownValueError as e:
             Utils.print_warning("Apiai Speech Recognition could not understand audio; {0}".format(e))
             # callback anyway, we need to listen again for a new order
-            self._analyse_audio(audio=None)
+            self._analyse_audio(audio_to_text=None)
         except sr.RequestError as e:
             Utils.print_danger("Could not request results from Apiai Speech Recognition service; {0}".format(e))
             # callback anyway, we need to listen again for a new order
-            self._analyse_audio(audio=None)
+            self._analyse_audio(audio_to_text=None)
 
-    def _analyse_audio(self, audio):
+        # stop listening for an audio
+        self.stop_listening()
+
+    def _analyse_audio(self, audio_to_text):
         """
         Confirm the audio exists and run it in a Callback
-        :param audio: the captured audio
+        :param audio_to_text: the captured audio
         """
-        if self.callback is not None:
-            self.callback(audio)
+        if self.main_controller_callback is not None:
+            self.main_controller_callback(audio_to_text)

+ 13 - 8
kalliope/stt/bing/bing.py

@@ -15,12 +15,14 @@ class Bing(SpeechRecognition):
         SpeechRecognition.__init__(self)
 
         # callback function to call after the translation speech/tex
-        self.callback = callback
+        self.main_controller_callback = callback
         self.key = kwargs.get('key', None)
         self.language = kwargs.get('language', "en-US")
         self.show_all = kwargs.get('show_all', False)
+
         # start listening in the background
-        self.stop_listening = self.start_listening(self.bing_callback)
+        self.set_callback(self.bing_callback)
+        self.start_listening()
 
     def bing_callback(self, recognizer, audio):
         """
@@ -37,16 +39,19 @@ class Bing(SpeechRecognition):
         except sr.UnknownValueError:
             Utils.print_warning("Bing Speech Recognition could not understand audio")
             # callback anyway, we need to listen again for a new order
-            self._analyse_audio(audio=None)
+            self._analyse_audio(audio_to_text=None)
         except sr.RequestError as e:
             Utils.print_danger("Could not request results from Bing Speech Recognition service; {0}".format(e))
             # callback anyway, we need to listen again for a new order
-            self._analyse_audio(audio=None)
+            self._analyse_audio(audio_to_text=None)
+
+        # stop listening for an audio
+        self.stop_listening()
 
-    def _analyse_audio(self, audio):
+    def _analyse_audio(self, audio_to_text):
         """
         Confirm the audio exists and run it in a Callback
-        :param audio: the captured audio
+        :param audio_to_text: the captured audio
         """
-        if self.callback is not None:
-            self.callback(audio)
+        if self.main_controller_callback is not None:
+            self.main_controller_callback(audio_to_text)

+ 12 - 8
kalliope/stt/cmusphinx/cmusphinx.py

@@ -15,10 +15,11 @@ class Cmusphinx(SpeechRecognition):
         SpeechRecognition.__init__(self)
 
         # callback function to call after the translation speech/tex
-        self.callback = callback
+        self.main_controller_callback = callback
 
         # start listening in the background
-        self.stop_listening = self.start_listening(self.sphinx_callback)
+        self.set_callback(self.sphinx_callback)
+        self.start_listening()
 
     def sphinx_callback(self, recognizer, audio):
         """
@@ -32,16 +33,19 @@ class Cmusphinx(SpeechRecognition):
         except sr.UnknownValueError:
             Utils.print_warning("Sphinx Speech Recognition could not understand audio")
             # callback anyway, we need to listen again for a new order
-            self._analyse_audio(audio=None)
+            self._analyse_audio(audio_to_text=None)
         except sr.RequestError as e:
             Utils.print_danger("Could not request results from Sphinx Speech Recognition service; {0}".format(e))
             # callback anyway, we need to listen again for a new order
-            self._analyse_audio(audio=None)
+            self._analyse_audio(audio_to_text=None)
 
-    def _analyse_audio(self, audio):
+        # stop listening for an audio
+        self.stop_listening()
+
+    def _analyse_audio(self, audio_to_text):
         """
         Confirm the audio exists and run it in a Callback
-        :param audio: the captured audio
+        :param audio_to_text: the captured audio
         """
-        if self.callback is not None:
-            self.callback(audio)
+        if self.main_controller_callback is not None:
+            self.main_controller_callback(audio_to_text)

+ 9 - 5
kalliope/stt/google/google.py

@@ -1,3 +1,5 @@
+from time import sleep
+
 import speech_recognition as sr
 
 from kalliope.core import Utils
@@ -15,13 +17,14 @@ class Google(SpeechRecognition):
         SpeechRecognition.__init__(self)
 
         # callback function to call after the translation speech/tex
-        self.callback = callback
+        self.main_controller_callback = callback
         self.key = kwargs.get('key', None)
         self.language = kwargs.get('language', "en-US")
         self.show_all = kwargs.get('show_all', False)
 
         # start listening in the background
-        self.stop_listening = self.start_listening(self.google_callback)
+        self.set_callback(self.google_callback)
+        self.start_listening()
 
     def google_callback(self, recognizer, audio):
         """
@@ -34,7 +37,6 @@ class Google(SpeechRecognition):
                                                          show_all=self.show_all)
             Utils.print_success("Google Speech Recognition thinks you said %s" % captured_audio)
             self._analyse_audio(audio_to_text=captured_audio)
-
         except sr.UnknownValueError:
             Utils.print_warning("Google Speech Recognition could not understand audio")
             # callback anyway, we need to listen again for a new order
@@ -44,11 +46,13 @@ class Google(SpeechRecognition):
             # callback anyway, we need to listen again for a new order
             self._analyse_audio(audio_to_text=None)
 
+        self.stop_listening()
+
     def _analyse_audio(self, audio_to_text):
         """
         Confirm the audio exists and run it in a Callback
         :param audio_to_text: the captured audio
         """
-        if self.callback is not None:
-            self.callback(audio_to_text)
+        if self.main_controller_callback is not None:
+            self.main_controller_callback(audio_to_text)
 

+ 12 - 8
kalliope/stt/houndify/houndify.py

@@ -15,7 +15,7 @@ class Houndify(SpeechRecognition):
         SpeechRecognition.__init__(self)
 
         # callback function to call after the translation speech/tex
-        self.callback = callback
+        self.main_controller_callback = callback
         self.client_id = kwargs.get('client_id', None)
         self.key = kwargs.get('key', None)
         # only english supported
@@ -23,7 +23,8 @@ class Houndify(SpeechRecognition):
         self.show_all = kwargs.get('show_all', False)
 
         # start listening in the background
-        self.stop_listening = self.start_listening(self.houndify_callback)
+        self.set_callback(self.houndify_callback)
+        self.start_listening()
 
     def houndify_callback(self, recognizer, audio):
         """
@@ -40,16 +41,19 @@ class Houndify(SpeechRecognition):
         except sr.UnknownValueError:
             Utils.print_warning("Houndify Speech Recognition could not understand audio")
             # callback anyway, we need to listen again for a new order
-            self._analyse_audio(audio=None)
+            self._analyse_audio(audio_to_text=None)
         except sr.RequestError as e:
             Utils.print_danger("Could not request results from Houndify Speech Recognition service; {0}".format(e))
             # callback anyway, we need to listen again for a new order
-            self._analyse_audio(audio=None)
+            self._analyse_audio(audio_to_text=None)
 
-    def _analyse_audio(self, audio):
+        # stop listening for an audio
+        self.stop_listening()
+
+    def _analyse_audio(self, audio_to_text):
         """
         Confirm the audio exists and run it in a Callback
-        :param audio: the captured audio
+        :param audio_to_text: the captured audio
         """
-        if self.callback is not None:
-            self.callback(audio)
+        if self.main_controller_callback is not None:
+            self.main_controller_callback(audio_to_text)

+ 13 - 12
kalliope/stt/wit/wit.py

@@ -15,12 +15,13 @@ class Wit(SpeechRecognition):
         SpeechRecognition.__init__(self)
 
         # callback function to call after the translation speech/tex
-        self.callback = callback
+        self.main_controller_callback = callback
         self.key = kwargs.get('key', None)
         self.show_all = kwargs.get('show_all', False)
 
         # start listening in the background
-        self.stop_listening = self.start_listening(self.wit_callback)
+        self.set_callback(self.wit_callback)
+        self.start_listening()
 
     def wit_callback(self, recognizer, audio):
         try:
@@ -33,19 +34,19 @@ class Wit(SpeechRecognition):
         except sr.UnknownValueError:
             Utils.print_warning("Wit.ai Speech Recognition could not understand audio")
             # callback anyway, we need to listen again for a new order
-            self._analyse_audio(audio=None)
+            self._analyse_audio(audio_to_text=None)
         except sr.RequestError as e:
             Utils.print_danger("Could not request results from Wit.ai Speech Recognition service; {0}".format(e))
             # callback anyway, we need to listen again for a new order
-            self._analyse_audio(audio=None)
+            self._analyse_audio(audio_to_text=None)
 
-    def _analyse_audio(self, audio):
+        # stop listening for an audio
+        self.stop_listening()
+
+    def _analyse_audio(self, audio_to_text):
         """
-        Confirm the audio exists annd run it in a Callback
-        :param audio: the captured audio
+        Confirm the audio exists and run it in a Callback
+        :param audio_to_text: the captured audio
         """
-
-        # if self.main_controller is not None:
-        #     self.main_controller.analyse_order(audio)
-        if self.callback is not None:
-            self.callback(audio)
+        if self.main_controller_callback is not None:
+            self.main_controller_callback(audio_to_text)

BIN=BIN
kalliope/trigger/snowboy/resources/kalliope-FR-13samples.pmdl


+ 32 - 16
kalliope/trigger/snowboy/snowboy.py

@@ -2,9 +2,11 @@ import inspect
 import logging
 import os
 import time
+from threading import Thread
 
-from kalliope.core.TriggerModule import TriggerModule
+from kalliope import Utils
 from kalliope.trigger.snowboy import snowboydecoder
+from cffi import FFI as _FFI
 
 
 class SnowboyModelNotFounfd(Exception):
@@ -18,10 +20,11 @@ logging.basicConfig()
 logger = logging.getLogger("kalliope")
 
 
-class Snowboy(TriggerModule):
+class Snowboy(Thread):
 
     def __init__(self, **kwargs):
         super(Snowboy, self).__init__()
+        self._ignore_stderr()
         # pause listening boolean
         self.interrupted = False
         self.kill_received = False
@@ -36,7 +39,7 @@ class Snowboy(TriggerModule):
         if self.pmdl is None:
             raise MissingParameterException("Pmdl file is required with snowboy")
 
-        self.pmdl_path = self.get_file_from_path(self.pmdl)
+        self.pmdl_path = Utils.get_real_file_path(self.pmdl)
         if not os.path.isfile(self.pmdl_path):
             raise SnowboyModelNotFounfd("The snowboy model file %s does not exist" % self.pmdl_path)
 
@@ -51,24 +54,15 @@ class Snowboy(TriggerModule):
         """
         return self.interrupted
 
-    def start(self):
+    def run(self):
         """
         Start the snowboy thread and wait for a Kalliope trigger word
         :return:
         """
-        # start snowboy loop
+        # start snowboy loop forever
         self.detector.daemon = True
-        try:
-            self.detector.start()
-            while not self.kill_received:
-                #  once the main thread has started child thread, there's nothing else for it to do.
-                # So it exits, and the threads are destroyed instantly. So let's keep the main thread alive
-                time.sleep(1)
-        except KeyboardInterrupt:
-            self.kill_received = True
-            self.detector.kill_received = True
-        # we wait that a callback
-        self.detector.terminate()
+        self.detector.start()
+        self.detector.join()
 
     def pause(self):
         """
@@ -84,3 +78,25 @@ class Snowboy(TriggerModule):
         logger.debug("Unpausing snowboy process")
         self.detector.paused = False
 
+    @staticmethod
+    def _ignore_stderr():
+        """
+        Try to forward PortAudio messages from stderr to /dev/null.
+        """
+        ffi = _FFI()
+        ffi.cdef("""
+            /* from stdio.h */
+            FILE* fopen(const char* path, const char* mode);
+            int fclose(FILE* fp);
+            FILE* stderr;  /* GNU C library */
+            FILE* __stderrp;  /* Mac OS X */
+            """)
+        stdio = ffi.dlopen(None)
+        devnull = stdio.fopen(os.devnull.encode(), b'w')
+        try:
+            stdio.stderr = devnull
+        except KeyError:
+            try:
+                stdio.__stderrp = devnull
+            except KeyError:
+                stdio.fclose(devnull)

+ 2 - 1
setup.py

@@ -76,7 +76,8 @@ setup(
         'Flask-Testing==0.6.1',
         'apscheduler==3.3.0',
         'GitPython==2.1.1',
-        'packaging>=16.8'
+        'packaging>=16.8',
+        'transitions>=0.4.3'
     ],