Browse Source

Merge pull request #308 from kalliope-project/dev

v0.4.5 release
Nicolas Marcq 7 years ago
parent
commit
eebe45072d

+ 10 - 0
CHANGELOG.md

@@ -1,3 +1,13 @@
+v0.4.5 / 2017-07-23
+===================
+- add keyword_entries attribute to CMU Sphinx
+- add simplerate attribute to Pico2wav TTS
+- API: convert mp3 file to wav automatically
+- add sensitivity attribute to snowboy trigger
+- add grammar attribute to CMU Sphinx
+- add no_voice flag to api
+- add possibility to send parameters when using api with run synapse by name
+
 v0.4.4 / 2017-05-20
 ===================
 - Fix: Uppercase in order/parameters/global variables are now handled correctly

+ 35 - 0
Docs/rest_api.md

@@ -147,6 +147,21 @@ Output example:
 }
 ```
 
+The [no_voice flag](#no-voice-flag) can be added to this call.
+Curl command:
+```bash
+curl -i -H "Content-Type: application/json" --user admin:secret -X POST \
+-d '{"no_voice":"true"}' http://127.0.0.1:5000/synapses/start/id/say-hello-fr
+```
+
+Some neuron inside a synapse will wait for parameters that comes from the order. 
+You can provide those parameters by adding a `parameters` list of data.
+Curl command:
+```bash
+curl -i -H "Content-Type: application/json" --user admin:secret -X POST  \
+-d '{"parameters": {"parameter1": "value1" }}' \
+http://127.0.0.1:5000/synapses/start/id/synapse-id
+```
 
 ### Run a synapse from an order
 
@@ -217,6 +232,13 @@ Or return an empty list of matched synapse
 }
 ```
 
