Răsfoiți Sursa

add feature normal, strict and ordered-strict order (#398)

* add feature normal, strict and ordered-strict order

* [Review] fix useless variable
Nicolas Marcq 7 ani în urmă
părinte
comite
5762272c9a

+ 203 - 24
Tests/test_order_analyser.py

@@ -26,14 +26,34 @@ class TestOrderAnalyser(unittest.TestCase):
         signal1 = Signal(name="order", parameters="this is the sentence")
         signal2 = Signal(name="order", parameters="this is the second sentence")
         signal3 = Signal(name="order", parameters="that is part of the third sentence")
+        signal4 = Signal(name="order", parameters={"matching-type": "strict",
+                                                   "text": "that is part of the fourth sentence"})
+        signal5 = Signal(name="order", parameters={"matching-type": "ordered-strict",
+                                                   "text": "sentence 5 with specific order"})
+        signal6 = Signal(name="order", parameters={"matching-type": "normal",
+                                                   "text": "matching type normal"})
+        signal7 = Signal(name="order", parameters={"matching-type": "non-existing",
+                                                   "text": "matching type non existing"})
+        signal8 = Signal(name="order", parameters={"matching-type": "non-existing",
+                                                   "non-existing-parameter": "will not match order"})
 
         synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1])
         synapse2 = Synapse(name="Synapse2", neurons=[neuron3, neuron4], signals=[signal2])
         synapse3 = Synapse(name="Synapse3", neurons=[neuron2, neuron4], signals=[signal3])
+        synapse4 = Synapse(name="Synapse4", neurons=[neuron2, neuron4], signals=[signal4])
+        synapse5 = Synapse(name="Synapse5", neurons=[neuron1, neuron2], signals=[signal5])
+        synapse6 = Synapse(name="Synapse6", neurons=[neuron1, neuron2], signals=[signal6])
+        synapse7 = Synapse(name="Synapse6", neurons=[neuron1, neuron2], signals=[signal7])
+        synapse8 = Synapse(name="Synapse6", neurons=[neuron1, neuron2], signals=[signal8])
 
         all_synapse_list = [synapse1,
                             synapse2,
-                            synapse3]
+                            synapse3,
+                            synapse4,
+                            synapse5,
+                            synapse6,
+                            synapse7,
+                            synapse8]
 
         br = Brain(synapses=all_synapse_list)
 
@@ -41,39 +61,70 @@ class TestOrderAnalyser(unittest.TestCase):
         spoken_order = "this is the sentence"
 
         # Create the matched synapse
-        matched_synapse_1 = MatchedSynapse(matched_synapse=synapse1,
-                                           matched_order=spoken_order,
-                                           user_order=spoken_order)
+        expected_matched_synapse_1 = MatchedSynapse(matched_synapse=synapse1,
+                                                    matched_order=spoken_order,
+                                                    user_order=spoken_order)
 
         matched_synapses = OrderAnalyser.get_matching_synapse(order=spoken_order, brain=br)
         self.assertEqual(len(matched_synapses), 1)
-        self.assertTrue(matched_synapse_1 in matched_synapses)
+        self.assertTrue(expected_matched_synapse_1 in matched_synapses)
+
+        # with defined normal matching type
+        spoken_order = "matching type normal"
+        expected_matched_synapse_5 = MatchedSynapse(matched_synapse=synapse6,
+                                                    matched_order=spoken_order,
+                                                    user_order=spoken_order)
+
+        matched_synapses = OrderAnalyser.get_matching_synapse(order=spoken_order, brain=br)
+        self.assertEqual(len(matched_synapses), 1)
+        self.assertTrue(expected_matched_synapse_5 in matched_synapses)
 
         # TEST2: should return synapse1 and 2
         spoken_order = "this is the second sentence"
+        expected_matched_synapse_2 = MatchedSynapse(matched_synapse=synapse1,
+                                                    matched_order=spoken_order,
+                                                    user_order=spoken_order)
         matched_synapses = OrderAnalyser.get_matching_synapse(order=spoken_order, brain=br)
         self.assertEqual(len(matched_synapses), 2)
