Browse Source

Merge pull request #189 from kalliope-project/dev_nico

refactor
Monf 8 years ago
parent
commit
aaddda39ea

+ 2 - 1
CHANGELOG.md

@@ -1,4 +1,4 @@
-v0.4.1 / 2017-01-12
+v0.4.1 / 2017-01-15
 ===================
 - add CORS support for REST API
 - fix installation of community STT and TTS
@@ -7,6 +7,7 @@ v0.4.1 / 2017-01-12
 - add offline STT (CMUSphinx)
 - orders are not anymore case sensitive
 - add STT utils class to split the audio catching from the processing
+- fix CTRL-C to kill Kalliope
 
 v0.4.0 / 2016-12-28
 ===================

+ 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)
     ```
 
 

+ 2 - 0
Docs/installation.md

@@ -86,6 +86,8 @@ Start kalliope:
 kalliope start
 ```
 
+> **Note:** Do not start Kalliope as root user or with sudo
+
 Kalliope will load default settings and brain, the output should looks the following
 ```bash
 Starting event manager

+ 3 - 0
Docs/installation/raspbian_jessie.md

@@ -7,6 +7,9 @@
 [raspbian-2016-11-29](http://downloads.raspberrypi.org/raspbian/images/raspbian-2016-11-29/)
 [raspbian-2017-01-10](http://downloads.raspberrypi.org/raspbian/images/raspbian-2017-01-10/)
 
+> **Note:** We recommend to use a **lite** installation of Raspbian without any graphical interface for a better experience. 
+
+> **Note:** The first Raspberry Pi is not officially supported. The installation will works but a single core with only 700Mhz may produce a some latencies.
 
 ## Debian packages requirements
 

+ 7 - 1
Docs/installation/ubuntu_14.04.md

@@ -6,5 +6,11 @@ Install some required system libraries and softwares:
 
 ```bash
 sudo apt-get update
-sudo apt-get install git python-pip python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev libjack0 libjack-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer
+sudo apt-get install git python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev libjack0 libjack-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer
+```
+
+Let's install the last release of python-pip
+```bash
+wget https://bootstrap.pypa.io/get-pip.py
+sudo python get-pip.py
 ```

+ 6 - 1
Docs/installation/ubuntu_16.04.md

@@ -6,5 +6,10 @@ Install some required system libraries and softwares:
 
 ```bash
 sudo apt-get update
-sudo apt-get install git python-pip python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer
+sudo apt-get install git python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer
+```
+Let's install the last release of python-pip
+```bash
+wget https://bootstrap.pypa.io/get-pip.py
+sudo python get-pip.py
 ```

+ 2 - 0
Docs/kalliope_cli.md

@@ -11,6 +11,8 @@ For example, to start Kalliope we simply use
 kalliope start
 ```
 
+> **Note:** Do not use the CLI as root user or with sudo. Kalliope must run with standard user privileges.
+
 ## ARGUMENTS
 
 ### start

+ 21 - 25
Docs/settings.md

@@ -177,41 +177,37 @@ random_wake_up_sounds:
 E.g: `# random_wake_up_answers:`
 
 
-## On ready answers configuration
+## On ready notification
+This section is used to notify the user when Kalliope is waiting for a trigger detection by playing a sound or speak a sentence out loud
 
-#### random_on_ready_answers (optional)
-When Kalliope is ready, it lets you know that it's operational and now waiting for order. It's done by answering randomly
-one of the sentences provided in the variable random_on_ready_answers.
+#### play_on_ready_notification
+This parameter define if you play the on ready notification:
+ - `always`: every time Kalliope is ready to be awaken
+ - `never`: never play a sound or sentences when kalliope is ready
+ - `once`: at the first start of Kalliope
 
-This variable must contain a list of strings as shown bellow
+E.g:
 ```yml
-random_on_ready_answers:
-  - "Your sentence"
-  - "Another sentence"
+play_on_ready_notification: always
 ```
+#### on_ready_answers
+The on ready notification can be a sentence. Place here a sentence or a list of sentence. If you set a list, one sentence will be picked up randomly
 