+The [no_voice flag](#no-voice-flag) can be added to this call.
+Curl command:
+```bash
+curl -i --user admin:secret -H "Content-Type: application/json" -X POST \
+-d '{"order":"my order", "no_voice":"true"}' http://localhost:5000/synapses/start/order
+```
+
 ### Run a synapse from an audio file
 
 Normal response codes: 201
@@ -277,3 +299,16 @@ Or return an empty list of matched synapse
   "user_order": "not existing order"
 }
 ```
+
+The [no_voice flag](#no-voice-flag) can be added to this call with a form.
+Curl command:
+```bash
+curl -i --user admin:secret -X POST http://localhost:5000/synapses/start/audio -F "file=@path/to/file.wav" -F no_voice="true"
+```
+
+
+## No voice flag
+
+When you use the API, by default Kalliope will generate a text and process it into the TTS engine.
+Some calls to the API can be done with a flag that will tell Kalliope to only return the generated text without processing it into the audio player.
+When `no_voice` is switched to true, Kalliope will not speak out loud on the server side.

+ 7 - 3
Docs/trigger.md

@@ -7,9 +7,13 @@ With Kalliope project, you can set whatever Hotword you want to wake it up.
 
 You can create your magic word by connecting to [Snowboy](https://snowboy.kitt.ai/) and then download the trained model file.
 
-Once downloaded:
-- place the file in your personal config folder.
-- update the path of **pmdl_file** in [your settings](settings.md).
+Once downloaded, place the file in your personal config folder and configure snowboy in your [your settings](settings.md) following the table bellow
+
+| parameter   | required | type   | default | choices         | comment                                                                                          |
+|-------------|----------|--------|---------|-----------------|--------------------------------------------------------------------------------------------------|
+| pmdl_file   | TRUE     | string |         |                 | Path to the snowboy model file. The path can be absolute or relative to the brain file           |
+| sensitivity | FALSE    | string | 0.5     | between 0 and 1 | Increasing the sensitivity value lead to better detection rate, but also higher false alarm rate |
+
 
 If you want to keep "Kalliope" as the name of your bot, we recommend you to __enhance the existing Snowboy model for your language__.
 

+ 1 - 0
README.md

@@ -84,6 +84,7 @@ Ka-li-o-pé      (French)
 
 Demo French   : [video](https://www.youtube.com/watch?v=t4J42yO2rkM)
 Demo English  : [video](https://www.youtube.com/watch?v=PcLzo4H18S4)
+Android app   : [Playstore](https://play.google.com/store/apps/details?id=kalliope.project)
 
 ## License
 

+ 9 - 0
Tests/brains/brain_test_api.yml

@@ -17,3 +17,12 @@
   
   - includes:
     - included_brain_test.yml
+
+  - name: "test4"
+    signals:
+      - order: "test_order_with_parameter"
+    neurons:
+      - say:
+          message:
+            - "test message {{ parameter1 }}"
+

+ 110 - 68
Tests/test_rest_api.py

@@ -1,16 +1,16 @@
 import json
 import os
 import unittest
-import ast
 
 from flask import Flask
 from flask_testing import LiveServerTestCase
+from mock import mock
 
-from kalliope.core import LIFOBuffer
-from kalliope.core.Models import Singleton
 from kalliope._version import version_str
+from kalliope.core import LIFOBuffer
 from kalliope.core.ConfigurationManager import BrainLoader
 from kalliope.core.ConfigurationManager import SettingLoader
+from kalliope.core.Models import Singleton
 from kalliope.core.RestAPI.FlaskAPI import FlaskAPI
 
 
@@ -65,74 +65,65 @@ class TestRestAPI(LiveServerTestCase):
         expected_content = {
             "Kalliope version": "%s" % version_str
         }
-        self.assertEqual(json.dumps(expected_content, sort_keys=True), json.dumps(json.loads(response.get_data().decode('utf-8')), sort_keys=True))
+        self.assertEqual(json.dumps(expected_content, sort_keys=True),
+                         json.dumps(json.loads(response.get_data().decode('utf-8')), sort_keys=True))
 
     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"
-                        }
-                    ]
-                }
-            ]
+            "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": "test4",
+                "neurons": [{
+                    "name": "say",
+                    "parameters": {
+                        "message": ["test message {{parameter1}}"]
+                    }
+                }],
+                "signals": [{
+                    "order": "test_order_with_parameter"
+                }]
+            }, {
+                "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)
-        self.assertEqual(json.dumps(expected_content, sort_keys=True), json.dumps(json.loads(response.get_data().decode('utf-8')), sort_keys=True))
+        self.assertEqual(json.dumps(expected_content, sort_keys=True),
+                         json.dumps(json.loads(response.get_data().decode('utf-8')), sort_keys=True))
 
     def test_get_one_synapse(self):
         url = self.get_server_url() + "/synapses/test"
@@ -158,7 +149,8 @@ class TestRestAPI(LiveServerTestCase):
                 ]
             }
         }
-        self.assertEqual(json.dumps(expected_content, sort_keys=True), json.dumps(json.loads(response.get_data().decode('utf-8')), sort_keys=True))
+        self.assertEqual(json.dumps(expected_content, sort_keys=True),
+                         json.dumps(json.loads(response.get_data().decode('utf-8')), sort_keys=True))
 
     def test_get_synapse_not_found(self):
         url = self.get_server_url() + "/synapses/test-none"
@@ -185,7 +177,35 @@ class TestRestAPI(LiveServerTestCase):
                                   'synapse_name': 'test'}],
                             'user_order': None
                             }
-        self.assertEqual(json.dumps(expected_content, sort_keys=True), json.dumps(json.loads(result.get_data().decode('utf-8')), sort_keys=True))
+        self.assertEqual(json.dumps(expected_content, sort_keys=True),
+                         json.dumps(json.loads(result.get_data().decode('utf-8')), sort_keys=True))
+        self.assertEqual(result.status_code, 201)
+
+        # run a synapse by its name with parameter
+        url = self.get_server_url() + "/synapses/start/id/test4"
+        headers = {"Content-Type": "application/json"}
+        data = {"parameters": {"parameter1": "replaced_value"}}
+        result = self.client.post(url, headers=headers, data=json.dumps(data))
+
+        expected_content = {
+          "matched_synapses": [
+            {
+              "matched_order": None,
+              "neuron_module_list": [
+                {
+                  "generated_message": "test message replaced_value",
+                  "neuron_name": "Say"
+                }
+              ],
+              "synapse_name": "test4"
+            }
+          ],
+          "status": "complete",
+          "user_order": None
+        }
+
+        self.assertEqual(json.dumps(expected_content, sort_keys=True),
+                         json.dumps(json.loads(result.get_data().decode('utf-8')), sort_keys=True))
         self.assertEqual(result.status_code, 201)
 
     def test_post_synapse_not_found(self):
@@ -198,7 +218,8 @@ class TestRestAPI(LiveServerTestCase):
             }
         }
 
-        self.assertEqual(json.dumps(expected_content, sort_keys=True), json.dumps(json.loads(result.get_data().decode('utf-8')), sort_keys=True))
+        self.assertEqual(json.dumps(expected_content, sort_keys=True),
+                         json.dumps(json.loads(result.get_data().decode('utf-8')), sort_keys=True))
         self.assertEqual(result.status_code, 404)
 
     def test_run_synapse_with_order(self):
@@ -218,12 +239,13 @@ class TestRestAPI(LiveServerTestCase):
                                                     'generated_message': 'test message', 'neuron_name': 'Say'
                                                 }
                                             ],
-                                  'synapse_name': 'test'
+                                        'synapse_name': 'test'
                                     }
                                 ],
                             'user_order': "test_order"
                             }
-        self.assertEqual(json.dumps(expected_content, sort_keys=True), json.dumps(json.loads(result.get_data().decode('utf-8')), sort_keys=True))
+        self.assertEqual(json.dumps(expected_content, sort_keys=True),
+                         json.dumps(json.loads(result.get_data().decode('utf-8')), sort_keys=True))
         self.assertEqual(result.status_code, 201)
 
     def test_post_synapse_by_order_not_found(self):
@@ -236,7 +258,8 @@ class TestRestAPI(LiveServerTestCase):
 
         expected_content = {'status': None, 'matched_synapses': [], 'user_order': u'non existing order'}
 
-        self.assertEqual(json.dumps(expected_content, sort_keys=True), json.dumps(json.loads(result.get_data().decode('utf-8')), sort_keys=True))
+        self.assertEqual(json.dumps(expected_content, sort_keys=True),
+                         json.dumps(json.loads(result.get_data().decode('utf-8')), sort_keys=True))
         self.assertEqual(result.status_code, 201)
 
         # TODO this doesn't work on travis but works locally with python -m unittest discover
@@ -275,10 +298,29 @@ class TestRestAPI(LiveServerTestCase):
         #         self.assertEqual(json.dumps(expected_content), json.dumps(json.loads(result.get_data())))
         #         self.assertEqual(result.status_code, 201)
 
+    def test_convert_to_wav(self):
+        """
+        Test the api function to convert incoming sound file to wave.
+        """
+
+        with mock.patch("os.system") as mock_os_system:
+            # Scenario 1 : input wav file
+            temp_file = "/tmp/kalliope/tempfile.wav"  # tempfile.NamedTemporaryFile(suffix=".wav")
+            result_file = FlaskAPI._convert_to_wav(temp_file)
+            self.assertEqual(temp_file, result_file)
+            mock_os_system.assert_not_called()
+
+            # Scenario 2 : input not a wav file
+            temp_file = "/tmp/kalliope/tempfile.amr"  # tempfile.NamedTemporaryFile(suffix=".wav")
+            expected_result = "/tmp/kalliope/tempfile.wav"
+            result_file = FlaskAPI._convert_to_wav(temp_file)
+            self.assertEqual(expected_result, result_file)
+            mock_os_system.assert_called_once_with("avconv -y -i " + temp_file + " " + expected_result)
+
 if __name__ == '__main__':
     unittest.main()
 
     # suite = unittest.TestSuite()
-    # suite.addTest(TestRestAPI("test_post_synapse_by_order_not_found"))
+    # suite.addTest(TestRestAPI("test_run_synapse_by_name"))
     # runner = unittest.TextTestRunner()
     # runner.run(suite)

+ 6 - 0
docker/debian8.dockerfile

@@ -16,6 +16,12 @@ RUN apt-get update && apt-get install -y \
 RUN wget https://bootstrap.pypa.io/get-pip.py
 RUN python get-pip.py
 
+# fix install in container
+RUN pip install --upgrade pip six
+RUN pip install --upgrade pip pyyaml
+RUN pip install --upgrade pip SpeechRecognition
+RUN pip install --upgrade pip Werkzeug
+
 # add a standart user. tests must not be ran as root
 RUN useradd -m -u 1000 tester
 RUN usermod -aG sudo tester

+ 6 - 0
docker/ubuntu_16_04.dockerfile

@@ -16,6 +16,12 @@ RUN apt-get update && apt-get install -y \
 RUN wget https://bootstrap.pypa.io/get-pip.py
 RUN python get-pip.py
 
+# fix install in container
+RUN pip install --upgrade pip six
+RUN pip install --upgrade pip pyyaml
+RUN pip install --upgrade pip SpeechRecognition
+RUN pip install --upgrade pip Werkzeug
+
 # add a standart user. tests must not be ran as root
 RUN useradd -m -u 1000 tester
 RUN usermod -aG sudo tester

+ 4 - 4
install/files/python_requirements.txt

@@ -1,7 +1,7 @@
-six==1.10.0
-SpeechRecognition>=3.5.0
+six>=1.10.0
+SpeechRecognition>=3.7.1
 markupsafe==0.23
-pyaudio==0.2.9
+pyaudio==0.2.11
 pyasn1>=0.1.8
 ansible==2.2.0.0
 #python2-pythondialog==3.4.0
@@ -23,4 +23,4 @@ sounddevice>=0.3.7
 SoundFile>=0.9.0
 pyalsaaudio>=0.8.4
 RPi.GPIO>=0.6.3
-
+sox>=1.3.0

+ 1 - 1
kalliope/_version.py

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

+ 8 - 3
kalliope/core/LIFOBuffer.py

@@ -39,6 +39,7 @@ class LIFOBuffer(object):
     logger.debug("[LIFOBuffer] LIFO buffer created")
     answer = None
     is_api_call = False
+    no_voice = False
 
     @classmethod
     def set_answer(cls, value):
@@ -80,7 +81,7 @@ class LIFOBuffer(object):
         return returned_api_response
 
     @classmethod
-    def execute(cls, answer=None, is_api_call=False):
+    def execute(cls, answer=None, is_api_call=False, no_voice=False):
         """
         Process the LIFO list.
         
@@ -90,13 +91,15 @@ class LIFOBuffer(object):
         If a neuron add a Synapse list to the lifo, this synapse list is processed before executing the first list 
         in which we were in.
         
-        :param answer: String answer to give the the last neuron which whas waiting for an answer
+        :param answer: String answer to give the the last neuron which was waiting for an answer
         :param is_api_call: Boolean passed to all neuron in order to let them know if the current call comes from API
+        :param no_voice: If true, the generated text will not be processed by the TTS engine
         :return: serialized APIResponse object
         """
         # store the answer if present
         cls.answer = answer
         cls.is_api_call = is_api_call
+        cls.no_voice = no_voice
 
         try:
             # we keep looping over the LIFO til we have synapse list to process in it
@@ -165,7 +168,9 @@ class LIFOBuffer(object):
                 cls.answer = None
             # todo fix this when we have a full client/server call. The client would be the voice or api call
             neuron.parameters["is_api_call"] = cls.is_api_call
-            logger.debug("[LIFOBuffer] process_neuron_list: is_api_call: %s" % cls.is_api_call)
+            neuron.parameters["no_voice"] = cls.no_voice
+            logger.debug("[LIFOBuffer] process_neuron_list: is_api_call: %s, no_voice: %s" % (cls.is_api_call,
+                                                                                              cls.no_voice))
             # execute the neuron
             instantiated_neuron = NeuronLauncher.start_neuron(neuron=neuron,
                                                               parameters_dict=matched_synapse.parameters)

+ 6 - 1
kalliope/core/Models/MatchedSynapse.py

@@ -8,11 +8,12 @@ class MatchedSynapse(object):
     This class represent a synapse that has matched an order send by an User.
     """
 
-    def __init__(self, matched_synapse=None, matched_order=None, user_order=None):
+    def __init__(self, matched_synapse=None, matched_order=None, user_order=None, overriding_parameter=None):
         """
         :param matched_synapse: The synapse that has matched in the brain.
         :param matched_order: The order from the synapse that have matched.
         :param user_order: The order said by the user.
+        :param overriding_parameter: If set, those parameters will over
         """
 
         # create a copy of the synapse. the received synapse come from the brain.
@@ -26,6 +27,10 @@ class MatchedSynapse(object):
         if matched_order is not None:
             self.parameters = NeuronParameterLoader.get_parameters(synapse_order=self.matched_order,
                                                                    user_order=user_order)
+        if overriding_parameter is not None:
+            # we suppose that we don't have any parameters.
+            # We replace the current parameter object with the received one
+            self.parameters = overriding_parameter
 
         # list of Neuron Module
         self.neuron_module_list = list()

+ 32 - 25
kalliope/core/NeuronModule.py

@@ -97,6 +97,8 @@ class NeuronModule(object):
         self.tts_message = None
         # if the current call is api one
         self.is_api_call = kwargs.get('is_api_call', False)
+        # if the current call want to mute kalliope
+        self.no_voice = kwargs.get('no_voice', False)
         # boolean to know id the synapse is waiting for an answer
         self.is_waiting_for_answer = False
         # the synapse name to add the the buffer
@@ -131,43 +133,48 @@ class NeuronModule(object):
 
         .. raises:: TTSModuleNotFound
         """
-        logger.debug("NeuronModule Say() called with message: %s" % message)
+        logger.debug("[NeuronModule] Say() called with message: %s" % message)
 
         tts_message = None
 
         if isinstance(message, str) or isinstance(message, six.text_type):
-            logger.debug("message is string")
+            logger.debug("[NeuronModule] message is string")
             tts_message = message
 
         if isinstance(message, list):
-            logger.debug("message is list")
+            logger.debug("[NeuronModule] message is list")
             tts_message = random.choice(message)
 
         if isinstance(message, dict):
-            logger.debug("message is dict")
+            logger.debug("[NeuronModule] message is dict")
             tts_message = self._get_message_from_dict(message)
 
         if tts_message is not None:
-            logger.debug("tts_message to say: %s" % tts_message)
+            logger.debug("[NeuronModule] tts_message to say: %s" % tts_message)
             self.tts_message = tts_message
             Utils.print_success(tts_message)
 
-            # get the instance of the TTS module
-            tts_folder = None
-            if self.settings.resources:
-                tts_folder = self.settings.resources.tts_folder
-            tts_module_instance = Utils.get_dynamic_class_instantiation(package_name="tts",
-                                                                        module_name=self.tts.name,
-                                                                        parameters=self.tts.parameters,
-                                                                        resources_dir=tts_folder)
-            # Kalliope will talk, turn on the LED
-            self.switch_on_led_talking(rpi_settings=self.settings.rpi_settings, on=True)
-
-            # generate the audio file and play it
-            tts_module_instance.say(tts_message)
-
-            # Kalliope has finished to talk, turn off the LED
-            self.switch_on_led_talking(rpi_settings=self.settings.rpi_settings, on=False)
+            # process the audio only if the no_voice flag is false
+            if self.no_voice:
+                logger.debug("[NeuronModule] no_voice is True, Kalliope is muted")
+            else:
+                logger.debug("[NeuronModule] no_voice is False, make Kalliope speaking")
+                # get the instance of the TTS module
+                tts_folder = None
+                if self.settings.resources:
+                    tts_folder = self.settings.resources.tts_folder
+                tts_module_instance = Utils.get_dynamic_class_instantiation(package_name="tts",
+                                                                            module_name=self.tts.name,
+                                                                            parameters=self.tts.parameters,
+                                                                            resources_dir=tts_folder)
+                # Kalliope will talk, turn on the LED
+                self.switch_on_led_talking(rpi_settings=self.settings.rpi_settings, on=True)
+
+                # generate the audio file and play it
+                tts_module_instance.say(tts_message)
+
+                # Kalliope has finished to talk, turn off the LED
+                self.switch_on_led_talking(rpi_settings=self.settings.rpi_settings, on=False)
 
     def _get_message_from_dict(self, message_dict):
         """
@@ -284,15 +291,15 @@ class NeuronModule(object):
         # create a tts object from the tts the user want to use
         tts_object = next((x for x in settings.ttss if x.name == tts_name), None)
         if tts_object is None:
-            raise TTSModuleNotFound("The tts module name %s does not exist in settings file" % tts_name)
+            raise TTSModuleNotFound("[NeuronModule] The tts module name %s does not exist in settings file" % tts_name)
 
         if override_parameter is not None:  # the user want to override the default TTS configuration
-            logger.debug("args for TTS plugin before update: %s" % str(tts_object.parameters))
+            logger.debug("[NeuronModule] args for TTS plugin before update: %s" % str(tts_object.parameters))
             for key, value in override_parameter.items():
                 tts_object.parameters[key] = value
-            logger.debug("args for TTS plugin after update: %s" % str(tts_object.parameters))
+            logger.debug("[NeuronModule] args for TTS plugin after update: %s" % str(tts_object.parameters))
 
-        logger.debug("NeuroneModule: TTS args: %s" % tts_object)
+        logger.debug("[NeuronModule] TTS args: %s" % tts_object)
         return tts_object
 
     @staticmethod

+ 157 - 47
kalliope/core/RestAPI/FlaskAPI.py

@@ -1,25 +1,22 @@
 import logging
 import os
 import threading
-
 import time
 
-from kalliope.core.LIFOBuffer import LIFOBuffer
-from kalliope.core.Models.MatchedSynapse import MatchedSynapse
-from kalliope.core.Utils.FileManager import FileManager
-
-from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
-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
 from flask_cors import CORS
+from flask_restful import abort
+from werkzeug.utils import secure_filename
 
+from kalliope._version import version_str
+from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
+from kalliope.core.LIFOBuffer import LIFOBuffer
+from kalliope.core.Models.MatchedSynapse import MatchedSynapse
+from kalliope.core.OrderListener import OrderListener
 from kalliope.core.RestAPI.utils import requires_auth
 from kalliope.core.SynapseLauncher import SynapseLauncher
-from kalliope._version import version_str
+from kalliope.core.Utils.FileManager import FileManager
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
@@ -62,7 +59,10 @@ class FlaskAPI(threading.Thread):
         self.app.config['JSON_AS_ASCII'] = False
 
         if self.allowed_cors_origin is not False:
-            cors = CORS(app, resources={r"/*": {"origins": allowed_cors_origin}}, supports_credentials=True)
+            CORS(app, resources={r"/*": {"origins": allowed_cors_origin}}, supports_credentials=True)
+
+        # no voice flag
+        self.no_voice = False
 
         # Add routing rules
         self.app.add_url_rule('/', view_func=self.get_main_page, methods=['GET'])
@@ -76,7 +76,9 @@ class FlaskAPI(threading.Thread):
     def run(self):
         self.app.run(host='0.0.0.0', port="%s" % int(self.port), debug=True, threaded=True, use_reloader=False)
 
+    @requires_auth
     def get_main_page(self):
+        logger.debug("[FlaskAPI] get_main_page")
         data = {
             "Kalliope version": "%s" % version_str
         }
@@ -109,6 +111,7 @@ class FlaskAPI(threading.Thread):
         test with curl:
         curl -i --user admin:secret  -X GET  http://127.0.0.1:5000/synapses
         """
+        logger.debug("[FlaskAPI] get_synapses: all")
         data = jsonify(synapses=[e.serialize() for e in self.brain.synapses])
         return data, 200
 
@@ -119,6 +122,7 @@ class FlaskAPI(threading.Thread):
         test with curl:
         curl --user admin:secret -i -X GET  http://127.0.0.1:5000/synapses/say-hello-en
         """
+        logger.debug("[FlaskAPI] get_synapse: synapse_name -> %s" % synapse_name)
         synapse_target = self._get_synapse_by_name(synapse_name)
         if synapse_target is not None:
             data = jsonify(synapses=synapse_target.serialize())
@@ -135,12 +139,29 @@ class FlaskAPI(threading.Thread):
         Run a synapse by its name
         test with curl:
         curl -i --user admin:secret -X POST  http://127.0.0.1:5000/synapses/start/id/say-hello-fr
-        :param synapse_name:
+
+        run a synapse without making kalliope speaking
+        curl -i -H "Content-Type: application/json" --user admin:secret -X POST  \
+        -d '{"no_voice":"true"}' http://127.0.0.1:5000/synapses/start/id/say-hello-fr
+
+        Run a synapse by its name and pass order's parameters
+        curl -i -H "Content-Type: application/json" --user admin:secret -X POST  \
+        -d '{"no_voice":"true", "parameters": {"parameter1": "value1" }}' \
+        http://127.0.0.1:5000/synapses/start/id/say-hello-fr
+
+        :param synapse_name: name(id) of the synapse to execute
         :return:
         """
         # get a synapse object from the name
+        logger.debug("[FlaskAPI] run_synapse_by_name: synapse name -> %s" % synapse_name)
         synapse_target = BrainLoader().get_brain().get_synapse_by_name(synapse_name=synapse_name)
 
+        # get no_voice_flag if present
+        no_voice = self.get_no_voice_flag_from_request(request)
+
+        # get parameters
+        parameters = self.get_parameters_from_request(request)
+
         if synapse_target is None:
             data = {
                 "synapse name not found": "%s" % synapse_name
@@ -148,13 +169,13 @@ class FlaskAPI(threading.Thread):
             return jsonify(error=data), 404
         else:
             # generate a MatchedSynapse from the synapse
-            matched_synapse = MatchedSynapse(matched_synapse=synapse_target)
+            matched_synapse = MatchedSynapse(matched_synapse=synapse_target, overriding_parameter=parameters)
             # get the current LIFO buffer
             lifo_buffer = LIFOBuffer()
             # this is a new call we clean up the LIFO
             lifo_buffer.clean()
             lifo_buffer.add_synapse_list_to_lifo([matched_synapse])
-            response = lifo_buffer.execute(is_api_call=True)
+            response = lifo_buffer.execute(is_api_call=True, no_voice=no_voice)
             data = jsonify(response)
             return data, 201
 
@@ -163,25 +184,36 @@ class FlaskAPI(threading.Thread):
         """
         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/synapses/start/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"}
-        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/order/
+
+        Can be used with no_voice flag
+        curl -i --user admin:secret -H "Content-Type: application/json" -X POST \
+        -d '{"order":"my order", "no_voice":"true"}' http://localhost:5000/synapses/start/order
+
         :return:
         """
         if not request.get_json() or 'order' not in request.get_json():
             abort(400)
 
         order = request.get_json('order')
+        # get no_voice_flag if present
+        no_voice = self.get_no_voice_flag_from_request(request)
         if order is not None:
             # get the order
             order_to_run = order["order"]
-
+            logger.debug("[FlaskAPI] run_synapse_by_order: order to run -> %s" % order_to_run)
             api_response = SynapseLauncher.run_matching_synapse_from_order(order_to_run,
                                                                            self.brain,
                                                                            self.settings,
-                                                                           is_api_call=True)
+                                                                           is_api_call=True,
+                                                                           no_voice=no_voice)
 
             data = jsonify(api_response)
             return data, 201
@@ -191,13 +223,21 @@ class FlaskAPI(threading.Thread):
             }
             return jsonify(error=data), 400
 
+    @requires_auth
     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"
+
+        With no_voice flag
+        curl -i -H "Content-Type: application/json" --user admin:secret -X POST \
+        http://localhost:5000/synapses/start/audio -F "file=@path/to/file.wav" -F no_voice="true"
         :return:
         """
+        # get no_voice_flag if present
+        self.no_voice = self.str_to_bool(request.form.get("no_voice"))
+
         # check if the post request has the file part
         if 'file' not in request.files:
             data = {
@@ -205,38 +245,59 @@ class FlaskAPI(threading.Thread):
             }
             return jsonify(error=data), 400
 
-        file = request.files['file']
+        uploaded_file = request.files['file']
         # if user does not select file, browser also
         # submit a empty part without filename
-        if file.filename == '':
+        if uploaded_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.api_response is not None and self.api_response:
-                data = jsonify(self.api_response)
-                self.api_response = None
-                return data, 201
-            else:
-                data = {
-                    "error": "The given order doesn't match any synapses"
-                }
-                return jsonify(error=data), 400
+        # save the file
+        filename = secure_filename(uploaded_file.filename)
+        base_path = os.path.join(self.app.config['UPLOAD_FOLDER'])
+        uploaded_file.save(os.path.join(base_path, filename))
+
+        # now start analyse the audio with STT engine
+        audio_path = base_path + os.sep + filename
+        logger.debug("[FlaskAPI] run_synapse_by_audio: with file path %s" % audio_path)
+        if not self.allowed_file(audio_path):
+            audio_path = self._convert_to_wav(audio_file_path=audio_path)
+        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.api_response is not None and self.api_response:
+            data = jsonify(self.api_response)
+            self.api_response = None
+            logger.debug("[FlaskAPI] run_synapse_by_audio: data %s" % data)
+            return data, 201
+        else:
+            data = {
+                "error": "The given order doesn't match any synapses"
+            }
+            return jsonify(error=data), 400
+
+    @staticmethod
+    def _convert_to_wav(audio_file_path):
+        """
+        If not already .wav, convert an incoming audio file to wav format. Using system avconv (raspberry)
+        :param audio_file_path: the current full file path
+        :return: Wave file path
+        """
+        # Not allowed so convert into wav using avconv (raspberry)
+        base = os.path.splitext(audio_file_path)[0]
+        extension = os.path.splitext(audio_file_path)[1]
+        if extension != ".wav":
+            current_file_path = audio_file_path
+            audio_file_path = base + ".wav"
+            os.system("avconv -y -i " + current_file_path + " " + audio_file_path)  # --> deprecated
+            # subprocess.call(['avconv', '-y', '-i', audio_path, new_file_path], shell=True) # Not working ...
+
+        return audio_file_path
 
     @requires_auth
     def shutdown_server(self):
@@ -257,12 +318,61 @@ class FlaskAPI(threading.Thread):
         :param order: string order to analyse
         :return:
         """
-        logger.debug("order to process %s" % order)
+        logger.debug("[FlaskAPI] audio_analyser_callback: order to process -> %s" % order)
         api_response = SynapseLauncher.run_matching_synapse_from_order(order,
                                                                        self.brain,
                                                                        self.settings,
-                                                                       is_api_call=True)
+                                                                       is_api_call=True,
+                                                                       no_voice=self.no_voice)
         self.api_response = api_response
 
         # this boolean will notify the main process that the order have been processed
         self.order_analyser_return = True
+
+    def get_no_voice_flag_from_request(self, http_request):
+        """
+        Get the no_voice flag from the request if exist
+        :param http_request:
+        :return:
+        """
+
+        no_voice = False
+        try:
+            received_json = http_request.get_json(force=True, silent=True, cache=True)
+            if 'no_voice' in received_json:
+                no_voice = self.str_to_bool(received_json['no_voice'])
+        except TypeError:
+            # no json received
+            pass
+        logger.debug("[FlaskAPI] no_voice: %s" % no_voice)
+        return no_voice
+
+    @staticmethod
+    def str_to_bool(s):
+        if isinstance(s, bool):  # do not convert if already a boolean
+            return s
+        else:
+            if s == 'True' or s == 'true' or s == '1':
+                return True
+            elif s == 'False' or s == 'false' or s == '0':
+                return False
+            else:
+                return False
+
+    @staticmethod
+    def get_parameters_from_request(http_request):
+        """
+        Get "parameters" object from the
+        :param http_request:
+        :return:
+        """
+        parameters = None
+        try:
+            received_json = http_request.get_json(silent=False, force=True)
+            if 'parameters' in received_json:
+                parameters = received_json['parameters']
+        except TypeError:
+            pass
+        logger.debug("[FlaskAPI] Overridden parameters: %s" % parameters)
+
+        return parameters

+ 4 - 3
kalliope/core/SynapseLauncher.py

@@ -47,13 +47,14 @@ class SynapseLauncher(object):
             return lifo_buffer.execute(is_api_call=True)
 
     @classmethod
-    def run_matching_synapse_from_order(cls, order_to_process, brain, settings, is_api_call=False):
+    def run_matching_synapse_from_order(cls, order_to_process, brain, settings, is_api_call=False, no_voice=False):
         """
         
         :param order_to_process: the spoken order sent by the user
         :param brain: Brain object
         :param settings: Settings object
         :param is_api_call: if True, the current call come from the API. This info must be known by launched Neuron
+        :param no_voice: If true, the generated text will not be processed by the TTS engine
         :return: list of matched synapse
         """
 
@@ -63,7 +64,7 @@ class SynapseLauncher(object):
         # if the LIFO is not empty, so, the current order is passed to the current processing synapse as an answer
         if len(lifo_buffer.lifo_list) > 0:
             # the LIFO is not empty, this is an answer to a previous call
-            return lifo_buffer.execute(answer=order_to_process, is_api_call=is_api_call)
+            return lifo_buffer.execute(answer=order_to_process, is_api_call=is_api_call, no_voice=no_voice)
 
         else:  # the LIFO is empty, this is a new call
             # get a list of matched synapse from the order
@@ -85,4 +86,4 @@ class SynapseLauncher(object):
             lifo_buffer.add_synapse_list_to_lifo(list_synapse_to_process)
             lifo_buffer.api_response.user_order = order_to_process
 
-            return lifo_buffer.execute(is_api_call=is_api_call)
+            return lifo_buffer.execute(is_api_call=is_api_call, no_voice=no_voice)

+ 27 - 3
kalliope/stt/cmusphinx/README.md

@@ -15,11 +15,35 @@ Then install the python lib
 sudo pip install pocketsphinx
 ```
 