-        self.assertTrue(synapse1, synapse2 in matched_synapses)
+        self.assertTrue(expected_matched_synapse_1, expected_matched_synapse_2 in matched_synapses)
 
         # TEST3: should empty
         spoken_order = "not a valid order"
         matched_synapses = OrderAnalyser.get_matching_synapse(order=spoken_order, brain=br)
         self.assertFalse(matched_synapses)
 
-    def test_spelt_order_match_brain_order_via_table(self):
-        order_to_test = "this is the order"
-        sentence_to_test = "this is the order"
+        # TEST4: with matching type strict
+        spoken_order = "that is part of the fourth sentence"
+        expected_matched_synapse_3 = MatchedSynapse(matched_synapse=synapse4,
+                                                    matched_order=spoken_order,
+                                                    user_order=spoken_order)
+        matched_synapses = OrderAnalyser.get_matching_synapse(order=spoken_order, brain=br)
+        self.assertTrue(expected_matched_synapse_3 in matched_synapses)
 
-        # Success
-        self.assertTrue(OrderAnalyser.spelt_order_match_brain_order_via_table(order_to_test, sentence_to_test))
+        spoken_order = "that is part of the fourth sentence with more word"
+        matched_synapses = OrderAnalyser.get_matching_synapse(order=spoken_order, brain=br)
+        self.assertFalse(matched_synapses)
+
+        # TEST5: with matching type ordered strict
+        spoken_order = "sentence 5 with specific order"
+        expected_matched_synapse_4 = MatchedSynapse(matched_synapse=synapse5,
+                                                    matched_order=spoken_order,
+                                                    user_order=spoken_order)
+        matched_synapses = OrderAnalyser.get_matching_synapse(order=spoken_order, brain=br)
+        self.assertEqual(len(matched_synapses), 1)
+        self.assertTrue(expected_matched_synapse_4 in matched_synapses)
 
-        # Failure
-        sentence_to_test = "unexpected sentence"
-        self.assertFalse(OrderAnalyser.spelt_order_match_brain_order_via_table(order_to_test, sentence_to_test))
+        spoken_order = "order specific with 5 sentence"
+        matched_synapses = OrderAnalyser.get_matching_synapse(order=spoken_order, brain=br)
+        self.assertFalse(matched_synapses)
 
-        # Upper/lower cases
-        sentence_to_test = "THIS is THE order"
-        self.assertTrue(OrderAnalyser.spelt_order_match_brain_order_via_table(order_to_test, sentence_to_test))
+        # TEST6: non supported type of matching. should fallback to normal
+        spoken_order = "matching type non existing"
+        expected_matched_synapse_5 = MatchedSynapse(matched_synapse=synapse7,
+                                                    matched_order=spoken_order,
+                                                    user_order=spoken_order)
+        matched_synapses = OrderAnalyser.get_matching_synapse(order=spoken_order, brain=br)
+        self.assertTrue(expected_matched_synapse_5 in matched_synapses)
 
     def test_get_split_order_without_bracket(self):
         # Success
@@ -102,15 +153,143 @@ class TestOrderAnalyser(unittest.TestCase):
         self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result,
                          "No space brackets Fails to return the expected list")
 
-    def test_counter_subset(self):
-        list1 = ("word1", "word2")
-        list2 = ("word3", "word4")
-        list3 = ("word1", "word2", "word3", "word4")
+    def test_is_normal_matching(self):
+        # same order
+        test_order = "expected order in the signal"
+        test_signal = "expected order in the signal"
+
+        self.assertTrue(OrderAnalyser.is_normal_matching(user_order=test_order,
+                                                         signal_order=test_signal))
+
+        # not the same order
+        test_order = "this is an order"
+        test_signal = "expected order in the signal"
+
+        self.assertFalse(OrderAnalyser.is_normal_matching(user_order=test_order,
+                                                          signal_order=test_signal))
+
+        # same order with more word in the user order
+        test_order = "expected order in the signal with more word"
+        test_signal = "expected order in the signal"
+
+        self.assertTrue(OrderAnalyser.is_normal_matching(user_order=test_order,
+                                                         signal_order=test_signal))
+
+        # same order with bracket
+        test_order = "expected order in the signal"
+        test_signal = "expected order in the signal {{ variable }}"
+
+        self.assertTrue(OrderAnalyser.is_normal_matching(user_order=test_order,
+                                                         signal_order=test_signal))
+
+        # same order with bracket
+        test_order = "expected order in the signal variable_to_catch"
+        test_signal = "expected order in the signal {{ variable }}"
+
+        self.assertTrue(OrderAnalyser.is_normal_matching(user_order=test_order,
+                                                         signal_order=test_signal))
 
