Browse Source

Merge pull request #190 from kalliope-project/dev

Dev
Nicolas Marcq 8 years ago
parent
commit
faa735498e
68 changed files with 1128 additions and 487 deletions
  1. 2 0
      .travis.yml
  2. 11 0
      CHANGELOG.md
  3. 9 9
      Docs/brain.md
  4. 1 1
      Docs/contributing.md
  5. 4 4
      Docs/contributing/contribute_neuron.md
  6. 30 24
      Docs/contributing/contribute_stt.md
  7. 3 3
      Docs/contributing/contribute_tts.md
  8. 3 3
      Docs/contributing/dna.md
  9. 2 2
      Docs/contributing/installation_file.md
  10. 2 2
      Docs/contributing/neuron_template.md
  11. 3 2
      Docs/contributing/stt_template.md
  12. 17 15
      Docs/installation.md
  13. 5 5
      Docs/installation/debian_jessie.md
  14. 30 13
      Docs/installation/raspbian_jessie.md
  15. 8 2
      Docs/installation/ubuntu_14.04.md
  16. 7 2
      Docs/installation/ubuntu_16.04.md
  17. 14 12
      Docs/kalliope_cli.md
  18. 12 8
      Docs/neuron_list.md
  19. 11 11
      Docs/neurons.md
  20. 11 11
      Docs/rest_api.md
  21. 85 22
      Docs/settings.md
  22. 10 10
      Docs/signals.md
  23. 5 10
      Docs/stt.md
  24. 8 10
      Docs/stt_list.md
  25. 8 6
      Docs/trigger.md
  26. 6 11
      Docs/tts.md
  27. 10 10
      Docs/tts_list.md
  28. 9 5
      README.md
  29. 1 1
      Tests/modules/test_invalid_dna.yml
  30. 1 1
      Tests/modules/test_valid_dna.yml
  31. 26 3
      Tests/settings/settings_test.yml
  32. 15 6
      Tests/test_dna_loader.py
  33. 43 0
      Tests/test_order_analyser.py
  34. 10 3
      Tests/test_resources_manager.py
  35. 1 0
      Tests/test_rest_api.py
  36. 34 16
      Tests/test_settings_loader.py
  37. 0 1
      install/files/deb-packages_requirements.txt
  38. 3 0
      install/files/python_requirements.txt
  39. 1 1
      kalliope/__init__.py
  40. 1 1
      kalliope/_version.py
  41. 0 2
      kalliope/brain.yml
  42. 10 0
      kalliope/core/ConfigurationManager/DnaLoader.py
  43. 74 6
      kalliope/core/ConfigurationManager/SettingLoader.py
  44. 161 22
      kalliope/core/MainController.py
  45. 6 4
      kalliope/core/Models/RestAPI.py
  46. 6 0
      kalliope/core/Models/Settings.py
  47. 5 1
      kalliope/core/NeuronModule.py
  48. 27 1
      kalliope/core/OrderAnalyser.py
  49. 10 4
      kalliope/core/OrderListener.py
  50. 11 5
      kalliope/core/ResourcesManager.py
  51. 9 1
      kalliope/core/RestAPI/FlaskAPI.py
  52. 0 30
      kalliope/core/TriggerModule.py
  53. 31 6
      kalliope/settings.yml
  54. 0 0
      kalliope/sounds/ding.wav
  55. 0 0
      kalliope/sounds/dong.wav
  56. 55 0
      kalliope/stt/Utils.py
  57. 34 28
      kalliope/stt/apiai/apiai.py
  58. 29 28
      kalliope/stt/bing/bing.py
  59. 24 0
      kalliope/stt/cmusphinx/README.md
  60. 1 0
      kalliope/stt/cmusphinx/__init__.py
  61. 51 0
      kalliope/stt/cmusphinx/cmusphinx.py
  62. 26 27
      kalliope/stt/google/google.py
  63. 2 2
      kalliope/stt/houndify/README.md
  64. 32 30
      kalliope/stt/houndify/houndify.py
  65. 25 27
      kalliope/stt/wit/wit.py
  66. BIN
      kalliope/trigger/snowboy/resources/kalliope-FR-20samples.pmdl
  67. 32 16
      kalliope/trigger/snowboy/snowboy.py
  68. 5 1
      setup.py

+ 2 - 0
.travis.yml

@@ -9,6 +9,8 @@ before_install:
 - sudo apt-get update
 - sudo apt-get install $(cat install/files/deb-packages_requirements.txt)
 - sudo apt-get install libstdc++6
+- wget https://bootstrap.pypa.io/get-pip.py
+- sudo python get-pip.py
 install: "pip install -r install/files/python_requirements.txt"
 # command to run tests
 script: pytest

+ 11 - 0
CHANGELOG.md

@@ -1,3 +1,14 @@
+v0.4.1 / 2017-01-15
+===================
+- add CORS support for REST API
+- fix installation of community STT and TTS
+- add feature: sound/sentence when Kalliope is ready
+- fix Raspbian install
+- 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
 ===================
 - Add resources directory (neuron, stt, tts, (trigger))

+ 9 - 9
Docs/brain.md

@@ -20,7 +20,7 @@ Kalliope will look for the brain in the order bellow:
 
 Let's take a look on a basic synapse in our brain:
 
-```
+```yml
 ---
   - name: "Say-hello"
     signals:
@@ -37,14 +37,14 @@ The file starts with: `---`. This is a requirement for YAML to interpret the fil
 Items that begin with a ```-``` are considered as list items. Items have the format of ```key: value``` where value can be a simple string or a sequence of other items.
 
 At the top level we have a "name" tag. This is the **unique identifier** of the synapse. It must be an unique word with the only accepted values : alphanumerics and dash. ([a - zA - Z0 - 9\-])
-```
+```yml
 - name: "Say-hello"
 ```
 
 
 The first part, called **signals** is a list of input actions. This works exactly the same way as neurons. You must place here at least one action.
 In the following example, we use just one signal, an order. See the complete list of [available signals](signals.md) here.
-```
+```yml
 signals:
   - order: "say-hello"
 ```
