Parcourir la source

Merge pull request #230 from kalliope-project/dev

Version 4.3
Nicolas Marcq il y a 8 ans
Parent
commit
4c6586bd4c
72 fichiers modifiés avec 1479 ajouts et 673 suppressions
  1. 15 0
      CHANGELOG.md
  2. 6 5
      Docs/contributing/contribute_stt.md
  3. 7 2
      Docs/installation.md
  4. 2 0
      Docs/installation/debian_jessie.md
  5. 2 1
      Docs/installation/quickstart.md
  6. 3 1
      Docs/installation/raspbian_jessie.md
  7. 2 0
      Docs/installation/ubuntu_14.04.md
  8. 2 0
      Docs/installation/ubuntu_16.04.md
  9. 9 0
      Docs/kalliope_cli.md
  10. 86 36
      Docs/neuron_list.md
  11. 14 18
      Docs/neurons.md
  12. 71 11
      Docs/rest_api.md
  13. 37 3
      Docs/settings.md
  14. 0 3
      Docs/tts.md
  15. 1 1
      Docs/tts_list.md
  16. 2 2
      README.md
  17. 19 0
      Tests/brains/brain_test_api.yml
  18. BIN
      Tests/files/bonjour.wav
  19. 7 0
      Tests/settings/settings_test.yml
  20. 3 0
      Tests/settings/variables.yml
  21. 179 2
      Tests/test_brain_loader.py
  22. 2 0
      Tests/test_launchers.py
  23. 40 0
      Tests/test_neuron_model.py
  24. 2 1
      Tests/test_neuron_module.py
  25. 9 55
      Tests/test_order_analyser.py
  26. 53 0
      Tests/test_order_listener.py
  27. 2 1
      Tests/test_resources_manager.py
  28. 244 183
      Tests/test_rest_api.py
  29. 17 3
      Tests/test_settings_loader.py
  30. 89 4
      Tests/test_utils.py
  31. 1 1
      brain_examples/systemdate.yml
  32. 1 1
      install/files/python_requirements.txt
  33. 58 25
      kalliope/__init__.py
  34. 1 1
      kalliope/_version.py
  35. 12 0
      kalliope/brain.yml
  36. 60 5
      kalliope/core/ConfigurationManager/BrainLoader.py
  37. 24 0
      kalliope/core/ConfigurationManager/SettingLoader.py
  38. 9 0
      kalliope/core/ConfigurationManager/YAMLLoader.py
  39. 5 2
      kalliope/core/MainController.py
  40. 15 1
      kalliope/core/Models/Neuron.py
  41. 2 0
      kalliope/core/Models/Settings.py
  42. 3 1
      kalliope/core/NeuronLauncher.py
  43. 5 2
      kalliope/core/NeuronModule.py
  44. 11 33
      kalliope/core/OrderAnalyser.py
  45. 5 1
      kalliope/core/OrderListener.py
  46. 68 22
      kalliope/core/ResourcesManager.py
  47. 123 8
      kalliope/core/RestAPI/FlaskAPI.py
  48. 1 1
      kalliope/core/TTS/TTSModule.py
  49. 62 3
      kalliope/core/Utils/Utils.py
  50. 5 0
      kalliope/neurons/sleep/sleep.py
  51. 2 2
      kalliope/neurons/sleep/tests/test_sleep.py
  52. 15 6
      kalliope/settings.yml
  53. 22 11
      kalliope/stt/Utils.py
  54. 4 2
      kalliope/stt/apiai/apiai.py
  55. 4 2
      kalliope/stt/bing/bing.py
  56. 4 2
      kalliope/stt/cmusphinx/cmusphinx.py
  57. 5 3
      kalliope/stt/google/google.py
  58. 4 2
      kalliope/stt/houndify/houndify.py
  59. 4 2
      kalliope/stt/wit/wit.py
  60. 0 7
      kalliope/templates/en_rss.j2
  61. 0 50
      kalliope/templates/en_systemdate_template_example.j2
  62. 0 12
      kalliope/templates/fr_gmail.j2
  63. 0 5
      kalliope/templates/fr_gmail_count_unread.j2
  64. 0 7
      kalliope/templates/fr_rss.j2
  65. 0 5
      kalliope/templates/fr_systemdate_template_example.j2
  66. 0 5
      kalliope/templates/remove_file.j2
  67. 0 12
      kalliope/templates/wikipedia_returned_value.j2
  68. 2 1
      kalliope/tts/acapela/acapela.py
  69. 0 11
      kalliope/tts/voxygen/README.md
  70. 0 1
      kalliope/tts/voxygen/__init__.py
  71. 0 69
      kalliope/tts/voxygen/voxygen.py
  72. 17 17
      setup.py

+ 15 - 0
CHANGELOG.md

@@ -1,3 +1,18 @@
+v0.4.3 / 2017-03-11
+===================
+- Update Documentation
+- Improve API + manage incoming audio
+- Fix bug maximum recursion
+- Disable REST API by default 
+- Add command line run-order to start a synapse
+- Run default synapse if STT engine do not return an order
+- Manage global variables
+- Remove password from log
+- cli command to delete a neuron
+- Fix neurotransmitter
+- Fix Acapela TTS
+- Remove Voxygen TTS (not working anymore)
+
 v0.4.2 / 2017-01-18
 ===================
 - fix community neuron installation

+ 6 - 5
Docs/contributing/contribute_stt.md

@@ -37,14 +37,15 @@ The constructor has a __**kwargs argument__ which is corresponding to the Dict o
 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. 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. Use self.start_processing() from the mother class to get an audio from the microphone or read the audio file path if provided. 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 or read from an audio file path
 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)
+        # give the audio file path to process directly to the mother class if exist
+        SpeechRecognition.__init__(self, kwargs.get('audio_file_path', None))
         # here is the main controller callback. We will return the text at the end of the process
         self.main_controller_callback = callback
         
@@ -52,8 +53,8 @@ The constructor has a __**kwargs argument__ which is corresponding to the Dict o
         
         #  give our callback   
         self.set_callback(self.my_callback)