-        self.assertFalse(OrderAnalyser._counter_subset(list1, list2))
-        self.assertTrue(OrderAnalyser._counter_subset(list1, list3))
-        self.assertTrue(OrderAnalyser._counter_subset(list2, list3))
+        # same order with bracket and words after brackets
+        test_order = "expected order in the signal variable_to_catch other word"
+        test_signal = "expected order in the signal {{ variable }} other word"
+
+        self.assertTrue(OrderAnalyser.is_normal_matching(user_order=test_order,
+                                                         signal_order=test_signal))
+
+    def test_is_strict_matching(self):
+        # same order with same amount of word
+        test_order = "expected order in the signal"
+        test_signal = "expected order in the signal"
+
+        self.assertTrue(OrderAnalyser.is_strict_matching(user_order=test_order,
+                                                         signal_order=test_signal))
+
+        # same order but not the same amount of word
+        test_order = "expected order in the signal with more word"
+        test_signal = "expected order in the signal"
+
+        self.assertFalse(OrderAnalyser.is_strict_matching(user_order=test_order,
+                                                          signal_order=test_signal))
+
+        # same order with same amount of word and brackets
+        test_order = "expected order in the signal variable_to_catch"
+        test_signal = "expected order in the signal {{ variable }}"
+
+        self.assertTrue(OrderAnalyser.is_strict_matching(user_order=test_order,
+                                                         signal_order=test_signal))
+
+        # same order with same amount of word and brackets with words after last brackets
+        test_order = "expected order in the signal variable_to_catch other word"
+        test_signal = "expected order in the signal {{ variable }} other word"
+
+        self.assertTrue(OrderAnalyser.is_strict_matching(user_order=test_order,
+                                                         signal_order=test_signal))
+
+        # same order with same amount of word and brackets with words after last brackets but more words
+        test_order = "expected order in the signal variable_to_catch other word and more word"
+        test_signal = "expected order in the signal {{ variable }} other word"
+
+        self.assertFalse(OrderAnalyser.is_strict_matching(user_order=test_order,
+                                                          signal_order=test_signal))
+
+    def test_ordered_strict_matching(self):
+        # same order with same amount of word with same order
+        test_order = "expected order in the signal"
+        test_signal = "expected order in the signal"
+        self.assertTrue(OrderAnalyser.is_ordered_strict_matching(user_order=test_order,
+                                                                 signal_order=test_signal))
+
+        # same order with same amount of word without same order
+        test_order = "signal the in order expected"
+        test_signal = "expected order in the signal"
+        self.assertFalse(OrderAnalyser.is_ordered_strict_matching(user_order=test_order,
+                                                                  signal_order=test_signal))
+
+        # same order with same amount of word and brackets in the same order
+        test_order = "expected order in the signal variable_to_catch"
+        test_signal = "expected order in the signal {{ variable }}"
+
+        self.assertTrue(OrderAnalyser.is_ordered_strict_matching(user_order=test_order,
+                                                                 signal_order=test_signal))
+
+        # same order with same amount of word and brackets in the same order with words after bracket
+        test_order = "expected order in the signal variable_to_catch with word"
+        test_signal = "expected order in the signal {{ variable }} with word"
+
+        self.assertTrue(OrderAnalyser.is_ordered_strict_matching(user_order=test_order,
+                                                                 signal_order=test_signal))
+
+        # not same order with same amount of word and brackets
+        test_order = "signal the in order expected"
+        test_signal = "expected order in the signal {{ variable }}"
+        self.assertFalse(OrderAnalyser.is_ordered_strict_matching(user_order=test_order,
+                                                                  signal_order=test_signal))
+
+        # not same order with same amount of word and brackets with words after bracket
+        test_order = "word expected order in the signal variable_to_catch with"
+        test_signal = "expected order in the signal {{ variable }} with word"
+
+        self.assertFalse(OrderAnalyser.is_ordered_strict_matching(user_order=test_order,
+                                                                  signal_order=test_signal))
+
+    def test_is_order_matching(self):
+        # all lowercase
+        test_order = "expected order in the signal"
+        test_signal = "expected order in the signal"
+        self.assertTrue(OrderAnalyser.is_order_matching(user_order=test_order,
+                                                        signal_order=test_signal))
+
+        # with uppercase
+        test_order = "Expected Order In The Signal"
+        test_signal = "expected order in the signal"
+        self.assertTrue(OrderAnalyser.is_order_matching(user_order=test_order,
+                                                        signal_order=test_signal))
 
 
 if __name__ == '__main__':
     unittest.main()