@@ -62,13 +62,13 @@ To know if your order will be triggered by Kalliope, we recommend you to [use th
 
 >**Note:**
 You must pay attention to define the orders as precise as possible. As Kalliope is based on matching, if you define your orders in different synapses too similiary, Kalliope risks to trigger more actions that you were expecting. For exemple, if you define two different synapses as shown below:
-```
+```yml
 - name: "Say-hello"
   signals:
     - order: "say hello"
 ```
 and 
-```
+```yml
 - name: "Say-something"
   signals:
     - order: "say"
@@ -76,7 +76,7 @@ and
 When you will pronounce "say hello", it will trigger both synapses. 
 
 Then we have the neurons declaration. Neurons are modules that will be executed when the input action is triggered. You can define as many neurons as you want to the same input action (for example: say somethning, then do something etc...). This declaration contains a list (because it starts with a "-") of neurons
-```
+```yml
 neurons:
     - neuron_1_name
     - neuron_2_name
@@ -86,7 +86,7 @@ neurons:
 The order of execution of neurons is defined by the order in which they are listed in neurons declaration.
 
 Some neurons need parameters that can be passed as arguments following the syntax bellow:
-```
+```yml
 neurons:
     - neuron_name:
         parameter1: "value1"
@@ -107,14 +107,14 @@ Kalliope provides also a REST API to manage your synapses (get the list, get one
 If you want a better visibly, or simply sort your actions in different files, you can split the main brain file into multiple ones.
 
 To do that, use the import statement in the entry brain.yml file with the following syntax:
-```
+```yml
   - includes:
       - path/to/sub_brain.yml
       - path/to/another_sub_brain.yml
 ```
 
 E.g:
-```
+```yml
   - includes:
       - brains/rolling_shutter_commands.yml
       - brains/find_my_phone.yml

+ 1 - 1
Docs/contributing.md

@@ -29,7 +29,7 @@ See the dedicated documentation depending on the type of module you want to code
 ## Constraints
 
 1. Respect [PEP 257](https://www.python.org/dev/peps/pep-0257/) -- Docstring conventions. For each class or method add a description with summary, input parameter, returned parameter,  type of parameter
-    ```
+    ```python
     def my_method(my_parameter):
         """
         Description of he method

+ 4 - 4
Docs/contributing/contribute_neuron.md

@@ -26,7 +26,7 @@ Creating a new Neuron must follow some rules:
 1. Inside the Neuron file, the Neuron Class name is in __uppercase__.
 1. The Neuron __inherits from the NeuronModule__ coming from the Core.
 
-    ```
+    ```python
     from core.NeuronModule import NeuronModule
     class Say(NeuronModule):
     ```
@@ -36,13 +36,13 @@ Creating a new Neuron must follow some rules:
 The constructor has a __**kwargs argument__ which is corresponding to the Dict of incoming variables:values defined either in the brain file or in the signal.
 1. The Neuron must refer to its __parent structure__ in the init by calling the super of NeuronModule.
 
-    ```
+    ```Python
     def __init__(self, **kwargs):
         super(Say, self).__init__(**kwargs)
     ```
 
 1. You must run unit tests with success before sending a pull request. Add new tests that cover the code you want to publish.
-    ```
+    ```bash
     cd /path/to/kalliope
     python -m unittest discover
     ```
@@ -75,7 +75,7 @@ myneuron/
 ```
 
 Example of neuron code
-```
+```python
 class Myneuron(NeuronModule):
 def __init__(self, **kwargs):
     super(Myneuron, self).__init__(**kwargs)

+ 30 - 24
Docs/contributing/contribute_stt.md

@@ -23,29 +23,46 @@ Creating a new STT must follow some rules:
 1. The STT must be coded in __Python 2.7__.
 1. Under the STT repository, include the __init__.py file which contains: *from stt import STT* (/!\ respect the Case)
 1. Inside the STT file, the STT Class name is in __uppercase__.
-1. The STT __inherits from the OrderListener__ coming from the Core.
+1. The STT __inherits from the SpeechRecognition__ coming from the Utils file in the STT package.
 
-    ```
-    from kalliope.core.OrderListener import OrderListener
-    class Google(OrderListener):
+    ```python
+    from kalliope.stt.Utils import SpeechRecognition
+    class Google(SpeechRecognition):
     ```
 
 1. The STT has a constructor __init__ which is the entry point.
 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. Once you get the text back, let give it to the callback
+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
-        # -------------------
-        # do amazing code
-        # -------------------
-        self.callback(audio_to_text)
+        # 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)
+        
+        #  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 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.main_controller_callback(audio_to_text)
     ```
 
 
@@ -63,15 +80,4 @@ mystt/
 ```
 
 Example of STT code
-```
-class Mystt(OrderListener):
-def __init__(self, callback=None, **kwargs):
-        OrderListener.__init__(self)
-        self.callback = callback
-        # -------------------
-        # - get the microphone audio 
-        # - do amazing code to retrieve the text
-        # - call the callback giving it the result text -> self.callback(audio_to_text)
-        # -------------------
-        
-```
+- [Google STT](../../kalliope/stt/google/README.md)

+ 3 - 3
Docs/contributing/contribute_tts.md

@@ -25,7 +25,7 @@ Creating a new TTS must follow some rules:
 1. Inside the TTS file, the TTS Class name is in __uppercase__.
 1. The TTS __inherits from the TTSModule__ coming from the Core.
 
-    ```
+    ```python
     from kalliope.core.TTS.TTSModule import TTSModule
     class Pico2wave(TTSModule):
     ```
@@ -35,7 +35,7 @@ Creating a new TTS must follow some rules:
 The constructor has a __**kwargs argument__ which is corresponding to the Dict of incoming variables:values defined either in the settings file.
 1. The TTS must refer to its __parent structure__ in the init by calling the super of TTSModule.
 
-    ```
+    ```python
     def __init__(self, **kwargs):
         super(Pico2wave, self).__init__(**kwargs)
     ```
@@ -59,7 +59,7 @@ mytts/
 ```
 
 Example of TTS code
-```
+```python
 class Mytts(TTSModule):
 def __init__(self, **kwargs):
     super(Mytts, self).__init__(**kwargs)

+ 3 - 3
Docs/contributing/dna.md

@@ -10,19 +10,19 @@ This file has a yaml syntax and must be present to allow Kalliope to install it
 | name                       | string | yes      |         |                  | Lowercase. It will be the name of the folder installed in ressources_dir for the target type of resource                              |
 | type                       | string | yes      |         | neuron, stt, tts | The type of resource. This will be used by Kalliope install process to place the resource in the right directory set in resources_dir |
 | author                     | string | no       |         |                  | String that contain info about the author of the modul like a name or a github profile page                                           |
-| kalliope_supported_version | list   | yes      |         | 0.4.0            | list of kalliope version the module support. E.g `- 0.4.0`                                                                            |
+| kalliope_supported_version | list   | yes      |         | 0.4              | list of kalliope __MAJOR__ version the module support. E.g `- 0.4`                                                                    |
 | tags                       | list   | no       |         |                  | list of tags that can help to categorize the module. E.g: "email", "social network", "search engine"                                  |
 
 ## DNA file examples
 
 A dna file for a neuron
-```
+```yml
 name: "wikipedia_searcher"
 type: "neuron"
 author: "The dream team of Kalliope project"
 
 kalliope_supported_version:
-  - 0.4.0
+  - 0.4
 
 tags:
   - "search engine"

+ 2 - 2
Docs/contributing/installation_file.md

@@ -11,7 +11,7 @@ A playbook is like a recipe or an instructions manual which tells Ansible what t
 
 Let's see a basic playbook, the one used by the neuron [wikipedia_searcher](https://github.com/kalliope-project/kalliope_neuron_wikipedia)
 
-```
+```yml
 - name: Kalliope wikipedia_searcher neuron install
   hosts: localhost
   gather_facts: no
@@ -43,7 +43,7 @@ The only task we've added here is based on the [pip Ansible module](http://docs.
 Ansible comes with a lot of modules, see the [complete list here](http://docs.ansible.com/ansible/modules_by_category.html).
 
 Here is an example which use the [apt module](http://docs.ansible.com/ansible/apt_module.html) to install Debian packages
-```
+```yml
 tasks:
   - name: Install packages
     apt: name={{ item }} update_cache=yes

+ 2 - 2
Docs/contributing/neuron_template.md

@@ -5,7 +5,7 @@
 Little description of what the neuron does.
 
 ## Installation
-```
+```bash
 kalliope install --git-url https://github.com/my_user/my_neuron.git
 ```
 
@@ -34,7 +34,7 @@ Only necessary when the neuron use a template to say something
 ## Synapses example
 
 Description of what the synapse will do
-```
+```yml
  - name: "type here your name"
    signals:
      - order: "this is what I have to say to run this synapse"

+ 3 - 2
Docs/contributing/stt_template.md

@@ -5,8 +5,9 @@
 Description of your STT
 
 ## Installation
-
-    kalliope install --git-url https://github.com/username/mystt.git
+```bash
+kalliope install --git-url https://github.com/username/mystt.git
+```
 
 ## Options
 

+ 17 - 15
Docs/installation.md

@@ -13,81 +13,83 @@ Please follow the right link bellow to install requirements depending on your ta
 ### Method 1 - User install using the PIP package
 
 You can install kalliope on your system by using Pypi:
-```
+```bash
 sudo pip install kalliope
 ```
 
 ### Method 2 - Manual setup using sources
 
 Clone the project:
-```
+```bash
 git clone https://github.com/kalliope-project/kalliope.git
 cd kalliope
 ```
 
 Install the project:
-```
+```bash
 sudo python setup.py install
 ```
 
 ### Method 3 - Developer install using Virtualenv
 
 Install the `python-virtualenv` package:
-```
+```bash
 sudo apt-get install python-virtualenv
 ```
 
 Clone the project:
-```
+```bash
 git clone https://github.com/kalliope-project/kalliope.git
 cd kalliope
 ```
 
 Generate a local python environment:
-```
+```bash
 virtualenv venv
 ```
 
 Install the project using the local environment:
-```
+```bash
 venv/bin/pip install --editable .
 ```
 
 ### Method 4 - Developer, dependencies install only
 
 Clone the project:
-```
+```bash
 git clone https://github.com/kalliope-project/kalliope.git
 cd kalliope
 ```
 
 Install the python dependencies directly:
-```
+```bash
 sudo pip install -r install/python_requirements.txt
 ```
 
 ## Test your env
 
 To ensure that you can record your voice, run the following command to capture audio input from your microphone:
-```
+```bash
 rec test.wav
 ```
 
 Press CTRL-C after capturing a sample of your voice.
 
 Then play the recorded audio file
-```
+```bash
 mplayer test.wav
 ```
 
 You can then test that your Kalliope is working by using the "bonjour" order integrated in the [default brain](../kalliope/brain.yml).
 Start kalliope:
-```
+```bash
 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
 Events loaded
 Starting Kalliope
@@ -97,13 +99,13 @@ Starting REST API Listening port: 5000
 
 Then speak the hotwork out loud to wake up Kalliope. By default, the hotwork is "Kalliopé" with the french pronunciation.
 If the trigger is successfully raised, you'll see "say something" into the console. 
-```
+```bash
 2016-12-05 20:54:21,950 :: INFO :: Keyword 1 detected at time: 2016-12-05 20:54:21
 Say something!
 ```
 
 Then you can say "bonjour" and listen the Kalliope response.
-```
+```bash
 Say something!
 Google Speech Recognition thinks you said Bonjour
 Order matched in the brain. Running synapse "say-hello-fr"

+ 5 - 5
Docs/installation/debian_jessie.md

@@ -3,26 +3,26 @@
 ## Debian packages requirements
 
 Edit `/etc/apt/sources.list` and check that you have `contrib` and `non-free` are enabled:
-```
+```bash
 deb http://httpredir.debian.org/debian jessie main contrib non-free
 deb-src http://httpredir.debian.org/debian jessie main contrib non-free
 ```
 
 Install some required system libraries and softwares:
 
-```
+```bash
 sudo apt-get update
 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
 ```
 
 Then, with pip, the last release of setuptools
-```
-sudo pip install -U pip setuptools
+```bash
+sudo pip install -U setuptools
 ```
 

+ 30 - 13
Docs/installation/raspbian_jessie.md

@@ -1,12 +1,29 @@
 # Kalliope requirements for Raspbian
 
+
+## Supported Version
+
+[raspbian-2016-09-28](http://downloads.raspberrypi.org/raspbian/images/raspbian-2016-09-28/)
+[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
 
 Install some required system libraries and software:
 
-```
+```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
 ```
 
 ## Raspberry Pi configuration
@@ -16,24 +33,24 @@ This part deals with the special configuration needed to get kalliope working on
 ### Packages
 
 On a Raspberry Pi, pulseaudio is not installed by default
-```
+```bash
 sudo apt-get install pulseaudio pulseaudio-utils
 ```
 
 Start the pulseaudio server
-```
+```bash
 pulseaudio -D
 ```
 
 ### Microphone configuration
 
 Get your output card
-```
+```bash
 aplay -l
 ```
 
 Output example with a USB headset connected
-```
+```bash
 **** List of PLAYBACK Hardware Devices ****
 card 0: ALSA [bcm2835 ALSA], device 0: bcm2835 ALSA [bcm2835 ALSA]
   Subdevices: 7/8
@@ -59,12 +76,12 @@ Here, we can see that we have
 
 
 Get your input (microphone card)
-```
+```bash
 arecord -l
 ```
 
 Output example with a USB headset connected
-```
+```bash
 **** List of CAPTURE Hardware Devices ****
 card 1: Headset [Logitech USB Headset], device 0: USB Audio [USB Audio]
   Subdevices: 0/1
@@ -95,26 +112,26 @@ pcm.!default {
 Where `playback.pcm` is the output audio and the `capture.pcm` is the input audio.
 
 Restart alsa to apply changes
-```
+```bash
 sudo /etc/init.d/alsa-utils restart
 ```
 
 You can adjust the microphone sensibility by running alsamixer:
-```
+```bash
 alsamixer
 ```
 And then select your microphone device by pressing F6 and finally move up the `mic` sensibility level
 ![logo](../../images/alsamixer_mic_level.png)
 
 To ensure that you can record your voice, run the following command to capture audio input from your microphone
-```
+```bash
 rec test.wav
 ```
 
 Press CTRL-C after capturing a sample of your voice.
 
 Then play the recorded audio file
-```
+```bash
 mplayer test.wav
 ```
 
@@ -124,6 +141,6 @@ mplayer test.wav
 By default the audio stream will get out by HDMI if something is plugged to this port.
 Check the [official documentation](https://www.raspberrypi.org/documentation/configuration/audio-config.md) to switch from HDMI to analog.
 
-```
+```bash
 sudo raspi-config
 ```

+ 8 - 2
Docs/installation/ubuntu_14.04.md

@@ -4,7 +4,13 @@
 
 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
 ```

+ 7 - 2
Docs/installation/ubuntu_16.04.md

@@ -4,7 +4,12 @@
 
 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
 ```

+ 14 - 12
Docs/kalliope_cli.md

@@ -2,22 +2,24 @@
 
 ## SYNOPSIS
 This is the syntax used to run Kalliope from command line
-```
+```bash
 kalliope command --option <argument>
 ```
 
 For example, to start Kalliope we simply use
-```
+```bash
 kalliope start
 ```
 
+> **Note:** Do not use the CLI as root user or with sudo. Kalliope must run with standard user privileges.
+
 ## ARGUMENTS
 
 ### start
 Start Kalliope main program
 
 Example of use
-```
+```bash
 kalliope start
 ```
 
@@ -28,7 +30,7 @@ Launch the Kalliope shell Graphical User Interface.
 The GUI allows you to test your [STT](stt.md) and [TTS](tts.md) that you have configured in [settings.yml](default_settings.md) file of Kalliope.
 
 Example of use
-```
+```bash
 kalliope gui
 ```
 
@@ -36,12 +38,12 @@ kalliope gui
 Install a community module. You must set an install type option. Currently the only available option is `--git-url`.
 
 Syntax
-```
+```bash
 kalliope install --git-url <url>
 ```
 
 Example of use
-```
+```bash
 kalliope install --git-url https://github.com/kalliope-project/kalliope_neuron_wikipedia.git
 ```
 
@@ -53,11 +55,11 @@ Commands can be completed by the following options:
 Display the current isntalled version of Kalliope.
 
 Example of use
-```
+```bash
 kalliope --version
 ```
 
-```
+```bash
 kalliope -v
 ```
 
@@ -66,7 +68,7 @@ kalliope -v
 Run a specific synapse from the brain file.
 
 Example of use
-```
+```bash
 kalliope start --run-synapse "say-hello"
 ```
 
@@ -76,12 +78,12 @@ Replace the default brain file from the root of the project folder by a custom o
 > **Important note:** The path must be absolute. The absolute path contains the root directory and all other subdirectories in which a file or folder is contained. 
 
 Example of use
-```
+```bash
 kalliope start --brain-file /home/me/my_other_brain.yml
 ```
 
 You can combine the options together like, for example:
-```
+```bash
 kalliope start --run-synapse "say-hello" --brain-file /home/me/my_other_brain.yml
 ```
 
@@ -90,7 +92,7 @@ kalliope start --run-synapse "say-hello" --brain-file /home/me/my_other_brain.ym
 Show debug output in the console
 
 Example of use
-```
+```bash
 kalliope start --debug
 ```
 

+ 12 - 8
Docs/neuron_list.md

@@ -19,15 +19,19 @@ A neuron is a module that will perform some actions attached to an order. You ca
 ## Community neuron
 
 | Name                                                                                 | Description                                                                             |
-|--------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
-| [gmail_checker](https://github.com/kalliope-project/kalliope_neuron_gmail)           | Get the number of unread email and their subjects from a gmail account                  |
-| [openweathermap](https://github.com/kalliope-project/kalliope_neuron_openweathermap) | Get the weather of a location                                                           |
+|--------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|
+| [gmail_checker](https://github.com/kalliope-project/kalliope_neuron_gmail)           | Get the number of unread email and their subjects from a gmail account                     |
+| [google agenda](https://github.com/bacardi55/kalliope-google-calendar)               | Get your next meetings on google calendar                                                |
+| [list available orders](https://github.com/bacardi55/kalliope-list-available-orders) | Let kalliope tell you what she how she can help                                            |
+| [openweathermap](https://github.com/kalliope-project/kalliope_neuron_openweathermap) | Get the weather of a location                                                               |
+| [pi camera](https://github.com/bacardi55/kalliope-picamera)                          | Take picture with your picamera                                                             |
 | [pushetta](https://github.com/kalliope-project/kalliope_neuron_pushetta)             | Send a push message to a remote device like Android/iOS/Windows Phone or Chrome browser |
-| [rss_reader](https://github.com/kalliope-project/kalliope_neuron_rss_reader)         | get rss feed from website                                                               |
-| [tasker](https://github.com/kalliope-project/kalliope_neuron_tasker)                 | Send a message to Android tasker app                                                    |
-| [twitter](https://github.com/kalliope-project/kalliope_neuron_twitter)               | Send a Twit from kalliope                                                               |
-| [wake_on_lan](https://github.com/kalliope-project/kalliope_neuron_wake_on_lan)       | Wake on lan a computer                                                                  |
-| [wikipedia](https://github.com/kalliope-project/kalliope_neuron_wikipedia)           | Search for a page on Wikipedia                                                          |
+| [rss_reader](https://github.com/kalliope-project/kalliope_neuron_rss_reader)         | get rss feed from website                                                                   |
+| [tasker](https://github.com/kalliope-project/kalliope_neuron_tasker)                 | Send a message to Android tasker app                                                         |
+| [twitter](https://github.com/kalliope-project/kalliope_neuron_twitter)               | Send a Twit from kalliope                                                                   |
+| [wake_on_lan](https://github.com/kalliope-project/kalliope_neuron_wake_on_lan)       | Wake on lan a computer                                                                       |
+| [web scraper](https://github.com/bacardi55/kalliope-web-scraper)                     | Read web pages that don't provide RSS feed or APIs (by scraping html)                                                    |
+| [wikipedia](https://github.com/kalliope-project/kalliope_neuron_wikipedia)           | Search for a page on Wikipedia                                                               |
 
 Wanna add your neuron in the list? Open [an issue](../../issues) with the link of your neuron or send a pull request to update the list directly.
 

+ 11 - 11
Docs/neurons.md

@@ -8,12 +8,12 @@ You can add as many neurons as you want to a synapse. The neurons are executed o
 Core neurons are already packaged with the installation of kalliope an can be used out of the box. Community neuron need to be installed manually.
 
 Use the CLI
-```
+```bash
 kalliope install --git-url <git_url>
 ```
 
 E.g:
-```
+```bash
 kalliope install --git-url https://github.com/kalliope-project/kalliope_neuron_wikipedia.git
 ```
 
@@ -22,7 +22,7 @@ You may be prompted to type your `sudo` password during the process. You can see
 ## Usage
 Neurons are declared in the `neurons` section of a synapse in your brain file.
 The `neurons` section is a list (because it starts with a "-") which contains neuron modules names
-```
+```yml
 neurons:
     - neuron_1_name
     - neuron_2_name
@@ -30,7 +30,7 @@ neurons:
 ```
 
 Some neurons need parameters that can be passed as arguments following the syntax bellow:
-```
+```yml
 neurons:
     - neuron_name:
         parameter1: "value1"
@@ -50,7 +50,7 @@ Neurons require some **parameters** from the synapse declaration to work. Those
 - from the captured order
 
 From the neuron declaration:
-```
+```yml
 neurons:
     - neuron_name:
         parameter1: "value1"
@@ -58,7 +58,7 @@ neurons:
 ```
 
 From the captured order:
-```
+```yml
   - name: "run-neuron-with-parameter-in-order"
     signals:
       - order: "this is an order with the parameter {{ parameter3 }}"
@@ -93,7 +93,7 @@ It is {{ hours }} and {{ minutes }} minutes.
 ```
 
 Placed in a complete synapse, it looks like the following
-```
+```yml
 - name: "time"
     signals:
       - order: "what time is it"
@@ -116,7 +116,7 @@ We can add some logic to a template with tags. Here a simple example with [a tes
 ```
 
 As this is multi-lines, we can put the content in a file and use a `file_template` instead of a `say_template` for more clarity.
-```
+```yml
 - name: "time"
     signals:
       - order: "what time is it"
@@ -135,14 +135,14 @@ set in [settings.yml](settings.md) file.
 
 You can override the default cache configuration. By default Kalliope uses a cache to save a generated audio from a TTS engine.
 This cache is useful to manage sentences that are not suppose to be changed very often. For example, the following sentence will not change in time, so it's more optimized to generate it once and to keep it in cash:
-```
+```yml
 - say:
     message:
       - "Hello, sir"
 ```
 
 In some cases, especially when the neuron is based on a template, the generated audio will change on each new call of the neuron and so the usage of a cache is not necessary. The best example of the case like this is the `systemdate` neuron. As the time changes every minute, the generated audio will change too and so, saving the generated audio in the cache is useless. In this case, you can override the cache usage for this neuron:
-```
+```yml
 - systemdate:
     say_template:
       - "It's {{ hours }} hour and {{ minutes }} minute"
@@ -152,7 +152,7 @@ In some cases, especially when the neuron is based on a template, the generated
 ### tts
 
 You can override the default tts for each neurons. Just add the "tts" parameter to the neuron like in the example bellow
-```
+```yml
 neurons:
   - say:
     message:

+ 11 - 11
Docs/rest_api.md

@@ -20,12 +20,12 @@ Kalliope provides the REST API to manage the synapses. For configuring the API r
 Normal response codes: 200
 Error response codes: unauthorized(401), itemNotFound(404)
 Curl command:
-```
+```bash
 curl -i --user admin:secret -X GET  http://localhost:5000/synapses/
 ```
 
 Output example:
-```
+```JSON
 {
   "synapses": [
     [
@@ -74,12 +74,12 @@ Output example:
 Normal response codes: 200
 Error response codes: unauthorized(401), itemNotFound(404)
 Curl command:
-```
+```bash
 curl -i --user admin:secret -X GET  http://localhost:5000/synapses/say-hello
 ```
 
 Output example:
-```
+```JSON
 {
   "synapses": {
     "name": "say-hello",
@@ -106,12 +106,12 @@ Output example:
 Normal response codes: 201
 Error response codes: unauthorized(401), itemNotFound(404)
 Curl command:
-```
+```bash
 curl -i --user admin:secret -X POST  http://localhost:5000/synapses/say-hello
 ```
 
 Output example:
-```
+```JSON
 {
   "synapses": {
     "name": "say-hello",
@@ -140,22 +140,22 @@ Normal response codes: 201
 Error response codes: unauthorized(401), itemNotFound(404)
 
 Curl command:
-```
+```bash
 curl -i --user admin:secret -H "Content-Type: application/json" -X POST -d '{"order":"my order"}' http://localhost:5000/order/
 ```
 
 If the order contains accent or quotes, use a file for testing with curl
-```
+```bash
 cat post.json 
 {"order":"j'aime"}
 ```
 Then
-```
+```bash
 curl -i --user admin:secret -H "Content-Type: application/json" -X POST --data @post.json http://localhost:5000/order/
 ```
 
 Output example if the order have matched and so launched synapses:
-```
+```JSON
 {
   "synapses": [
     {
@@ -177,7 +177,7 @@ Output example if the order have matched and so launched synapses:
 ```
 
 If the order haven't match ny synapses:
-```
+```JSON
 {
   "error": {
     "error": "The given order doesn't match any synapses"

+ 85 - 22
Docs/settings.md

@@ -28,7 +28,7 @@ The trigger is the module detecting the hotword that will wake up Kalliope.
 Common usage of hotword include Alexa on Amazon Echo, OK Google on some Android devices and Hey Siri on iPhones.
 
 Specify the name of the trigger module you want to use.
-```
+```yml
 default_trigger: "trigger_name"
 ```
 
@@ -39,14 +39,14 @@ Available triggers for Kalliope are:
 The hotword (also called a wake word or trigger word) detector is the engine in charge of waking up Kalliope.
 
 Each Trigger has it own configuration. This configuration is passed as argument following the syntax bellow
-```
+```yml
 triggers:
   - trigger_name:
       parameter_name: "value"
 ```
 
 E.g, the default Snowboy trigger configuration is
-```
+```yml
 triggers:
   - snowboy:
       pmdl_file: "trigger/snowboy/resources/model.pmdl"
@@ -62,12 +62,12 @@ A Speech To Text(STT) is an engine used to translate what you say into a text th
 By default, Kalliope uses google STT engine.
 
 The following syntax is used to provide the engine name:
-```
+```yml
 default_speech_to_text: "stt_name"
 ```
 
 E.g
-```
+```yml
 default_speech_to_text: "google"
 ```
 
@@ -75,14 +75,14 @@ Get the full list of [SST engine here](stt.md).
 
 #### speech_to_text
 Each STT has it own configuration. This configuration is passed as argument as shown bellow
-```
+```yml
 speech_to_text:
   - stt_name:
       parameter_name: "value"
 ```
 
 E.g:
-```
+```yml
 speech_to_text:
   - google:
       language: "fr-FR"
@@ -98,12 +98,12 @@ A Text To Speech is an engine used to translate written text into a speech, into
 By default, Kalliope use Pico2wave TTS engine.
 
 The following syntax is used to provide the TTS engine name
-```
+```yml
 default_text_to_speech: "tts_name"
 ```
 
 Eg
-```
+```yml
 default_text_to_speech: "pico2wave"
 ```
 
@@ -111,14 +111,14 @@ Get the full list of [TTS engine here](tts.md).
 
 #### text_to_speech
 Each TTS has it own configuration. This configuration is passed as argument following the syntax bellow
-```
+```yml
 text_to_speech:
   - tts_name:
       parameter_name: "value"
 ```
 
 E.g
-```
+```yml
 text_to_speech:
   - pico2wave:
       language: "fr-FR"
@@ -134,15 +134,15 @@ Some arguments are required, some other optional, please refer to the [TTS docum
 When Kalliope detects your trigger/hotword/magic word, 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_wake_up_answers.
 
-This variable must contain a list of strings as shownbellow
-```
+This variable must contain a list of strings as shown bellow
+```yml
 random_wake_up_answers:
   - "You sentence"
   - "Another sentence"
 ```
 
 E.g
-```
+```yml
 random_wake_up_answers:
   - "Yes sir?"
   - "I'm listening"
@@ -159,14 +159,14 @@ Place here a list of full paths of the sound files you want to use. Otherwise, y
 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.
 
-```
+```yml
 random_wake_up_sounds:
   - "local_file_in_sounds_folder.wav"
   - "/my/personal/full/path/my_file.mp3"
 ```
 
 E.g
-```
+```yml
 random_wake_up_sounds:
   - "ding.wav"
   - "dong.wav"
@@ -177,6 +177,46 @@ random_wake_up_sounds:
 E.g: `# random_wake_up_answers:`
 
 
+## 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
+
+#### 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
+
+E.g:
+```yml
+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:
+```yml
+on_ready_answers:
+  - "I'm ready"
+  - "Waiting for order"
+```
+
+#### 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
+on_ready_sounds:
+  - "ding.wav"
+  - "dong.wav"
+  - "/my/personal/full/path/my_file.mp3"
+```
+
+>**Note: ** If you want to use a on ready sound instead of a on ready you must comment out the `random_on_ready_answers` section.
+E.g: `# random_on_ready_answers:`
+
+
 ## Rest API
 
 A Rest API can be activated in order to:
@@ -187,13 +227,14 @@ A Rest API can be activated in order to:
 For the complete API ref see the [REST API documentation](rest_api.md)
 
 Settings examples:
-```
+```yml
 rest_api:
   active: True
   port: 5000
   password_protected: True
   login: admin
   password: secret
+  allowed_cors_origin: "*"
 ```
 
 #### active
@@ -211,17 +252,39 @@ Login used by the basic HTTP authentication. Must be provided if `password_prote
 #### Password
 Password used by the basic HTTP authentication. Must be provided if `password_protected` is `True`
 
+#### Cors request
+If you want to allow request from external application, you'll need to enable the CORS requests settings by defining authorized origins.
+To do so, just indicated the origins that are allowed to leverage the API. The authorize values are:
+
+False to forbid CORS request.
+```yml
+allowed_cors_origin: False
+```
+
+or either a string or a list:
+```yml
+allowed_cors_origin: "*"
+```
+(in case of "*", all origins are accepted).
+or
+```yml
+allowed_cors_origin:
+  - 'http://mydomain.com/*'
+  - 'http://localhost:4200/*'
+```
+
+Remember that an origin is composed of the scheme (http(s)), the port (eg: 80, 4200,…) and the domain (mydomain.com, localhost).
 
 ## Default synapse
 
-Run a default [synapse](brain.md) when Kalliope can't find the order in any synapse. 
+Run a default [synapse](brain.md) when Kalliope can't find the order in any synapse.
 
-```
+```yml
 default_synapse: "synapse-name"
 ```
 
 E.g
-```
+```yml
 default_synapse: "Default-response"
 ```
 
@@ -230,13 +293,13 @@ default_synapse: "Default-response"
 The resources directory is the path where Kalliope will try to load community modules like Neurons, STTs or TTSs.
 Set a valid path is required if you want to install community neuron. The path can be relative or absolute.
 
-```
+```yml
 resource_directory:
   resource_name: "path"
 ```
 
 E.g
-```
+```yml
 resource_directory:
   neuron: "resources/neurons"
   stt: "resources/stt"

+ 10 - 10
Docs/signals.md

@@ -3,7 +3,7 @@
 A signal is an input event triggered by a synapse. When a signal is caught, Kalliope runs attached neurons of the synapse. The signal can be of two types: order and event.
 
 The syntax is the following
-```
+```yml
 signals:
     - signal_type: parameter
 ```
@@ -14,13 +14,13 @@ signals:
 An **order** signal is a word, or a sentence caught by the microphone and processed by the STT engine.
 
 Syntax:
-```
+```yml
 signals:
     - order: "<sentence>"
 ```
 
 Example:
-```
+```yml
 signals:
     - order: "please do this action"
 ```
@@ -40,7 +40,7 @@ will be started by Kalliope. So keep in mind that the best practice is to use re
 You can add one or more arguments to an order by adding bracket to the sentence.
 
 Syntax:
-```
+```yml
 signals:
     - order: "<sentence> {{ arg_name }}"
     - order: "<sentence> {{ arg_name }} <sentence>"
@@ -48,7 +48,7 @@ signals:
 ```
 
 Example:
-```
+```yml
 signals:
     - order: "I want to listen {{ artist_name }}"
     - order: "start the {{ episode_number }} episode"
@@ -75,7 +75,7 @@ The event system is based on [APScheduler](http://apscheduler.readthedocs.io/en/
 When you declare an event in the signal, Kalliope will schedule the launching of the target synapse.
 
 The syntax of an event declaration in a synapse is the following
-```
+```yml
 signals:
   - event:
       parameter1: "value1"
@@ -83,7 +83,7 @@ signals:
 ```
 
 For example, if we want Kalliope to run the synapse every day a 8:30, the event will be declared like this:
-```
+```yml
 - event:
     hour: "8"
     minute: "30"
@@ -132,7 +132,7 @@ Let's make a complete example. We want Kalliope to wake us up each morning of wo
 - Play our favourite web radio
 
 The synapse in the brain would be
-```
+```yml
   - name: "wake-up"
     signals:
       - event:
@@ -152,7 +152,7 @@ The synapse in the brain would be
 ```
 
 After setting up an event, you must restart Kalliope
-```
+```bash
 python kalliope.py start
 ```
 
@@ -166,7 +166,7 @@ That's it, the synapse is now scheduled and will be started automatically.
 
 
 ####  Make Kalliope say something on the third Friday of June, July, August, November and December at 00:00, 01:00, 02:00 and 03:00
-```
+```yml
 - name: "wake-up"
   signals:
     - event:

+ 5 - 10
Docs/stt.md

@@ -10,12 +10,12 @@ The configuration of each STT you use must appear in the [settings.yml](settings
 ## Settings
 
 The setting.yml defines the STT you want to use by default
-```
+```yml
 default_speech_to_text: "type default STT here"
 ```
 
 Then, still in the settings.yml file, each STT must set up its configuration following the 'speech_to_text' tag :
-```
+```yml
 speech_to_text:
    - STT1:
       STT1parameter1: "value option1"
@@ -26,20 +26,15 @@ 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)
-- [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
 
 Community STTs need to be installed manually.
 
 Use the CLI
-```
+```bash
 kalliope install --git-url <git_url>
 ```
 
@@ -49,7 +44,7 @@ You may be prompted to type your `sudo` password during the process. You can see
 
 In the settings.yml file :
 
-```
+```yml
 default_speech_to_text: "google"
 speech_to_text:
   - google:

+ 8 - 10
Docs/stt_list.md

@@ -6,13 +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) |
-| 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
@@ -20,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.
 
 

+ 8 - 6
Docs/trigger.md

@@ -11,7 +11,7 @@ Once downloaded:
 - place the file in your personal config folder.
 - update the path of **pmdl_file** in [your settings](settings.md).
 
-If you want to keep "Kalliope" as the name of your bot, we recommend you to enhance the existing Snowboy model for your language.
+If you want to keep "Kalliope" as the name of your bot, we recommend you to __enhance the existing Snowboy model for your language__.
 
 We will update the following list with all Kalliope model created by the community. If the model doesn't exist, please create one with the following syntax:
 ```
@@ -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  | каллиопа       |

+ 6 - 11
Docs/tts.md

@@ -13,14 +13,14 @@ The configuration of each TTS you use must appear in the [settings.yml](settings
 ### default_text_to_speech
 
 The `setting.yml` defines the STT you want to use by default
-```
+```yml
 default_text_to_speech: "type default TTS engine name here"
 ```
 
 ### text_to_speech
 
 Still in the `settings.yml` file, each TTS must set up its configuration following the 'text_to_speech' tag :
-```
+```yml
 text_to_speech:
    - TTS1:
       TTS1parameter1: "value option1"
@@ -38,7 +38,7 @@ is played by the audio player. Before generating a new audio file, Kalliope will
 TSS engine if the file has been generated before.
 
 You must set a path where the cache will be saved in the tag `cache_path`. This one is placed in /tmp by default.
-```
+```yml
 cache_path: "/tmp/kalliope_tts_cache"
 ```
 
@@ -50,20 +50,15 @@ 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
 
 Community TTSs need to be installed manually.
 
 Use the CLI
-```
+```bash
 kalliope install --git-url <git_url>
 ```
 
@@ -71,7 +66,7 @@ You may be prompted to type your `sudo` password during the process. You can see
 
 ## Full Example
 
-```
+```yml
 default_text_to_speech: "voicerss"
 
 cache_path: "/tmp/kalliope_tts_cache"

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

+ 9 - 5
README.md

@@ -15,7 +15,7 @@ write it by yourself and add it to your project. You can also propose it to the
 Kalliope can run on all Linux Debian based distribution including a Raspberry Pi and it's multi-lang. The only thing you need is a microphone.
 
 Kalliope is easy-peasy to use, see the hello world
-```
+```yml
   - name: "Hello-world"
     signals:
       - order: "say hello"
@@ -67,13 +67,17 @@ Reading through this guide before writing any code is recommended.
 
 > **Meaning of Kalliope** Kalliope means "beautiful voice" from Greek καλλος (kallos) "beauty" and οψ (ops) "voice". 
 In Greek mythology she was a goddess of epic poetry and eloquence, one of the nine Muses.
-PRONOUNCED: kə-LIE-ə-pee (English) 
-PRONOUNCED: Ka-li-o-pé (French)
-PRONOUNCED: каллиопа (Russian)
+
+kə-LIE-ə-pee    (English) 
+Ka-li-o-pé      (French)
+каллиопа        (Russian)
+
+[Improve models with Snowboy](Docs/trigger.md)
 
 ## Links
 
-Demo French : [video](https://www.youtube.com/watch?v=t4J42yO2rkM)
+Demo French   : [video](https://www.youtube.com/watch?v=t4J42yO2rkM)
+Demo English  : [video](https://www.youtube.com/watch?v=PcLzo4H18S4)
 
 ## License
 

+ 1 - 1
Tests/modules/test_invalid_dna.yml

@@ -4,7 +4,7 @@
   author: "Kalliope project team"
 
   kalliope_supported_version:
-    - 0.4.0
+    - 0.4
 
   tags:
     - "test"

+ 1 - 1
Tests/modules/test_valid_dna.yml

@@ -4,7 +4,7 @@
   author: "Kalliope project team"
 
   kalliope_supported_version:
-    - 0.4.0
+    - 0.4
 
   tags:
     - "test"

+ 26 - 3
Tests/settings/settings_test.yml

@@ -59,8 +59,31 @@ random_wake_up_answers:
 # 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_wake_up_sounds:
-  - "ding.wav"
-  - "dong.wav"
+  - "sounds/ding.wav"
+  - "sounds/dong.wav"
+
+# ---------------------------
+# 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
+
+# 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"
+
+# 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"
+
 
 # ---------------------------
 # Rest API
@@ -71,7 +94,7 @@ rest_api:
   password_protected: True
   login: admin
   password: secret
-
+  allowed_cors_origin: False
 
 # ---------------------------
 # Default Synapse

+ 15 - 6
Tests/test_dna_loader.py

@@ -18,7 +18,7 @@ class TestDnaLoader(unittest.TestCase):
 
     def test_get_yaml_config(self):
 
-        expected_result = {'kalliope_supported_version': ['0.4.0'],
+        expected_result = {'kalliope_supported_version': [0.4],
                            'author': 'Kalliope project team',
                            'type': 'neuron',
                            'name': 'neuron_test',
@@ -35,7 +35,7 @@ class TestDnaLoader(unittest.TestCase):
         expected_result.module_type = "neuron"
         expected_result.tags = ['test']
         expected_result.author = 'Kalliope project team'
-        expected_result.kalliope_supported_version = ['0.4.0']
+        expected_result.kalliope_supported_version = [0.4]
 
         dna_to_test = DnaLoader(self.dna_test_file).get_dna()
 
@@ -57,7 +57,7 @@ class TestDnaLoader(unittest.TestCase):
 
     def test_check_dna(self):
         # check with valid DNA file
-        test_dna = {'kalliope_supported_version': ['0.4.0'],
+        test_dna = {'kalliope_supported_version': [0.4],
                     'author': 'Kalliope project team',
                     'type': 'neuron',
                     'name': 'neuron_test',
@@ -66,7 +66,7 @@ class TestDnaLoader(unittest.TestCase):
         self.assertTrue(DnaLoader(file_path=self.dna_test_file)._check_dna_file(test_dna))
 
         # invalid DNA file, no name
-        test_dna = {'kalliope_supported_version': ['0.4.0'],
+        test_dna = {'kalliope_supported_version': [0.4],
                     'author': 'Kalliope project team',
                     'type': 'neuron',
                     'tags': ['test']}
@@ -74,7 +74,7 @@ class TestDnaLoader(unittest.TestCase):
         self.assertFalse(DnaLoader(file_path=self.dna_test_file)._check_dna_file(test_dna))
 
         # invalid DNA file, no type
-        test_dna = {'kalliope_supported_version': ['0.4.0'],
+        test_dna = {'kalliope_supported_version': [0.4],
                     'author': 'Kalliope project team',
                     'name': 'neuron_test',
                     'tags': ['test']}
@@ -82,7 +82,7 @@ class TestDnaLoader(unittest.TestCase):
         self.assertFalse(DnaLoader(file_path=self.dna_test_file)._check_dna_file(test_dna))
 
         # invalid DNA, wrong type
-        test_dna = {'kalliope_supported_version': ['0.4.0'],
+        test_dna = {'kalliope_supported_version': [0.4],
                     'author': 'Kalliope project team',
                     'type': 'doesnotexist',
                     'name': 'neuron_test',
@@ -105,3 +105,12 @@ class TestDnaLoader(unittest.TestCase):
                     'tags': ['test']}
 
         self.assertFalse(DnaLoader(file_path=self.dna_test_file)._check_dna_file(test_dna))
+
+        # invalid DNA, kalliope_supported_version wrong format
+        test_dna = {'kalliope_supported_version': ['0.4.1'],
+                    'author': 'Kalliope project team',
+                    'type': 'neuron',
+                    'name': 'neuron_test',
+                    'tags': ['test']}
+
+        self.assertFalse(DnaLoader(file_path=self.dna_test_file)._check_dna_file(test_dna))

+ 43 - 0
Tests/test_order_analyser.py

@@ -218,6 +218,49 @@ class TestOrderAnalyser(unittest.TestCase):
         self.assertFalse(OrderAnalyser.spelt_order_match_brain_order_via_table(order_to_test, sentence_to_test),
                          "Fail to ensure the expected sentence is not matching the order")
 
+        # Upper/lower cases
+        sentence_to_test = "THIS is THE order"
+        self.assertTrue(OrderAnalyser.spelt_order_match_brain_order_via_table(order_to_test, sentence_to_test),
+                        "Fail matching Upper/lower cases")
+
+    def test_format_sentences_to_analyse(self):
+
+        # First capital in sentence
+        order_to_test = "this is the order"
+        sentence_to_test = "This is the order"
+        expected_result = "this is the order", "this is the order"
+        self.assertEqual(OrderAnalyser._format_sentences_to_analyse(order_to_analyse=order_to_test,
+                                                                    user_said=sentence_to_test),
+                         expected_result,
+                         "Fails formatting the sentences with first capital in sentence")
+
+        # random uppercase in sentence
+        order_to_test = "this is the order"
+        sentence_to_test = "This IS the ordeR"
+        expected_result = "this is the order", "this is the order"
+        self.assertEqual(OrderAnalyser._format_sentences_to_analyse(order_to_analyse=order_to_test,
+                                                                    user_said=sentence_to_test),
+                         expected_result,
+                         "Fails formatting the sentences with random in sentence")
+
+        # random uppercase in order
+        order_to_test = "thiS is THE orDer"
+        sentence_to_test = "this is the order"
+        expected_result = "this is the order", "this is the order"
+        self.assertEqual(OrderAnalyser._format_sentences_to_analyse(order_to_analyse=order_to_test,
+                                                                    user_said=sentence_to_test),
+                         expected_result,
+                         "Fails formatting the sentences with random in order")
+
+        # random uppercase in both order and sentence
+        order_to_test = "thiS is THE orDer"
+        sentence_to_test = "THIS is the Order"
+        expected_result = "this is the order", "this is the order"
+        self.assertEqual(OrderAnalyser._format_sentences_to_analyse(order_to_analyse=order_to_test,
+                                                                    user_said=sentence_to_test),
+                         expected_result,
+                         "Fails formatting the sentences with random in both order and sentence")
+
     def test_get_split_order_without_bracket(self):
 
         # Success

+ 10 - 3
Tests/test_resources_manager.py

@@ -134,14 +134,21 @@ class TestResourcesmanager(unittest.TestCase):
     def test_check_supported_version(self):
         # version ok
         current_version = '0.4.0'
-        supported_version = ['0.4.0', '0.3.0', '0.2.0']
+        supported_version = ['0.4', '0.3', '0.2']
 
         self.assertTrue(ResourcesManager._check_supported_version(current_version=current_version,
                                                                   supported_versions=supported_version))
 
-        # version non ok, useer does not confir
+        # version ok
+        current_version = '11.23.0'
+        supported_version = ['11.23', '12.3', '2.23']
+
+        self.assertTrue(ResourcesManager._check_supported_version(current_version=current_version,
+                                                                  supported_versions=supported_version))
+
+        # version non ok, user does not config
         current_version = '0.4.0'
-        supported_version = ['0.3.0', '0.2.0']
+        supported_version = ['0.3', '0.2']
 
         with mock.patch('kalliope.Utils.query_yes_no', return_value=True):
             self.assertTrue(ResourcesManager._check_supported_version(current_version=current_version,

+ 1 - 0
Tests/test_rest_api.py

@@ -28,6 +28,7 @@ class TestRestAPI(LiveServerTestCase):
         sl.settings.rest_api.password_protected = False
         sl.settings.active = True
         sl.settings.port = 5000
+        sl.settings.allowed_cors_origin = "*"
 
         # prepare a test brain
         brain_to_test = full_path_brain_to_test

+ 34 - 16
Tests/test_settings_loader.py

@@ -25,33 +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},
+                 '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': ['ding.wav', 'dong.wav'],
+            'random_wake_up_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")
@@ -86,12 +89,16 @@ class TestSettingLoader(unittest.TestCase):
         stt = Stt(name="google", parameters={'language': 'fr-FR'})
         settings_object.stts = [stt]
         settings_object.random_wake_up_answers = ['Oui monsieur?']
-        settings_object.random_wake_up_sounds = ['ding.wav', 'dong.wav']
+        settings_object.random_wake_up_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]
         settings_object.rest_api = RestAPI(password_protected=True, active=True,
-                                           login="admin", password="secret", port=5000)
+                                           login="admin", password="secret", port=5000,
+                                           allowed_cors_origin=False)
         settings_object.cache_path = '/tmp/kalliope_tts_cache'
         settings_object.default_synapse = 'Default-synapse'
         resources = Resources(neuron_folder="/tmp/kalliope/tests/kalliope_resources_dir/neurons",
@@ -99,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)
@@ -144,9 +151,20 @@ 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_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_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,
-                                    login="admin", password="secret", port=5000)
+                                    login="admin", password="secret", port=5000,
+                                    allowed_cors_origin=False)
 
         sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual(expected_rest_api, sl._get_rest_api(self.settings_dict))

+ 0 - 1
install/files/deb-packages_requirements.txt

@@ -1,4 +1,3 @@
-python-pip
 libssl-dev
 libportaudio0
 libportaudio2

+ 3 - 0
install/files/python_requirements.txt

@@ -2,6 +2,7 @@ six==1.10.0
 SpeechRecognition>=3.5.0
 markupsafe==0.23
 pyaudio==0.2.9
+pyasn1>=0.1.8
 ansible==2.2.0.0
 python2-pythondialog==3.4.0
 jinja2==2.8
@@ -9,6 +10,7 @@ cffi==1.9.1
 ipaddress==1.0.17
 flask==0.11.1
 Flask-Restful==0.3.5
+flask_cors==3.0.2
 requests==2.12.4
 httpretty==0.8.14
 mock==2.0.0
@@ -16,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"

+ 0 - 2
kalliope/brain.yml

@@ -1,7 +1,6 @@
 ---
   - name: "say-hello-fr"
     signals:
-      - order: "bonjour"
       - order: "Bonjour"
     neurons:
       - say:
@@ -10,7 +9,6 @@
 
   - name: "say-hello-en"
     signals:
-      - order: "hello"
       - order: "Hello"
     neurons:
       - say:

+ 10 - 0
kalliope/core/ConfigurationManager/DnaLoader.py

@@ -1,3 +1,5 @@
+import re
+
 from kalliope.core import Utils
 from kalliope.core.ConfigurationManager import YAMLLoader
 from kalliope.core.Models.Dna import Dna
@@ -89,5 +91,13 @@ class DnaLoader(object):
                 if not dna_file["kalliope_supported_version"]:
                     Utils.print_danger("kalliope_supported_version cannot be empty")
                     success_loading = False
+                else:
+                    for supported_version in dna_file["kalliope_supported_version"]:
+                        # check if major version is provided
+                        if not re.search('^[\d]*[.][\d]*$', str(supported_version)):
+                            Utils.print_danger("kalliope_supported_version cannot handle this format of version %s. "
+                                               "Only major version should be provided" % supported_version)
+                            success_loading = False
+
 
         return success_loading

+ 74 - 6
kalliope/core/ConfigurationManager/SettingLoader.py

@@ -104,7 +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_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)
@@ -118,7 +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_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
@@ -243,8 +249,8 @@ class SettingLoader(object):
                     new_stt = Stt(name=name, parameters=parameters)
                     stts.append(new_stt)
             else:
-                # the neuron does not have parameter
-                new_stt = Stt(name=speechs_to_text_el)
+                # the stt does not have parameter
+                new_stt = Stt(name=speechs_to_text_el, parameters=dict())
                 stts.append(new_stt)
         return stts
 
@@ -379,6 +385,10 @@ class SettingLoader(object):
 
         try:
             random_wake_up_sounds_list = settings["random_wake_up_sounds"]
+            # In case files are declared in settings.yml, make sure kalliope can access them.
+            for sound in random_wake_up_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
@@ -423,7 +433,7 @@ class SettingLoader(object):
                 if password_protected:
                     if login is None:
                         raise NullSettingException("login setting cannot be null if password_protected is True")
-                    if login is None:
+                    if password is None:
                         raise NullSettingException("password setting cannot be null if password_protected is True")
                 active = rest_api["active"]
                 if active is None:
@@ -440,13 +450,18 @@ class SettingLoader(object):
                 if not 1024 <= port <= 65535:
                     raise SettingInvalidException("port must be in range 1024-65535")
 
+                # check the CORS request settings
+                allowed_cors_origin = False
+                if "allowed_cors_origin" in rest_api:
+                     allowed_cors_origin = rest_api["allowed_cors_origin"]
+
             except KeyError, e:
                 # print e
                 raise SettingNotFound("%s settings not found" % e)
 
             # config ok, we can return the rest api object
             rest_api_obj = RestAPI(password_protected=password_protected, login=login, password=password,
-                                   active=active, port=port)
+                                   active=active, port=port, allowed_cors_origin=allowed_cors_origin)
             return rest_api_obj
         else:
             raise NullSettingException("rest_api settings cannot be null")
@@ -574,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
+
+
 

+ 161 - 22
kalliope/core/MainController.py

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

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

@@ -3,28 +3,30 @@ class RestAPI(object):
     This Class is representing the rest API with all its configuration.
     """
 
-    def __init__(self, password_protected=None, login=None, password=None, active=None, port=None):
+    def __init__(self, password_protected=None, login=None, password=None, active=None, port=None, allowed_cors_origin=None):
         """
-
         :param password_protected: If true, the rest api will ask for an authentication
         :param login: login used if auth is activated
         :param password: password used if auth is activated
         :param active: specify if the rest api is loaded on start with Kalliope
+        :param allowed_cors_origin: specify allowed origins
         """
         self.password_protected = password_protected
         self.login = login
         self.password = password
         self.active = active
         self.port = port
+        self.allowed_cors_origin = allowed_cors_origin
 
     def __str__(self):
         return "%s: RestAPI: password_protected: %s, login: %s, " \
-               "password: %s, active: %s, port: %s" % (self.__class__.__name__,
+              "password: %s, active: %s, port: %s, allow_cors_request: %s" % (self.__class__.__name__,
                                                        self.password_protected,
                                                        self.login,
                                                        self.password,
                                                        self.active,
-                                                       self.port)
+                                                       self.port,
+                                                       self.allowed_cors_origin)
 
     def __eq__(self, other):
         """

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

@@ -16,6 +16,9 @@ class Settings(object):
                  stts=None,
                  random_wake_up_answers=None,
                  random_wake_up_sounds=None,
+                 play_on_ready_notification=None,
+                 on_ready_answers=None,
+                 on_ready_sounds=None,
                  triggers=None,
                  rest_api=None,
                  cache_path=None,
@@ -31,6 +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.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

+ 5 - 1
kalliope/core/NeuronModule.py

@@ -129,9 +129,13 @@ class NeuronModule(object):
             logger.debug("NeuroneModule: TTS args: %s" % tts_object)
 
             # get the instance of the TTS module
+            tts_folder = None
+            if self.settings.resources:
+                tts_folder = self.settings.resources.tts_folder
             tts_module_instance = Utils.get_dynamic_class_instantiation(package_name="tts",
                                                                         module_name=tts_object.name,
-                                                                        parameters=tts_object.parameters)
+                                                                        parameters=tts_object.parameters,
+                                                                        resources_dir=tts_folder)
             # generate the audio file and play it
             tts_module_instance.say(tts_message)
 

+ 27 - 1
kalliope/core/OrderAnalyser.py

@@ -260,17 +260,43 @@ class OrderAnalyser:
     @classmethod
     def spelt_order_match_brain_order_via_table(cls, order_to_analyse, user_said):
         """
-        return true if all string that are in the sentence are present in the order to test
+        return true if all formatted(_format_sentences_to_analyse(order_to_analyse, user_said)) strings
+                that are in the sentence are present in the order to test.
         :param order_to_analyse: String order to test
         :param user_said: String to compare to the order
         :return: True if all string are present in the order
         """
+        logger.debug("[spelt_order_match_brain_order_via_table] before formatting, "
+                     "order to analyse: %s, "
+                     "user sentence: %s"
+                     % (order_to_analyse, user_said))
+        order_to_analyse, user_said = cls._format_sentences_to_analyse(order_to_analyse=order_to_analyse,
+                                                                       user_said=user_said)
         list_word_user_said = user_said.split()
         split_order_without_bracket = cls._get_split_order_without_bracket(order_to_analyse)
 
         # if all words in the list of what the user said in in the list of word in the order
         return cls._counter_subset(split_order_without_bracket, list_word_user_said)
 
+    @staticmethod
+    def _format_sentences_to_analyse(order_to_analyse, user_said):
+        """
+        return formatted tuple of the order_to_analyse, user_said
+        :param order_to_analyse: String order to test
+        :param user_said: String to compare to the order
+        :return: tuple of the order_to_analyse, user_said
+        """
+        # Lowercase all incoming
+        order_to_analyse = order_to_analyse.lower()
+        user_said = user_said.lower()
+
+        logger.debug("[_format_sentences_to_analyse] formatted, "
+                     "order to analyse: %s, "
+                     "user sentence: %s"
+                     % (order_to_analyse, user_said))
+
+        return order_to_analyse, user_said
+
     @staticmethod
     def _get_split_order_without_bracket(order):
         """

+ 10 - 4
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:
@@ -55,9 +56,14 @@ class OrderListener(Thread):
         for stt_object in self.settings.stts:
             if stt_object.name == self.stt_module_name:
                 stt_object.parameters["callback"] = self.callback
-                Utils.get_dynamic_class_instantiation(package_name='stt',
-                                                      module_name=stt_object.name.capitalize(),
-                                                      parameters=stt_object.parameters)
+
+                stt_folder = None
+                if self.settings.resources:
+                    stt_folder = self.settings.resources.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():

+ 11 - 5
kalliope/core/ResourcesManager.py

@@ -2,6 +2,7 @@ import getpass
 import logging
 import os
 import shutil
+import re
 
 from git import Repo
 from packaging import version
@@ -257,11 +258,16 @@ class ResourcesManager(object):
         logger.debug("[ResourcesManager] Module supported version: %s" % str(supported_versions))
 
         supported_version_found = False
-        for supported_version in supported_versions:
-            if version.parse(current_version) == version.parse(supported_version):
-                # we found the exact version
-                supported_version_found = True
-                break
+        # Extract major version
+        match_current_version = re.search('^[\d]*[.][\d]*', current_version)
+        if match_current_version:
+            current_version = match_current_version.group(0)
+
+            for supported_version in supported_versions:
+                if version.parse(current_version) == version.parse(supported_version):
+                    # we found the exact version
+                    supported_version_found = True
+                    break
 
         if not supported_version_found:
             # we ask the user if we want to install the module even if the version doesn't match

+ 9 - 1
kalliope/core/RestAPI/FlaskAPI.py

@@ -1,16 +1,20 @@
+import logging
 import threading
 
 from flask import jsonify
 from flask import request
 from flask_restful import abort
+from flask_cors import CORS, cross_origin
 
 from kalliope.core import OrderAnalyser
 from kalliope.core.RestAPI.utils import requires_auth
 from kalliope.core.SynapseLauncher import SynapseLauncher
 
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
 
 class FlaskAPI(threading.Thread):
-    def __init__(self, app, port=5000, brain=None):
+    def __init__(self, app, port=5000, brain=None, allowed_cors_origin=False):
         """
 
         :param app: Flask API
@@ -22,11 +26,15 @@ class FlaskAPI(threading.Thread):
         self.app = app
         self.port = port
         self.brain = brain
+        self.allowed_cors_origin = allowed_cors_origin
 
         # Flask configuration remove default Flask behaviour to encode to ASCII
         self.app.url_map.strict_slashes = False
         self.app.config['JSON_AS_ASCII'] = False
 
+        if self.allowed_cors_origin is not False:
+            cors = CORS(app, resources={r"/*": {"origins": allowed_cors_origin}}, supports_credentials=True)
+
         # Add routing rules
         self.app.add_url_rule('/synapses', view_func=self.get_synapses, methods=['GET'])
         self.app.add_url_rule('/synapses/<synapse_name>', view_func=self.get_synapse, methods=['GET'])

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

+ 31 - 6
kalliope/settings.yml

@@ -36,12 +36,13 @@ speech_to_text:
       key: "B5JI3YUSLYOYWNIDBINBVM34XUODME2K"
   - bing: # API not working : credential fails ..
       key: "9e48ddaf75904838bedc11aea6b36fb0"
-  - apiai: # Fail retriving the text from the audio file ..
+  - apiai:
       key: "e0cbff154af44944a6b9f82c0668b527"
       language: "fr"
-  - houndify: # In the online documentation but Not implemented yet in the speech recognition lib ..?
+  - houndify:
       key: "7zj90T7qAV74OYXk4X4vI2Xhk7wPsJu4aEZ0G5Ll-BMmV1JGtFpCxtSH9SmTY4G3bpEJ7a5y_GTQid-CAKI6vw=="
       client_id: "lN4JXeaSticbSo9-llczbA=="
+  #- cmusphinx
 
 
 # ---------------------------
@@ -74,7 +75,6 @@ text_to_speech:
       language: "fr-fr"
       cache: True
 
-
 # ---------------------------
 # Wake up answers
 # ---------------------------
@@ -92,12 +92,36 @@ random_wake_up_answers:
 # the `random_wake_up_answers`.
 # 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_wake_up_sounds:
-  - "ding.wav"
-  - "dong.wav"
+#random_wake_up_sounds:
+#  - "sounds/ding.wav"
+#  - "sounds/dong.wav"
   # - "/my/personal/full/path/my_file.mp3"
 
 
+# ---------------------------
+# 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
+
+# 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"
+
+
 # ---------------------------
 # Rest API
 # ---------------------------
@@ -107,6 +131,7 @@ rest_api:
   password_protected: True
   login: admin
   password: secret
+  allowed_cors_origin: False
 
 # ---------------------------
 # Default Synapse

+ 0 - 0
sounds/ding.wav → kalliope/sounds/ding.wav


+ 0 - 0
sounds/dong.wav → kalliope/sounds/dong.wav


+ 55 - 0
kalliope/stt/Utils.py

@@ -0,0 +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(Thread):
+
+    def __init__(self):
+        """
+        Thread used to caught n audio from the microphone and pass it to a callback method
+        """
+        super(SpeechRecognition, self).__init__()
+        self.recognizer = sr.Recognizer()
+        self.microphone = sr.Microphone()
+        self.callback = None
+        self.stop_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 run(self):
+        """
+        Start the thread that listen the microphone and then give the audio to the callback method
+        """
+        Utils.print_info("Say something!")
+        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 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

+ 34 - 28
kalliope/stt/apiai/apiai.py

@@ -1,10 +1,10 @@
 import speech_recognition as sr
 
 from kalliope.core import Utils
-from kalliope.core.OrderListener import OrderListener
+from kalliope.stt.Utils import SpeechRecognition
 
 
-class Apiai(OrderListener):
+class Apiai(SpeechRecognition):
 
     def __init__(self, callback=None, **kwargs):
         """
@@ -12,45 +12,51 @@ class Apiai(OrderListener):
         :param callback: The callback function to call to send the text
         :param kwargs:
         """
-        OrderListener.__init__(self)
+        SpeechRecognition.__init__(self)
 
         # callback function to call after the translation speech/tex
-        self.callback = callback
-        # obtain audio from the microphone
-        r = sr.Recognizer()
-        with sr.Microphone() as source:
-            # listen for 1 second to calibrate the energy threshold for ambient noise levels
-            r.adjust_for_ambient_noise(source)
-            Utils.print_info("Say something!")
-            audio = r.listen(source)
-
-        # recognize speech using Apiai Speech Recognition
-        try:
+        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)
 
-            key = kwargs.get('key', None)
-            language = kwargs.get('language', "en")
-            session_id= kwargs.get('session_id', None)
-            show_all = kwargs.get('show_all', False)
+        # start listening in the background
+        self.set_callback(self.apiai_callback)
+        self.start_listening()
 
-            captured_audio = r.recognize_api(audio, client_access_token=key, language=language, session_id=session_id, show_all=show_all)
+    def apiai_callback(self, recognizer, audio):
+        """
+        called from the background thread
+        :param recognizer:
+        :param audio:
+        :return:
+        """
+        try:
+            captured_audio = recognizer.recognize_api(audio,
+                                                      client_access_token=self.key,
+                                                      language=self.language,
+                                                      session_id=self.session_id,
+                                                      show_all=self.show_all)
             Utils.print_success("Apiai Speech Recognition thinks you said %s" % captured_audio)
             self._analyse_audio(captured_audio)
 
         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)
+
+        # stop listening for an audio
+        self.stop_listening()
 
-    def _analyse_audio(self, audio):
+    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)

+ 29 - 28
kalliope/stt/bing/bing.py

@@ -1,10 +1,10 @@
 import speech_recognition as sr
 
 from kalliope.core import Utils
-from kalliope.core.OrderListener import OrderListener
+from kalliope.stt.Utils import SpeechRecognition
 
 
-class Bing(OrderListener):
+class Bing(SpeechRecognition):
 
     def __init__(self, callback=None, **kwargs):
         """
@@ -12,45 +12,46 @@ class Bing(OrderListener):
         :param callback: The callback function to call to send the text
         :param kwargs:
         """
-        OrderListener.__init__(self)
+        SpeechRecognition.__init__(self)
 
         # callback function to call after the translation speech/tex
-        self.callback = callback
-        # obtain audio from the microphone
-        r = sr.Recognizer()
-        with sr.Microphone() as source:
-            # listen for 1 second to calibrate the energy threshold for ambient noise levels
-            r.adjust_for_ambient_noise(source)
-            Utils.print_info("Say something!")
-            audio = r.listen(source)
-
-        # recognize speech using Bing Speech Recognition
-        try:
+        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)
 
-            key = kwargs.get('key', None)
-            language = kwargs.get('language', "en-US")
-            show_all = kwargs.get('show_all', False)
+        # start listening in the background
+        self.set_callback(self.bing_callback)
+        self.start_listening()
 
-            captured_audio = r.recognize_bing(audio, key=key, language=language, show_all=show_all)
+    def bing_callback(self, recognizer, audio):
+        """
+        called from the background thread
+        """
+        try:
+            captured_audio = recognizer.recognize_bing(audio,
+                                                       key=self.key,
+                                                       language=self.language,
+                                                       show_all=self.show_all)
             Utils.print_success("Bing Speech Recognition thinks you said %s" % captured_audio)
             self._analyse_audio(captured_audio)
 
         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 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)

+ 24 - 0
kalliope/stt/cmusphinx/README.md

@@ -0,0 +1,24 @@
+### CMU Sphinx
+
+This module is based on the self hosted STT solution [CMUSPhinx engine](http://cmusphinx.sourceforge.net/wiki/).
+By default, only english language is available. You can download [another language model](https://sourceforge.net/projects/cmusphinx/files/Acoustic%20and%20Language%20Models/) from the main repository and install it following [the official documentation](http://cmusphinx.sourceforge.net/wiki/tutoriallm).
+
+#### Installation
+
+Install packages
+```bash
+sudo apt-get install swig libpulse-dev
+```
+
+Then install the python lib
+```bash
+sudo pip install pocketsphinx
+```
+
+Then, declare it as usual in your settings
+```YAML
+default_speech_to_text: "cmusphinx"
+# no parameters for this one
+speech_to_text:  
+  - cmusphinx
+```

+ 1 - 0
kalliope/stt/cmusphinx/__init__.py

@@ -0,0 +1 @@
+from cmusphinx import Cmusphinx

+ 51 - 0
kalliope/stt/cmusphinx/cmusphinx.py

@@ -0,0 +1,51 @@
+import speech_recognition as sr
+
+from kalliope.core import Utils
+from kalliope.stt.Utils import SpeechRecognition
+
+
+class Cmusphinx(SpeechRecognition):
+
+    def __init__(self, callback=None, **kwargs):
+        """
+        Start recording the microphone and analyse audio with CMU sphinx api
+        :param callback: The callback function to call to send the text
+        :param kwargs:
+        """
+        SpeechRecognition.__init__(self)
+
+        # callback function to call after the translation speech/tex
+        self.main_controller_callback = callback
+
+        # start listening in the background
+        self.set_callback(self.sphinx_callback)
+        self.start_listening()
+
+    def sphinx_callback(self, recognizer, audio):
+        """
+        called from the background thread
+        """
+        try:
+            captured_audio = recognizer.recognize_sphinx(audio)
+            Utils.print_success("Sphinx Speech Recognition thinks you said %s" % captured_audio)
+            self._analyse_audio(captured_audio)
+
+        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_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_to_text=None)
+
+        # 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_to_text: the captured audio
+        """
+        if self.main_controller_callback is not None:
+            self.main_controller_callback(audio_to_text)

+ 26 - 27
kalliope/stt/google/google.py

@@ -1,10 +1,12 @@
+from time import sleep
+
 import speech_recognition as sr
 
 from kalliope.core import Utils
-from kalliope.core.OrderListener import OrderListener
+from kalliope.stt.Utils import SpeechRecognition
 
 
-class Google(OrderListener):
+class Google(SpeechRecognition):
 
     def __init__(self, callback=None, **kwargs):
         """
@@ -12,32 +14,29 @@ class Google(OrderListener):
         :param callback: The callback function to call to send the text
         :param kwargs:
         """
-        OrderListener.__init__(self)
+        SpeechRecognition.__init__(self)
 
         # callback function to call after the translation speech/tex
-        self.callback = callback
-        # obtain audio from the microphone
-        r = sr.Recognizer()
-        with sr.Microphone() as source:
-            # listen for 1 second to calibrate the energy threshold for ambient noise levels
-            r.adjust_for_ambient_noise(source)
-            Utils.print_info("Say something!")
-            audio = r.listen(source)
-
-        # recognize speech using Google Speech Recognition
-        try:
-            # for testing purposes, we're just using the default API key
-            # to use another API key, use `r.recognize_google(audio, key="GOOGLE_SPEECH_RECOGNITION_API_KEY")`
-            # instead of `r.recognize_google(audio)`
+        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)
 
-            key = kwargs.get('key', None)
-            language = kwargs.get('language', "en-US")
-            show_all = kwargs.get('show_all', False)
+        # start listening in the background
+        self.set_callback(self.google_callback)
+        self.start_listening()
 
-            captured_audio = r.recognize_google(audio, key=key, language=language, show_all=show_all)
+    def google_callback(self, recognizer, audio):
+        """
+        called from the background thread
+        """
+        try:
+            captured_audio = recognizer.recognize_google(audio,
+                                                         key=self.key,
+                                                         language=self.language,
+                                                         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
@@ -47,13 +46,13 @@ class Google(OrderListener):
             # 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: the captured audio
+        :param audio_to_text: the captured audio
         """
+        if self.main_controller_callback is not None:
+            self.main_controller_callback(audio_to_text)
 
-        # if self.main_controller is not None:
-        #     self.main_controller.analyse_order(audio)
-        if self.callback is not None:
-            self.callback(audio_to_text)

+ 2 - 2
kalliope/stt/houndify/README.md

@@ -1,9 +1,9 @@
 ### Houndify
 
-Not working yet ... in coming
+English only
 
 | parameter | required  | default | choices      | comment   |
 |:---------:|-----------|---------|--------------|-----------|
 | key       | Yes       | None    |              | User info |
 | client_id | Yes       | None    |              | User info |
-| language  | No        | en-US   | en-US        |           |
+

+ 32 - 30
kalliope/stt/houndify/houndify.py

@@ -1,10 +1,10 @@
 import speech_recognition as sr
 
 from kalliope.core import Utils
-from kalliope.core.OrderListener import OrderListener
+from kalliope.stt.Utils import SpeechRecognition
 
 
-class Houndify(OrderListener):
+class Houndify(SpeechRecognition):
 
     def __init__(self, callback=None, **kwargs):
         """
@@ -12,46 +12,48 @@ class Houndify(OrderListener):
         :param callback: The callback function to call to send the text
         :param kwargs:
         """
-        OrderListener.__init__(self)
+        SpeechRecognition.__init__(self)
 
         # callback function to call after the translation speech/tex
-        self.callback = callback
-        # obtain audio from the microphone
-        r = sr.Recognizer()
-        with sr.Microphone() as source:
-            # listen for 1 second to calibrate the energy threshold for ambient noise levels
-            r.adjust_for_ambient_noise(source)
-            Utils.print_info("Say something!")
-            audio = r.listen(source)
-
-        # recognize speech using Houndify Speech Recognition
+        self.main_controller_callback = callback
+        self.client_id = kwargs.get('client_id', None)
+        self.key = kwargs.get('key', None)
+        # only english supported
+        # self.language = kwargs.get('language', "en-US")
+        self.show_all = kwargs.get('show_all', False)
+
+        # start listening in the background
+        self.set_callback(self.houndify_callback)
+        self.start_listening()
+
+    def houndify_callback(self, recognizer, audio):
+        """
+        called from the background thread
+        """
         try:
-            client_id = kwargs.get('client_id', None)
-            key = kwargs.get('key', None)
-            language = kwargs.get('language', "en-US")
-            show_all = kwargs.get('show_all', False)
-
-            captured_audio = r.recognize_houndify(audio, client_id=client_id, client_key=key,
-                                                  language=language, show_all=show_all)
+            captured_audio = recognizer.recognize_houndify(audio,
+                                                           client_id=self.client_id,
+                                                           client_key=self.key,
+                                                           show_all=self.show_all)
             Utils.print_success("Houndify Speech Recognition thinks you said %s" % captured_audio)
             self._analyse_audio(captured_audio)
 
         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 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)

+ 25 - 27
kalliope/stt/wit/wit.py

@@ -1,10 +1,10 @@
 import speech_recognition as sr
 
 from kalliope.core import Utils
-from kalliope.core.OrderListener import OrderListener
+from kalliope.stt.Utils import SpeechRecognition
 
 
-class Wit(OrderListener):
+class Wit(SpeechRecognition):
 
     def __init__(self, callback=None, **kwargs):
         """
@@ -12,43 +12,41 @@ class Wit(OrderListener):
         :param callback: The callback function to call to send the text
         :param kwargs:
         """
-        OrderListener.__init__(self)
+        SpeechRecognition.__init__(self)
 
         # callback function to call after the translation speech/tex
-        self.callback = callback
-        # obtain audio from the microphone
-        r = sr.Recognizer()
-        with sr.Microphone() as source:
-            # listen for 1 second to calibrate the energy threshold for ambient noise levels
-            r.adjust_for_ambient_noise(source)
-            Utils.print_info("Say something!")
-            audio = r.listen(source)
-
-        # recognize speech using Wit.ai Speech Recognition
-        try:
-            key = kwargs.get('key', None)
-            show_all = kwargs.get('show_all', False)
+        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.set_callback(self.wit_callback)
+        self.start_listening()
 
-            captured_audio = r.recognize_wit(audio, key=key, show_all=show_all)
+    def wit_callback(self, recognizer, audio):
+        try:
+            captured_audio = recognizer.recognize_wit(audio,
+                                                      key=self.key,
+                                                      show_all=self.show_all)
             Utils.print_success("Wit.ai Speech Recognition thinks you said %s" % captured_audio)
             self._analyse_audio(captured_audio)
 
         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-20samples.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)

+ 5 - 1
setup.py

@@ -61,6 +61,7 @@ setup(
         'SpeechRecognition>=3.5.0',
         'markupsafe==0.23',
         'pyaudio==0.2.9',
+        'pyasn1>=0.1.8',
         'ansible==2.2.0.0',
         'python2-pythondialog==3.4.0',
         'jinja2==2.8',
@@ -68,13 +69,15 @@ setup(
         'ipaddress==1.0.17',
         'flask==0.11.1',
         'Flask-Restful==0.3.5',
+        'flask_cors==3.0.2',
         'requests==2.12.4',
         'httpretty==0.8.14',
         'mock==2.0.0',
         'Flask-Testing==0.6.1',
         'apscheduler==3.3.0',
         'GitPython==2.1.1',
-        'packaging>=16.8'
+        'packaging>=16.8',
+        'transitions>=0.4.3'
     ],
 
 
@@ -86,6 +89,7 @@ setup(
             'trigger/snowboy/armv7l/_snowboydetect.so',
             'trigger/snowboy/x86_64/_snowboydetect.so',
             'trigger/snowboy/resources/*',
+            'sounds/*'
          ],
     },