Browse Source

add no_voice flag to rest api
update
fix type

nico 7 years ago
parent
commit
e2ab8c57c8

+ 27 - 0
Docs/rest_api.md

@@ -147,6 +147,13 @@ 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
+```
+
 
 ### Run a synapse from an order
 
@@ -217,6 +224,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 +291,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.

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

+ 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

+ 78 - 24
kalliope/core/RestAPI/FlaskAPI.py

@@ -1,26 +1,22 @@
 import logging
 import os
 import threading
-
-import subprocess
 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")
@@ -63,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'])
@@ -140,13 +139,20 @@ 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
+        :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)
+
         if synapse_target is None:
             data = {
                 "synapse name not found": "%s" % synapse_name
@@ -160,7 +166,7 @@ class FlaskAPI(threading.Thread):
             # 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
 
@@ -169,17 +175,27 @@ 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"]
@@ -187,7 +203,8 @@ class FlaskAPI(threading.Thread):
             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
@@ -203,28 +220,34 @@ class FlaskAPI(threading.Thread):
         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:
         """
-        # check if the post request has the file part
+        # 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 = {
                 "error": "No file provided"
             }
             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
         # save the file
-        filename = secure_filename(file.filename)
+        filename = secure_filename(uploaded_file.filename)
         base_path = os.path.join(self.app.config['UPLOAD_FOLDER'])
-        file.save(os.path.join(base_path, filename))
+        uploaded_file.save(os.path.join(base_path, filename))
 
         # now start analyse the audio with STT engine
         audio_path = base_path + os.sep + filename
@@ -290,8 +313,39 @@ class FlaskAPI(threading.Thread):
         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()
+            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

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