+
+    # suite = unittest.TestSuite()
+    # suite.addTest(TestOrderAnalyser("test_get_matching_synapse"))
+    # runner = unittest.TextTestRunner()
+    # runner.run(suite)

+ 2 - 2
kalliope/core/NeuronModule.py

@@ -260,8 +260,8 @@ class NeuronModule(object):
 
     @staticmethod
     def is_order_matching(order_said, order_match):
-        return OrderAnalyser().spelt_order_match_brain_order_via_table(order_to_analyse=order_match,
-                                                                       user_said=order_said)
+        return OrderAnalyser().is_order_matching(signal_order=order_match,
+                                                 user_order=order_said)
 
     @staticmethod
     def _get_content_of_file(real_file_template_path):

+ 144 - 35
kalliope/core/OrderAnalyser.py

@@ -2,6 +2,9 @@
 import collections
 from collections import Counter
 import six
+from jinja2 import Template
+
+from kalliope.core.NeuronParameterLoader import NeuronParameterLoader
 
 from kalliope.core.Models.MatchedSynapse import MatchedSynapse
 from kalliope.core.Utils.Utils import Utils
@@ -50,15 +53,35 @@ class OrderAnalyser:
 
         # test each synapse from the brain
         for synapse in cls.brain.synapses:
-            # we are only concerned by synapse with a order type of signal
             for signal in synapse.signals:
-
+                # we are only concerned by synapse with a order type of signal
                 if signal.name == "order":
-                    if cls.spelt_order_match_brain_order_via_table(signal.parameters, order):
+                    # get the type of matching expected, by default "normal"
+                    expected_matching_type = "normal"
+                    signal_order = None
+
+                    if isinstance(signal.parameters, str):
+                        signal_order = signal.parameters
+                    if isinstance(signal.parameters, dict):
+                        try:
+                            signal_order = signal.parameters["text"]
+                        except KeyError:
+                            logger.debug("[OrderAnalyser] Warning, missing parameter 'text' in order. "
+                                         "Order will be skipped")
+                            continue
+                        try:
+                            expected_matching_type = signal.parameters["matching-type"]
+                        except KeyError:
+                            logger.debug("[OrderAnalyser] Warning, missing parameter 'matching-type' in order. "
+                                         "Fallback to 'normal'")
+
+                    if cls.is_order_matching(user_order=order,
+                                             signal_order=signal_order,
+                                             expected_order_type=expected_matching_type):
                         # the order match the synapse, we add it to the returned list
                         logger.debug("Order found! Run synapse name: %s" % synapse.name)
                         Utils.print_success("Order matched in the brain. Running synapse \"%s\"" % synapse.name)
-                        list_match_synapse.append(synapse_order_tuple(synapse=synapse, order=signal.parameters))
+                        list_match_synapse.append(synapse_order_tuple(synapse=synapse, order=signal_order))
 
         # create a list of MatchedSynapse from the tuple list
         list_synapse_to_process = list()
@@ -70,30 +93,6 @@ class OrderAnalyser:
 
         return list_synapse_to_process
 
