瀏覽代碼

Merge pull request #189 from kalliope-project/dev_nico

refactor
Monf 8 年之前
父節點
當前提交
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
 - add CORS support for REST API
 - fix installation of community STT and TTS
 - fix installation of community STT and TTS
@@ -7,6 +7,7 @@ v0.4.1 / 2017-01-12
 - add offline STT (CMUSphinx)
 - add offline STT (CMUSphinx)
 - orders are not anymore case sensitive
 - orders are not anymore case sensitive
 - add STT utils class to split the audio catching from the processing
 - add STT utils class to split the audio catching from the processing
+- fix CTRL-C to kill Kalliope
 
 
 v0.4.0 / 2016-12-28
 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 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.
 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. 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. 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
     ```python
     def __init__(self, callback=None, **kwargs):
     def __init__(self, callback=None, **kwargs):
         OrderListener.__init__(self)
         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)
         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):
         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
             # 'audio' contain stream caught by the microphone
             # ---------------------------------------------
             # ---------------------------------------------
             
             
             # at the end of the process, send the text into the received callback method
             # 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
 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
 Kalliope will load default settings and brain, the output should looks the following
 ```bash
 ```bash
 Starting event manager
 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-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/)
 [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
 ## Debian packages requirements
 
 

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

@@ -6,5 +6,11 @@ Install some required system libraries and softwares:
 
 
 ```bash
 ```bash
 sudo apt-get update
 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
 ```bash
 sudo apt-get update
 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
 kalliope start
 ```
 ```
 
 
+> **Note:** Do not use the CLI as root user or with sudo. Kalliope must run with standard user privileges.
+
 ## ARGUMENTS
 ## ARGUMENTS
 
 
 ### start
 ### start

+ 21 - 25
Docs/settings.md

