Эх сурвалжийг харах

Merge branch 'neurotransmitter' into dev

monf 8 жил өмнө
parent
commit
272826558d

+ 9 - 6
Docs/brain.md

@@ -104,18 +104,21 @@ When you will pronounce "say hello", it will trigger both synapses.
 If you want a better visibly, or simply sort your actions with different file, you can split the main brain file into multiple one.
 
 To do that, use the import statement in the entry brain.yml file with the following syntax:
-
 ```
 ---
-  - !include path/to/sub_brain.yml
-  - !include path/to/another_sub_brain.yml
+  - includes:
+      - path/to/sub_brain.yml
+      - path/to/another_sub_brain.yml
 ```
 
 E.g:
 ```
 ---
-  - !include brains/rolling_shutter_commands.yml
-  - !include brains/find_my_phone.yml
+  - includes:
+      - brains/rolling_shutter_commands.yml
+      - brains/find_my_phone.yml
 ```
 
->**Note:** You can only use the `!include` statement in the main brain file. 
+>**Note:** You can only use the `include` statement in the main brain file. 
+
+>**Note:** the includes statement must start with a `-`

+ 1 - 0
Docs/neuron_list.md

@@ -7,6 +7,7 @@ A neuron is a module you can use in your synapses. See the [complete neuron docu
 | [ansible_task](../neurons/ansible_task/)           | Run an ansible playbook                                                                 | ansible_task      |
 | [gmail_checker](../neurons/gmail_checker/)         | Get the number of unread email and their subjects from a gmail account                  | gmail_checker     |
 | [kill_switch](../neurons/kill_switch/)             | Stop Jarvis process                                                                     | kill_switch       |
+| [neurotransmitter](../neurons/neurotransmitter/)   | Link synapses together                                                                  | neurotransmitter  |
 | [push_message](../neurons/push_message/)           | Send a push message to a remote device like Android/iOS/Windows Phone or Chrome browser | push_message      |
 | [say](../neurons/say/)                             | Make Jarvis talk by using TTS                                                           | say               |
 | [script](../neurons/script/)                       | Run an executable script                                                                | script            |

+ 14 - 13
brain.yml

@@ -1,14 +1,15 @@
 ---
-
-  - !include brains/ansible_playbook.yml
-  - !include brains/gmail_checker.yml
-  - !include brains/kill_switch.yml
-  - !include brains/openweathermap.yml
-  - !include brains/push_message.yml
-  - !include brains/say.yml
-  - !include brains/script.yml
-  - !include brains/shell.yml
-  - !include brains/systemdate.yml
-  - !include brains/tasker_autoremote.yml
-  - !include brains/wake_on_lan.yml
-  - !include brains/twitter.yml
+- includes:
+  - brains/ansible_playbook.yml
+  - brains/gmail_checker.yml
+  - brains/kill_switch.yml
+  - brains/openweathermap.yml
+  - brains/push_message.yml
+  - brains/say.yml
+  - brains/script.yml
+  - brains/shell.yml
+  - brains/systemdate.yml
+  - brains/tasker_autoremote.yml
+  - brains/wake_on_lan.yml
+  - brains/twitter.yml
+  - brains/neurotransmitter.yml

+ 40 - 0
brains/neurotransmitter.yml

@@ -0,0 +1,40 @@
+---
+  - name: "synapse1"
+    neurons:
+      - say:
+          message: "aimez vous les frites?"
+      - neurotransmitter:
+          links:
+            - synapse: "synapse2"
+              answers:
+                - "absolument"
+                - "peut-être"
+            - synapse: "synapse3"
+              answers:
+                - "non"
+          default: "synapse4"
+    signals:
+      - order: "pose moi une question"
+
+  - name: "synapse2"
+    neurons:
+      - say:
+          message: "Vous aimez les frites! Moi aussi!"
+    signals:
+      - order: "synapse2"
+
+  - name: "synapse3"
+    neurons:
+      - say:
+          message: "vous n'aimez pas les frites. c'est pas grave."
+    signals:
+      - order: "synapse3"
+
+  - name: "synapse4"
+    neurons:
+      - say:
+          message: "Je n'ai pas compris votre réponse"
+    signals:
+      - order: "synapse4"
+
+

+ 8 - 11
core/ConfigurationManager/BrainLoader.py

@@ -41,17 +41,14 @@ class BrainLoader(object):
         # create list of Synapse
         synapses = list()
         for synapses_dict in dict_brain:
-            # print synapses_dict
-            # if the synapse is a list, it come from an include
-            if isinstance(synapses_dict, list):
-                synapses_dict = synapses_dict[0]
-            if ConfigurationChecker().check_synape_dict(synapses_dict):
-                # print "synapses_dict ok"
-                name = synapses_dict["name"]
-                neurons = cls._get_neurons(synapses_dict["neurons"])
-                signals = cls._get_signals(synapses_dict["signals"])
-                new_synapse = Synapse(name=name, neurons=neurons, signals=signals)
-                synapses.append(new_synapse)
+            if "includes" not in synapses_dict: # we don't need to check includes as it's not a synapse
+                if ConfigurationChecker().check_synape_dict(synapses_dict):
+                    # print "synapses_dict ok"
+                    name = synapses_dict["name"]
+                    neurons = cls._get_neurons(synapses_dict["neurons"])
+                    signals = cls._get_signals(synapses_dict["signals"])
+                    new_synapse = Synapse(name=name, neurons=neurons, signals=signals)
+                    synapses.append(new_synapse)
         brain.synapses = synapses
         if file_path is None:
             brain.brain_file = cls._get_root_brain_path()

+ 1 - 1
core/ConfigurationManager/ConfigurationChecker.py

@@ -50,7 +50,7 @@ class ConfigurationChecker:
 
         # check that the name is conform
         # Regex for [a - zA - Z0 - 9\-] with dashes allowed in between but not at the start or end
-        pattern = r'(?=[a-zA-Z0-9\-]{4,25}$)^[a-zA-Z0-9]+(\-[a-zA-Z0-9]+)*$'
+        pattern = r'(?=[a-zA-Z0-9\-]{4,100}$)^[a-zA-Z0-9]+(\-[a-zA-Z0-9]+)*$'
         prog = re.compile(pattern)
         result = prog.match(synape_dict["name"])
         if result is None:

+ 1 - 1
core/ConfigurationManager/SettingLoader.py

@@ -253,7 +253,7 @@ class SettingLoader(object):
                     raise SettingInvalidException("port must be in range 1024-65535")
 
             except KeyError, e:
-                print e
+                # print e
                 raise SettingNotFound("%s settings not found" % e)
 
             # config ok, we can return the rest api object

+ 32 - 11
core/ConfigurationManager/YAMLLoader.py

@@ -23,14 +23,6 @@ class YAMLLoader:
         Load settings file
         :return: cfg : the configuration file
         """
-        # # Load settings.
-        # __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
-        # try:
-        #     with open(os.path.join(__location__, yaml_file)) as ymlfile:
-        #         cfg = yaml.load(ymlfile)
-        #     return cfg
-        # except IOError:
-        #     raise YAMLFileNotFound("The file path %s does not exist" % yaml_file)
         current_dir = os.path.dirname(os.path.abspath(__file__))
         logger.debug("Current dir: %s " % current_dir)
         root_dir = os.path.join(current_dir, "../../")
@@ -39,8 +31,9 @@ class YAMLLoader:
         file_path_to_load = os.path.join(root_dir, yaml_file)
         logger.debug("File path to load: %s " % file_path_to_load)
         if os.path.isfile(yaml_file):
-            data = IncludeLoader(open(file_path_to_load, 'r')).get_data()
+            # data = IncludeLoader(open(file_path_to_load, 'r')).get_data()
             # print Utils.print_yaml_nicely(data)
+            data = IncludeImport(file_path_to_load).get_data()
             return data
         else:
             raise YAMLFileNotFound("File %s not found" % file_path_to_load)
@@ -50,7 +43,7 @@ class IncludeLoader(yaml.Loader):
 
     def __init__(self, *args, **kwargs):
         super(IncludeLoader, self).__init__(*args, **kwargs)
-        self.add_constructor('!include', self._include)
+        self.add_constructor('#include', self._include)
         if 'root' in kwargs:
             self.root = kwargs['root']
         elif isinstance(self.stream, file):
@@ -64,4 +57,32 @@ class IncludeLoader(yaml.Loader):
         self.root = os.path.dirname(filename)
         data = yaml.load(open(filename, 'r'))
         self.root = oldRoot
-        return data
+        return data
+
+
+class IncludeImport(object):
+
+    def __init__(self, file_path):
+        """
+        Load yaml file, with includes statement
+        :param file_path: path to the yaml file to load
+        """
+        self.data = yaml.load(open(file_path, 'r'))
+        # print "content: %s" % self.data
+        if isinstance(self.data, list):
+            for el in self.data:
+                if "includes" in el:
+                    for inc in el["includes"]:
+                        self.update(yaml.load(open(inc)))
+
+    def get_data(self):
+        return self.data
+
+    def update(self, data_to_add):
+        # print "cur_data: %s" % self.data
+        # print "data to add %s" % data_to_add
+        # we add each synapse inside the extended brain into the main brain data
+        for el in data_to_add:
+            self.data.append(el)
+        # print "final data: %s" % self.data
+

+ 8 - 1
core/MainController.py

@@ -5,6 +5,7 @@ import random
 from core import AudioPlayer
 from core import Utils
 from core.ConfigurationManager import SettingLoader
+from core.ConfigurationManager.BrainLoader import BrainLoader
 from core.OrderAnalyser import OrderAnalyser
 from core.OrderListener import OrderListener
 from core.TriggerLauncher import TriggerLauncher
@@ -22,6 +23,12 @@ class MainController:
         # get global configuration
         self.settings = SettingLoader.get_settings()
 
+        # load the brain
+        if brain_file is None:
+            self.brain = BrainLoader.get_brain()
+        else:
+            self.brain = BrainLoader.get_brain(file_path=brain_file)
+
         # run the api if the user want it
         if self.settings.rest_api.active:
             Utils.print_info("Starting REST API Listening port: %s" % self.settings.rest_api.port)
@@ -60,7 +67,7 @@ class MainController:
         Receive an order, try to retreive it in the brain.yml to launch to attached plugins
         :return:
         """
-        order_analyser = OrderAnalyser(order, main_controller=self, brain_file=self.brain_file)
+        order_analyser = OrderAnalyser(order, main_controller=self, brain=self.brain)
         order_analyser.start()
         # restart the trigger when the order analyser has finish his job
         Utils.print_info("Waiting for trigger detection")

+ 23 - 13
core/NeuronModule.py

@@ -6,6 +6,8 @@ import random
 import sys
 from jinja2 import Template
 
+from core import OrderListener
+from core.SynapseLauncher import SynapseLauncher
 from core.Utils import Utils
 from core.ConfigurationManager import SettingLoader
 
@@ -91,7 +93,7 @@ class NeuronModule(object):
 
         if isinstance(message, list):
             logger.debug("message is list")
-            tts_message = self._get_message_from_list(message)
+            tts_message = random.choice(message)
 
         if isinstance(message, dict):
             logger.debug("message is dict")
@@ -99,7 +101,7 @@ class NeuronModule(object):
 
         if tts_message is not None:
             # get an instance of the target TTS
-            tts_instance = self._get_tts_instance(self.tts)
+            tts_instance = Utils.get_dynamic_class_instantiation("tts", self.tts.capitalize())
             tts_args = None
             for tts_object in self.settings.ttss:
                 if tts_object.name == self.tts:
@@ -114,16 +116,12 @@ class NeuronModule(object):
 
             tts_instance.say(words=tts_message, **(tts_args if tts_args is not None else {}))
 
-    @staticmethod
-    def _get_message_from_list(message_list):
+    def _get_message_from_dict(self, message_dict):
         """
-        Return an element from the list randomly
-        :param message_list:
+        Generate a message taht can be played by a TTS engine from a dict of variable and the jinja template
+        :param message_dict:
         :return:
         """
-        return random.choice(message_list)
-
-    def _get_message_from_dict(self, message_dict):
         returned_message = None
 
         if (self.say_template is not None and self.file_template is None) or \
@@ -161,13 +159,25 @@ class NeuronModule(object):
         with open(real_file_template_path, 'r') as content_file:
             return content_file.read()
 
-    @staticmethod
-    def _get_tts_instance(tts_name):
-        return Utils.get_dynamic_class_instantiation("tts", tts_name.capitalize())
-
     @staticmethod
     def _update_cache_var(new_override_cache, args_list):
         logger.debug("args for TTS plugin before update: %s" % str(args_list))
         args_list["cache"] = new_override_cache
         logger.debug("args for TTS plugin after update: %s" % str(args_list))
         return args_list
+
+    @staticmethod
+    def get_audio_from_stt(callback):
+        """
+        Call the default STT to get an audio sample and return it into the callback method
+        :param callback:
+        :return:
+        """
+        # call the order listener
+        oa = OrderListener(callback=callback)
+        oa.start()
+
+    @staticmethod
+    def run_synapse_ny_name(name):
+        # TODO find a way to get the current brain file. NeuronModule doesn't have any ref about it
+        SynapseLauncher.start_synapse(name=name)

+ 3 - 8
core/OrderAnalyser.py

@@ -3,7 +3,6 @@ import re
 from collections import Counter
 
 from core.Utils import Utils
-from core.ConfigurationManager.BrainLoader import BrainLoader
 from core.Models import Order
 from core.NeuroneLauncher import NeuroneLauncher
 
@@ -14,21 +13,18 @@ logger = logging.getLogger("kalliope")
 
 
 class OrderAnalyser:
-    def __init__(self, order, main_controller=None, brain_file=None):
+    def __init__(self, order, main_controller=None, brain=None):
         """
         Class used to load brain and run neuron attached to the received order
         :param order: spelt order
         :param main_controller
-        :param brain_file: To override the default brain.yml file
+        :param brain: loaded brain
         """
         self.main_controller = main_controller
         self.order = order
         if isinstance(self.order, str):
             self.order = order.decode('utf-8')
-        if brain_file is None:
-            self.brain = BrainLoader.get_brain()
-        else:
-            self.brain = BrainLoader.get_brain(file_path=brain_file)
+        self.brain = brain
         logger.debug("OrderAnalyser, Received order: %s" % self.order)
 
     def start(self):
@@ -78,7 +74,6 @@ class OrderAnalyser:
                             else:
                                 Utils.print_danger("A problem has been found in the Synapse.")
 
-
         if not synapses_found:
             Utils.print_info("No synapse match the captured order: %s" % self.order)
 

+ 1 - 0
neurons/__init__.py

@@ -11,3 +11,4 @@ from openweathermap import Openweathermap
 from tasker_autoremote import Tasker_autoremote
 from wake_on_lan import Wake_on_lan
 from twitter import Twitter
+from neurotransmitter import Neurotransmitter

+ 67 - 0
neurons/neurotransmitter/README.md

@@ -0,0 +1,67 @@
+# neurotransmitter
+
+## Synopsis
+
+Link synapses together. Call a synapse from another one depending on the captured speech from the user.
+
+## Options
+
+| parameter | required | default | choices | comment                                                                                        |
+|-----------|----------|---------|---------|------------------------------------------------------------------------------------------------|
+| links     | yes      |         |         | List of tuple synapse/answer object                                                            |
+| synapse   | yes      |         |         | Name of the synapse to launch if the captured audio from the STT is present in the answer list |
+| answers   | yes      |         |         | List of sentences that are valid for running the attached synapse                              |
+| default   | yes      |         |         | Name of the synapse to launch if the captured audio doesn't match any answers                  |
+
+## Return Values
+
+None
+
+## Synapses example
+
+Here the synapse will ask the user if he likes french fries. If the user answer "yes" or "maybe", he will be redirected to the synapse2 that say something.
+If the user answer no, he will be redirected to another synapse that say something else.
+If the user say something that is not present in `answers`, he will be redirected to the synapse4.
+
+```
+ - name: "synapse1"
+    neurons:
+      - say:
+          message: "do you like french fries?"
+      - neurotransmitter:
+          links:
+            - synapse: "synapse2"
+              answers:
+                - "absolutely"
+                - "maybe"
+            - synapse: "synapse3"
+              answers:
+                - "no at all"
+          default: "synapse4"
+    signals:
+      - order: "ask me a question"
+
+  - name: "synapse2"
+    neurons:
+      - say:
+          message: "You like french fries!! Me too! I suppose..."
+    signals:
+      - order: "synapse2"
+
+  - name: "synapse3"
+    neurons:
+      - say:
+          message: "You don't like french fries. It's ok."
+    signals:
+      - order: "synapse3"
+      
+  - name: "synapse4"
+    neurons:
+      - say:
+          message: "I havn't understood your answer"
+    signals:
+      - order: "synapse4"
+```
+
+
+

+ 1 - 0
neurons/neurotransmitter/__init__.py

@@ -0,0 +1 @@
+from neurotransmitter import Neurotransmitter

+ 51 - 0
neurons/neurotransmitter/neurotransmitter.py

@@ -0,0 +1,51 @@
+import logging
+
+from core.NeuronModule import NeuronModule, MissingParameterException
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+
+class Neurotransmitter(NeuronModule):
+    def __init__(self, **kwargs):
+        super(Neurotransmitter, self).__init__(**kwargs)
+
+        # get links
+        self.links = kwargs.get('links', None)
+        self.default = kwargs.get('default', None)
+        # do some check
+        if self._links_content_ok():
+            # the brain seems fine, we call the stt to get an audio
+            self.get_audio_from_stt(callback=self.callback)
+
+    def callback(self, audio):
+        logger.debug("Neurotransmitter, receiver audio from STT: %s" % audio)
+        # print self.links
+        # set a bool to know if we have found a valid answer
+        found = False
+        for el in self.links:
+            if audio in el["answers"]:
+                found = True
+                self.run_synapse_ny_name(el["synapse"])
+                # we don't need to check to rest of answer
+                break
+        if not found:
+            # the answer do not correspond to any answer. We run the default synapse
+            self.run_synapse_ny_name(self.default)
+
+    def _links_content_ok(self):
+        """
+        Check the content of the links parameter
+        :return:
+        """
+        if self.links is None:
+            raise MissingParameterException("links parameter required and must contain at least one link")
+        if self.default is None:
+            raise MissingParameterException("default parameter is required and must contain a valid synapse name")
+        for el in self.links:
+            if "synapse" not in el:
+                raise MissingParameterException("Links must contain a synapse name: %s" % el)
+            if "answers" not in el:
+                raise MissingParameterException("Links must contain answers: %s" % el)
+
+        return True

+ 7 - 3
test.py

@@ -23,10 +23,14 @@ logger.setLevel(logging.DEBUG)
 
 SettingLoader.get_settings()
 
+brain = BrainLoader.get_brain()
+
+order = "pose moi une question"
+# order = "synapse2"
+oa = OrderAnalyser(order=order, brain=brain)
+
+oa.start()
 
-# app = Flask(__name__)
-# flask_api = FlaskAPI(app)
-# flask_api.start()