-Then, declare it as usual in your settings
+#### Parameters
+
+| parameter       | requiered | type   | default | choices | comment                                                                                                                                                |
+|-----------------|-----------|--------|---------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
+| language        | no        | string | en-US   |         | [Installing other languages](https://github.com/Uberi/speech_recognition/blob/master/reference/pocketsphinx.rst#installing-other-languages)            |
+| keyword_entries | no        | list   |         |         | List of tuples of the form (keyword, sensitivity), where keyword is a phrase, and sensitivity is how sensitive to this phrase the recognizer should be |
+| grammar_file    | no        | string |         |         | FSG or JSGF grammars file path. Note: If `keyword_entries` are passed, `grammar_file` will be ignored                                                  |
+
+Settings example
+```YAML
+default_speech_to_text: "cmusphinx"
+
+speech_to_text:  
+  - cmusphinx:
+      language: "en-US" #  [Installing other languages](https://github.com/Uberi/speech_recognition/blob/master/reference/pocketsphinx.rst#installing-other-languages)
+```
+
+#### Using keywords
+
+Sphinx usually operates in 'transcription mode' and will return whatever words it recognizes.
+Adding `keyword_entries` to the settings narrows down its search space and is more accurate than just looking for those same keywords in non-keyword-based transcriptions, because Sphinx knows specifically what sounds to look for.
+The parameter `keyword_entries` expects a list of tuples consisting of a phrase and a sensitivity level defining how sensitive to this phrase the recognizer should be, on a scale from 0 (very insensitive, more false negatives) to 1 (very sensitive, more false positives).
 ```YAML
 default_speech_to_text: "cmusphinx"
 
 speech_to_text:  
-  - cmusphinx
-    language: "en-US" #  [Installing other languages](https://github.com/Uberi/speech_recognition/blob/master/reference/pocketsphinx.rst#installing-other-languages)
+  - cmusphinx:
+      language: "en-US"
+      keyword_entries:
+        - ["hello", 0.8]
+        - ["stop the music", 0.6]
 ```

+ 7 - 1
kalliope/stt/cmusphinx/cmusphinx.py

@@ -18,6 +18,9 @@ class Cmusphinx(SpeechRecognition):
         # callback function to call after the translation speech/tex
         self.main_controller_callback = callback
         self.language = kwargs.get('language', "en-US")
+        self.keyword_entries = kwargs.get('keyword_entries', None)
+        # ge the grammar file if exist
+        self.grammar_file = kwargs.get('grammar_file', None)
 
         # start listening in the background
         self.set_callback(self.sphinx_callback)
@@ -29,7 +32,10 @@ class Cmusphinx(SpeechRecognition):
         called from the background thread
         """
         try:
-            captured_audio = recognizer.recognize_sphinx(audio, language=self.language)
+            captured_audio = recognizer.recognize_sphinx(audio,
+                                                         language=self.language,
+                                                         keyword_entries=self.keyword_entries,
+                                                         grammar=self.grammar_file)
             Utils.print_success("Sphinx Speech Recognition thinks you said %s" % captured_audio)
             self._analyse_audio(captured_audio)
 

+ 7 - 2
kalliope/trigger/snowboy/snowboy.py

@@ -27,6 +27,9 @@ class Snowboy(Thread):
         self.interrupted = False
         self.kill_received = False
 
+        # get the sensitivity if set by the user
+        self.sensitivity = kwargs.get('sensitivity', 0.5)
+
         # callback function to call when hotword caught
         self.callback = kwargs.get('callback', None)
         if self.callback is None:
@@ -41,7 +44,9 @@ class Snowboy(Thread):
         if not os.path.isfile(self.pmdl_path):
             raise SnowboyModelNotFounfd("The snowboy model file %s does not exist" % self.pmdl_path)
 
-        self.detector = snowboydecoder.HotwordDetector(self.pmdl_path, sensitivity=0.5, detected_callback=self.callback,
+        self.detector = snowboydecoder.HotwordDetector(self.pmdl_path,
+                                                       sensitivity=self.sensitivity,
+                                                       detected_callback=self.callback,
                                                        interrupt_check=self.interrupt_callback,
                                                        sleep_time=0.03)
 
@@ -106,4 +111,4 @@ class Snowboy(Thread):
             try:
                 stdio.__stderrp = devnull
             except KeyError:
-                stdio.fclose(devnull)
+                stdio.fclose(devnull)

+ 2 - 1
kalliope/tts/pico2wave/README.md

@@ -6,6 +6,7 @@ This TTS is based on the SVOX picoTTS engine
 |------------|----------|---------|--------------|-------------------------------------------------|
 | language   | YES      |         | 6 languages  | List of supported languages in the Note section |
 | cache      | No       | TRUE    | True / False | True if you want to use the cache with this TTS |
+| samplerate | No       | None    | int          | Pico2wave creates 16 khz files but not all USB devices support this. Set a value to                                                        convert to a specific samplerate. For Example: 44100|
 
 
 #### Notes :
@@ -17,4 +18,4 @@ Supported languages :
 - French fr-FR
 - Spanish es-ES
 - German de-DE
-- Italian it-IT
+- Italian it-IT

+ 11 - 1
kalliope/tts/pico2wave/pico2wave.py

@@ -2,6 +2,8 @@ import os
 import subprocess
 
 from kalliope.core.TTS.TTSModule import TTSModule
+import sox
+
 import logging
 import sys
 
@@ -13,6 +15,7 @@ class Pico2wave(TTSModule):
 
     def __init__(self, **kwargs):
         super(Pico2wave, self).__init__(**kwargs)
+        self.samplerate = kwargs.get('samplerate', None)
 
     def say(self, words):
         """
@@ -44,6 +47,13 @@ class Pico2wave(TTSModule):
 
         # generate the file with pico2wav
         subprocess.call(final_command, stderr=sys.stderr)
-
+        
+        # convert samplerate
+        if self.samplerate is not None:
+            tfm = sox.Transformer()
+            tfm.rate(samplerate=self.samplerate)
+            tfm.build(str(tmp_path), str(tmp_path)+("tmp_name.wav"))
+            os.rename(str(tmp_path)+("tmp_name.wav"), tmp_path)
+        
         # remove the extension .wav
         os.rename(tmp_path, self.file_path)

+ 5 - 4
setup.py

@@ -66,10 +66,10 @@ setup(
     # required libs
     install_requires=[
         'pyyaml>=3.12',
-        'six==1.10.0',
-        'SpeechRecognition>=3.6.0',
+        'six>=1.10.0',
+        'SpeechRecognition>=3.7.1',
         'markupsafe>=1.0',
-        'pyaudio>=0.2.10',
+        'pyaudio>=0.2.11',
         'pyasn1>=0.2.3',
         'ansible>=2.3',
         py2_prefix + 'pythondialog>=3.4.0',
@@ -90,7 +90,8 @@ setup(
         'sounddevice>=0.3.7',
         'SoundFile>=0.9.0',
         'pyalsaaudio>=0.8.4',
-        'RPi.GPIO>=0.6.3'
+        'RPi.GPIO>=0.6.3',
+        'sox>=1.3.0'
     ],