-        # start the microphone to capture an audio
-        self.start_listening()
+        # start processing, record a sample from the microphone if no audio file path provided, else read the file
+        self.start_processing()
         
         def my_callback((self, recognizer, audio):
             # ---------------------------------------------

+ 7 - 2
Docs/installation.md

@@ -53,6 +53,11 @@ Install the project using the local environment:
 venv/bin/pip install --editable .
 ```
 
+Activate the local environment:
+```bash
+source venv/bin/activate
+```
+
 ### Method 4 - Developer, dependencies install only
 
 Clone the project:
@@ -63,7 +68,7 @@ cd kalliope
 
 Install the python dependencies directly:
 ```bash
-sudo pip install -r install/python_requirements.txt
+sudo pip install -r install/files/python_requirements.txt
 ```
 
 ## Test your env
@@ -88,7 +93,7 @@ Those repositories provide you a basic structure to start playing with kalliope.
 
 - [French starter config](https://github.com/kalliope-project/kalliope_starter_fr)
 - [English starter config](https://github.com/kalliope-project/kalliope_starter_en)
-
+- [German starter config](https://github.com/kalliope-project/kalliope_starter_de)
 
 ## Next: 
 If everything is ok, you can start playing with Kalliope. First, take a look to the [default settings](settings.md).

+ 2 - 0
Docs/installation/debian_jessie.md

@@ -26,3 +26,5 @@ Then, with pip, the last release of setuptools
 sudo pip install -U setuptools
 ```
 
+Then, follow the [main installation documentation](../installation.md).
+

+ 2 - 1
Docs/installation/quickstart.md

@@ -8,6 +8,7 @@ If you are using kalliope from a Rpi, the idea would be to configure your assist
 We made starter kits that only needs to be cloned, placed into the Rpi and launched.
 - [French starter kit](https://github.com/kalliope-project/kalliope_starter_fr)
 - [English starter kit](https://github.com/kalliope-project/kalliope_starter_en)
+- [German starter kit](https://github.com/kalliope-project/kalliope_starter_de)
 
 Those repositories provide you a structure to start playing and learning basics of Kalliope.
 Download the starter kit of your choice and open the folder with your IDE. 
@@ -17,7 +18,7 @@ When you start kalliope using the CLI (`kalliope start`), the program will try t
 - From `/etc/kalliope/settings.yml`
 - From the default `settings.yml`. You can take a look into the default [`settings.yml`](../kalliope/settings.yml) file which is located in the root of the project tree.
 
-This a common tree of a Kalliope configuration folder:
+This is a common tree of a Kalliope configuration folder:
 ```
 kalliope_config/
 ├── brains

+ 3 - 1
Docs/installation/raspbian_jessie.md

@@ -28,7 +28,7 @@ Install some required system libraries and software:
 
 ```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
+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 libyaml-dev libpython2.7-dev
 ```
 
 Let's install the last release of python-pip
@@ -153,3 +153,5 @@ Check the [official documentation](https://www.raspberrypi.org/documentation/con
 ```bash
 sudo raspi-config
 ```
+
+Then, follow the [main installation documentation](../installation.md).

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

@@ -14,3 +14,5 @@ Let's install the last release of python-pip
 wget https://bootstrap.pypa.io/get-pip.py
 sudo python get-pip.py
 ```
+
+Then, follow the [main installation documentation](../installation.md).

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

@@ -13,3 +13,5 @@ Let's install the last release of python-pip
 wget https://bootstrap.pypa.io/get-pip.py
 sudo python get-pip.py
 ```
+
+Then, follow the [main installation documentation](../installation.md).

+ 9 - 0
Docs/kalliope_cli.md

@@ -72,6 +72,15 @@ Example of use
 kalliope start --run-synapse "say-hello"
 ```
 
+### --run-order "Your Order"
+
+Run a specific order from command line.
+
+Example of use
+```bash
+kalliope start --run-order "hello"
+```
+
 ### --brain-file BRAIN_FILE
 
 Replace the default brain file from the root of the project folder by a custom one.

+ 86 - 36
Docs/neuron_list.md

@@ -1,38 +1,88 @@
-# List of available neuron
-
+# Neuron installation
 A neuron is a module that will perform some actions attached to an order. You can use it in your synapses. See the [complete neuron documentation](neurons.md) for more information.
 
-## Core neuron
-
-| Name                                                      | Description                                       |
-|-----------------------------------------------------------|---------------------------------------------------|
-| [ansible_playbook](../kalliope/neurons/ansible_playbook/) | Run an ansible playbook                           |
-| [kill_switch](../kalliope/neurons/kill_switch/)           | Stop Kalliope process                             |
-| [neurotransmitter](../kalliope/neurons/neurotransmitter/) | Link synapse together                             |
-| [say](../kalliope/neurons/say/)                           | Make Kalliope talk by using TTS                   |
-| [script](../kalliope/neurons/script/)                     | Run an executable script                          |
-| [shell](../kalliope/neurons/shell/)                       | Run a shell command                               |
-| [sleep](../kalliope/neurons/sleep/)                       | Make Kalliope sleep for a while before continuing |
-| [systemdate](../kalliope/neurons/systemdate/)             | Give the local system date and time               |
-| [uri](../kalliope/neurons/uri/)                           | Interacts with HTTP and HTTPS web services.       |
-
-## 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                     |
-| [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                                                                       |
-| [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.
-
-To know how to install a community neuron, read the "Installation" section of the [neuron documentation](neurons.md).
+## Neuron list
+
+Get the full neuron list available on [the project web site](https://kalliope-project.github.io/).
+
+## Installation
+
+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.
+>**Note:** To install a neuron, you must declare your `resource_directory` in your [settings](settings.md).
+
+### Via the kalliope's CLI
+
+CLI syntax
+```bash
+kalliope install --git-url <git_url>
+```
+
+E.g:
+```bash
+kalliope install --git-url https://github.com/kalliope-project/kalliope_neuron_wikipedia.git
+```
+You may be prompted to type your `sudo` password during the process.
+
+### Manually
+
+You can also install a neuron manually.
+Fist, clone the repo in the right resource folder. 
+```bash
+cd /path/to/resource_folder
+git clone <plugin_url>
+```
+
+Then install it manually via Ansible (Ansible has been installed with kalliope)
+```bash
+cd <cloned_repo>
+ansible-playbook install.yml -K
+```
+
+Full example
+```bash
+cd /home/me/my_kalliope_config/resources/neurons
+git clone https://github.com/kalliope-project/kalliope_neuron_hue.git
+cd hue
+ansible-playbook install.yml -K
+```
+
+## Uninstall a resource
+### Via the kalliope's CLI
+>**Note:** To uninstall a resource, you must declare the `resource_directory` in your [settings](settings.md).
+
+CLI syntax
+```bash
+kalliope uninstall --neuron-name <neuron_name>
+kalliope uninstall --stt-name <stt_name>
+kalliope uninstall --tts-name <tts_name>
+kalliope uninstall --trigger-name <trigger_name>
+```
+
+E.g:
+```bash
+kalliope uninstall --neuron-name hue
+```
+
+### Manually
+
+To remove a resource, you only need to delete the folder from the corresponding `resource_directory`.
+```bash
+cd /path/to/resource_folder
+rm -rf <resource_name>
+```
+
+E.g
+```bash
+cd /home/me/my_kalliope_config/resources/neurons
+rm -rf hue
+```
+
+>**Note:** When deleting a resource folder, libraries that have been installed by the resource are not removed from the system. If you want a complete cleanup, you'll have to open the install.yml file of the resource to see what have been installed and rollback manually each task.
+For example, when installing the neuron hue, the python lib called phue has been installed. To perform a complete cleanup, you need then to run "sudo pip uninstall phue".
+
+
+## Update a resource
+
+To update a resource, you can either:
+- uninstall the resource and then reinstall it back
+- go into the resource directory and run a `git pull`

+ 14 - 18
Docs/neurons.md

@@ -2,22 +2,7 @@
 
 A neuron is a plugin that performs a specific action. You use it to create a synapse.
 You can add as many neurons as you want to a synapse. The neurons are executed one by one when the input order is triggered.
-
-## Installation
-
-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
-```
-
-You may be prompted to type your `sudo` password during the process. You can see the list of [available neuron here](neuron_list.md)
+If you want to install a community neuron, see the [neuron list documentation](neuron_list.md).
 
 ## Usage
 Neurons are declared in the `neurons` section of a synapse in your brain file.
@@ -47,6 +32,7 @@ Full list of [available neuron here](neuron_list.md)
 
 Neurons require some **parameters** from the synapse declaration to work. Those parameters, also called arguments, can be passed to the neuron in two way:
 - from the neuron declaration
+- from global variables
 - from the captured order
 
 From the neuron declaration:
@@ -57,6 +43,16 @@ neurons:
         parameter2: "value2"
 ```
 
+From global variables: (cf: [settings.md](settings.md))
+```yml
+  - name: "run-simple-sleep"
+    signals:
+      - order: "Wait for me "
+    neurons:
+      - sleep:
+          seconds: {{variable}}
+```
+
 From the captured order:
 ```yml
   - name: "run-neuron-with-parameter-in-order"
@@ -160,10 +156,10 @@ neurons:
   - say:
     message:
       - "My name is Kalliope"
-    tts: "voxygen"
+    tts: "acapela"
 ```
 
-Here, the first neuron will use the default tts as set in the settings.yml file. The second neuron will use the tts "voxygen".
+Here, the first neuron will use the default tts as set in the settings.yml file. The second neuron will use the tts "acapela".
 
 >**Note:** The TTS must has been configured with its required parameters in the settings.yml file. See [TTS documentation](tts.md).
 

+ 71 - 11
Docs/rest_api.md

@@ -4,24 +4,41 @@ Kalliope provides the REST API to manage the synapses. For configuring the API r
 
 ## Synapse API
 
-| Method | URL                      | Action                      |
-|--------|--------------------------|-----------------------------|
-| GET    | /synapses                | List synapses               |
-| GET    | /synapses/<synapse_name> | Show synapse details        |
-| POST   | /synapses/<synapse_name> | Run a synapse by its name   |
-| POST   | /order                   | Run a synapse from an order |
+| Method | URL                               | Action                             |
+|--------|-----------------------------------|------------------------------------|
+| GET    | /                                 | Get kaliope version                |
+| GET    | /synapses                         | List synapses                      |
+| GET    | /synapses/<synapse_name>          | Get synapse details by name        |
+| POST   | /synapses/start/id/<synapse_name> | Run a synapse by its name          |
+| POST   | /synapses/start/order             | Run a synapse from a text order    |
+| POST   | /synapses/start/audio             | Run a synapse from an audio sample |
 
 ## Curl examples
 
 >**Note:** --user is only needed if `password_protected` is True
 
+### Get Kalliope's version
+
+Normal response codes: 200
+Error response codes: unauthorized(401)
+Curl command:
+```bash
+curl -i --user admin:secret -X GET  http://localhost:5000/
+```
+Output example:
+```JSON
+{
+  "Kalliope version": "0.4.2"
+}
+```
+
 ### List synapses
 
 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/
+curl -i --user admin:secret -X GET  http://localhost:5000/synapses
 ```
 
 Output example:
@@ -107,7 +124,7 @@ 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
+curl -i --user admin:secret -X POST  http://localhost:5000/synapses/start/id/say-hello
 ```
 
 Output example:
@@ -141,7 +158,7 @@ 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/
+curl -i --user admin:secret -H "Content-Type: application/json" -X POST -d '{"order":"my order"}' http://localhost:5000/synapses/start/order
 ```
 
 If the order contains accent or quotes, use a file for testing with curl
@@ -151,7 +168,7 @@ cat post.json
 ```
 Then
 ```bash
-curl -i --user admin:secret -H "Content-Type: application/json" -X POST --data @post.json http://localhost:5000/order/
+curl -i --user admin:secret -H "Content-Type: application/json" -X POST --data @post.json http://localhost:5000/synapses/start/order
 ```
 
 Output example if the order have matched and so launched synapses:
@@ -176,7 +193,50 @@ Output example if the order have matched and so launched synapses:
 }
 ```
 
-If the order haven't match ny synapses:
+If the order haven't match any synapses:
+```JSON
+{
+  "error": {
+    "error": "The given order doesn't match any synapses"
+  }
+}
+```
+
+### Run a synapse from an audio file
+
+Normal response codes: 201
+Error response codes: unauthorized(401), itemNotFound(404)
+
+The audio file must use WAV or MP3 extension.
+
+Curl command:
+```bash
+curl -i --user admin:secret -X POST  http://localhost:5000/synapses/start/audio -F "file=@/home/nico/Desktop/input.wav"
+```
+
+Output example if the order inside the audio have matched and so launched synapses:
+```JSON
+{
+  "synapses": [
+    {
+      "name": "Say-hello", 
+      "neurons": [
+        {
+          "name": "say", 
+          "parameters": "{'message': ['Hello sir']}"
+        }
+      ], 
+      "signals": [
+        {
+          "order": "hello"
+        }
+      ]
+    }
+  ]
+}
+```
+
+If the order haven't match any synapses:
 ```JSON
 {
   "error": {

+ 37 - 3
Docs/settings.md

@@ -104,8 +104,8 @@ E.g
 text_to_speech:
   - pico2wave:
       language: "fr-FR"
-  - voxygen:
-      voice: "michel"
+  - googletts:
+      language: "fr"
 ```
 
 Some arguments are required, some other optional, please refer to the [TTS documentation](tts.md) to know available parameters for each supported TTS.
@@ -259,7 +259,7 @@ Remember that an origin is composed of the scheme (http(s)), the port (eg: 80, 4
 
 ## 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 or if the SST engine haven't understood the order.
 
 ```yml
 default_synapse: "synapse-name"
@@ -289,5 +289,39 @@ resource_directory:
   trigger: "/full/path/to/trigger"
 ```
 
+
+## Global Variables
+
+The Global Variables paths list where to load the global variables.
+Those variables can be reused in neuron parameters within double brackets.
+
+E.g 
+```yml
+var_files:
+  - variables.yml
+  - variables2.yml
+```
+/!\ If a variable is defined in different files, the last file defines the value.
+
+In the files the variables are defined by key/value:
+```yml
+variable: 60
+baseURL: "http://blabla.com/"
+password: "secret"
+```
+
+And use variables in your neurons:
+/!\ Because YAML format does no allow double braces not surrounded by quotes: you must use the variable between double quotes. 
+```yml
+  - name: "run-simple-sleep"
+    signals:
+      - order: "Wait for me "
+    neurons:
+      - uri:
+          url: "{{baseURL}}get/1"        
+          user: "admin"
+          password: "{{password}}"
+```
+
 ## Next: configure the brain of Kalliope
 Now your settings are ok, you can start creating the [brain](brain.md) of your assistant.

+ 0 - 3
Docs/tts.md

@@ -75,9 +75,6 @@ text_to_speech:
   - pico2wave:
       language: "fr-FR"
       cache: True
-  - voxygen:
-      voice: "Agnes"
-      cache: True
   - acapela:
       language: "sonid15"
       voice: "Manon"

+ 1 - 1
Docs/tts_list.md

@@ -12,7 +12,7 @@ Core TTSs are already packaged with the installation of Kalliope an can be used
 | 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 |
+| ~~Voxygen~~ | ~~[Voxygen](../kalliope/tts/voxygen/README.md)~~ |~~Cloud based~~|
 
 ## Community TTS
 Community TTSs need to be installed manually.

+ 2 - 2
README.md

@@ -46,12 +46,12 @@ Once installed, you can start learning basics of Kalliope from a [quick start co
 | [Settings](Docs/settings.md)       | The main Kalliope configuration                                                             |
 | [Brain](Docs/brain.md)             | What is the brain and how to create your own bot                                            |
 | [neuron](Docs/neurons.md)          | What is a neuron and how to use it                                                          |
-| [neuron list](Docs/neuron_list.md) | List of availlable neurons                                                                  |
+| [neuron list](Docs/neuron_list.md) | List of available neurons                                                                   |
 | [CLI](Docs/kalliope_cli.md)        | How to use Kalliope from the command line interface                                         |
 | [Signals](Docs/signals.md)         | Signals are input event that can wake up kalliope (spoken order, scheduled event, REST API) |
 | [STT](Docs/stt.md)                 | Speech to text configuration                                                                |
 | [TTS](Docs/tts.md)                 | Text to speech configuration                                                                |
-| [Triggers](Docs/trigger.md)        | Magic hotword engine used to make Kalliope listtening for an order                          |
+| [Triggers](Docs/trigger.md)        | Magic hotword engine used to make Kalliope listening for an order                           |
 | [REST API](Docs/rest_api.md)       | Integrated REST API. Can be used to send an order                                           |
 
 ## Contributing

+ 19 - 0
Tests/brains/brain_test_api.yml

@@ -0,0 +1,19 @@
+---
+  - name: "test"
+    signals:
+      - order: "test_order"
+    neurons:
+      - say:
+          message:
+            - "test message"
+
+  - name: "test2"
+    signals:
+      - order: "bonjour"
+    neurons:
+      - say:
+          message:
+            - "test message"
+  
+  - includes:
+    - included_brain_test.yml

BIN
Tests/files/bonjour.wav


+ 7 - 0
Tests/settings/settings_test.yml

@@ -110,3 +110,10 @@ resource_directory:
   stt: "/tmp/kalliope/tests/kalliope_resources_dir/stt"
   tts: "/tmp/kalliope/tests/kalliope_resources_dir/tts"
   trigger: "/tmp/kalliope/tests/kalliope_resources_dir/trigger"
+
+# ---------------------------
+# Global files variables
+# /!\ If a variable is defined in different files, the last file defines the value.
+# ---------------------------
+var_files:
+  - "../Tests/settings/variables.yml"

+ 3 - 0
Tests/settings/variables.yml

@@ -0,0 +1,3 @@
+test: kalliope
+test_number: 60
+author: Lamonf

+ 179 - 2
Tests/test_brain_loader.py

@@ -9,12 +9,17 @@ from kalliope.core.Models import Neuron
 from kalliope.core.Models import Synapse
 from kalliope.core.Models import Order
 from kalliope.core.Models.Brain import Brain
+from kalliope.core.Models.Settings import Settings
 
 
 class TestBrainLoader(unittest.TestCase):
 
     def setUp(self):
-        self.brain_to_test = os.getcwd() + os.sep + "Tests/brains/brain_test.yml"
+        if "/Tests" in os.getcwd():
+            self.brain_to_test = os.getcwd() + os.sep + "brains/brain_test.yml"
+        else:
+            self.brain_to_test = os.getcwd() + os.sep + "Tests/brains/brain_test.yml"
+
         self.expected_result = [
             {'signals': [{'order': 'test_order'}],
              'neurons': [{'say': {'message': ['test message']}}],
@@ -63,12 +68,37 @@ class TestBrainLoader(unittest.TestCase):
         self.assertEqual(brain, brain_loader.brain)
 
     def test_get_neurons(self):
+        """
+        Test to get neurons from the brainLoader
+        scenarii:
+            - 1/ get a simple neuron from the brainloader
+            - 2/ get a neuron with global variables as parameters
+        """
+        # 1/ get a simple neuron from the brainloader
+        st = Settings()
         neuron_list = [{'say': {'message': ['test message']}}]
 
         neuron = Neuron(name='say', parameters={'message': ['test message']})
 
         bl = BrainLoader(file_path=self.brain_to_test)
-        neurons_from_brain_loader = bl._get_neurons(neuron_list)
+        neurons_from_brain_loader = bl._get_neurons(neuron_list,
+                                                    settings=st)
+
+        self.assertEqual([neuron], neurons_from_brain_loader)
+
+        # 2/ get a neuron with global variables as parameters
+        neuron_list = [{'say': {'message': ['bonjour {{name}}']}}]
+        variables = {
+            "author": "Lamonf",
+            "test_number": 60,
+            "name": "kalliope"
+        }
+        st = Settings(variables=variables)
+        bl = BrainLoader(file_path=self.brain_to_test)
+        neurons_from_brain_loader = bl._get_neurons(neuron_list,
+                                                    settings=st)
+
+        neuron = Neuron(name='say', parameters={'message': ['bonjour kalliope']})
 
         self.assertEqual([neuron], neurons_from_brain_loader)
 
@@ -103,5 +133,152 @@ class TestBrainLoader(unittest.TestCase):
 
         self.assertTrue(bl1.brain is bl2.brain)
 
+    def test_replace_global_variables(self):
+        """
+        Testing the _replace_global_variables function from the NeuronLauncher.
+        Scenarii:
+            - 1/ only one global variable
+            - 2/ global variable with string after
+            - 3/ global variable with int after
+            - 4/ multiple global variables
+            - 5/ parameter value is a list
+            - 6/ parameter is a dict
+
+        """
+
+        # 1/ only one global variable
+        parameters = {
+            'var1': '{{hello}}'
+        }
+
+        variables = {
+            "hello": "test",
+            "hello2": "test2",
+        }
+        st = Settings(variables=variables)
+
+        expected_parameters = {
+            'var1': 'test'
+        }
+
+        self.assertEquals(BrainLoader._replace_global_variables(parameter=parameters,
+                                                                settings=st),
+                          expected_parameters,
+                          "Fail to assign a single global variable to parameters")
+
+        # 2/ global variable with string after
+        parameters = {
+            'var1': '{{hello}} Sispheor'
+        }
+        variables = {
+            "hello": "test",
+            "hello2": "test2",
+        }
+        st = Settings(variables=variables)
+
+        expected_parameters = {
+            'var1': 'test Sispheor'
+        }
+
+        self.assertEquals(BrainLoader._replace_global_variables(parameter=parameters,
+                                                                settings=st),
+                          expected_parameters,
+                          "Fail to assign a global variable with string after to parameters")
+
+        # 3/ global variable with int after
+        parameters = {
+            'var1': '{{hello}}0'
+        }
+        variables = {
+            "hello": 60,
+            "hello2": "test2",
+        }
+        st = Settings(variables=variables)
+
+        expected_parameters = {
+            'var1': '600'
+        }
+
+        self.assertEquals(BrainLoader._replace_global_variables(parameter=parameters,
+                                                                settings=st),
+                          expected_parameters,
+                          "Fail to assign global variable with int after to parameters")
+
+        # 4/ multiple global variables
+        parameters = {
+            'var1': '{{hello}} {{me}}'
+        }
+        variables = {
+            "hello": "hello",
+            "me": "LaMonf"
+        }
+        st = Settings(variables=variables)
+
+        expected_parameters = {
+            'var1': 'hello LaMonf'
+        }
+
+        self.assertEquals(BrainLoader._replace_global_variables(parameter=parameters,
+                                                                settings=st),
+                          expected_parameters,
+                          "Fail to assign multiple global variables to parameters")
+
+        # 5/ parameter value is a list
+        parameters = {
+            'var1': '[hello {{name}}, bonjour {{name}}]'
+        }
+        variables = {
+            "name": "LaMonf",
+            "hello2": "test2",
+        }
+        st = Settings(variables=variables)
+
+        expected_parameters = {
+            'var1': '[hello LaMonf, bonjour LaMonf]'
+        }
+
+        self.assertEquals(BrainLoader._replace_global_variables(parameter=parameters,
+                                                                settings=st),
+                          expected_parameters,
+                          "Fail to assign a single global when parameter value is a list to neuron")
+
+        # 6/ parameter is a dict
+        parameters = {'from_answer_link': [{'synapse': 'synapse2', 'answers': ['absolument', '{{ name }}']},
+                                           {'synapse': 'synapse3', 'answers': ['{{ name }}']}], 'default': 'synapse4'}
+
+        variables = {
+            "name": "nico"
+        }
+        st = Settings(variables=variables)
+
+        expected_parameters = {
+            'from_answer_link': [
+                {'synapse': 'synapse2', 'answers': ['absolument', 'nico']},
+                {'synapse': 'synapse3', 'answers': ['nico']}], 'default': 'synapse4'
+        }
+
+        self.assertEquals(BrainLoader._replace_global_variables(parameter=parameters,
+                                                                settings=st),
+                          expected_parameters,
+                          "Fail to assign a single global when parameter value is a list to neuron")
+
+    def test_get_global_variable(self):
+        """
+        Test the get_global_variable of the OrderAnalyser Class
+        """
+        sentence = "i am {{name2}}"
+        variables = {
+            "name": "LaMonf",
+            "name2": "kalliope",
+        }
+        st = Settings(variables=variables)
+
+        expected_result = "i am kalliope"
+
+        self.assertEquals(BrainLoader._get_global_variable(sentence=sentence,
+                                                           settings=st),
+                          expected_result,
+                          "Fail to get the global variable from the sentence")
+
 if __name__ == '__main__':
     unittest.main()

+ 2 - 0
Tests/test_launchers.py

@@ -6,6 +6,7 @@ from kalliope.core.NeuronLauncher import NeuronLauncher
 from kalliope.core.SynapseLauncher import SynapseLauncher, SynapseNameNotFound
 from kalliope.core.TriggerLauncher import TriggerLauncher
 from kalliope.core.ConfigurationManager import SettingLoader
+from kalliope.core.Models.Settings import Settings
 
 from kalliope.core.Models.Trigger import Trigger
 from kalliope.core.Models.Neuron import Neuron
@@ -136,3 +137,4 @@ class TestLaunchers(unittest.TestCase):
                                                                  resources_dir=sl.settings.resources.neuron_folder)
             mock_get_class_instantiation.reset_mock()
 
+

+ 40 - 0
Tests/test_neuron_model.py

@@ -0,0 +1,40 @@
+import os
+import unittest
+
+from kalliope.core.Models import Neuron
+
+
+class TestNeuronModule(unittest.TestCase):
+
+    def test_password_parameter(self):
+        neuron_name = "test"
+        neuron_parameters = {
+            "password": "my secret",
+            "parameter": "test"
+        }
+
+        neuron = Neuron()
+        neuron.name = neuron_name
+        neuron.parameters = neuron_parameters
+
+        expected_result = "Neuron: name: test, parameters: {'password': '*****', 'parameter': 'test'}"
+
+        self.assertEqual(neuron.__str__(), expected_result)
+
+    def test_password_in_parameter(self):
+        neuron_name = "test"
+        neuron_parameters = {
+            "password_parameter": "my secret",
+            "parameter": "test"
+        }
+
+        neuron = Neuron()
+        neuron.name = neuron_name
+        neuron.parameters = neuron_parameters
+
+        expected_result = "Neuron: name: test, parameters: {'parameter': 'test', 'password_parameter': '*****'}"
+
+        self.assertEqual(neuron.__str__(), expected_result)
+
+if __name__ == '__main__':
+    unittest.main()

+ 2 - 1
Tests/test_neuron_module.py

@@ -36,7 +36,8 @@ class TestNeuronModule(unittest.TestCase):
             with mock.patch("kalliope.core.OrderListener.join") as mock_orderListener_join:
                 def callback():
                     pass
-                NeuronModule.get_audio_from_stt(callback=callback())
+
+                self.neuron_module_test.get_audio_from_stt(callback=callback())
                 mock_orderListener_start.assert_called_once_with()
                 mock_orderListener_start.reset_mock()
 

+ 9 - 55
Tests/test_order_analyser.py

@@ -160,51 +160,6 @@ class TestOrderAnalyser(unittest.TestCase):
             mock_start_neuron_method.assert_not_called()
             mock_start_neuron_method.reset_mock()
 
-
-    def test_is_containing_bracket(self):
-        #  Success
-        order_to_test = "This test contains {{ bracket }}"
-        self.assertTrue(OrderAnalyser._is_containing_bracket(order_to_test),
-                        "Fail returning True when order contains spaced brackets")
-
-        order_to_test = "This test contains {{bracket }}"
-        self.assertTrue(OrderAnalyser._is_containing_bracket(order_to_test),
-                        "Fail returning True when order contains right spaced bracket")
-
-        order_to_test = "This test contains {{ bracket}}"
-        self.assertTrue(OrderAnalyser._is_containing_bracket(order_to_test),
-                        "Fail returning True when order contains left spaced bracket")
-
-        order_to_test = "This test contains {{bracket}}"
-        self.assertTrue(OrderAnalyser._is_containing_bracket(order_to_test),
-                        "Fail returning True when order contains no spaced bracket")
-
-        #  Failure
-        order_to_test = "This test does not contain bracket"
-        self.assertFalse(OrderAnalyser._is_containing_bracket(order_to_test),
-                         "Fail returning False when order has no brackets")
-
-        #  Behaviour
-        order_to_test = ""
-        self.assertFalse(OrderAnalyser._is_containing_bracket(order_to_test),
-                         "Fail returning False when no order")
-
-    def test_get_next_value_list(self):
-        # Success
-        list_to_test = {1, 2, 3}
-        self.assertEqual(OrderAnalyser._get_next_value_list(list_to_test), 2,
-                         "Fail to match the expected next value from the list")
-
-        # Failure
-        list_to_test = {1}
-        self.assertEqual(OrderAnalyser._get_next_value_list(list_to_test), None,
-                         "Fail to ensure there is no next value from the list")
-
-        # Behaviour
-        list_to_test = {}
-        self.assertEqual(OrderAnalyser._get_next_value_list(list_to_test), None,
-                         "Fail to ensure the empty list return None value")
-
     def test_spelt_order_match_brain_order_via_table(self):
         order_to_test = "this is the order"
         sentence_to_test = "this is the order"
@@ -224,7 +179,6 @@ class TestOrderAnalyser(unittest.TestCase):
                         "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"
@@ -262,7 +216,6 @@ class TestOrderAnalyser(unittest.TestCase):
                          "Fails formatting the sentences with random in both order and sentence")
 
     def test_get_split_order_without_bracket(self):
-
         # Success
         order_to_test = "this is the order"
         expected_result = ["this", "is", "the", "order"]
@@ -565,9 +518,9 @@ class TestOrderAnalyser(unittest.TestCase):
         """
         Test to find the good synapse to run
         Scenarii:
-            - Find the synapse
-            - No synpase found, no default synapse
-            - No synapse found, run the default synapse
+            - 1/ Find the synapse
+            - 2/ No synpase found, no default synapse
+            - 3/ No synapse found, run the default synapse
         """
         # Init
         neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'})
@@ -589,7 +542,7 @@ class TestOrderAnalyser(unittest.TestCase):
 
         br = Brain(synapses=all_synapse_list)
         st = Settings()
-        # Find synapse
+        # 1/ Find synapse
         order = "this is the sentence"
         expected_result = synapse1
         oa_tuple_list = OrderAnalyser._find_synapse_to_run(brain=br,settings=st, order=order)
@@ -599,16 +552,17 @@ class TestOrderAnalyser(unittest.TestCase):
 
         expected_result = signal1.sentence
         self.assertEquals(oa_tuple_list[0].order,
-                        expected_result,
-                        "Fail to run the proper synapse matching the order")
-        # No Default synapse
+                          expected_result,
+                          "Fail to run the proper synapse matching the order")
+
+        # 2/ No Default synapse
         order = "No default synapse"
         expected_result = []
         self.assertEquals(OrderAnalyser._find_synapse_to_run(brain=br,settings=st, order=order),
                           expected_result,
                           "Fail to run no synapse, when no default is defined")
 
-        # Default synapse
+        # 3/ Default synapse
         st = Settings(default_synapse="Synapse2")
         order = "default synapse"
         expected_result = synapse2

+ 53 - 0
Tests/test_order_listener.py

@@ -0,0 +1,53 @@
+import unittest
+
+from mock import mock
+
+from kalliope.core import OrderListener
+from kalliope.core.Models import Resources
+from kalliope.core.Models.Stt import Stt
+
+
+class TestOrderListener(unittest.TestCase):
+
+    """Test case for the OrderListener Class"""
+
+    def setUp(self):
+        pass
+
+    def test_load_stt_plugin(self):
+
+        # Test getting default stt
+        ol = OrderListener()
+
+        stt1 = Stt(name="default-stt",
+                   parameters=dict())
+
+        stt2 = Stt(name="second-stt",
+                   parameters=dict())
+
+        stt3 = Stt(name="third-stt",
+                   parameters=dict())
+
+        resources = Resources(stt_folder="/tmp")
+        ol.settings = mock.MagicMock(default_stt_name="default-stt",
+                                     stts=[stt1,stt2,stt3],
+                                     resources=resources)
+
+        callback = mock.MagicMock()
+
+        ol.callback = callback
+
+        with mock.patch("kalliope.core.Utils.get_dynamic_class_instantiation") as mock_get_dynamic_class_instantiation:
+            mock_get_dynamic_class_instantiation.return_value = 'class_instance'
+            self.assertEquals(ol.load_stt_plugin(),
+                              "class_instance",
+                              "Fail getting the proper value")
+
+            mock_get_dynamic_class_instantiation.assert_called_once_with(package_name= "stt",
+                                                                         module_name= "Default-stt",
+                                                                         parameters={'callback' : callback},
+                                                                         resources_dir= "/tmp")
+
+
+if __name__ == '__main__':
+    unittest.main()

+ 2 - 1
Tests/test_resources_manager.py

@@ -147,8 +147,9 @@ class TestResourcesmanager(unittest.TestCase):
                                                                   supported_versions=supported_version))
 
         # version non ok, user does not config
+        # Testing with integer values instead of string
         current_version = '0.4.0'
-        supported_version = ['0.3', '0.2']
+        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,

+ 244 - 183
Tests/test_rest_api.py

@@ -1,11 +1,14 @@
+import json
 import os
 import unittest
 
+from werkzeug.datastructures import FileStorage
+
 from flask import Flask
 from flask_testing import LiveServerTestCase
 
 from kalliope.core.Models import Singleton
-
+from kalliope._version import version_str
 from kalliope.core.ConfigurationManager import BrainLoader
 from kalliope.core.ConfigurationManager import SettingLoader
 from kalliope.core.RestAPI.FlaskAPI import FlaskAPI
@@ -20,8 +23,12 @@ class TestRestAPI(LiveServerTestCase):
         # be sure that the singleton haven't been loaded before
         Singleton._instances = {}
         current_path = os.getcwd()
-        full_path_brain_to_test = current_path + os.sep + "Tests/brains/brain_test.yml"
-        print full_path_brain_to_test
+        if "/Tests" in os.getcwd():
+            full_path_brain_to_test = current_path + os.sep + "brains/brain_test_api.yml"
+            self.audio_file = "files/bonjour.wav"
+        else:
+            full_path_brain_to_test = current_path + os.sep + "Tests/brains/brain_test_api.yml"
+            self.audio_file = "Tests/files/bonjour.wav"
 
         # rest api config
         sl = SettingLoader()
@@ -36,196 +43,250 @@ class TestRestAPI(LiveServerTestCase):
         brain = brain_loader.brain
 
         self.app = Flask(__name__)
+        self.app.config['TESTING'] = True
         self.flask_api = FlaskAPI(self.app, port=5000, brain=brain)
-        self.flask_api.app.config['TESTING'] = True
+        self.client = self.app.test_client()
         return self.flask_api.app
 
-    # TODO all following test passes with 'python -m unittest Tests.TestRestAPI' but not with discover
-    # def test_get_all_synapses(self):
-    #     url = "http://127.0.0.1:5000/synapses"
-    #
-    #     result = requests.get(url=url)
-    #     expected_content = {
-    #         "synapses": [
-    #             {
-    #                 "name": "test",
-    #                 "neurons": [
-    #                     {
-    #                         "say": {
-    #                             "message": [
-    #                                 "test message"
-    #                             ]
-    #                         }
-    #                     }
-    #                 ],
-    #                 "signals": [
-    #                     {
-    #                         "order": "test_order"
-    #                     }
-    #                 ]
-    #             },
-    #             {
-    #                 "name": "test2",
-    #                 "neurons": [
-    #                     {
-    #                         "say": {
-    #                             "message": [
-    #                                 "test message"
-    #                             ]
-    #                         }
-    #                     }
-    #                 ],
-    #                 "signals": [
-    #                     {
-    #                         "order": "test_order_2"
-    #                     }
-    #                 ]
-    #             },
-    #             {
-    #                 "includes": [
-    #                     "included_brain_test.yml"
-    #                 ]
-    #             },
-    #             {
-    #                 "name": "test3",
-    #                 "neurons": [
-    #                     {
-    #                         "say": {
-    #                             "message": [
-    #                                 "test message"
-    #                             ]
-    #                         }
-    #                     }
-    #                 ],
-    #                 "signals": [
-    #                     {
-    #                         "order": "test_order_3"
-    #                     }
-    #                 ]
-    #             }
-    #         ]
-    #     }
-    #
-    #     self.assertEqual(result.status_code, 200)
-    #     self.assertEqual(expected_content, json.loads(result.content))
-    #
-    # def test_get_one_synapse(self):
-    #     url = "http://127.0.0.1:5000/synapses/test"
-    #     result = requests.get(url=url)
-    #
-    #     expected_content = {
-    #         "synapses": {
-    #             "name": "test",
-    #             "neurons": [
-    #                 {
-    #                     "say": {
-    #                         "message": [
-    #                             "test message"
-    #                         ]
-    #                     }
-    #                 }
-    #             ],
-    #             "signals": [
-    #                 {
-    #                     "order": "test_order"
-    #                 }
-    #             ]
-    #         }
-    #     }
-    #
-    #     self.assertEqual(expected_content, json.loads(result.content))
-    #
-    # def test_get_synapse_not_found(self):
-    #     url = "http://127.0.0.1:5000/synapses/test-none"
-    #     result = requests.get(url=url)
-    #
-    #     expected_content = {
-    #         "error": {
-    #             "synapse name not found": "test-none"
+    def test_server_is_up_and_running(self):
+        # response = urllib2.urlopen(self.get_server_url())
+        response = self.client.get(self.get_server_url())
+        self.assertEqual(response.status_code, 200)
+
+    def test_get_main_page(self):
+        url = self.get_server_url() + "/"
+        response = self.client.get(url)
+        expected_content = {
+            "Kalliope version": "%s" % version_str
+        }
+        self.assertEqual(json.dumps(expected_content), json.dumps(json.loads(response.get_data())))
+
+    def test_get_all_synapses(self):
+        url = self.get_server_url()+"/synapses"
+
+        response = self.client.get(url)
+        expected_content = {
+            "synapses": [
+                {
+                    "name": "test",
+                    "neurons": [
+                        {
+                            "name": "say",
+                            "parameters": {
+                                "message": [
+                                    "test message"
+                                ]
+                            }
+                        }
+                    ],
+                    "signals": [
+                        {
+                            "order": "test_order"
+                        }
+                    ]
+                },
+                {
+                    "name": "test2",
+                    "neurons": [
+                        {
+                            "name": "say",
+                            "parameters": {
+                                "message": [
+                                    "test message"
+                                ]
+                            }
+                        }
+                    ],
+                    "signals": [
+                        {
+                            "order": "bonjour"
+                        }
+                    ]
+                },
+                {
+                    "name": "test3",
+                    "neurons": [
+                        {
+                            "name": "say",
+                            "parameters": {
+                                "message": [
+                                    "test message"
+                                ]
+                            }
+                        }
+                    ],
+                    "signals": [
+                        {
+                            "order": "test_order_3"
+                        }
+                    ]
+                }
+            ]
+        }
+        # a lot of char ti process
+        self.maxDiff = None
+        self.assertEqual(response.status_code, 200)
+        # print response.get_data()
+        # print json.dumps(expected_content)
+        self.assertEqual(json.dumps(expected_content), json.dumps(json.loads(response.get_data())))
+
+    def test_get_one_synapse(self):
+        url = self.get_server_url() + "/synapses/test"
+        response = self.client.get(url)
+
+        expected_content = {
+            "synapses": {
+                "name": "test",
+                "neurons": [
+                    {
+                        "name": "say",
+                        "parameters": {
+                            "message": [
+                                "test message"
+                            ]
+                        }
+                    }
+                ],
+                "signals": [
+                    {
+                        "order": "test_order"
+                    }
+                ]
+            }
+        }
+        self.assertEqual(json.dumps(expected_content), json.dumps(json.loads(response.get_data())))
+
+    def test_get_synapse_not_found(self):
+        url = self.get_server_url() + "/synapses/test-none"
+        result = self.client.get(url)
+
+        expected_content = {
+            "error": {
+                "synapse name not found": "test-none"
+            }
+        }
+
+        self.assertEqual(expected_content, json.loads(result.get_data()))
+        self.assertEqual(result.status_code, 404)
+
+    def test_run_synapse_by_name(self):
+        url = self.get_server_url() + "/synapses/start/id/test"
+        result = self.client.post(url)
+
+        expected_content = {
+            "synapses": {
+                "name": "test",
+                "neurons": [
+                    {
+                        "name": "say",
+                        "parameters": {
+                            "message": [
+                                "test message"
+                            ]
+                        }
+                    }
+                ],
+                "signals": [
+                    {
+                        "order": "test_order"
+                    }
+                ]
+            }
+        }
+        self.assertEqual(json.dumps(expected_content), json.dumps(json.loads(result.get_data())))
+        self.assertEqual(result.status_code, 201)
+
+    def test_post_synapse_not_found(self):
+        url = self.get_server_url() + "/synapses/start/id/test-none"
+        result = self.client.post(url)
+
+        expected_content = {
+            "error": {
+                "synapse name not found": "test-none"
+            }
+        }
+
+        self.assertEqual(json.dumps(expected_content), json.dumps(json.loads(result.get_data())))
+        self.assertEqual(result.status_code, 404)
+
+    def test_run_synapse_with_order(self):
+        url = self.get_server_url() + "/synapses/start/order"
+        headers = {"Content-Type": "application/json"}
+        data = {"order": "test_order"}
+        result = self.client.post(url, headers=headers, data=json.dumps(data))
+
+        expected_content = {
+            "synapses": [
+                {
+                    "name": "test",
+                    "neurons": [
+                        {
+                            "name": "say",
+                            "parameters": {
+                                "message": [
+                                    "test message"
+                                ]
+                            }
+                        }
+                    ],
+                    "signals": [
+                        {
+                            "order": "test_order"
+                        }
+                    ]
+                }
+            ]
+        }
+        print result.get_data()
+        self.assertEqual(json.dumps(expected_content), json.dumps(json.loads(result.get_data())))
+        self.assertEqual(result.status_code, 201)
+
+    def test_post_synapse_by_order_not_found(self):
+        url = self.get_server_url() + "/synapses/start/order"
+        data = {"order": "non existing order"}
+        headers = {"Content-Type": "application/json"}
+        result = self.client.post(url, headers=headers, data=json.dumps(data))
+
+        expected_content = {'error': {'error': "The given order doesn't match any synapses"}}
+
+        self.assertEqual(json.dumps(expected_content), json.dumps(json.loads(result.get_data())))
+        self.assertEqual(result.status_code, 400)
+
+    # TODO this doesn't work on travis but works locally with python -m unittest discover
+    # def test_post_synapse_by_audio(self):
+    #     url = self.get_server_url() + "/synapses/start/audio"
+    #     with open(os.path.join(self.audio_file), 'rb') as fp:
+    #         file = FileStorage(fp)
+    #         data = {
+    #             'file': file
     #         }
-    #     }
+    #         result = self.client.post(url, data=data, content_type='multipart/form-data')
     #
-    #     self.assertEqual(expected_content, json.loads(result.content))
-    #     self.assertEqual(result.status_code, 404)
-    #
-    # def test_run_synapse_by_name(self):
-    #     url = "http://127.0.0.1:5000/synapses/test"
-    #     result = requests.post(url=url)
-    #
-    #     expected_content = {
-    #         "synapses": {
-    #             "name": "test",
-    #             "neurons": [
-    #                 {
-    #                     "say": {
-    #                         "message": [
-    #                             "test message"
-    #                         ]
-    #                     }
-    #                 }
-    #             ],
-    #             "signals": [
+    #         expected_content = {
+    #             "synapses": [
     #                 {
-    #                     "order": "test_order"
+    #                     "name": "test2",
+    #                     "neurons": [
+    #                         {
+    #                             "name": "say",
+    #                             "parameters": {
+    #                                 "message": [
+    #                                     "test message"
+    #                                 ]
+    #                             }
+    #                         }
+    #                     ],
+    #                     "signals": [
+    #                         {
+    #                             "order": "bonjour"
+    #                         }
+    #                     ]
     #                 }
     #             ]
     #         }
-    #     }
-    #
-    #     self.assertEqual(expected_content, json.loads(result.content))
-    #     self.assertEqual(result.status_code, 201)
-    #
-    # def test_post_synapse_not_found(self):
-    #     url = "http://127.0.0.1:5000/synapses/test-none"
-    #     result = requests.post(url=url)
-    #
-    #     expected_content = {
-    #         "error": {
-    #             "synapse name not found": "test-none"
-    #         }
-    #     }
-    #
-    #     self.assertEqual(expected_content, json.loads(result.content))
-    #     self.assertEqual(result.status_code, 404)
-    #
-    # def test_run_synapse_with_order(self):
-    #     url = "http://127.0.0.1:5000/order/"
-    #     headers = {"Content-Type": "application/json"}
-    #     data = {"order": "test_order"}
-    #     result = requests.post(url=url, headers=headers, json=data)
-    #
-    #     expected_content = {
-    #         "synapses": [
-    #             {
-    #                 "name": "test",
-    #                 "neurons": [
-    #                     {
-    #                         "name": "say",
-    #                         "parameters": "{'message': ['test message']}"
-    #                     }
-    #                 ],
-    #                 "signals": [
-    #                     {
-    #                         "order": "test_order"
-    #                     }
-    #                 ]
-    #             }
-    #         ]
-    #     }
-    #
-    #     self.assertEqual(expected_content, json.loads(result.content))
-    #     self.assertEqual(result.status_code, 201)
-    #
-    # def test_post_synapse_by_order_not_found(self):
-    #     url = "http://127.0.0.1:5000/order/"
-    #     data = {"order": "non existing order"}
-    #     headers = {"Content-Type": "application/json"}
-    #     result = requests.post(url=url, headers=headers, json=data)
-    #
-    #     expected_content = {'error': {'error': "The given order doesn't match any synapses"}}
     #
-    #     self.assertEqual(expected_content, json.loads(result.content))
-    #     self.assertEqual(result.status_code, 400)
+    #         self.assertEqual(json.dumps(expected_content), json.dumps(json.loads(result.get_data())))
+    #         self.assertEqual(result.status_code, 201)
 
 if __name__ == '__main__':
     unittest.main()

+ 17 - 3
Tests/test_settings_loader.py

@@ -51,10 +51,10 @@ class TestSettingLoader(unittest.TestCase):
             'text_to_speech': [
                 {'pico2wave': {'cache': True, 'language': 'fr-FR'}},
                 {'voxygen': {'voice': 'Agnes', 'cache': True}}
-            ]
+            ],
+            'var_files': ["../Tests/settings/variables.yml"]
         }
 
-
         # 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")
@@ -105,8 +105,12 @@ class TestSettingLoader(unittest.TestCase):
                               stt_folder="/tmp/kalliope/tests/kalliope_resources_dir/stt",
                               tts_folder="/tmp/kalliope/tests/kalliope_resources_dir/tts",
                               trigger_folder="/tmp/kalliope/tests/kalliope_resources_dir/trigger")
-
         settings_object.resources = resources
+        settings_object.variables = {
+            "author": "Lamonf",
+            "test_number": 60,
+            "test": "kalliope"
+        }
         settings_object.machine = platform.machine()
 
         sl = SettingLoader(file_path=self.settings_file_to_test)
@@ -189,6 +193,16 @@ class TestSettingLoader(unittest.TestCase):
         sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEquals(expected_resource, sl._get_resources(self.settings_dict))
 
+    def test_get_variables(self):
+        expected_result = {
+            "author": "Lamonf",
+            "test_number": 60,
+            "test": "kalliope"
+        }
+        sl = SettingLoader(file_path=self.settings_file_to_test)
+        self.assertEqual(expected_result,
+                         sl._get_variables(self.settings_dict))
+
 
 if __name__ == '__main__':
     unittest.main()

+ 89 - 4
Tests/test_utils.py

@@ -1,15 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
 import unittest
 import os
-import mock
 
 from kalliope.core.Models.Neuron import Neuron
-from kalliope.core.Models.Order import Order
-from kalliope.core.Models.Synapse import Synapse
 from kalliope.neurons.say.say import Say
 from kalliope.core.Utils.Utils import Utils
 
 from kalliope.core.ConfigurationManager import SettingLoader
-from kalliope.core.ConfigurationManager import BrainLoader
 
 
 class TestUtils(unittest.TestCase):
@@ -138,3 +137,89 @@ class TestUtils(unittest.TestCase):
                                    Say),
                         "Fail instantiate a class")
 
+    def test_is_containing_bracket(self):
+        #  Success
+        order_to_test = "This test contains {{ bracket }}"
+        self.assertTrue(Utils.is_containing_bracket(order_to_test),
+                        "Fail returning True when order contains spaced brackets")
+
+        order_to_test = "This test contains {{bracket }}"
+        self.assertTrue(Utils.is_containing_bracket(order_to_test),
+                        "Fail returning True when order contains right spaced bracket")
+
+        order_to_test = "This test contains {{ bracket}}"
+        self.assertTrue(Utils.is_containing_bracket(order_to_test),
+                        "Fail returning True when order contains left spaced bracket")
+
+        order_to_test = "This test contains {{bracket}}"
+        self.assertTrue(Utils.is_containing_bracket(order_to_test),
+                        "Fail returning True when order contains no spaced bracket")
+
+        # Failure
+        order_to_test = "This test does not contain bracket"
+        self.assertFalse(Utils.is_containing_bracket(order_to_test),
+                         "Fail returning False when order has no brackets")
+
+        # Behaviour
+        order_to_test = ""
+        self.assertFalse(Utils.is_containing_bracket(order_to_test),
+                         "Fail returning False when no order")
+
+        # Behaviour int
+        order_to_test = 6
+        self.assertFalse(Utils.is_containing_bracket(order_to_test),
+                         "Fail returning False when an int")
+
+        # Behaviour unicode
+        order_to_test = "j'aime les goûters l'été"
+        self.assertFalse(Utils.is_containing_bracket(order_to_test),
+                         "Fail returning False when an int")
+
+    def test_get_next_value_list(self):
+        # Success
+        list_to_test = {1, 2, 3}
+        self.assertEqual(Utils.get_next_value_list(list_to_test), 2,
+                         "Fail to match the expected next value from the list")
+
+        # Failure
+        list_to_test = {1}
+        self.assertEqual(Utils.get_next_value_list(list_to_test), None,
+                         "Fail to ensure there is no next value from the list")
+
+        # Behaviour
+        list_to_test = {}
+        self.assertEqual(Utils.get_next_value_list(list_to_test), None,
+                         "Fail to ensure the empty list return None value")
+
+    def test_find_all_matching_brackets(self):
+        """
+        Test the Utils find all matching brackets
+        """
+        sentence = "This is the {{bracket}}"
+        expected_result = ["{{bracket}}"]
+        self.assertEqual(Utils.find_all_matching_brackets(sentence=sentence),
+                         expected_result,
+                         "Fail to match one bracket")
+
+        sentence = "This is the {{bracket}} {{second}}"
+        expected_result = ["{{bracket}}", "{{second}}"]
+        self.assertEqual(Utils.find_all_matching_brackets(sentence=sentence),
+                         expected_result,
+                         "Fail to match two brackets")
+
+    def test_remove_spaces_in_brackets(self):
+        """
+        Test the Utils remove_spaces_in_brackets
+        """
+
+        sentence = "This is the {{ bracket   }}"
+        expected_result = "This is the {{bracket}}"
+        self.assertEqual(Utils.remove_spaces_in_brackets(sentence=sentence),
+                         expected_result,
+                         "Fail to remove spaces in one bracket")
+
+        sentence = "This is the {{ bracket   }} {{  second     }}"
+        expected_result = "This is the {{bracket}} {{second}}"
+        self.assertEqual(Utils.remove_spaces_in_brackets(sentence=sentence),
+                         expected_result,
+                         "Fail to remove spaces in two brackets")

+ 1 - 1
brain_examples/systemdate.yml

@@ -5,7 +5,7 @@
       - order: "quelle heure est-il"
     neurons:
       - systemdate:
-          tts: "voxygen"
+          tts: "acapela"
           say_template:
             - "il est {{ hours }} heure et {{ minutes }} minute"
 

+ 1 - 1
install/files/python_requirements.txt

@@ -14,7 +14,7 @@ flask_cors==3.0.2
 requests==2.12.4
 httpretty==0.8.14
 mock==2.0.0
-Flask-Testing==0.6.1
+Flask-Testing>=0.6.1
 apscheduler==3.3.0
 GitPython==2.1.1
 packaging>=16.8

+ 58 - 25
kalliope/__init__.py

@@ -15,6 +15,7 @@ import sys
 
 from kalliope.core.ResourcesManager import ResourcesManager
 from kalliope.core.SynapseLauncher import SynapseLauncher
+from kalliope.core.OrderAnalyser import OrderAnalyser
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
@@ -22,30 +23,39 @@ logger = logging.getLogger("kalliope")
 
 def signal_handler(signal, frame):
     """
-    Used to catch a keyboard signal like Ctrl+C in order to kill the kalliope program
+    Used to catch a keyboard signal like Ctrl+C in order to kill the kalliope program.
+
     :param signal: signal handler
     :param frame: execution frame
+
     """
     print "\n"
     Utils.print_info("Ctrl+C pressed. Killing Kalliope")
     sys.exit(0)
 
 # actions available
-ACTION_LIST = ["start", "gui", "install"]
+ACTION_LIST = ["start", "gui", "install", "uninstall"]
 
 
 def main():
-    """
-    Entry point of Kalliope program
-    """
+    """Entry point of Kalliope program."""
+
     # create arguments
     parser = argparse.ArgumentParser(description='Kalliope')
-    parser.add_argument("action", help="[start|gui|install]")
-    parser.add_argument("--run-synapse", help="Name of a synapse to load surrounded by quote")
+    parser.add_argument("action", help="[start|gui|install|uninstall]")
+    parser.add_argument("--run-synapse",
+                        help="Name of a synapse to load surrounded by quote")
+    parser.add_argument("--run-order", help="order surrounded by a quote")
     parser.add_argument("--brain-file", help="Full path of a brain file")
-    parser.add_argument("--debug", action='store_true', help="Show debug output")
+    parser.add_argument("--debug", action='store_true',
+                        help="Show debug output")
     parser.add_argument("--git-url", help="Git URL of the neuron to install")
-    parser.add_argument('-v', '--version', action='version', version='Kalliope ' + version_str)
+    parser.add_argument("--neuron-name", help="Neuron name to uninstall")
+    parser.add_argument("--stt-name", help="STT name to uninstall")
+    parser.add_argument("--tts-name", help="TTS name to uninstall")
+    parser.add_argument("--trigger-name", help="Trigger name to uninstall")
+    parser.add_argument('-v', '--version', action='version',
+                        version='Kalliope ' + version_str)
 
     # parse arguments from script parameters
     args = parser.parse_args()
@@ -60,26 +70,57 @@ def main():
 
     logger.debug("kalliope args: %s" % args)
 
-    # by default, no brain file is set. Use the default one: brain.yml in the root path
+    # by default, no brain file is set.
+    # Use the default one: brain.yml in the root path
     brain_file = None
+
     # check if user set a brain.yml file
     if args.brain_file:
         brain_file = args.brain_file
-    # load the brain once
-    brain_loader = BrainLoader(file_path=brain_file)
-    brain = brain_loader.brain
 
     # check the user provide a valid action
     if args.action not in ACTION_LIST:
         Utils.print_warning("%s is not a recognised action\n" % args.action)
         parser.print_help()
 
+    # install modules
+    if args.action == "install":
+        if not args.git_url:
+            Utils.print_danger("You must specify the git url")
+        else:
+            parameters = {
+                "git_url": args.git_url
+            }
+            res_manager = ResourcesManager(**parameters)
+            res_manager.install()
+        return
+
+    # uninstall modules
+    if args.action == "uninstall":
+        if not args.neuron_name and not args.stt_name and not args.tts_name and not args.trigger_name:
+            Utils.print_danger("You must specify a module name with --neuron-name or --stt-name or --tts-name "
+                               "or --trigger-name")
+        else:
+            res_manager = ResourcesManager()
+            res_manager.uninstall(neuron_name=args.neuron_name, stt_name=args.stt_name,
+                                  tts_name=args.tts_name, trigger_name=args.trigger_name)
+        return
+
+    # load the brain once
+    brain_loader = BrainLoader(file_path=brain_file)
+    brain = brain_loader.brain
+
     if args.action == "start":
+
         # user set a synapse to start
         if args.run_synapse is not None:
             SynapseLauncher.start_synapse(args.run_synapse, brain=brain)
 
-        if args.run_synapse is None:
+        if args.run_order is not None:
+            order_analyser = OrderAnalyser(args.run_order, brain=brain)
+            order_analyser.start()
+
+        if (args.run_synapse is None) and (args.run_order is None):
             # first, load events in event manager
             EventManager(brain.synapses)
             Utils.print_success("Events loaded")
@@ -94,21 +135,13 @@ def main():
     if args.action == "gui":
         ShellGui(brain=brain)
 
-    if args.action == "install":
-        if not args.git_url:
-            Utils.print_danger("You must specify the git url")
-        else:
-            parameters = {
-                "git_url": args.git_url
-            }
-            res_manager = ResourcesManager(**parameters)
-            res_manager.install()
-
 
 def configure_logging(debug=None):
     """
-    Prepare log folder in current home directory
+    Prepare log folder in current home directory.
+
     :param debug: If true, set the lof level to debug
+
     """
     logger = logging.getLogger("kalliope")
     logger.propagate = False

+ 1 - 1
kalliope/_version.py

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

+ 12 - 0
kalliope/brain.yml

@@ -14,3 +14,15 @@
       - say:
           message:
             - "Hello sir"
+
+  - name: "default-synapse"
+    signals:
+      - order: "default-synapse-order"
+    neurons:
+      - say:
+          message:
+            - "Je n'ai pas compris vôtre ordre"
+            - "Je ne connais pas cet ordre"
+            - "Veuillez renouveller votre ordre"
+            - "Veuillez reformuller s'il vous plait"
+            - "Je n'ai pas saisi cet ordre"

+ 60 - 5
kalliope/core/ConfigurationManager/BrainLoader.py

@@ -4,6 +4,7 @@ import os
 
 from YAMLLoader import YAMLLoader
 from kalliope.core.Utils import Utils
+from kalliope.core.ConfigurationManager import SettingLoader
 from kalliope.core.ConfigurationManager.ConfigurationChecker import ConfigurationChecker
 from kalliope.core.Models import Singleton
 from kalliope.core.Models.Brain import Brain
@@ -29,6 +30,9 @@ class BrainLoader(object):
     __metaclass__ = Singleton
 
     def __init__(self, file_path=None):
+        sl = SettingLoader()
+        self.settings = sl.settings
+
         self.file_path = file_path
         if self.file_path is None:  # we don't provide a file path, so search for the default one
             self.file_path = Utils.get_real_file_path(FILE_NAME)
@@ -85,7 +89,7 @@ class BrainLoader(object):
                 if ConfigurationChecker().check_synape_dict(synapses_dict):
                     # print "synapses_dict ok"
                     name = synapses_dict["name"]
-                    neurons = self._get_neurons(synapses_dict["neurons"])
+                    neurons = self._get_neurons(synapses_dict["neurons"], self.settings)
                     signals = self._get_signals(synapses_dict["signals"])
                     new_synapse = Synapse(name=name, neurons=neurons, signals=signals)
                     synapses.append(new_synapse)
@@ -100,13 +104,14 @@ class BrainLoader(object):
 
         return brain
 
-    @staticmethod
-    def _get_neurons(neurons_dict):
+    @classmethod
+    def _get_neurons(cls, neurons_dict, settings):
         """
         Get a list of Neuron object from a neuron dict
 
         :param neurons_dict: Neuron name or dictionary of Neuron_name/Neuron_parameters
         :type neurons_dict: String or dict
+        :param settings:  The Settings with the global variables
         :return: A list of Neurons
         :rtype: List
 
@@ -127,7 +132,11 @@ class BrainLoader(object):
 
                         name = neuron_name
                         parameters = neuron_dict[name]
-                        # print parameters
+
+                        # Update brackets with the global parameter if exist
+                        parameters = cls._replace_global_variables(parameter=parameters,
+                                                                   settings=settings)
+
                         new_neuron = Neuron(name=name, parameters=parameters)
                         neurons.append(new_neuron)
             else:
@@ -234,4 +243,50 @@ class BrainLoader(object):
         second = get_key("second")
 
         return Event(year=year, month=month, day=day, week=week,
-                     day_of_week=day_of_week, hour=hour, minute=minute, second=second)
+                     day_of_week=day_of_week, hour=hour, minute=minute, second=second)
+
+    @classmethod
+    def _replace_global_variables(cls, parameter, settings):
+        """
+        replace a parameter that contains bracket by the instantiated parameter from the var file
+        This function will call itself multiple time to handle different level of parameter in a neuron
+
+        :param parameter: the parameter to update. can be a dict, a list or a string
+        :param settings: the settings
+        :return: the parameter dict
+        """
+
+        if isinstance(parameter, dict):
+            # print "parameter is dict %s" % str(parameter)
+            for key, value in parameter.iteritems():
+                parameter[key] = cls._replace_global_variables(value, settings=settings)
+            return parameter
+        if isinstance(parameter, list):
+            # print "parameter is list %s" % str(parameter)
+            new_parameter_list = list()
+            for el in parameter:
+                new_parameter_list.append(cls._replace_global_variables(el, settings=settings))
+            return new_parameter_list
+        if isinstance(parameter, str) or isinstance(parameter, unicode):
+            # print "parameter is string %s" % parameter
+            if Utils.is_containing_bracket(parameter):
+                return cls._get_global_variable(sentence=parameter, settings=settings)
+            return parameter
+
+    @staticmethod
+    def _get_global_variable(sentence, settings):
+        """
+        Get the global variable from the sentence with brackets
+        :param sentence: the sentence to check
+        :return: the global variable
+        """
+        sentence_no_spaces = Utils.remove_spaces_in_brackets(sentence=sentence)
+        list_of_bracket_params = Utils.find_all_matching_brackets(sentence=sentence_no_spaces)
+        for param_with_bracket in list_of_bracket_params:
+            param_no_brackets = param_with_bracket.replace("{{", "").replace("}}", "")
+            if param_no_brackets in settings.variables:
+                logger.debug("Replacing variable %s with  %s" % (param_with_bracket,
+                                                                 settings.variables[param_no_brackets]))
+                sentence_no_spaces = sentence_no_spaces.replace(param_with_bracket,
+                                                                str(settings.variables[param_no_brackets]))
+        return sentence_no_spaces

+ 24 - 0
kalliope/core/ConfigurationManager/SettingLoader.py

@@ -112,6 +112,7 @@ class SettingLoader(object):
         cache_path = self._get_cache_path(settings)
         default_synapse = self._get_default_synapse(settings)
         resources = self._get_resources(settings)
+        variables = self._get_variables(settings)
 
         # Load the setting singleton with the parameters
         setting_object.default_tts_name = default_tts_name
@@ -129,6 +130,7 @@ class SettingLoader(object):
         setting_object.cache_path = cache_path
         setting_object.default_synapse = default_synapse
         setting_object.resources = resources
+        setting_object.variables = variables
 
         return setting_object
 
@@ -641,5 +643,27 @@ class SettingLoader(object):
 
         return on_ready_sounds
 
+    @staticmethod
+    def _get_variables(settings):
+        """
+        Return the dict of variables from the settings.
+        :param settings: The YAML settings file
+        :return: dict
+        """
+
+        variables = dict()
+        try:
+            variables_files_name = settings["var_files"]
+            # In case files are declared in settings.yml, make sure kalliope can access them.
+            for files in variables_files_name:
+                var = Utils.get_real_file_path(files)
+                if var is None:
+                    raise SettingInvalidException("Variables file %s not found" % files)
+                else:
+                    variables.update(YAMLLoader.get_config(var))
+            return variables
+        except KeyError:
+            # User does not provide this settings
+            return dict()
 
 

+ 9 - 0
kalliope/core/ConfigurationManager/YAMLLoader.py

@@ -14,6 +14,13 @@ class YAMLFileNotFound(Exception):
     pass
 
 
+class YAMLFileEmpty(Exception):
+    """
+    YAML file empty
+    """
+    pass
+
+
 class YAMLLoader:
     """
     Simple Class to Verify / Load a YAML file.
@@ -67,6 +74,8 @@ class IncludeImport(object):
         # load the yaml file
         self.data = yaml.load(open(file_path, 'r'))
 
+        if self.data is None:
+            raise YAMLFileEmpty("File %s is empty" % file_path)
         # add included brain
         if isinstance(self.data, list):
             for el in self.data:

+ 5 - 2
kalliope/core/MainController.py

@@ -3,6 +3,7 @@ import random
 from time import sleep
 
 from flask import Flask
+from kalliope.core.SynapseLauncher import SynapseLauncher
 from transitions import Machine
 
 from kalliope.core import Utils
@@ -55,7 +56,7 @@ class MainController:
         self.on_ready_notification_played_once = False
 
         # Initialize the state machine
-        self.machine = Machine(model=self, states=MainController.states, initial='init')
+        self.machine = Machine(model=self, states=MainController.states, initial='init', queued=True)
 
         # define transitions
         self.machine.add_transition('start_trigger', 'init', 'starting_trigger')
@@ -189,12 +190,14 @@ class MainController:
     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()
+        else:
+            if self.settings.default_synapse is not None:
+                SynapseLauncher.start_synapse(name=self.settings.default_synapse, brain=self.brain)
         # return to the state "unpausing_trigger"
         self.unpause_trigger()
 

+ 15 - 1
kalliope/core/Models/Neuron.py

@@ -22,7 +22,21 @@ class Neuron(object):
         }
 
     def __str__(self):
-        return "Neuron: name: %s, parameters: %s" % (self.name, self.parameters)
+        """
+        Return a string that describe the neuron. If a parameter contains the word "password",
+        the output of this parameter will be masked in order to not appears in clean in the console
+        :return: string description of the neuron
+        """
+        returned_string = str()
+        returned_string += "Neuron: name: %s" % self.name
+        cleaned_parameters = dict()
+        for key, value in self.parameters.iteritems():
+            if "password" in key:
+                cleaned_parameters[key] = "*****"
+            else:
+                cleaned_parameters[key] = value
+        returned_string += ", parameters: %s" % cleaned_parameters
+        return returned_string
 
     def __eq__(self, other):
         """

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

@@ -24,6 +24,7 @@ class Settings(object):
                  cache_path=None,
                  default_synapse=None,
                  resources=None,
+                 variables= None, # dict()
                  machine=None,
                  kalliope_version=None):
 
@@ -42,6 +43,7 @@ class Settings(object):
         self.cache_path = cache_path
         self.default_synapse = default_synapse
         self.resources = resources
+        self.variables = variables
         self.machine = platform.machine()   # can be x86_64 or armv7l
         self.kalliope_version = current_kalliope_version
 

+ 3 - 1
kalliope/core/NeuronLauncher.py

@@ -20,13 +20,15 @@ class NeuronLauncher:
         :type neuron: Neuron
         :return:
         """
-        logger.debug("Run plugin \"%s\" with parameters %s" % (neuron.name, neuron.parameters))
+        logger.debug("Run neuron: \"%s\"" % (neuron.__str__()))
         sl = SettingLoader()
         settings = sl.settings
         neuron_folder = None
         if settings.resources:
             neuron_folder = settings.resources.neuron_folder
+
         return Utils.get_dynamic_class_instantiation(package_name="neurons",
                                                      module_name=neuron.name,
                                                      parameters=neuron.parameters,
                                                      resources_dir=neuron_folder)
+

+ 5 - 2
kalliope/core/NeuronModule.py

@@ -64,7 +64,6 @@ class NeuronModule(object):
         # get the child who called the class
         child_name = self.__class__.__name__
         self.neuron_name = child_name
-        logger.debug("NeuronModule called from class %s with parameters: %s" % (child_name, str(kwargs)))
 
         sl = SettingLoader()
         self.settings = sl.settings
@@ -210,7 +209,8 @@ class NeuronModule(object):
             oa = OrderAnalyser(order=order, brain=self.brain)
             oa.start(synapses_to_run=list_to_run, external_order=order_template)
         else:
-            logger.debug("[NeuronModule]-> run_synapse_by_name_with_order, the synapse has not been found : %s" % synapse_name)
+            logger.debug("[NeuronModule]-> run_synapse_by_name_with_order, the synapse has not been found : %s"
+                         % synapse_name)
         return synapse_to_run is not None
 
     @staticmethod
@@ -246,6 +246,9 @@ class NeuronModule(object):
         ol = OrderListener(callback=callback)
         ol.start()
         ol.join()
+        # wait that the STT engine has finish his job (or the neurotransmitter neuron will be killed)
+        if ol.stt_instance is not None:
+            ol.stt_instance.join()
 
     def get_neuron_name(self):
         """

+ 11 - 33
kalliope/core/OrderAnalyser.py

@@ -65,7 +65,8 @@ class OrderAnalyser:
 
             # Start a neuron list with params
             self._start_list_neurons(list_neurons=tuple.synapse.neurons,
-                                     params=params)
+                                     params=params,
+                                     settings=self.settings)
             synapses_launched.append(tuple.synapse)
 
         # return the list of launched synapse
@@ -139,13 +140,13 @@ class OrderAnalyser:
         :return: the dict key/value
         """
         params = dict()
-        if cls._is_containing_bracket(string_order):
+        if Utils.is_containing_bracket(string_order):
             params = cls._associate_order_params_to_values(order_to_check, string_order)
             logger.debug("Parameters for order: %s" % params)
         return params
 
     @classmethod
-    def _start_list_neurons(cls, list_neurons, params):
+    def _start_list_neurons(cls, list_neurons, params, settings):
         # start neurons
         for neuron in list_neurons:
             cls._start_neuron(neuron, params)
@@ -190,8 +191,8 @@ class OrderAnalyser:
         else:
             Utils.print_danger("A problem has been found in the Synapse.")
 
-    @classmethod
-    def _associate_order_params_to_values(cls, order, order_to_check):
+    @staticmethod
+    def _associate_order_params_to_values(order, order_to_check):
         """
         Associate the variables from the order to the incoming user order
         :param order_to_check: the order to check incoming from the brain
@@ -203,9 +204,7 @@ class OrderAnalyser:
         logger.debug("[OrderAnalyser._associate_order_params_to_values] user order: %s, "
                      "order to check: %s" % (order, order_to_check))
 
-        pattern = '\s+(?=[^\{\{\}\}]*\}\})'
-        # Remove white spaces (if any) between the variable and the double brace then split
-        list_word_in_order = re.sub(pattern, '', order_to_check).split()
+        list_word_in_order = Utils.remove_spaces_in_brackets(order_to_check).split()
 
         # get the order, defined by the first words before {{
         # /!\ Could be empty if order starts with double brace
@@ -218,10 +217,10 @@ class OrderAnalyser:
         # make dict var:value
         dict_var = dict()
         for idx, ow in enumerate(list_word_in_order):
-            if cls._is_containing_bracket(ow):
+            if Utils.is_containing_bracket(ow):
                 # remove bracket and grab the next value / stop value
                 var_name = ow.replace("{{", "").replace("}}", "")
-                stop_value = cls._get_next_value_list(list_word_in_order[idx:])
+                stop_value = Utils.get_next_value_list(list_word_in_order[idx:])
                 if stop_value is None:
                     dict_var[var_name] = " ".join(truncate_list_word_said)
                     break
@@ -236,26 +235,6 @@ class OrderAnalyser:
             truncate_list_word_said = truncate_list_word_said[1:]
         return dict_var
 
-    @staticmethod
-    def _is_containing_bracket(sentence):
-        """
-        Return True if the text in <sentence> contains brackets
-        :param sentence:
-        :return:
-        """
-        # print "sentence to test %s" % sentence
-        pattern = r"{{|}}"
-        # prog = re.compile(pattern)
-        check_bool = re.search(pattern, sentence)
-        if check_bool is not None:
-            return True
-        return False
-
-    @staticmethod
-    def _get_next_value_list(list_to_check):
-        ite = list_to_check.__iter__()
-        next(ite, None)
-        return next(ite, None)
 
     @classmethod
     def spelt_order_match_brain_order_via_table(cls, order_to_analyse, user_said):
@@ -305,9 +284,8 @@ class OrderAnalyser:
         :param order: sentence to split
         :return: list of string without bracket
         """
-        pattern = r"((?:{{\s*)[\w\.]+(?:\s*}}))"
-        # find everything like {{ word }}
-        matches = re.findall(pattern, order)
+
+        matches = Utils.find_all_matching_brackets(order)
         for match in matches:
             order = order.replace(match, "")
         # then split

+ 5 - 1
kalliope/core/OrderListener.py

@@ -19,7 +19,7 @@ class OrderListener(Thread):
         starting to listen the incoming order. Basically it avoids delays.
     """
 
-    def __init__(self, callback=None, stt=None):
+    def __init__(self, callback=None, stt=None, audio_file_path=None):
         """
         This class is called after we catch the hotword that has woken up Kalliope.
         We now wait for an order spoken out loud by the user, translate the order into a text and run the action
@@ -42,6 +42,7 @@ class OrderListener(Thread):
         sl = SettingLoader()
         self.settings = sl.settings
         self.stt_instance = None
+        self.audio_file_path = audio_file_path
 
     def run(self):
         """
@@ -56,6 +57,9 @@ class OrderListener(Thread):
         for stt_object in self.settings.stts:
             if stt_object.name == self.stt_module_name:
                 stt_object.parameters["callback"] = self.callback
+                # add the audio file path to the list of parameter if set
+                if self.audio_file_path is not None:
+                    stt_object.parameters["audio_file_path"] = self.audio_file_path
 
                 stt_folder = None
                 if self.settings.resources:

+ 68 - 22
kalliope/core/ResourcesManager.py

@@ -3,6 +3,7 @@ import logging
 import os
 import shutil
 import re
+import tempfile
 
 from git import Repo
 from packaging import version
@@ -17,7 +18,7 @@ logging.basicConfig()
 logger = logging.getLogger("kalliope")
 
 # Global values for processing:
-LOCAL_TMP_FOLDER = "/tmp/kalliope/resources/"
+# LOCAL_TMP_FOLDER = "/tmp/kalliope/resources/"
 TMP_GIT_FOLDER = "kalliope_new_module_temp_name"
 DNA_FILE_NAME = "dna.yml"
 INSTALL_FILE_NAME = "install.yml"
@@ -53,7 +54,7 @@ class ResourcesManager(object):
         self.git_url = kwargs.get('git_url', None)
 
         # temp path where we install the new module
-        self.tmp_path = LOCAL_TMP_FOLDER + TMP_GIT_FOLDER
+        self.tmp_path = tempfile.gettempdir() + "/kalliope/resources/" + TMP_GIT_FOLDER
         self.dna_file_path = self.tmp_path + os.sep + DNA_FILE_NAME
         self.install_file_path = self.tmp_path + os.sep + INSTALL_FILE_NAME
         self.dna = None
@@ -93,13 +94,48 @@ class ResourcesManager(object):
                             # if the target_path exists, then run the install file within the new repository
                             if target_path is not None:
                                 self.install_file_path = target_path + os.sep + INSTALL_FILE_NAME
-                                self.run_ansible_playbook_module(install_file_path=self.install_file_path)
-                                Utils.print_success("Module: %s installed" % module_name)
+                                if self.run_ansible_playbook_module(install_file_path=self.install_file_path):
+                                    Utils.print_success("Module: %s installed" % module_name)
+                                else:
+                                    Utils.print_danger("Module: %s not installed" % module_name)
                 else:
                     logger.debug("[ResourcesManager] installation cancelled, deleting temp repo %s"
                                  % str(self.tmp_path))
                     shutil.rmtree(self.tmp_path)
 
+    def uninstall(self, neuron_name=None, tts_name=None, stt_name=None, trigger_name=None):
+        """
+        Uninstall a community resource
+        """
+        target_path_to_delete = None
+        module_name = ""
+        if neuron_name is not None:
+            target_path_to_delete = self._get_target_folder(resources=self.settings.resources,
+                                                            module_type="neuron")
+            module_name = neuron_name
+        if tts_name is not None:
+            target_path_to_delete = self._get_target_folder(resources=self.settings.resources,
+                                                            module_type="neuron")
+            module_name = tts_name
+        if stt_name is not None:
+            target_path_to_delete = self._get_target_folder(resources=self.settings.resources,
+                                                            module_type="neuron")
+            module_name = stt_name
+        if trigger_name is not None:
+            target_path_to_delete = self._get_target_folder(resources=self.settings.resources,
+                                                            module_type="neuron")
+            module_name = trigger_name
+
+        if target_path_to_delete is not None:
+            try:
+                shutil.rmtree(target_path_to_delete + os.sep + module_name.lower())
+                Utils.print_success("Module %s deleted" % module_name.lower())
+            except shutil.Error:
+                Utils.print_warning("The module %s doest not exist in the path %s" % (module_name.lower(), target_path_to_delete))
+            except OSError:
+                Utils.print_warning(
+                    "The module %s doest not exist in the path %s" % (module_name.lower(), target_path_to_delete))
+
     @staticmethod
     def is_settings_ok(resources, dna):
         """
@@ -165,23 +201,28 @@ class ResourcesManager(object):
         Return the folder from the resources and given a module type
         :param resources: Resource object
         :type resources: Resources
-        :param module_type: type of the module
+        :param module_type: type of the module (TYPE_NEURON, TYPE_STT, TYPE_TTS, TYPE_TRIGGER)
         :return: path of the folder
         """
+        module_type_converter = dict()
         # dict to get the path behind a type of resource
-        module_type_converter = {
-            TYPE_NEURON: resources.neuron_folder,
-            TYPE_STT: resources.stt_folder,
-            TYPE_TTS: resources.tts_folder,
-            TYPE_TRIGGER: resources.trigger_folder
-        }
+        try:
+            module_type_converter = {
+                TYPE_NEURON: resources.neuron_folder,
+                TYPE_STT: resources.stt_folder,
+                TYPE_TTS: resources.tts_folder,
+                TYPE_TRIGGER: resources.trigger_folder
+            }
+        except AttributeError:
+            # will be raised if the resource folder is not set in settings
+            pass
         # Let's find the right path depending of the type
         try:
             folder_path = module_type_converter[module_type]
         except KeyError:
             folder_path = None
         # No folder_path has been found
-        message = "No %s folder set in settings, cannot install." % module_type
+        message = "No %s folder set in settings." % module_type
         if folder_path is None:
             logger.debug(message)
             Utils.print_danger(message)
@@ -214,9 +255,9 @@ class ResourcesManager(object):
         logger.debug("[ResourcesManager] Rename temp folder")
         new_absolute_neuron_path = target_folder + os.sep + name
         try:
-            os.rename(tmp_path, new_absolute_neuron_path)
+            shutil.move(tmp_path, new_absolute_neuron_path)
             return new_absolute_neuron_path
-        except OSError:
+        except shutil.Error:
             # the folder already exist
             Utils.print_warning("The module %s already exist in the path %s" % (name, target_folder))
             # remove the cloned repo
@@ -235,14 +276,19 @@ class ResourcesManager(object):
         Utils.print_info("Starting neuron installation")
         # ask the sudo password
         pswd = getpass.getpass('Sudo password:')
-        ansible_neuron_parameters = {
-            "task_file": install_file_path,
-            "sudo": True,
-            "sudo_user": "root",
-            "sudo_password": pswd
-        }
-        neuron = Neuron(name="ansible_playbook", parameters=ansible_neuron_parameters)
-        NeuronLauncher.start_neuron(neuron)
+        if not pswd or pswd == "":
+            Utils.print_warning("You must enter a sudo password")
+            return False
+        else:
+            ansible_neuron_parameters = {
+                "task_file": install_file_path,
+                "sudo": True,
+                "sudo_user": "root",
+                "sudo_password": pswd
+            }
+            neuron = Neuron(name="ansible_playbook", parameters=ansible_neuron_parameters)
+            NeuronLauncher.start_neuron(neuron)
+            return True
 
     @staticmethod
     def _check_supported_version(current_version, supported_versions):

+ 123 - 8
kalliope/core/RestAPI/FlaskAPI.py

@@ -1,6 +1,15 @@
 import logging
+import os
 import threading
 
+import time
+
+from kalliope.core.Utils.FileManager import FileManager
+
+from kalliope.core.ConfigurationManager import SettingLoader
+from kalliope.core.OrderListener import OrderListener
+from werkzeug.utils import secure_filename
+
 from flask import jsonify
 from flask import request
 from flask_restful import abort
@@ -9,10 +18,15 @@ 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
+from kalliope._version import version_str
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
 
+UPLOAD_FOLDER = '/tmp/kalliope/tmp_uploaded_audio'
+ALLOWED_EXTENSIONS = {'mp3', 'wav'}
+
+
 class FlaskAPI(threading.Thread):
     def __init__(self, app, port=5000, brain=None, allowed_cors_origin=False):
         """
@@ -28,6 +42,20 @@ class FlaskAPI(threading.Thread):
         self.brain = brain
         self.allowed_cors_origin = allowed_cors_origin
 
+        # get current settings
+        sl = SettingLoader()
+        self.settings = sl.settings
+
+        # list of launched synapse by the Order Analyser when using the /synapses/start/audio URL
+        self.launched_synapses = None
+        # boolean used to notify the main process that we get the list of returned synapse
+        self.order_analyser_return = False
+
+        # configure the upload folder
+        app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
+        # create the temp folder
+        FileManager.create_directory(UPLOAD_FOLDER)
+
         # Flask configuration remove default Flask behaviour to encode to ASCII
         self.app.url_map.strict_slashes = False
         self.app.config['JSON_AS_ASCII'] = False
@@ -36,16 +64,28 @@ class FlaskAPI(threading.Thread):
             cors = CORS(app, resources={r"/*": {"origins": allowed_cors_origin}}, supports_credentials=True)
 
         # Add routing rules
+        self.app.add_url_rule('/', view_func=self.get_main_page, methods=['GET'])
         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'])
-        self.app.add_url_rule('/synapses/<synapse_name>', view_func=self.run_synapse, methods=['POST'])
-        self.app.add_url_rule('/order/', view_func=self.run_order, methods=['POST'])
+        self.app.add_url_rule('/synapses/start/id/<synapse_name>', view_func=self.run_synapse_by_name, methods=['POST'])
+        self.app.add_url_rule('/synapses/start/order', view_func=self.run_synapse_by_order, methods=['POST'])
+        self.app.add_url_rule('/synapses/start/audio', view_func=self.run_synapse_by_audio, methods=['POST'])
         self.app.add_url_rule('/shutdown/', view_func=self.shutdown_server, methods=['POST'])
 
-
     def run(self):
         self.app.run(host='0.0.0.0', port="%s" % int(self.port), debug=True, threaded=True, use_reloader=False)
 
+    def get_main_page(self):
+        data = {
+            "Kalliope version": "%s" % version_str
+        }
+        return jsonify(data), 200
+
+    @staticmethod
+    def allowed_file(filename):
+        return '.' in filename and \
+               filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
+
     def _get_synapse_by_name(self, synapse_name):
         """
         Find a synapse in the brain by its name
@@ -65,6 +105,8 @@ class FlaskAPI(threading.Thread):
     def get_synapses(self):
         """
         get all synapses.
+        test with curl:
+        curl -i --user admin:secret  -X GET  http://127.0.0.1:5000/synapses
         """
         data = jsonify(synapses=[e.serialize() for e in self.brain.synapses])
         return data, 200
@@ -73,6 +115,8 @@ class FlaskAPI(threading.Thread):
     def get_synapse(self, synapse_name):
         """
         get a synapse by its name
+        test with curl:
+        curl --user admin:secret -i -X GET  http://127.0.0.1:5000/synapses/say-hello-en
         """
         synapse_target = self._get_synapse_by_name(synapse_name)
         if synapse_target is not None:
@@ -85,11 +129,11 @@ class FlaskAPI(threading.Thread):
         return jsonify(error=data), 404
 
     @requires_auth
-    def run_synapse(self, synapse_name):
+    def run_synapse_by_name(self, synapse_name):
         """
         Run a synapse by its name
         test with curl:
-        curl -i --user admin:secret -X POST  http://localhost:5000/synapses/say-hello
+        curl -i --user admin:secret -X POST  http://127.0.0.1:5000/synapses/start/id/say-hello-fr
         :param synapse_name:
         :return:
         """
@@ -103,15 +147,15 @@ class FlaskAPI(threading.Thread):
 
         # run the synapse
         SynapseLauncher.start_synapse(synapse_name, brain=self.brain)
-        data = jsonify(synapses=synapse_target)
+        data = jsonify(synapses=synapse_target.serialize())
         return data, 201
 
     @requires_auth
-    def run_order(self):
+    def run_synapse_by_order(self):
         """
         Give an order to Kalliope via API like it was from a spoken one
         Test with curl
-        curl -i --user admin:secret -H "Content-Type: application/json" -X POST -d '{"order":"my order"}' http://localhost:5000/order
+        curl -i --user admin:secret -H "Content-Type: application/json" -X POST -d '{"order":"my order"}' http://localhost:5000/synapses/start/order
         In case of quotes in the order or accents, use a file
         cat post.json:
         {"order":"j'aime"}
@@ -144,6 +188,53 @@ class FlaskAPI(threading.Thread):
             }
             return jsonify(error=data), 400
 
+    def run_synapse_by_audio(self):
+        """
+        Give an order to Kalliope with an audio file
+        Test with curl
+        curl -i --user admin:secret -X POST  http://localhost:5000/synapses/start/audio -F "file=@/path/to/input.wav"
+        :return:
+        """
+        # check if the post request has the file part
+        if 'file' not in request.files:
+            data = {
+                "error": "No file provided"
+            }
+            return jsonify(error=data), 400
+
+        file = request.files['file']
+        # if user does not select file, browser also
+        # submit a empty part without filename
+        if file.filename == '':
+            data = {
+                "error": "No file provided"
+            }
+            return jsonify(error=data), 400
+        if file and self.allowed_file(file.filename):
+            # save the file
+            filename = secure_filename(file.filename)
+            base_path = os.path.join(self.app.config['UPLOAD_FOLDER'])
+            file.save(os.path.join(base_path, filename))
+
+            # now start analyse the audio with STT engine
+            audio_path = base_path + os.sep + filename
+            ol = OrderListener(callback=self.audio_analyser_callback, audio_file_path=audio_path)
+            ol.start()
+            ol.join()
+            # wait the Order Analyser processing. We need to wait in this thread to keep the context
+            while not self.order_analyser_return:
+                time.sleep(0.1)
+            self.order_analyser_return = False
+            if self.launched_synapses is not None and self.launched_synapses:
+                data = jsonify(synapses=[e.serialize() for e in self.launched_synapses])
+                self.launched_synapses = None
+                return data, 201
+            else:
+                data = {
+                    "error": "The given order doesn't match any synapses"
+                }
+                return jsonify(error=data), 400
+
     @requires_auth
     def shutdown_server(self):
         func = request.environ.get('werkzeug.server.shutdown')
@@ -151,3 +242,27 @@ class FlaskAPI(threading.Thread):
             raise RuntimeError('Not running with the Werkzeug Server')
         func()
         return "Shutting down..."
+
+    def audio_analyser_callback(self, order):
+        """
+        Callback of the OrderListener. Called after the processing of the audio file
+        This method will
+        - call the Order Analyser to analyse the  order and launch corresponding synapse as usual.
+        - get a list of launched synapse.
+        - give the list to the main process via self.launched_synapses
+        - notify that the processing is over via order_analyser_return
+        :param order: string order to analyse
+        :return:
+        """
+        logger.debug("order to process %s" % order)
+        if order is not None:  # maybe we have received a null audio from STT engine
+            order_analyser = OrderAnalyser(order, brain=self.brain)
+            synapses_launched = order_analyser.start()
+            self.launched_synapses = synapses_launched
+        else:
+            if self.settings.default_synapse is not None:
+                SynapseLauncher.start_synapse(name=self.settings.default_synapse, brain=self.brain)
+                self.launched_synapses = self.brain.get_synapse_by_name(synapse_name=self.settings.default_synapse)
+
+        # this boolean will notify the main process that the order have been processed
+        self.order_analyser_return = True

+ 1 - 1
kalliope/core/TTS/TTSModule.py

@@ -114,7 +114,7 @@ class TTSModule(object):
         </path/in/settings>/<tts.name>/tts.parameter["language"]/tts.parameter["voice"]/<md5_of_sentence.tts
 
         E.g:
-        /tmp/kalliope/voxygene/fr/abcd12345.tts
+        /tmp/kalliope/acapela/fr/abcd12345.tts
 
         :return: path String
         """

+ 62 - 3
kalliope/core/Utils/Utils.py

@@ -2,8 +2,7 @@ import logging
 import os
 import inspect
 import imp
-
-import sys
+import re
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
@@ -102,7 +101,6 @@ class Utils(object):
         :param resources_dir: the resource directory to check for external resources
         :return:
         """
-        logger.debug("Run plugin %s with parameter %s" % (module_name, parameters))
         package_path = "kalliope." + package_name + "." + module_name.lower() + "." + module_name.lower()
         if resources_dir is not None:
             neuron_resource_path = resources_dir + os.sep + module_name.lower() \
@@ -217,3 +215,64 @@ class Utils(object):
                 return valid[choice]
             else:
                 Utils.print_warning("Please respond with 'yes' or 'no' or 'y' or 'n').\n")
+
+    ##################
+    #
+    # Brackets management
+    #
+    #########
+    @staticmethod
+    def is_containing_bracket(sentence):
+        """
+        Return True if the text in <sentence> contains brackets
+        :param sentence:
+        :return:
+        """
+        # print "sentence to test %s" % sentence
+        pattern = r"{{|}}"
+        # prog = re.compile(pattern)
+        if not isinstance(sentence, unicode):
+            sentence = str(sentence)
+        check_bool = re.search(pattern, sentence)
+        if check_bool is not None:
+            return True
+        return False
+
+    @staticmethod
+    def find_all_matching_brackets(sentence):
+        """
+        Find all the bracket matches from a given sentence
+        :param sentence: the sentence to check
+        :return: the list with all the matches
+        """
+
+        pattern = r"((?:{{\s*)[\w\.]+(?:\s*}}))"
+        # find everything like {{ word }}
+        if not isinstance(sentence, unicode):
+            sentence = str(sentence)
+        return re.findall(pattern, sentence)
+
+    @staticmethod
+    def remove_spaces_in_brackets(sentence):
+        """
+        If has brackets it removes spaces in brackets
+        :param sentence: the sentence to work on
+        :return: the sentence without any spaces in brackets
+        """
+
+        pattern = '\s+(?=[^\{\{\}\}]*\}\})'
+        # Remove white spaces (if any) between the variable and the double brace then split
+        if not isinstance(sentence, unicode):
+            sentence = str(sentence)
+        return re.sub(pattern, '', sentence)
+
+    ##################
+    #
+    # Lists management
+    #
+    #########
+    @staticmethod
+    def get_next_value_list(list_to_check):
+        ite = list_to_check.__iter__()
+        next(ite, None)
+        return next(ite, None)

+ 5 - 0
kalliope/neurons/sleep/sleep.py

@@ -10,6 +10,10 @@ class Sleep(NeuronModule):
 
         # check parameters
         if self._is_parameters_ok():
+
+            if isinstance(self.seconds, str):
+                self.seconds = float(self.seconds)
+
             time.sleep(self.seconds)
 
     def _is_parameters_ok(self):
@@ -21,3 +25,4 @@ class Sleep(NeuronModule):
         """
         if self.seconds is None:
             raise MissingParameterException("You must set a number of seconds as parameter")
+        return True

+ 2 - 2
kalliope/neurons/sleep/tests/test_sleep.py

@@ -7,7 +7,7 @@ from kalliope.neurons.sleep.sleep import Sleep
 class TestSleep(unittest.TestCase):
 
     def setUp(self):
-        self.second="second"
+        self.seconds = 10
         self.random="random"
 
     def testParameters(self):
@@ -19,7 +19,7 @@ class TestSleep(unittest.TestCase):
         parameters = dict()
         run_test(parameters)
 
-        # missing second
+        # missing seconds
         parameters = {
             "random": self.random
         }

+ 15 - 6
kalliope/settings.yml

@@ -56,14 +56,14 @@ cache_path: "/tmp/kalliope_tts_cache"
 # Text to Spreech engines configuration
 # Available engine are:
 # - pico2wave
-# - voxygen
+# - acapela
+# - pico2wave
+# - googletts
+# - voicerss
 text_to_speech:
   - pico2wave:
       language: "fr-FR"
       cache: True
-  - voxygen:
-      voice: "Agnes"
-      cache: True
   - acapela:
       language: "sonid15"
       voice: "Manon"
@@ -126,7 +126,7 @@ on_ready_sounds:
 # Rest API
 # ---------------------------
 rest_api:
-  active: True
+  active: False
   port: 5000
   password_protected: True
   login: admin
@@ -137,7 +137,7 @@ rest_api:
 # Default Synapse
 # ---------------------------
 # Specify an optional default synapse response in case your order is not found.
-default_synapse: "Default-synapse"
+default_synapse: "default-synapse"
 
 # ---------------------------
 # Resource directory path
@@ -153,3 +153,12 @@ default_synapse: "Default-synapse"
 #  stt: "resources/stt"
 #  tts: "resources/tts"
 #  trigger: "resources/trigger"
+
+
+# ---------------------------
+# Global files variables
+# /!\ If a variable is defined in different files, the last file defines the value.
+# ---------------------------
+#var_files:
+#  - variables.yml
+#  - variables2.yml

+ 22 - 11
kalliope/stt/Utils.py

@@ -12,7 +12,7 @@ logger = logging.getLogger("kalliope")
 
 class SpeechRecognition(Thread):
 
-    def __init__(self):
+    def __init__(self, audio_file=None):
         """
         Thread used to caught n audio from the microphone and pass it to a callback method
         """
@@ -22,22 +22,33 @@ class SpeechRecognition(Thread):
         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)
+        self.audio_stream = None
+
+        if audio_file is None:
+            # audio file not set, we need to capture a sample from the microphone
+            with self.microphone as source:
+                # we only need to calibrate once, before we start listening
+                self.recognizer.adjust_for_ambient_noise(source)
+        else:
+            # audio file provided
+            with sr.AudioFile(audio_file) as source:
+                self.audio_stream = self.recognizer.record(source)  # read the entire audio file
 
     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()
+        if self.audio_stream is None:
+            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()
+        else:
+            self.callback(self.recognizer, self.audio_stream)
 
-    def start_listening(self):
+    def start_processing(self):
         """
         A method to start the thread
         """

+ 4 - 2
kalliope/stt/apiai/apiai.py

@@ -12,7 +12,8 @@ class Apiai(SpeechRecognition):
         :param callback: The callback function to call to send the text
         :param kwargs:
         """
-        SpeechRecognition.__init__(self)
+        # give the audio file path to process directly to the mother class if exist
+        SpeechRecognition.__init__(self, kwargs.get('audio_file_path', None))
 
         # callback function to call after the translation speech/tex
         self.main_controller_callback = callback
@@ -23,7 +24,8 @@ class Apiai(SpeechRecognition):
 
         # start listening in the background
         self.set_callback(self.apiai_callback)
-        self.start_listening()
+        # start processing, record a sample from the microphone if no audio file path provided, else read the file
+        self.start_processing()
 
     def apiai_callback(self, recognizer, audio):
         """

+ 4 - 2
kalliope/stt/bing/bing.py

@@ -12,7 +12,8 @@ class Bing(SpeechRecognition):
         :param callback: The callback function to call to send the text
         :param kwargs:
         """
-        SpeechRecognition.__init__(self)
+        # give the audio file path to process directly to the mother class if exist
+        SpeechRecognition.__init__(self, kwargs.get('audio_file_path', None))
 
         # callback function to call after the translation speech/tex
         self.main_controller_callback = callback
@@ -22,7 +23,8 @@ class Bing(SpeechRecognition):
 
         # start listening in the background
         self.set_callback(self.bing_callback)
-        self.start_listening()
+        # start processing, record a sample from the microphone if no audio file path provided, else read the file
+        self.start_processing()
 
     def bing_callback(self, recognizer, audio):
         """

+ 4 - 2
kalliope/stt/cmusphinx/cmusphinx.py

@@ -12,14 +12,16 @@ class Cmusphinx(SpeechRecognition):
         :param callback: The callback function to call to send the text
         :param kwargs:
         """
-        SpeechRecognition.__init__(self)
+        # give the audio file path to process directly to the mother class if exist
+        SpeechRecognition.__init__(self, kwargs.get('audio_file_path', None))
 
         # 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()
+        # start processing, record a sample from the microphone if no audio file path provided, else read the file
+        self.start_processing()
 
     def sphinx_callback(self, recognizer, audio):
         """

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

@@ -14,7 +14,8 @@ class Google(SpeechRecognition):
         :param callback: The callback function to call to send the text
         :param kwargs:
         """
-        SpeechRecognition.__init__(self)
+        # give the audio file path to process directly to the mother class if exist
+        SpeechRecognition.__init__(self, kwargs.get('audio_file_path', None))
 
         # callback function to call after the translation speech/tex
         self.main_controller_callback = callback
@@ -22,9 +23,10 @@ class Google(SpeechRecognition):
         self.language = kwargs.get('language', "en-US")
         self.show_all = kwargs.get('show_all', False)
 
-        # start listening in the background
+        # set the callback that will process the audio stream
         self.set_callback(self.google_callback)
-        self.start_listening()
+        # start processing, record a sample from the microphone if no audio file path provided, else read the file
+        self.start_processing()
 
     def google_callback(self, recognizer, audio):
         """

+ 4 - 2
kalliope/stt/houndify/houndify.py

@@ -12,7 +12,8 @@ class Houndify(SpeechRecognition):
         :param callback: The callback function to call to send the text
         :param kwargs:
         """
-        SpeechRecognition.__init__(self)
+        # give the audio file path to process directly to the mother class if exist
+        SpeechRecognition.__init__(self, kwargs.get('audio_file_path', None))
 
         # callback function to call after the translation speech/tex
         self.main_controller_callback = callback
@@ -24,7 +25,8 @@ class Houndify(SpeechRecognition):
 
         # start listening in the background
         self.set_callback(self.houndify_callback)
-        self.start_listening()
+        # start processing, record a sample from the microphone if no audio file path provided, else read the file
+        self.start_processing()
 
     def houndify_callback(self, recognizer, audio):
         """

+ 4 - 2
kalliope/stt/wit/wit.py

@@ -12,7 +12,8 @@ class Wit(SpeechRecognition):
         :param callback: The callback function to call to send the text
         :param kwargs:
         """
-        SpeechRecognition.__init__(self)
+        # give the audio file path to process directly to the mother class if exist
+        SpeechRecognition.__init__(self, kwargs.get('audio_file_path', None))
 
         # callback function to call after the translation speech/tex
         self.main_controller_callback = callback
@@ -21,7 +22,8 @@ class Wit(SpeechRecognition):
 
         # start listening in the background
         self.set_callback(self.wit_callback)
-        self.start_listening()
+        # start processing, record a sample from the microphone if no audio file path provided, else read the file
+        self.start_processing()
 
     def wit_callback(self, recognizer, audio):
         try:

+ 0 - 7
kalliope/templates/en_rss.j2

@@ -1,7 +0,0 @@
-Here's the news from {{ feed }}
-
-{% set count = 1 %}
-{% for item in items %}
-News {{ count }}. {{ item.title }}.
-{% set count = count + 1 %}
-{% endfor %}

+ 0 - 50
kalliope/templates/en_systemdate_template_example.j2

@@ -1,50 +0,0 @@
-{% set day_of_week = {
-    "0": "sunday",
-    "1": "monday",
-    "2": "tuesday",
-    "3": "wednesday",
-    "4": "thursday",
-    "5": "friday",
-    "6": "saturday"
-    }[weekday] | default("")
--%}
-
-{% set month_word = {"1": "january", "2": "february", "3": "march", "4": "april", "5": "may", "6": "june", "7": "july", "8": "august", "9": "september", "10": "october", "11": "november", "12": "december"}[month] | default("") -%}
-
-{% set day_month_formated = {
-    "1": "first",
-    "2": "second",
-    "3": "third",
-    "4": "fourth",
-    "5": "fifth",
-    "6": "sixth",
-    "7": "seventh",
-    "8": "eighth",
-    "9": "ninth",
-    "10": "tenth",
-    "11": "eleventh",
-    "12": "twelfth",
-    "13": "thirteenth",
-    "14": "fourteenth",
-    "15": "fifteenth",
-    "16": "sixteenth",
-    "17": "seventeenth",
-    "18": "eighteenth",
-    "19": "nineteenth",
-    "20": "twentieth",
-    "21": "twenty-first",
-    "22": "twenty-second",
-    "23": "twenty-third",
-    "24": "twenty-fourth",
-    "25": "twenty-fifth",
-    "26": "twenty-sixth",
-    "27": "twenty-seventh",
-    "28": "twenty-eighth",
-    "29": "twenty-ninth",
-    "30": "thirtieth",
-    "31": "thirty-first",
-
-}[day_month] | default("") -%}
-
-It' {{ hours }} hours and {{ minutes }} minutes.
-We are the {{ day_of_week }} {{ month_word }} the {{ day_month_formated }} {{ year }}

+ 0 - 12
kalliope/templates/fr_gmail.j2

@@ -1,12 +0,0 @@
-{% if unread == 0 %}
-    Vous n'avez aucun email
-{% else %}
-    Vous avez {{ unread }} email.
-    {% set count = 1 %}
-    {% if unread > 0 %}
-        {% for subject in subjects %}
-         i-maile {{ count }}. {{ subject }}.
-         {% set count = count + 1 %}
-        {% endfor %}
-    {% endif %}
-{% endif %}

+ 0 - 5
kalliope/templates/fr_gmail_count_unread.j2

@@ -1,5 +0,0 @@
-{% if unread == 0 %}
-    Vous n'avez aucun email.
-{% else %}
-    Vous avez {{ unread }} email.
-{% endif %}

+ 0 - 7
kalliope/templates/fr_rss.j2

@@ -1,7 +0,0 @@
-Voici les nouvelles de {{ feed }}
-
-{% set count = 1 %}
-{% for item in items %}
-Nouvelle {{ count }}. {{ item.title }}.
-{% set count = count + 1 %}
-{% endfor %}

+ 0 - 5
kalliope/templates/fr_systemdate_template_example.j2

@@ -1,5 +0,0 @@
-{% set day_of_week = {"0": "dimanche", "1": "lundi", "2": "mardi", "3": "mercredi", "4": "jeudi", "5": "vendredi", "6": "samedi"}[weekday] | default("") -%}
-{% set month_word = {"1": "janvier", "2": "février", "3": "mars", "4": "avril", "5": "mai", "6": "juin", "7": "juillet", "8": "août", "9": "septembre", "10": "octobre", "11": "novembre", "12": "décembre"}[month] | default("") -%}
-
-Il est {{ hours }} heures et {{ minutes }} minutes.
-Nous sommes le {{ day_of_week }} {{ day_month }} {{ month_word }} {{ year }}

+ 0 - 5
kalliope/templates/remove_file.j2

@@ -1,5 +0,0 @@
-{% if returncode == 0 %}
-    The command has succeeded
-{% else %}
-    The command has failed
-{% endif %}

+ 0 - 12
kalliope/templates/wikipedia_returned_value.j2

@@ -1,12 +0,0 @@
-{% if returncode == "DisambiguationError" %}
-    Veuillez etre plus précis. La recherche que vous venez d'éffectuez peut correspondre au page Wikipédia suivante:
-    {% if may_refer is not none %}
-        {% for el in may_refer %}
-            {{ el }}
-        {% endfor %}
-    {% endif %}
-{% elif returncode == "PageError" %}
-    Je n'ai rien trouvé la dessus
-{% else %}
-    {{ summary }}
-{% endif %}

+ 2 - 1
kalliope/tts/acapela/acapela.py

@@ -7,7 +7,7 @@ import logging
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
 
-TTS_URL = "http://www.acapela-group.com/demo-tts/DemoHTML5Form_V2_fr.php"
+TTS_URL = "http://dmbx.acapela-group.com/DemoHTML5Form_V2_fr.php"
 TTS_CONTENT_TYPE = "audio/mpeg"
 TTS_TIMEOUT_SEC = 30
 
@@ -80,6 +80,7 @@ class Acapela(TTSModule):
             "MySelectedVoice": self.voice,
             "MyTextForTTS": self.words,
             "t": "1",
+            "agreeterms": "on",
             "SendToVaaS": ""
         }
 

+ 0 - 11
kalliope/tts/voxygen/README.md

@@ -1,11 +0,0 @@
-### Voxygen
-
-This TTS is based on the demo of the [Voxygen engine](https://www.voxygen-group.com/)
-
-| Parameters | Required | Default | Choices                                                              | Comment                                                                                       |
-|------------|----------|---------|----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
-| voice      | yes      |         | [see the full list](https://www.voxygen-group.com/). Example: "Emma" | Do not set any accent. For Example the voice "Agnès" will be "Agnes" is the settings.yml file |
-| cache      | no       | TRUE    |                                                                      | True if you want to use the cache with this TTS                                               |
-
-
-

+ 0 - 1
kalliope/tts/voxygen/__init__.py

@@ -1 +0,0 @@
-from voxygen import Voxygen

+ 0 - 69
kalliope/tts/voxygen/voxygen.py

@@ -1,69 +0,0 @@
-import logging
-
-import requests
-
-from kalliope.core import FileManager
-from kalliope.core.TTS.TTSModule import TTSModule, MissingTTSParameter
-
-logging.basicConfig()
-logger = logging.getLogger("kalliope")
-
-TTS_URL = "https://www.voxygen.fr/sites/all/modules/voxygen_voices/assets/proxy/index.php"
-TTS_TIMEOUT_SEC = 30
-TTS_CONTENT_TYPE = "audio/mpeg"
-
-
-class Voxygen(TTSModule):
-
-    def __init__(self, **kwargs):
-        # voxygen does'nt need a language. The name of the voice correspond to a lang
-        super(Voxygen, self).__init__(language="any", **kwargs)
-
-        self.voice = kwargs.get('voice', None)
-        if self.voice is None:
-            raise MissingTTSParameter("voice parameter is required by the Voxygen TTS")
-
-    def say(self, words):
-        """
-        :param words: The sentence to say
-        """
-
-        self.generate_and_play(words, self._generate_audio_file)
-
-    def _generate_audio_file(self):
-        """
-        Generic method used as a Callback in TTSModule
-            - must provided the audio file and write it on the disk
-
-        .. raises:: FailToLoadSoundFile
-        """
-
-        payload = self.get_payload(self.voice, self.words)
-
-        # getting the mp3
-        r = requests.get(TTS_URL, params=payload, stream=True, timeout=TTS_TIMEOUT_SEC)
-        content_type = r.headers['Content-Type']
-
-        logger.debug("Voxygen : Trying to get url: %s response code: %s and content-type: %s",
-                     r.url,
-                     r.status_code,
-                     content_type)
-
-        if r.status_code == requests.codes.ok and content_type == TTS_CONTENT_TYPE:
-            FileManager.write_in_file(self.file_path, r.content)
-        else:
-            logger.debug("Unable to get a valid audio file. Returned code: %s" % r.status_code)
-
-    @staticmethod
-    def get_payload(voice, words):
-        """
-        Generic method used load the payload used to access the remote api
-
-        :return: Payload to use to access the remote api
-        """
-
-        return {
-            "method": "redirect",
-            "text": words,
-            "voice": voice
-        }

+ 17 - 17
setup.py

@@ -58,26 +58,26 @@ setup(
     # required libs
     install_requires=[
         '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',
+        'SpeechRecognition>=3.6.0',
+        'markupsafe>=1.0',
+        'pyaudio>=0.2.10',
+        'pyasn1>=0.2.3',
+        'ansible>=2.2',
+        'python2-pythondialog>=3.4.0',
+        'jinja2>=2.8,<2.9',
         'cffi==1.9.1',
-        'ipaddress==1.0.17',
-        'flask==0.11.1',
-        'Flask-Restful==0.3.5',
+        'ipaddress>=1.0.17',
+        'flask>=0.12',
+        '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',
+        'requests>=2.13',
+        'httpretty>=0.8.14',
+        'mock>=2.0.0',
+        'Flask-Testing>=0.6.2',
+        'apscheduler>=3.3.1',
+        'GitPython>=2.1.3',
         'packaging>=16.8',
-        'transitions>=0.4.3'
+        'transitions>=0.4.3',
     ],