-    @classmethod
-    def spelt_order_match_brain_order_via_table(cls, order_to_analyse, user_said):
-        """
-        return true if all formatted(_format_sentences_to_analyse(order_to_analyse, user_said)) strings
-                that are in the sentence are present in the order to test.
-        :param order_to_analyse: String order to test
-        :param user_said: String to compare to the order
-        :return: True if all string are present in the order
-        """
-        # Lowercase all incoming
-        order_to_analyse = order_to_analyse.lower()
-        user_said = user_said.lower()
-
-        logger.debug("[spelt_order_match_brain_order_via_table] "
-                     "order to analyse: %s, "
-                     "user sentence: %s"
-                     % (order_to_analyse, user_said))
-
-        list_word_user_said = user_said.split()
-        split_order_without_bracket = cls._get_split_order_without_bracket(order_to_analyse)
-
-        # if all words in the list of what the user said in in the list of word in the order
-        return cls._counter_subset(split_order_without_bracket, list_word_user_said)
-
     @staticmethod
     def _get_split_order_without_bracket(order):
         """
@@ -110,16 +109,126 @@ class OrderAnalyser:
         split_order = order.split()
         return split_order
 
-    @staticmethod
-    def _counter_subset(list1, list2):
+    @classmethod
+    def is_normal_matching(cls, user_order, signal_order):
         """
