Browse Source

[Fix][Feature] #424 implement mute feature

Mute feature will replace the no_voice API parameter. This feature allows a global management of the mute state of Kalliope. Allows to manage kalliope mute for hooks and default neurons
ThiBuff 7 years ago
parent
commit
9c0aa9795c

+ 47 - 9
Docs/rest_api.md

@@ -149,11 +149,11 @@ Output example:
 }
 ```
 
-The [no_voice flag](#no-voice-flag) can be added to this call.
+The [mute flag](#mute-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
+-d '{"mute":"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. 
@@ -234,11 +234,11 @@ Or return an empty list of matched synapse
 }
 ```
 
-The [no_voice flag](#no-voice-flag) can be added to this call.
+The [mute flag](#mute-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
+-d '{"order":"my order", "mute":"true"}' http://localhost:5000/synapses/start/order
 ```
 
 ### Run a synapse from an audio file
@@ -302,10 +302,10 @@ Or return an empty list of matched synapse
 }
 ```
 
-The [no_voice flag](#no-voice-flag) can be added to this call with a form.
+The [mute flag](#mute-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"
+curl -i --user admin:secret -X POST http://localhost:5000/synapses/start/audio -F "file=@path/to/file.wav" -F mute="true"
 ```
 
 #### The neurotransmitter case
@@ -351,7 +351,7 @@ The response should be as follow:
 The ```"status": "waiting_for_answer"``` indicates that it waits for a response, so let's send it:
 
 ```bash
- --user admin:secret -H "Content-Type: application/json" -X POST -d '{"order":"not at all"}' http://localhost:5000/synapses/start/order
+curl -i --user admin:secret -H "Content-Type: application/json" -X POST -d '{"order":"not at all"}' http://localhost:5000/synapses/start/order
 ```
 
 ```JSON
@@ -408,6 +408,8 @@ Output example:
 ```
 
 ### Switch deaf status
+Kalliope can switch to 'deaf' mode, so it can not ear you anymore, the trigger/hotword is desactivated.
+However Kalliope continues to process synapses.
 
 Normal response codes: 200
 Error response codes: unauthorized(401), Bad request(400)
@@ -424,8 +426,44 @@ Output example:
 }
 ```
 
-## No voice flag
+## Mute 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.
+When `mute` is switched to true, Kalliope will not speak out loud on the server side.
+
+### Get mute status
+
+Normal response codes: 200
+Error response codes : unauthorized(401), Bad request(400)
+
+
+Curl command:
+```bash
+curl -i --user admin:secret  -X GET  http://127.0.0.1:5000/mute
+```
+
+Output example:
+```JSON
+{
+  "mute": true
+}
+```
+
+### Set mute status
+
+Normal response codes: 200
+Error response codes : unauthorized(401), Bad request(400)
+
+
+Curl command:
+```bash
+curl -i -H "Content-Type: application/json" --user admin:secret  -X POST -d '{"mute": "True"}' http://127.0.0.1:5000/mute
+```
+
+Output example:
+```JSON
+{
+  "mute": true
+}
+```

+ 1 - 0
Tests/settings/settings_test.yml

@@ -115,3 +115,4 @@ var_files:
 
 start_options:
   deaf: True
+  mute: False

+ 5 - 3
Tests/test_settings_loader.py

@@ -51,7 +51,7 @@ class TestSettingLoader(unittest.TestCase):
                 {'voxygen': {'voice': 'Agnes', 'cache': True}}
                 ],
             'var_files': ["../Tests/settings/variables.yml"],
-            'start_options': {'deaf': True},
+            'start_options': {'deaf': True, 'mute': False},
             'hooks': {'on_waiting_for_trigger': 'test',
                       'on_stop_listening': None,
                       'on_start_listening': None,
@@ -124,7 +124,8 @@ class TestSettingLoader(unittest.TestCase):
             "test": "kalliope"
         }
         settings_object.start_options = {
-            "deaf": True
+            "deaf": True,
+            "mute": False
         }
         settings_object.machine = platform.machine()
         settings_object.recognition_options = RecognitionOptions()
@@ -223,7 +224,8 @@ class TestSettingLoader(unittest.TestCase):
 
     def test_get_start_options(self):
         expected_result = {
-            "deaf": True
+            "deaf": True,
+            "mute": False
         }
         sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual(expected_result,

+ 5 - 3
kalliope/core/ConfigurationManager/SettingLoader.py

@@ -643,6 +643,7 @@ class SettingLoader(with_metaclass(Singleton, object)):
         """
         options = dict()
         deaf = False
+        mute = False
 
         try:
             start_options = settings["start_options"]
@@ -650,12 +651,13 @@ class SettingLoader(with_metaclass(Singleton, object)):
             start_options = None
 
         if start_options is not None:
-            try:
+            if start_options['deaf']:
                 deaf = start_options['deaf']
-            except KeyError:
-                deaf = False
+            if start_options['mute']:
+                mute = start_options['mute']
 
         options['deaf'] = deaf
+        options['mute'] = mute
 
         logger.debug("Start options: %s" % options)
         return options

+ 2 - 7
kalliope/core/Lifo/LIFOBuffer.py

@@ -39,7 +39,6 @@ class LIFOBuffer(object):
         self.lifo_list = list()
         self.answer = None
         self.is_api_call = False
-        self.no_voice = False
         self.is_running = False
         self.reset_lifo = False
 
@@ -79,7 +78,7 @@ class LIFOBuffer(object):
         self.api_response = APIResponse()
         return returned_api_response
 
-    def execute(self, answer=None, is_api_call=False, no_voice=False):
+    def execute(self, answer=None, is_api_call=False):
         """
         Process the LIFO list.
 
@@ -91,13 +90,11 @@ class LIFOBuffer(object):
 
         :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
         self.answer = answer
         self.is_api_call = is_api_call
-        self.no_voice = no_voice
 
         try:
             if not self.is_running:
@@ -170,9 +167,7 @@ class LIFOBuffer(object):
                 self.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"] = self.is_api_call
-            neuron.parameters["no_voice"] = self.no_voice
-            logger.debug("[LIFOBuffer] process_neuron_list: is_api_call: %s, no_voice: %s" % (self.is_api_call,
-                                                                                              self.no_voice))
+            logger.debug("[LIFOBuffer] process_neuron_list: is_api_call: %s" % (self.is_api_call))
             # execute the neuron
             instantiated_neuron = NeuronLauncher.start_neuron(neuron=neuron,
                                                               parameters_dict=matched_synapse.parameters)

+ 6 - 8
kalliope/core/NeuronModule.py

@@ -105,8 +105,6 @@ 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 deaf 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
@@ -171,11 +169,11 @@ class NeuronModule(object):
             # save in kalliope memory the last tts message
             Cortex.save("kalliope_last_tts_message", tts_message)
 
-            # 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")
+            # process the audio only if the mute flag is false
+            if self.settings.start_options["mute"]:
+                logger.debug("[NeuronModule] mute is True, Kalliope is muted")
             else:
-                logger.debug("[NeuronModule] no_voice is False, make Kalliope speaking")
+                logger.debug("[NeuronModule] mute is False, make Kalliope speaking")
                 HookManager.on_start_speaking()
                 # get the instance of the TTS module
                 tts_folder = None
@@ -237,7 +235,7 @@ class NeuronModule(object):
 
     @staticmethod
     def run_synapse_by_name(synapse_name, user_order=None, synapse_order=None, high_priority=False,
-                            is_api_call=False, overriding_parameter_dict=None, no_voice=False):
+                            is_api_call=False, overriding_parameter_dict=None):
         """
         call the lifo for adding a synapse to execute in the list of synapse list to process
         :param synapse_name: The name of the synapse to run
@@ -258,7 +256,7 @@ class NeuronModule(object):
         # get the singleton
         lifo_buffer = LifoManager.get_singleton_lifo()
         lifo_buffer.add_synapse_list_to_lifo(list_synapse_to_process, high_priority=high_priority)
-        lifo_buffer.execute(is_api_call=is_api_call, no_voice=no_voice)
+        lifo_buffer.execute(is_api_call=is_api_call)
 
     @staticmethod
     def is_order_matching(order_said, order_match):

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

@@ -62,9 +62,6 @@ class FlaskAPI(threading.Thread):
         if self.allowed_cors_origin is not False:
             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'])
         self.app.add_url_rule('/synapses', view_func=self.get_synapses, methods=['GET'])
@@ -75,6 +72,8 @@ class FlaskAPI(threading.Thread):
         self.app.add_url_rule('/shutdown/', view_func=self.shutdown_server, methods=['POST'])
         self.app.add_url_rule('/deaf/', view_func=self.get_deaf, methods=['GET'])
         self.app.add_url_rule('/deaf/', view_func=self.set_deaf, methods=['POST'])
+        self.app.add_url_rule('/mute/', view_func=self.get_mute, methods=['GET'])
+        self.app.add_url_rule('/mute/', view_func=self.set_mute, methods=['POST'])
 
     def run(self):
         self.app.run(host='0.0.0.0', port=int(self.port), debug=True, threaded=True, use_reloader=False)
@@ -145,11 +144,11 @@ class FlaskAPI(threading.Thread):
 
         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
+        -d '{"mute":"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" }}' \
+        -d '{"mute":"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
@@ -159,8 +158,11 @@ class FlaskAPI(threading.Thread):
         logger.debug("[FlaskAPI] run_synapse_by_name: synapse name -> %s" % synapse_name)
         synapse_target = BrainLoader().brain.get_synapse_by_name(synapse_name=synapse_name)
 
-        # get no_voice_flag if present
-        no_voice = self.get_boolean_flag_from_request(request, boolean_flag_to_find="no_voice")
+        # Store the mute value, then apply depending of the request parameters
+        old_mute_value = self.settings.start_options["mute"]
+        mute = self.get_boolean_flag_from_request(request, boolean_flag_to_find="mute")
+        if mute is not None:
+            self.settings.start_options["mute"] = mute
 
         # get parameters
         parameters = self.get_parameters_from_request(request)
@@ -169,6 +171,7 @@ class FlaskAPI(threading.Thread):
             data = {
                 "synapse name not found": "%s" % synapse_name
             }
+            self.settings.start_options["mute"] = old_mute_value
             return jsonify(error=data), 404
         else:
             # generate a MatchedSynapse from the synapse
@@ -176,8 +179,9 @@ class FlaskAPI(threading.Thread):
             # get the current LIFO buffer from the singleton
             lifo_buffer = LifoManager.get_singleton_lifo()
             lifo_buffer.add_synapse_list_to_lifo([matched_synapse])
-            response = lifo_buffer.execute(is_api_call=True, no_voice=no_voice)
+            response = lifo_buffer.execute(is_api_call=True)
             data = jsonify(response)
+            self.settings.start_options["mute"] = old_mute_value
             return data, 201
 
     @requires_auth
@@ -194,9 +198,9 @@ class FlaskAPI(threading.Thread):
         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
+        Can be used with mute 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
+        -d '{"order":"my order", "mute":"true"}' http://localhost:5000/synapses/start/order
 
         :return:
         """
@@ -204,8 +208,13 @@ class FlaskAPI(threading.Thread):
             abort(400)
 
         order = request.get_json('order')
-        # get no_voice_flag if present
-        no_voice = self.get_boolean_flag_from_request(request, boolean_flag_to_find="no_voice")
+
+        # Store the mute value, then apply depending of the request parameters
+        old_mute_value = self.settings.start_options["mute"]
+        mute = self.get_boolean_flag_from_request(request, boolean_flag_to_find="mute")
+        if mute is not None:
+            self.settings.start_options["mute"] = mute
+
         if order is not None:
             # get the order
             order_to_run = order["order"]
@@ -213,15 +222,16 @@ class FlaskAPI(threading.Thread):
             api_response = SynapseLauncher.run_matching_synapse_from_order(order_to_run,
                                                                            self.brain,
                                                                            self.settings,
-                                                                           is_api_call=True,
-                                                                           no_voice=no_voice)
+                                                                           is_api_call=True)
 
             data = jsonify(api_response)
+            self.settings.start_options["mute"] = old_mute_value
             return data, 201
         else:
             data = {
                 "error": "order cannot be null"
             }
+            self.settings.start_options["mute"] = old_mute_value
             return jsonify(error=data), 400
 
     @requires_auth
@@ -231,13 +241,10 @@ class FlaskAPI(threading.Thread):
         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"
+        With mute flag
+        curl -i --user admin:secret -X POST http://localhost:5000/synapses/start/audio -F "file=@path/to/file.wav" -F mute="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:
@@ -254,6 +261,12 @@ class FlaskAPI(threading.Thread):
                 "error": "No file provided"
             }
             return jsonify(error=data), 400
+
+        # Store the mute value, then apply depending of the request parameters
+        old_mute_value = self.settings.start_options["mute"]
+        if request.form.get("mute"):
+            self.settings.start_options["mute"] = self.str_to_bool(request.form.get("mute"))
+
         # save the file
         filename = secure_filename(uploaded_file.filename)
         base_path = os.path.join(self.app.config['UPLOAD_FOLDER'])
@@ -275,11 +288,13 @@ class FlaskAPI(threading.Thread):
             data = jsonify(self.api_response)
             self.api_response = None
             logger.debug("[FlaskAPI] run_synapse_by_audio: data %s" % data)
+            self.settings.start_options["mute"] = old_mute_value
             return data, 201
         else:
             data = {
                 "error": "The given order doesn't match any synapses"
             }
+            self.settings.start_options["mute"] = old_mute_value
             return jsonify(error=data), 400
 
     @staticmethod
@@ -349,7 +364,7 @@ class FlaskAPI(threading.Thread):
 
         # find the order signal and call the deaf method
         signal_order = SignalLauncher.get_order_instance()
-        if signal_order is not None:
+        if signal_order is not None and deaf is not None:
             signal_order.set_deaf_status(deaf)
             data = {
                 "deaf": signal_order.get_deaf_status()
@@ -361,6 +376,50 @@ class FlaskAPI(threading.Thread):
         }
         return jsonify(error=data), 400
 
+    @requires_auth
+    def get_mute(self):
+        """
+        Return the current mute status
+
+        Curl test
+        curl -i --user admin:secret  -X GET  http://127.0.0.1:5000/mute
+        """
+
+        # find the order signal and call the deaf method
+        if self.settings.start_options["mute"] is not None:
+            data = {
+                "mute": self.settings.start_options["mute"]
+            }
+            return jsonify(data), 200
+
+        # if no Order instance
+        data = {
+            "error": "mute status unknow"
+        }
+        return jsonify(error=data), 400
+
+    @requires_auth
+    def set_mute(self):
+        """
+        Set the Kalliope Core mute status (mute or not)
+
+        Curl test:
+        curl -i -H "Content-Type: application/json" --user admin:secret  -X POST \
+        -d '{"mute": "True"}' http://127.0.0.1:5000/mute
+        """
+
+        if not request.get_json() or 'mute' not in request.get_json():
+            abort(400)
+
+        # get mute if present
+        mute = self.get_boolean_flag_from_request(request, boolean_flag_to_find="mute")
+
+        self.settings.start_options["mute"] = mute
+        data = {
+            "mute": mute
+        }
+        return jsonify(data), 200
+
     def audio_analyser_callback(self, order):
         """
         Callback of the OrderListener. Called after the processing of the audio file
@@ -376,8 +435,7 @@ class FlaskAPI(threading.Thread):
         api_response = SynapseLauncher.run_matching_synapse_from_order(order,
                                                                        self.brain,
                                                                        self.settings,
-                                                                       is_api_call=True,
-                                                                       no_voice=self.no_voice)
+                                                                       is_api_call=True)
         self.api_response = api_response
 
         # this boolean will notify the main process that the order have been processed
@@ -385,12 +443,12 @@ class FlaskAPI(threading.Thread):
 
     def get_boolean_flag_from_request(self, http_request, boolean_flag_to_find):
         """
-        Get the boolean flag from the request if exist
+        Get the boolean flag from the request if exist, None otherwise !
         :param http_request:
         :param boolean_flag_to_find: json flag to find in the http_request
         :return: True or False if the boolean flag has been found in the request
         """
-        boolean_flag = False
+        boolean_flag = None
         try:
             received_json = http_request.get_json(force=True, silent=True, cache=True)
             if boolean_flag_to_find in received_json:

+ 3 - 4
kalliope/core/SynapseLauncher.py

@@ -64,14 +64,13 @@ class SynapseLauncher(object):
         return None
 
     @classmethod
-    def run_matching_synapse_from_order(cls, order_to_process, brain, settings, is_api_call=False, no_voice=False):
+    def run_matching_synapse_from_order(cls, order_to_process, brain, settings, is_api_call=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
         """
 
@@ -81,7 +80,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, no_voice=no_voice)
+            return lifo_buffer.execute(answer=order_to_process, is_api_call=is_api_call)
 
         else:  # the LIFO is empty, this is a new call
             # get a list of matched synapse from the order