@@ -177,41 +177,37 @@ random_wake_up_sounds:
 E.g: `# random_wake_up_answers:`
 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
 ```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
 ```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
 ```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"
   - "ding.wav"
   - "dong.wav"
   - "dong.wav"
   - "/my/personal/full/path/my_file.mp3"
   - "/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.
 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
 ## 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
 ## 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 STT
 Core STTs are already packaged with the installation of Kalliope an can be used out of the box.
 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
 ## 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).
 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.
 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
 ## 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.
 generated audio files that will not be played more than once.
 
 
 ## Current Available TTS
 ## 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
 ## 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 TTS
 Core TTSs are already packaged with the installation of Kalliope an can be used out of the box.
 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 TTS
 Community TTSs need to be installed manually.
 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).
 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.
 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"
   - "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"
   - "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/ding.wav"
   - "sounds/dong.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_file_to_test = root_dir + os.sep + "Tests/settings/settings_test.yml"
 
 
         self.settings_dict = {
         self.settings_dict = {
+            'default_synapse': 'Default-synapse',
             'rest_api':
             'rest_api':
-                {'active': True,
+                {'allowed_cors_origin': False,
+                 'active': True,
                  'login': 'admin',
                  'login': 'admin',
-                 'password': 'secret',
                  'password_protected': True,
                  'password_protected': True,
-                 'port': 5000,
-                 'allowed_cors_origin': False},
+                 'password': 'secret', 'port': 5000},
             'default_trigger': 'snowboy',
             'default_trigger': 'snowboy',
+            'play_on_ready_notification': 'never',
             'triggers': [{'snowboy': {'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'}}],
             'triggers': [{'snowboy': {'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'}}],
             'speech_to_text': [{'google': {'language': 'fr-FR'}}],
             'speech_to_text': [{'google': {'language': 'fr-FR'}}],
+            'on_ready_answers': ['Kalliope is ready'],
             'cache_path': '/tmp/kalliope_tts_cache',
             'cache_path': '/tmp/kalliope_tts_cache',
             'random_wake_up_answers': ['Oui monsieur?'],
             '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_text_to_speech': 'pico2wave',
             'default_speech_to_text': 'google',
             'default_speech_to_text': 'google',
             'random_wake_up_sounds': ['sounds/ding.wav', 'sounds/dong.wav'],
             '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': [
             'text_to_speech': [
                 {'pico2wave': {'cache': True, 'language': 'fr-FR'}},
                 {'pico2wave': {'cache': True, 'language': 'fr-FR'}},
                 {'voxygen': {'voice': 'Agnes', 'cache': True}}
                 {'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
         # 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/neurons")
         os.makedirs("/tmp/kalliope/tests/kalliope_resources_dir/stt")
         os.makedirs("/tmp/kalliope/tests/kalliope_resources_dir/stt")
@@ -90,8 +90,9 @@ class TestSettingLoader(unittest.TestCase):
         settings_object.stts = [stt]
         settings_object.stts = [stt]
         settings_object.random_wake_up_answers = ['Oui monsieur?']
         settings_object.random_wake_up_answers = ['Oui monsieur?']
         settings_object.random_wake_up_sounds = ['sounds/ding.wav', 'sounds/dong.wav']
         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",
         trigger1 = Trigger(name="snowboy",
                            parameters={'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'})
                            parameters={'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'})
         settings_object.triggers = [trigger1]
         settings_object.triggers = [trigger1]
@@ -105,7 +106,7 @@ class TestSettingLoader(unittest.TestCase):
                               tts_folder="/tmp/kalliope/tests/kalliope_resources_dir/tts",
                               tts_folder="/tmp/kalliope/tests/kalliope_resources_dir/tts",
                               trigger_folder="/tmp/kalliope/tests/kalliope_resources_dir/trigger")
                               trigger_folder="/tmp/kalliope/tests/kalliope_resources_dir/trigger")
 
 
-        settings_object.resources=resources
+        settings_object.resources = resources
         settings_object.machine = platform.machine()
         settings_object.machine = platform.machine()
 
 
         sl = SettingLoader(file_path=self.settings_file_to_test)
         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)
         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))
         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)
         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):
     def test_get_rest_api(self):
         expected_rest_api = RestAPI(password_protected=True, active=True,
         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
 apscheduler==3.3.0
 GitPython==2.1.1
 GitPython==2.1.1
 packaging>=16.8
 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")
             Utils.print_info("Press Ctrl+C for stopping")
             # catch signal for killing on Ctrl+C pressed
             # catch signal for killing on Ctrl+C pressed
             signal.signal(signal.SIGINT, signal_handler)
             signal.signal(signal.SIGINT, signal_handler)
-            # start the main controller
+            # start the state machine
             MainController(brain=brain)
             MainController(brain=brain)
 
 
     if args.action == "gui":
     if args.action == "gui":

+ 1 - 1
kalliope/_version.py

@@ -1,2 +1,2 @@
 # https://www.python.org/dev/peps/pep-0440/
 # 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)
         ttss = self._get_ttss(settings)
         triggers = self._get_triggers(settings)
         triggers = self._get_triggers(settings)
         random_wake_up_answers = self._get_random_wake_up_answers(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)
         rest_api = self._get_rest_api(settings)
         cache_path = self._get_cache_path(settings)
         cache_path = self._get_cache_path(settings)
         default_synapse = self._get_default_synapse(settings)
         default_synapse = self._get_default_synapse(settings)
@@ -120,9 +121,10 @@ class SettingLoader(object):
         setting_object.ttss = ttss
         setting_object.ttss = ttss
         setting_object.triggers = triggers
         setting_object.triggers = triggers
         setting_object.random_wake_up_answers = random_wake_up_answers
         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.rest_api = rest_api
         setting_object.cache_path = cache_path
         setting_object.cache_path = cache_path
         setting_object.default_synapse = default_synapse
         setting_object.default_synapse = default_synapse
@@ -397,62 +399,6 @@ class SettingLoader(object):
 
 
         return random_wake_up_sounds_list
         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
     @staticmethod
     def _get_rest_api(settings):
     def _get_rest_api(settings):
         """
         """
@@ -643,4 +589,57 @@ class SettingLoader(object):
 
 
         return resource_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 logging
 import random
 import random
+from time import sleep
 
 
 from flask import Flask
 from flask import Flask
+from transitions import Machine
 
 
 from kalliope.core import Utils
 from kalliope.core import Utils
 from kalliope.core.ConfigurationManager import SettingLoader
 from kalliope.core.ConfigurationManager import SettingLoader
@@ -20,44 +22,151 @@ class MainController:
     """
     """
     This Class is the global controller of the application.
     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):
     def __init__(self, brain=None):
         self.brain = brain
         self.brain = brain
         # get global configuration
         # get global configuration
         sl = SettingLoader()
         sl = SettingLoader()
         self.settings = sl.settings
         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 = self._get_default_trigger()
+        self.trigger_instance.daemon = True
+        # Wait that the kalliope trigger is pronounced by the user
         self.trigger_instance.start()
         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")
         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
         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.
         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
         # pause the trigger process
         self.trigger_instance.pause()
         self.trigger_instance.pause()
         # start listening for an order
         # 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.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 random wake answer sentence are present, we play this
         if self.settings.random_wake_up_answers is not None:
         if self.settings.random_wake_up_answers is not None:
             Say(message=self.settings.random_wake_up_answers)
             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)
             random_sound_to_play = self._get_random_sound(self.settings.random_wake_up_sounds)
             Mplayer.play(random_sound_to_play)
             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
         Receive an order, try to retrieve it in the brain.yml to launch to attached plugins
         :param order: the sentence received
         :param order: the sentence received
         :type order: str
         :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):
     def _get_default_trigger(self):
         """
         """
@@ -88,7 +205,7 @@ class MainController:
         """
         """
         for trigger in self.settings.triggers:
         for trigger in self.settings.triggers:
             if trigger.name == self.settings.default_trigger_name:
             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
     @staticmethod
     def _get_random_sound(random_wake_up_sounds):
     def _get_random_sound(random_wake_up_sounds):
@@ -103,3 +220,18 @@ class MainController:
         random_path = random.choice(random_wake_up_sounds)
         random_path = random.choice(random_wake_up_sounds)
         logger.debug("Selected sound: %s" % random_path)
         logger.debug("Selected sound: %s" % random_path)
         return Utils.get_real_file_path(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,
                  stts=None,
                  random_wake_up_answers=None,
                  random_wake_up_answers=None,
                  random_wake_up_sounds=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,
                  triggers=None,
                  rest_api=None,
                  rest_api=None,
                  cache_path=None,
                  cache_path=None,
@@ -33,8 +34,9 @@ class Settings(object):
         self.stts = stts
         self.stts = stts
         self.random_wake_up_answers = random_wake_up_answers
         self.random_wake_up_answers = random_wake_up_answers
         self.random_wake_up_sounds = random_wake_up_sounds
         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.triggers = triggers
         self.rest_api = rest_api
         self.rest_api = rest_api
         self.cache_path = cache_path
         self.cache_path = cache_path

+ 6 - 5
kalliope/core/OrderListener.py

@@ -41,12 +41,13 @@ class OrderListener(Thread):
         self.callback = callback
         self.callback = callback
         sl = SettingLoader()
         sl = SettingLoader()
         self.settings = sl.settings
         self.settings = sl.settings
+        self.stt_instance = None
 
 
     def run(self):
     def run(self):
         """
         """
         Start thread
         Start thread
         """
         """
-        self.load_stt_plugin()
+        self.stt_instance = self.load_stt_plugin()
 
 
     def load_stt_plugin(self):
     def load_stt_plugin(self):
         if self.stt is None:
         if self.stt is None:
@@ -59,10 +60,10 @@ class OrderListener(Thread):
                 stt_folder = None
                 stt_folder = None
                 if self.settings.resources:
                 if self.settings.resources:
                     stt_folder = self.settings.resources.stt_folder
                     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
     @staticmethod
     def _ignore_stderr():
     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
 # - snowboy
 triggers:
 triggers:
   - snowboy:
   - 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
 import speech_recognition as sr
 
 
 from kalliope import Utils
 from kalliope import Utils
 
 
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
 
 
-class SpeechRecognition(object):
+class SpeechRecognition(Thread):
 
 
     def __init__(self):
     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.recognizer = sr.Recognizer()
         self.microphone = sr.Microphone()
         self.microphone = sr.Microphone()
-        self.stop_listening = None
+        self.callback = None
+        self.stop_thread = None
+        self.kill_yourself = False
         with self.microphone as source:
         with self.microphone as source:
             # we only need to calibrate once, before we start listening
             # we only need to calibrate once, before we start listening
             self.recognizer.adjust_for_ambient_noise(source)
             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!")
         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)
         SpeechRecognition.__init__(self)
 
 
         # callback function to call after the translation speech/tex
         # callback function to call after the translation speech/tex
-        self.callback = callback
+        self.main_controller_callback = callback
         self.key = kwargs.get('key', None)
         self.key = kwargs.get('key', None)
         self.language = kwargs.get('language', "en")
         self.language = kwargs.get('language', "en")
         self.session_id = kwargs.get('session_id', None)
         self.session_id = kwargs.get('session_id', None)
         self.show_all = kwargs.get('show_all', False)
         self.show_all = kwargs.get('show_all', False)
 
 
         # start listening in the background
         # 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):
     def apiai_callback(self, recognizer, audio):
         """
         """
@@ -43,16 +44,19 @@ class Apiai(SpeechRecognition):
         except sr.UnknownValueError as e:
         except sr.UnknownValueError as e:
             Utils.print_warning("Apiai Speech Recognition could not understand audio; {0}".format(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
             # 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:
         except sr.RequestError as e:
             Utils.print_danger("Could not request results from Apiai Speech Recognition service; {0}".format(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
             # 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
         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)
         SpeechRecognition.__init__(self)
 
 
         # callback function to call after the translation speech/tex
         # callback function to call after the translation speech/tex
-        self.callback = callback
+        self.main_controller_callback = callback
         self.key = kwargs.get('key', None)
         self.key = kwargs.get('key', None)
         self.language = kwargs.get('language', "en-US")
         self.language = kwargs.get('language', "en-US")
         self.show_all = kwargs.get('show_all', False)
         self.show_all = kwargs.get('show_all', False)
+
         # start listening in the background
         # 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):
     def bing_callback(self, recognizer, audio):
         """
         """
@@ -37,16 +39,19 @@ class Bing(SpeechRecognition):
         except sr.UnknownValueError:
         except sr.UnknownValueError:
             Utils.print_warning("Bing Speech Recognition could not understand audio")
             Utils.print_warning("Bing Speech Recognition could not understand audio")
             # callback anyway, we need to listen again for a new order
             # 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:
         except sr.RequestError as e:
             Utils.print_danger("Could not request results from Bing Speech Recognition service; {0}".format(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
             # 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
         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)
         SpeechRecognition.__init__(self)
 
 
         # callback function to call after the translation speech/tex
         # callback function to call after the translation speech/tex
-        self.callback = callback
+        self.main_controller_callback = callback
 
 
         # start listening in the background
         # 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):
     def sphinx_callback(self, recognizer, audio):
         """
         """
@@ -32,16 +33,19 @@ class Cmusphinx(SpeechRecognition):
         except sr.UnknownValueError:
         except sr.UnknownValueError:
             Utils.print_warning("Sphinx Speech Recognition could not understand audio")
             Utils.print_warning("Sphinx Speech Recognition could not understand audio")
             # callback anyway, we need to listen again for a new order
             # 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:
         except sr.RequestError as e:
             Utils.print_danger("Could not request results from Sphinx Speech Recognition service; {0}".format(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
             # 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
         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
 import speech_recognition as sr
 
 
 from kalliope.core import Utils
 from kalliope.core import Utils
@@ -15,13 +17,14 @@ class Google(SpeechRecognition):
         SpeechRecognition.__init__(self)
         SpeechRecognition.__init__(self)
 
 
         # callback function to call after the translation speech/tex
         # callback function to call after the translation speech/tex
-        self.callback = callback
+        self.main_controller_callback = callback
         self.key = kwargs.get('key', None)
         self.key = kwargs.get('key', None)
         self.language = kwargs.get('language', "en-US")
         self.language = kwargs.get('language', "en-US")
         self.show_all = kwargs.get('show_all', False)
         self.show_all = kwargs.get('show_all', False)
 
 
         # start listening in the background
         # 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):
     def google_callback(self, recognizer, audio):
         """
         """
@@ -34,7 +37,6 @@ class Google(SpeechRecognition):
                                                          show_all=self.show_all)
                                                          show_all=self.show_all)
             Utils.print_success("Google Speech Recognition thinks you said %s" % captured_audio)
             Utils.print_success("Google Speech Recognition thinks you said %s" % captured_audio)
             self._analyse_audio(audio_to_text=captured_audio)
             self._analyse_audio(audio_to_text=captured_audio)
-
         except sr.UnknownValueError:
         except sr.UnknownValueError:
             Utils.print_warning("Google Speech Recognition could not understand audio")
             Utils.print_warning("Google Speech Recognition could not understand audio")
             # callback anyway, we need to listen again for a new order
             # 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
             # callback anyway, we need to listen again for a new order
             self._analyse_audio(audio_to_text=None)
             self._analyse_audio(audio_to_text=None)
 
 
+        self.stop_listening()
+
     def _analyse_audio(self, audio_to_text):
     def _analyse_audio(self, audio_to_text):
         """
         """
         Confirm the audio exists and run it in a Callback
         Confirm the audio exists and run it in a Callback
         :param audio_to_text: the captured audio
         :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)
         SpeechRecognition.__init__(self)
 
 
         # callback function to call after the translation speech/tex
         # 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.client_id = kwargs.get('client_id', None)
         self.key = kwargs.get('key', None)
         self.key = kwargs.get('key', None)
         # only english supported
         # only english supported
@@ -23,7 +23,8 @@ class Houndify(SpeechRecognition):
         self.show_all = kwargs.get('show_all', False)
         self.show_all = kwargs.get('show_all', False)
 
 
         # start listening in the background
         # 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):
     def houndify_callback(self, recognizer, audio):
         """
         """
@@ -40,16 +41,19 @@ class Houndify(SpeechRecognition):
         except sr.UnknownValueError:
         except sr.UnknownValueError:
             Utils.print_warning("Houndify Speech Recognition could not understand audio")
             Utils.print_warning("Houndify Speech Recognition could not understand audio")
             # callback anyway, we need to listen again for a new order
             # 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:
         except sr.RequestError as e:
             Utils.print_danger("Could not request results from Houndify Speech Recognition service; {0}".format(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
             # 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
         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)
         SpeechRecognition.__init__(self)
 
 
         # callback function to call after the translation speech/tex
         # callback function to call after the translation speech/tex
-        self.callback = callback
+        self.main_controller_callback = callback
         self.key = kwargs.get('key', None)
         self.key = kwargs.get('key', None)
         self.show_all = kwargs.get('show_all', False)
         self.show_all = kwargs.get('show_all', False)
 
 
         # start listening in the background
         # 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):
     def wit_callback(self, recognizer, audio):
         try:
         try:
@@ -33,19 +34,19 @@ class Wit(SpeechRecognition):
         except sr.UnknownValueError:
         except sr.UnknownValueError:
             Utils.print_warning("Wit.ai Speech Recognition could not understand audio")
             Utils.print_warning("Wit.ai Speech Recognition could not understand audio")
             # callback anyway, we need to listen again for a new order
             # 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:
         except sr.RequestError as e:
             Utils.print_danger("Could not request results from Wit.ai Speech Recognition service; {0}".format(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
             # 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)

二進制
kalliope/trigger/snowboy/resources/kalliope-FR-13samples.pmdl


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

@@ -2,9 +2,11 @@ import inspect
 import logging
 import logging
 import os
 import os
 import time
 import time
+from threading import Thread
 
 
-from kalliope.core.TriggerModule import TriggerModule
+from kalliope import Utils
 from kalliope.trigger.snowboy import snowboydecoder
 from kalliope.trigger.snowboy import snowboydecoder
+from cffi import FFI as _FFI
 
 
 
 
 class SnowboyModelNotFounfd(Exception):
 class SnowboyModelNotFounfd(Exception):
@@ -18,10 +20,11 @@ logging.basicConfig()
 logger = logging.getLogger("kalliope")
 logger = logging.getLogger("kalliope")
 
 
 
 
-class Snowboy(TriggerModule):
+class Snowboy(Thread):
 
 
     def __init__(self, **kwargs):
     def __init__(self, **kwargs):
         super(Snowboy, self).__init__()
         super(Snowboy, self).__init__()
+        self._ignore_stderr()
         # pause listening boolean
         # pause listening boolean
         self.interrupted = False
         self.interrupted = False
         self.kill_received = False
         self.kill_received = False
@@ -36,7 +39,7 @@ class Snowboy(TriggerModule):
         if self.pmdl is None:
         if self.pmdl is None:
             raise MissingParameterException("Pmdl file is required with snowboy")
             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):
         if not os.path.isfile(self.pmdl_path):
             raise SnowboyModelNotFounfd("The snowboy model file %s does not exist" % 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
         return self.interrupted
 
 
-    def start(self):
+    def run(self):
         """
         """
         Start the snowboy thread and wait for a Kalliope trigger word
         Start the snowboy thread and wait for a Kalliope trigger word
         :return:
         :return:
         """
         """
-        # start snowboy loop
+        # start snowboy loop forever
         self.detector.daemon = True
         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):
     def pause(self):
         """
         """
@@ -84,3 +78,25 @@ class Snowboy(TriggerModule):
         logger.debug("Unpausing snowboy process")
         logger.debug("Unpausing snowboy process")
         self.detector.paused = False
         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',
         'Flask-Testing==0.6.1',
         'apscheduler==3.3.0',
         'apscheduler==3.3.0',
         'GitPython==2.1.1',
         'GitPython==2.1.1',
-        'packaging>=16.8'
+        'packaging>=16.8',
+        'transitions>=0.4.3'
     ],
     ],