-        check if the number of occurrences matches
-        :param list1:
-        :param list2:
-        :return:
+        True if :
+            - all word in the user_order are present in the signal_order
+        :param user_order: order from the user
+        :param signal_order: order in the signal
+        :return: Boolean
         """
-        c1, c2 = Counter(list1), Counter(list2)
+        logger.debug("[OrderAnalyser] is_normal_matching called with user_order: %s, signal_order: %s" % (user_order,
+                                                                                                          signal_order))
+        split_user_order = user_order.split()
+        split_signal_order_without_brackets = cls._get_split_order_without_bracket(signal_order)
+
+        c1, c2 = Counter(split_signal_order_without_brackets), Counter(split_user_order)
         for k, n in c1.items():
             if n > c2[k]:
                 return False
         return True
+
+    @classmethod
+    def is_strict_matching(cls, user_order, signal_order):
+        """
+        True if :
+            - all word in the user_order are present in the signal_order
+            - no additional word
+        :param user_order: order from the user
+        :param signal_order: order in the signal
+        :return: Boolean
+        """
+        logger.debug("[OrderAnalyser] is_strict_matching called with user_order: %s, signal_order: %s" % (user_order,
+                                                                                                          signal_order))
+        if cls.is_normal_matching(user_order=user_order, signal_order=signal_order):
+
+            # if the signal order contains bracket, we need to instantiate it with loaded parameters from the user order
+            if Utils.is_containing_bracket(signal_order):
+                signal_order = cls._get_instantiated_order_signal_from_user_order(signal_order, user_order)
+
+            split_user_order = user_order.split()
+            split_instantiated_signal = signal_order.split()
+
+            if len(split_user_order) == len(split_instantiated_signal):
+                return True
+
+        return False
+
+    @classmethod
+    def is_ordered_strict_matching(cls, user_order, signal_order):
+        """
+       True if :
+            - all word in the user_order are present in the signal_order
+            - no additional word
+            - same order as word present in signal_order
+        :param user_order: order from the user
+        :param signal_order: order in the signal
+        :return: Boolean
+        """
+        logger.debug(
+            "[OrderAnalyser] ordered_strict_matching called with user_order: %s, signal_order: %s" % (user_order,
+                                                                                                      signal_order))
+        if cls.is_normal_matching(user_order=user_order, signal_order=signal_order) and \
+                cls.is_strict_matching(user_order=user_order, signal_order=signal_order):
+            # if the signal order contains bracket, we need to instantiate it with loaded parameters from the user order
+            if Utils.is_containing_bracket(signal_order):
+                signal_order = cls._get_instantiated_order_signal_from_user_order(signal_order, user_order)
+
+            split_user_order = user_order.split()
+            split_signal_order = signal_order.split()
+            return split_user_order == split_signal_order
+
+        return False
+
+    @classmethod
+    def is_order_matching(cls, user_order, signal_order, expected_order_type="normal"):
+        """
+        return True if the user_order matches the signal_order following the expected_order_type
+        where "expected_order_type" is in
+        - normal: normal matching. all words are present in the user_order. this is the default
+        - strict: only word in the user order match. no more word
+        - ordered-strict: only word in the user order and in the same order
+        :param user_order: order from the user
+        :param signal_order: order in the signal
+        :param expected_order_type: type of order (normal, strict, ordered-strict)
+        :return: True if the order match
+        """
+        matching_type_function = {
+            "normal": cls.is_normal_matching,
+            "strict": cls.is_strict_matching,
+            "ordered-strict": cls.is_ordered_strict_matching
+        }
+
+        # Lowercase all incoming
+        user_order = user_order.lower()
+        signal_order = signal_order.lower()
+
+        if expected_order_type in matching_type_function:
+            return matching_type_function[expected_order_type](user_order, signal_order)
+        else:
+            logger.debug("[OrderAnalyser] non existing matching-type: '%s', fallback to 'normal'" % expected_order_type)
+            return matching_type_function["normal"](user_order, signal_order)
+
+    @classmethod
+    def _get_instantiated_order_signal_from_user_order(cls, signal_order, user_order):
+        """
+        return instantiated signal order with parameters loaded from the user order
+        E.g:
+        signal_order = "this is an {{ variable }}
+        user_order = "this is an order"
+
+        returned value is: "this is an order"
+
+        :param user_order: the order from the user
+        :param signal_order: the order with brackets from the synapse
+        :return: jinja instantiated order from the signal
+        """
+        # get parameters
+        parameters_from_user_order = NeuronParameterLoader.get_parameters(synapse_order=signal_order,
+                                                                          user_order=user_order)
+        # we load variables into the expected order from the signal
+        t = Template(signal_order)
+        signal_order = t.render(**parameters_from_user_order)
+
+        return signal_order

+ 76 - 3
kalliope/signals/order/README.md

@@ -10,26 +10,99 @@ An **order** signal is a word, or a sentence caught by the microphone and proces
 |-----------|----------|---------|---------|-----------------------------------------------------|
 | order     | YES      |         |         | The order is passed directly without any parameters |
 
+Other way to write an order, with parameters:
+
+| parameter     | required | default            | choices                        | comment                                  |
+|---------------|----------|--------------------|--------------------------------|------------------------------------------|
+| text          | YES      | The order to match |                                |                                          |
+| matching-type | NO       | normal             | normal, strict, ordered-strict | Type of matching. See explanation bellow |
+
+**Matching-type:**
+- **normal**: Will match if all words are present in the spoken order.
+- **strict**: All word are present. No more word must be present in the spoken order.
+- **ordered-strict**: All word are present, no more word and all word are in the same order as defined in the signal.
+
 ## Values sent to the synapse
 
 None
 
 ## Synapses example
 
-### Simple order
+### Normal order
 
 Syntax:
 ```yml
 signals:
-    - order: "<sentence>"
+  - order: "<sentence>"
+
+signals:
+  - order:
+      text: "<sentence>"
+      matching-type: "normal"
 ```
 
 Example:
 ```yml
 signals:
-    - order: "please do this action"
+  - order: "please do this action"
+
+signals:
+  - order:
+      text: "please do this action"
+      matching-type: "normal"
 ```
 
+In this example, with a `normal` matching type, the synapse would be triggered if the user say:
+- please do this action
+- please do this action with more word
+- action this do please
+- action this do please with more word
+
+### Strict order
+
+Syntax:
+```yml
+signals:
+    - order:
+        text: "<sentence>"
+        matching-type: "strict"
+```
+
+Example:
+```yml
+signals:
+    - order:
+        text: "please do this action"
+        matching-type: "strict"
+```
+
+In this example, with a `strict` matching type, the synapse would be triggered if the user say:
+- please do this action
+- action this do please
+
+### Ordered strict order
+
+Syntax:
+```yml
+signals:
+    - order:
+        text: "<sentence>"
+        matching-type: "ordered-strict"
+```
+
+Example:
+```yml
+signals:
+    - order:
+        text: "please do this action"
+        matching-type: "ordered-strict"
+```
+
+In this example, with a `strict` matching type, the synapse would be triggered if the user say:
+- please do this action
+
+### Notes
+
 > **Important note:** SST engines can misunderstand what you say, or translate your sentence into text containing some spelling mistakes.
 For example, if you say "Kalliope please do this", the SST engine can return "caliope please do this". So, to be sure that your speaking order will be correctly caught and executed, we recommend you to test your STT engine by using the [Kalliope GUI](kalliope_cli.md) and check the returned text for the given order.