@@ -95,6 +94,6 @@ class SynapseLauncher(object):
             lifo_buffer.add_synapse_list_to_lifo(list_synapse_to_process)
             lifo_buffer.api_response.user_order = order_to_process
 
-            execdata = lifo_buffer.execute(is_api_call=is_api_call, no_voice=no_voice)
+            execdata = lifo_buffer.execute(is_api_call=is_api_call)
             HookManager.on_processed_synapses()
             return execdata

+ 1 - 2
kalliope/neurons/neurotransmitter/neurotransmitter.py

@@ -53,8 +53,7 @@ class Neurotransmitter(NeuronModule):
                                                  user_order=audio,
                                                  synapse_order=answer,
                                                  high_priority=True,
-                                                 is_api_call=self.is_api_call,
-                                                 no_voice=self.no_voice)
+                                                 is_api_call=self.is_api_call)
                         found = True
                         break
             if not found:  # the answer do not correspond to any answer. We run the default synapse

+ 1 - 2
kalliope/neurons/neurotransmitter/tests/test_neurotransmitter.py

@@ -124,8 +124,7 @@ class TestNeurotransmitter(unittest.TestCase):
                                                                  user_order=audio_text,
                                                                  synapse_order="answer one",
                                                                  high_priority=True,
-                                                                 is_api_call=False,
-                                                                 no_voice=False)
+                                                                 is_api_call=False)
 
     def testInit(self):
         """

+ 2 - 1
kalliope/settings.yml

@@ -165,4 +165,5 @@ rest_api:
 # Start options
 # -------------
 #start_options:
-#  muted: False
+#  deaf: False
+#  mute: False