-E.g
+E.g:
 ```yml
-random_on_ready_answers:
-  - "I am ready"
-  - "Let's roll"
+on_ready_answers:
+  - "I'm ready"
+  - "Waiting for order"
 ```
 
-#### random_on_ready_sounds (optional)
-You can play a sound when Kalliope is ready instead of saying something from the `random_on_ready_answers`.
-Place here a list of full paths of the sound files you want to use. Otherwise, you can use some default sounds provided by Kalliope which you can find in `/usr/lib/kalliope/sounds`.
-By default two file are provided: ding.wav and dong.wav. In all cases, the file must be in `.wav` or `.mp3` format. If more than on file is present in the list,
-Kalliope will select one randomly at each wake up.
+#### on_ready_sounds
+You can play a sound instead of a sentence.
+Remove the `on_ready_answers` parameters by commenting it out and use this one instead.
+Place here the path of the sound file. Files must be .wav or .mp3 format.
 
+E.g:
 ```yml
-random_on_ready_sounds:
-  - "local_file_in_sounds_folder.wav"
-  - "/my/personal/full/path/my_file.mp3"
-```
-
-E.g
-```yml
-random_on_ready_sounds:
+on_ready_sounds:
   - "ding.wav"
   - "dong.wav"
   - "/my/personal/full/path/my_file.mp3"

+ 2 - 8
Docs/stt.md

@@ -26,14 +26,8 @@ speech_to_text:
 Sometime, an API key will be necessary to use an engine. Click on a TTS engine link in the `Current Available STT` section to know which parameter are required.
 
 ## Current CORE Available STT
-Core STTs are already packaged with the installation of Kalliope an can be used out of the box.
-
-- [apiai](../kalliope/stt/apiai/README.md)
-- [bing](../kalliope/stt/bing/README.md)
-- [CMUSphinx](../kalliope/stt/cmusphinx/README.md)
-- [google](../kalliope/stt/google/README.md)
-- [houndify](../kalliope/stt/houndify/README.md)
-- [witai](../kalliope/stt/wit/README.md)
+
+Core STTs are already packaged with the installation of Kalliope an can be used out of the box. See the [complete list here](stt_list.md).
 
 ## STT Community Installation
 

+ 8 - 11
Docs/stt_list.md

@@ -6,14 +6,14 @@ See the [complete STT documentation](stt.md) for more information.
 ## Core STT
 Core STTs are already packaged with the installation of Kalliope an can be used out of the box.
 
-| Name      | Description                                      |
-|-----------|--------------------------------------------------|
-| api.ai    | [apiai](../kalliope/stt/apiai/README.md)         |
-| bing      | [bing](../kalliope/stt/bing/README.md)           |
-| Google    | [google](../kalliope/stt/google/README.md)       |
-| Houndify  | [houndify](../kalliope/stt/houndify/README.md)   |
-| CMUSphinx | [CMUSphinx](../kalliope/stt/cmusphinx/README.md) |
-| wit.ai    | [wit.ai](../kalliope/stt/wit/README.md)          |
+| Name      | Description                                      | Type        |
+|-----------|--------------------------------------------------|-------------|
+| api.ai    | [apiai](../kalliope/stt/apiai/README.md)         | Cloud based |
+| bing      | [bing](../kalliope/stt/bing/README.md)           | Cloud based |
+| CMUSphinx | [CMUSphinx](../kalliope/stt/cmusphinx/README.md) | Self hosted |
+| Google    | [google](../kalliope/stt/google/README.md)       | Cloud based |
+| Houndify  | [houndify](../kalliope/stt/houndify/README.md)   | Cloud based |
+| wit.ai    | [wit.ai](../kalliope/stt/wit/README.md)          | Cloud based |
 
 
 ## Community STT
@@ -21,9 +21,6 @@ Community STTs need to be installed manually.
 
 To know how to install a community STT, read the "Installation" section of the [STT documentation](stt.md).
 
-| Name     | Description                                    |
-|----------|------------------------------------------------|
-
 Wanna add your STT in the list? Open [an issue](../../issues) with the link of your STT or send a pull request to update the list directly.
 
 

+ 7 - 5
Docs/trigger.md

@@ -28,8 +28,10 @@ Then, open an issue or create a pull request to add the model to the list bellow
 
 ## List of available Snowboy Kalliope model
 
-| Name                                                | language | Pronouced     |
-|-----------------------------------------------------|----------|---------------|
-| [kalliope-FR](https://snowboy.kitt.ai/hotword/1363) | French   | Ka-lio-pé     |
-| [kalliope-EN](https://snowboy.kitt.ai/hotword/2540) | English  | kə-LIE-ə-pee  |
-| [kalliope-RU](https://snowboy.kitt.ai/hotword/2964) | Russian  | каллиопа      |
+> **Important note:** Do not enhance a model in the wrong language. Check the pronunciation before recording your voice!
+
+| Name                                                | language | Pronounced     |
+|-----------------------------------------------------|----------|----------------|
+| [kalliope-FR](https://snowboy.kitt.ai/hotword/1363) | French   | Ka-lio-pé      |
+| [kalliope-EN](https://snowboy.kitt.ai/hotword/2540) | English  | kə-LIE-ə-pee   |
+| [kalliope-RU](https://snowboy.kitt.ai/hotword/2964) | Russian  | каллиопа       |

+ 1 - 6
Docs/tts.md

@@ -50,13 +50,8 @@ cache_path: "/tmp/kalliope_tts_cache"
 generated audio files that will not be played more than once.
 
 ## Current Available TTS
-Core TTSs are already packaged with the installation of Kalliope an can be used out of the box.
 
-- [acapela](../kalliope/tts/acapela/README.md)
-- [googletts](../kalliope/tts/googletts/README.md)
-- [pico2wave](../kalliope/tts/pico2wave/README.md)
-- [voicerss](../kalliope/tts/voicerss/README.md)
-- [voxygen](../kalliope/tts/voxygen/README.md)
+Core TTSs are already packaged with the installation of Kalliope an can be used out of the box. See the [complete list here](tts_list.md).
 
 ## TTS Community Installation
 

+ 10 - 10
Docs/tts_list.md

@@ -6,22 +6,22 @@ See the [complete TTS documentation](stt.md) for more information.
 ## Core TTS
 Core TTSs are already packaged with the installation of Kalliope an can be used out of the box.
 
-| Name      | Description                                      |
-|-----------|--------------------------------------------------|
-| Acapela   | [Acapela](../kalliope/tts/acapela/README.md)     |
-| GoogleTTS | [GoogleTTS](../kalliope/tts/googletts/README.md) |
-| VoiceRSS  | [VoiceRSS](../kalliope/tts/voicerss/README.md)   |
-| Pico2wave | [Pico2wave](../kalliope/tts/pico2wave/README.md) |
-| Voxygen   | [Voxygen](../kalliope/tts/voxygen/README.md)     |
+| Name      | Description                                      | Type        |
+|-----------|--------------------------------------------------|-------------|
+| Acapela   | [Acapela](../kalliope/tts/acapela/README.md)     | Cloud based |
+| GoogleTTS | [GoogleTTS](../kalliope/tts/googletts/README.md) | Cloud based |
+| VoiceRSS  | [VoiceRSS](../kalliope/tts/voicerss/README.md)   | Cloud based |
+| Pico2wave | [Pico2wave](../kalliope/tts/pico2wave/README.md) | Self hosted |
+| Voxygen   | [Voxygen](../kalliope/tts/voxygen/README.md)     | Cloud based |
 
 ## Community TTS
 Community TTSs need to be installed manually.
 
 To know how to install a community TTS, read the "Installation" section of the [TTS documentation](tts.md).
 
-| Name   | Description                                          |
-|--------|------------------------------------------------------|
-| Espeak | [Espeak](https://github.com/Ultchad/kalliope-espeak) |
+| Name   | Description                                          | Type        |
+|--------|------------------------------------------------------|-------------|
+| Espeak | [Espeak](https://github.com/Ultchad/kalliope-espeak) | Self hosted |
 
 Wanna add your TTS in the list? Open [an issue](../../issues) with the link of your TTS or send a pull request to update the list directly.
 

+ 15 - 8
Tests/settings/settings_test.yml

@@ -63,17 +63,24 @@ random_wake_up_sounds:
   - "sounds/dong.wav"
 
 # ---------------------------
-# On ready answers
+# On ready notification
 # ---------------------------
-# You can play a sound when Kalliope is started and is waiting for the trigger.
-random_on_ready_answers:
+# This section is used to notify the user when Kalliope is waiting for a trigger detection by playing a sound or speak a sentence out loud
+
+# This parameter define if you play the on ready answer:
+# - always: every time Kalliope is ready to be awaken
+# - never: never play a sound or sentences when kalliope is ready
+# - once: at the first start of Kalliope
+play_on_ready_notification: never
+
+# The on ready notification can be a sentence. Place here a sentence or a list of sentence. If you set a list, one sentence will be picked up randomly
+on_ready_answers:
   - "Kalliope is ready"
 
-# If you prefer, you can play a sound instead of a sentence.
-# Remove the `random_on_ready_answers` parameters to use this one.
-# Place here the full path of the sound file or just the name of the file in /usr/lib/kalliope/sounds
-# The file must be .wav or .mp3 format. By default two file are provided: ding.wav and dong.wav
-random_on_ready_sounds:
+# You can play a sound instead of a sentence.
+# Remove the `on_ready_answers` parameters by commenting it out and use this one instead.
+# Place here the path of the sound file. Files must be .wav or .mp3 format.
+on_ready_sounds:
   - "sounds/ding.wav"
   - "sounds/dong.wav"
 

+ 26 - 20
Tests/test_settings_loader.py

@@ -25,36 +25,36 @@ class TestSettingLoader(unittest.TestCase):
         self.settings_file_to_test = root_dir + os.sep + "Tests/settings/settings_test.yml"
 
         self.settings_dict = {
+            'default_synapse': 'Default-synapse',
             'rest_api':
-                {'active': True,
+                {'allowed_cors_origin': False,
+                 'active': True,
                  'login': 'admin',
-                 'password': 'secret',
                  'password_protected': True,
-                 'port': 5000,
-                 'allowed_cors_origin': False},
+                 'password': 'secret', 'port': 5000},
             'default_trigger': 'snowboy',
+            'play_on_ready_notification': 'never',
             'triggers': [{'snowboy': {'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'}}],
             'speech_to_text': [{'google': {'language': 'fr-FR'}}],
+            'on_ready_answers': ['Kalliope is ready'],
             'cache_path': '/tmp/kalliope_tts_cache',
             'random_wake_up_answers': ['Oui monsieur?'],
+            'on_ready_sounds': ['sounds/ding.wav', 'sounds/dong.wav'],
+            'resource_directory': {
+                'stt': '/tmp/kalliope/tests/kalliope_resources_dir/stt',
+                'tts': '/tmp/kalliope/tests/kalliope_resources_dir/tts',
+                'neuron': '/tmp/kalliope/tests/kalliope_resources_dir/neurons',
+                'trigger': '/tmp/kalliope/tests/kalliope_resources_dir/trigger'},
             'default_text_to_speech': 'pico2wave',
             'default_speech_to_text': 'google',
             'random_wake_up_sounds': ['sounds/ding.wav', 'sounds/dong.wav'],
-            'random_on_ready_answers': ['Kalliope is ready'],
-            'random_on_ready_sounds': ['sounds/ding.wav', 'sounds/dong.wav'],
             'text_to_speech': [
                 {'pico2wave': {'cache': True, 'language': 'fr-FR'}},
                 {'voxygen': {'voice': 'Agnes', 'cache': True}}
-            ],
-            'default_synapse': 'Default-synapse',
-            'resource_directory':{
-                'neuron': "/tmp/kalliope/tests/kalliope_resources_dir/neurons",
-                'stt': "/tmp/kalliope/tests/kalliope_resources_dir/stt",
-                'tts': "/tmp/kalliope/tests/kalliope_resources_dir/tts",
-                'trigger': "/tmp/kalliope/tests/kalliope_resources_dir/trigger"
-            }
+            ]
         }
 
+
         # Init the folders, otherwise it raises an exceptions
         os.makedirs("/tmp/kalliope/tests/kalliope_resources_dir/neurons")
         os.makedirs("/tmp/kalliope/tests/kalliope_resources_dir/stt")
@@ -90,8 +90,9 @@ class TestSettingLoader(unittest.TestCase):
         settings_object.stts = [stt]
         settings_object.random_wake_up_answers = ['Oui monsieur?']
         settings_object.random_wake_up_sounds = ['sounds/ding.wav', 'sounds/dong.wav']
-        settings_object.random_on_ready_answers = ['Kalliope is ready']
-        settings_object.random_on_ready_sounds = ['sounds/ding.wav', 'sounds/dong.wav']
+        settings_object.play_on_ready_notification = "never"
+        settings_object.on_ready_answers = ['Kalliope is ready']
+        settings_object.on_ready_sounds = ['sounds/ding.wav', 'sounds/dong.wav']
         trigger1 = Trigger(name="snowboy",
                            parameters={'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'})
         settings_object.triggers = [trigger1]
@@ -105,7 +106,7 @@ class TestSettingLoader(unittest.TestCase):
                               tts_folder="/tmp/kalliope/tests/kalliope_resources_dir/tts",
                               trigger_folder="/tmp/kalliope/tests/kalliope_resources_dir/trigger")
 
-        settings_object.resources=resources
+        settings_object.resources = resources
         settings_object.machine = platform.machine()
 
         sl = SettingLoader(file_path=self.settings_file_to_test)
@@ -150,10 +151,15 @@ class TestSettingLoader(unittest.TestCase):
         sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual(expected_random_wake_up_answers, sl._get_random_wake_up_answers(self.settings_dict))
 
-    def test_get_random_on_ready_answers(self):
-        expected_random_on_ready_answers = ['Kalliope is ready']
+    def test_get_on_ready_answers(self):
+        expected_on_ready_answers = ['Kalliope is ready']
+        sl = SettingLoader(file_path=self.settings_file_to_test)
+        self.assertEqual(expected_on_ready_answers, sl._get_on_ready_answers(self.settings_dict))
+
+    def test_get_on_ready_sounds(self):
+        expected_on_ready_sounds = ['sounds/ding.wav', 'sounds/dong.wav']
         sl = SettingLoader(file_path=self.settings_file_to_test)
-        self.assertEqual(expected_random_on_ready_answers, sl._get_random_on_ready_answers(self.settings_dict))
+        self.assertEqual(expected_on_ready_sounds, sl._get_on_ready_sounds(self.settings_dict))
 
     def test_get_rest_api(self):
         expected_rest_api = RestAPI(password_protected=True, active=True,

+ 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":

+ 1 - 1
kalliope/_version.py

@@ -1,2 +1,2 @@
 # https://www.python.org/dev/peps/pep-0440/
-version_str = "0.4.0"
+version_str = "0.4.1"

+ 61 - 62
kalliope/core/ConfigurationManager/SettingLoader.py

@@ -104,9 +104,10 @@ class SettingLoader(object):
         ttss = self._get_ttss(settings)
         triggers = self._get_triggers(settings)
         random_wake_up_answers = self._get_random_wake_up_answers(settings)
-        random_wake_up_sounds = self._get_random_wake_up_sounds(settings)
-        random_on_ready_answers = self._get_random_on_ready_answers(settings)
-        random_on_ready_sounds = self._get_random_on_ready_sounds(settings)
+        random_wake_up_sound = self._get_random_wake_up_sounds(settings)
+        play_on_ready_notification = self._get_play_on_ready_notification(settings)
+        on_ready_answers = self._get_on_ready_answers(settings)
+        on_ready_sounds = self._get_on_ready_sounds(settings)
         rest_api = self._get_rest_api(settings)
         cache_path = self._get_cache_path(settings)
         default_synapse = self._get_default_synapse(settings)
@@ -120,9 +121,10 @@ class SettingLoader(object):
         setting_object.ttss = ttss
         setting_object.triggers = triggers
         setting_object.random_wake_up_answers = random_wake_up_answers
-        setting_object.random_wake_up_sounds = random_wake_up_sounds
-        setting_object.random_on_ready_answers = random_on_ready_answers
-        setting_object.random_on_ready_sounds = random_on_ready_sounds
+        setting_object.random_wake_up_sounds = random_wake_up_sound
+        setting_object.play_on_ready_notification = play_on_ready_notification
+        setting_object.on_ready_answers = on_ready_answers
+        setting_object.on_ready_sounds = on_ready_sounds
         setting_object.rest_api = rest_api
         setting_object.cache_path = cache_path
         setting_object.default_synapse = default_synapse
@@ -397,62 +399,6 @@ class SettingLoader(object):
 
         return random_wake_up_sounds_list
 
-    @staticmethod
-    def _get_random_on_ready_answers(settings):
-        """
-        Return a list of the wake up answers set up on the settings.yml file
-
-        :param settings: The YAML settings file
-        :type settings: dict
-        :return: List of on ready answers
-        :rtype: list of str
-
-        :Example:
-
-            wakeup = cls._get_random_on_ready_answers(settings)
-
-        .. seealso::
-        .. warnings:: Class Method and Private
-        """
-
-        try:
-            random_on_ready_answers_list = settings["random_on_ready_answers"]
-        except KeyError:
-            # User does not provide this settings
-            return None
-
-        return random_on_ready_answers_list
-
-    @staticmethod
-    def _get_random_on_ready_sounds(settings):
-        """
-        Return a list of the on ready sounds set up on the settings.yml file
-
-        :param settings: The YAML settings file
-        :type settings: dict
-        :return: list of wake up sounds
-        :rtype: list of str
-
-        :Example:
-
-            wakeup_sounds = cls._get_random_on_ready_sounds(settings)
-
-        .. seealso::
-        .. warnings:: Class Method and Private
-        """
-
-        try:
-            random_on_ready_sounds_list = settings["random_on_ready_sounds"]
-            # In case files are declared in settings.yml, make sure kalliope can access them.
-            for sound in random_on_ready_sounds_list:
-                if Utils.get_real_file_path(sound) is None:
-                    raise SettingInvalidException("sound file %s not found" % sound)
-        except KeyError:
-            # User does not provide this settings
-            return None
-
-        return random_on_ready_sounds_list
-
     @staticmethod
     def _get_rest_api(settings):
         """
@@ -643,4 +589,57 @@ class SettingLoader(object):
 
         return resource_object
 
+    @staticmethod
+    def _get_play_on_ready_notification(settings):
+        """
+        Return the on_ready_notification setting. If the user didn't provided it the default is never
+        :param settings: The YAML settings file
+        :type settings: dict
+        :return:
+        """
+        try:
+            play_on_ready_notification = settings["play_on_ready_notification"]
+        except KeyError:
+            # User does not provide this settings, by default we set it to never
+            play_on_ready_notification = "never"
+            return play_on_ready_notification
+        return play_on_ready_notification
+
+    @staticmethod
+    def _get_on_ready_answers( settings):
+        """
+        Return the list of on_ready_answers string from the settings.
+        :param settings: The YAML settings file
+        :type settings: dict
+        :return: String parameter on_ready_answers
+        """
+        try:
+            on_ready_answers = settings["on_ready_answers"]
+        except KeyError:
+            # User does not provide this settings
+            return None
+
+        return on_ready_answers
+
+    @staticmethod
+    def _get_on_ready_sounds(settings):
+        """
+        Return the list of on_ready_sounds string from the settings.
+        :param settings: The YAML settings file
+        :type settings: dict
+        :return: String parameter on_ready_sounds
+        """
+        try:
+            on_ready_sounds = settings["on_ready_sounds"]
+            # In case files are declared in settings.yml, make sure kalliope can access them.
+            for sound in on_ready_sounds:
+                if Utils.get_real_file_path(sound) is None:
+                    raise SettingInvalidException("sound file %s not found" % sound)
+        except KeyError:
+            # User does not provide this settings
+            return None
+
+        return on_ready_sounds
+
+
 

+ 159 - 27
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,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,
-                                 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()
 
-        # create an order listener object. This last will the trigger callback before starting
-        self.order_listener = OrderListener(self.analyse_order)
+        # save an instance of the trigger
+        self.trigger_instance = None
+        self.trigger_callback_called = False
 
-        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)
+        # save the current order listener
+        self.order_listener = None
+        self.order_listener_callback_called = False
 
-        # Wait that the kalliope trigger is pronounced by the user
+        # 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 callback(self):
+    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 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)
@@ -65,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):
         """
@@ -88,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):
@@ -103,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()

+ 6 - 4
kalliope/core/Models/Settings.py

@@ -16,8 +16,9 @@ class Settings(object):
                  stts=None,
                  random_wake_up_answers=None,
                  random_wake_up_sounds=None,
-                 random_on_ready_answers=None,
-                 random_on_ready_sounds=None,
+                 play_on_ready_notification=None,
+                 on_ready_answers=None,
+                 on_ready_sounds=None,
                  triggers=None,
                  rest_api=None,
                  cache_path=None,
@@ -33,8 +34,9 @@ class Settings(object):
         self.stts = stts
         self.random_wake_up_answers = random_wake_up_answers
         self.random_wake_up_sounds = random_wake_up_sounds
-        self.random_on_ready_answers = random_on_ready_answers
-        self.random_on_ready_sounds = random_on_ready_sounds
+        self.play_on_ready_notification = play_on_ready_notification
+        self.on_ready_answers = on_ready_answers
+        self.on_ready_sounds = on_ready_sounds
         self.triggers = triggers
         self.rest_api = rest_api
         self.cache_path = cache_path

+ 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)

+ 20 - 12
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"
 
 
 # ---------------------------
@@ -99,19 +99,27 @@ random_wake_up_answers:
 
 
 # ---------------------------
-# On ready answers
+# On ready notification
 # ---------------------------
-# You can play a sound when Kalliope is started and is waiting for the trigger.
-#random_on_ready_answers:
-#  - "Kalliope is ready"
+# This section is used to notify the user when Kalliope is waiting for a trigger detection by playing a sound or speak a sentence out loud
 
-# If you prefer, you can play a sound instead of a sentence.
-# Remove the `random_on_ready_answers` parameters to use this one.
-# Place here the full path of the sound file or just the name of the file in /usr/lib/kalliope/sounds
-# The file must be .wav or .mp3 format. By default two file are provided: ding.wav and dong.wav
-#random_on_ready_sounds:
-#  - "sounds/ding.wav"
-#  - "sounds/dong.wav"
+# This parameter define if you play the on ready answer:
+# - always: every time Kalliope is ready to be awaken
+# - never: never play a sound or sentences when kalliope is ready
+# - once: at the first start of Kalliope
+play_on_ready_notification: never
+
+# The on ready notification can be a sentence. Place here a sentence or a list of sentence. If you set a list, one sentence will be picked up randomly
+on_ready_answers:
+  - "Kalliope is ready"
+  - "Waiting for order"
+
+# You can play a sound instead of a sentence.
+# Remove the `on_ready_answers` parameters by commenting it out and use this one instead.
+# Place here the path of the sound file. Files must be .wav or .mp3 format.
+on_ready_sounds:
+  - "sounds/ding.wav"
+  - "sounds/dong.wav"
 
 
 # ---------------------------

+ 40 - 6
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.stop_listening = None
+        self.callback = None
+        self.stop_thread = 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)
+        self.stop_thread = 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_thread()
+
+    def start_listening(self):
+        """
+        A method to start the thread
+        """
+        self.start()
+
+    def stop_listening(self):
+        self.kill_yourself = True
 
-    def interrupt(self):
-        self.stop_listening()
+    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
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'
     ],