diff --git a/.travis.yml b/.travis.yml index 9c7fd3c0..3c2e6408 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ before_install: - sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu trusty-backports main restricted universe multiverse" - sudo apt-get update - sudo apt-get install $(cat install/files/deb-packages_requirements.txt) -- sudo apt-get install libstdc++6 +- sudo apt-get install libstdc++6 libpython3.4-dev - wget https://bootstrap.pypa.io/get-pip.py - sudo python get-pip.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f2e9878..a4345a9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +v0.4.6 / 2017-10-03 +=================== +- add core neuron: neurotimer +- add core neuron: mqtt +- add core signal: mqtt +- new feature: community signal now supported +- dict can be used in global variables +- bug fix: python 3 execution with snowboy lib +- new feature: kalliope memory +- add espeak tts to core +- add stt options. manual or dynamic threshold +- Fix: neurotransmitter bracket in answer + v0.4.5 / 2017-07-23 =================== - add keyword_entries attribute to CMU Sphinx diff --git a/Docs/brain.md b/Docs/brain.md index 47939808..cea3474b 100644 --- a/Docs/brain.md +++ b/Docs/brain.md @@ -7,6 +7,9 @@ Brain is composed by synapses: a synapse is the link between input and output ac An input action, called a "[signal](signals.md)" can be: - **an order:** Something that has been spoke out loud by the user. - **an event:** A date or a frequency (E.G: repeat each morning at 8:30) +- **a mqtt message** A message received on a MQTT topic +- A signal made the community +- **No signal**. Then the synapse can be only called from another synapse or by the API An output action is - **a list of neurons:** A [neuron](neurons.md) is a module or plugin that will perform some actions like simply talking, run a script, run a command or call a web service. diff --git a/Docs/installation.md b/Docs/installation.md index 1c585980..fc94758a 100644 --- a/Docs/installation.md +++ b/Docs/installation.md @@ -3,10 +3,10 @@ ## Prerequisites Please follow the right link bellow to install requirements depending on your target environment: -- [Raspbian (Raspberry Pi 2 & 3)](installation/raspbian_jessie.md) +- [Raspbian (Raspberry Pi 2 & 3)](installation/raspbian.md) - [Ubuntu 14.04](installation/ubuntu_14.04.md) - [Ubuntu 16.04](installation/ubuntu_16.04.md) -- [Debian Jessie](installation/debian_jessie.md) +- [Debian Jessie/Strech](installation/debian.md) ## Installation diff --git a/Docs/installation/debian_jessie.md b/Docs/installation/debian.md similarity index 78% rename from Docs/installation/debian_jessie.md rename to Docs/installation/debian.md index 57830bec..180497ca 100644 --- a/Docs/installation/debian_jessie.md +++ b/Docs/installation/debian.md @@ -1,13 +1,21 @@ -# Kalliope requirements for Debian Jessie +# Kalliope requirements for Debian Jessie/Strech ## Debian packages requirements Edit `/etc/apt/sources.list` and check that you have `contrib` and `non-free` are enabled: + +On Debian Jessie: ```bash deb http://httpredir.debian.org/debian jessie main contrib non-free deb-src http://httpredir.debian.org/debian jessie main contrib non-free ``` +On Debian Strech: +```bash +deb http://httpredir.debian.org/debian strech main contrib non-free +deb-src http://httpredir.debian.org/debian strech main contrib non-free +``` + Install some required system libraries and softwares: ```bash diff --git a/Docs/installation/raspbian_jessie.md b/Docs/installation/raspbian.md similarity index 88% rename from Docs/installation/raspbian_jessie.md rename to Docs/installation/raspbian.md index 6ac3bb75..316148a9 100644 --- a/Docs/installation/raspbian_jessie.md +++ b/Docs/installation/raspbian.md @@ -1,6 +1,6 @@ # Kalliope requirements for Raspbian -## Install the pre-compiled disk image +## Install via the pre-compiled disk image Download the last image [from the release page](https://github.com/kalliope-project/kalliope/releases) of Kalliope and load it as usual onto an SD card. @@ -14,10 +14,12 @@ The two starter config files are located in `/home/pi` for [French](https://gith ## Manual installation Supported Raspbian images: -[raspbian-2016-09-28](http://downloads.raspberrypi.org/raspbian/images/raspbian-2016-09-28/) -[raspbian-2016-11-29](http://downloads.raspberrypi.org/raspbian/images/raspbian-2016-11-29/) -[raspbian-2017-01-10](http://downloads.raspberrypi.org/raspbian/images/raspbian-2017-01-10/) -[raspbian-2017-04-10](http://downloads.raspberrypi.org/raspbian/images/raspbian-2017-04-10/) +- [raspbian-2016-09-28](http://downloads.raspberrypi.org/raspbian/images/raspbian-2016-09-28/) (Jessie based) +- [raspbian-2016-11-29](http://downloads.raspberrypi.org/raspbian/images/raspbian-2016-11-29/) (Jessie based) +- [raspbian-2017-01-10](http://downloads.raspberrypi.org/raspbian/images/raspbian-2017-01-10/) (Jessie based) +- [raspbian-2017-04-10](http://downloads.raspberrypi.org/raspbian/images/raspbian-2017-04-10/) (Jessie based) +- [raspbian-2017-08-16](http://downloads.raspberrypi.org/raspbian/images/raspbian-2017-04-10/) (Strech based) + > **Note:** It is recommended to use a **lite** installation of Raspbian without any graphical interface for a better experience. diff --git a/Docs/neurons.md b/Docs/neurons.md index 5878b98e..076562f5 100644 --- a/Docs/neurons.md +++ b/Docs/neurons.md @@ -119,6 +119,160 @@ As this is multi-lines, we can put the content in a file and use a `file_templat file_template: /path/to/file/template.j2 ``` +## kalliope_memory: Store in memory a variable from an order or generated from a neuron + +Kalliope can store in a short term memory: +- output parameters from a neuron +- variable parameters captured from an order. + +Stored parameters can then be used in other synapses during a future call. +Please not that this memory is not preserved after a restart of Kalliope. + +### Store parameters generated by a neuron + +Syntax with output parameters from a neuron +```yml +- name: "synapse-name" + signals: + - order: "my order" + neurons: + - neuron_name: + kalliope_memory: + key_name_in_memory: "{{ output_variable_from_neuron }}" + other_key_name_in_memory: "{{ other_output_variable_from_neuron }}" +``` + +Syntax to reuse memorized parameters in another synapse +```yml +- name: "synapse-name" + signals: + - order: "an order" + neurons: + - neuron_name: + parameter1: "{{ kalliope_memory['key_name_in_memory'] }}" + parameter2: "{{ kalliope_memory['other_key_name_in_memory'] }}" +``` + +>**Note:** The key name need to be placed into simple quotes + + +Example with a core neuron like `systemdate` +```yml +- name: "synapse-name" + signals: + - order: "my order" + neurons: + - systemdate: + say_template: + - "It' {{ hours }} hours and {{ minutes }} minutes" + kalliope_memory: + hours_when_asked: "{{ hours }}" + minutes_when_asked: "{{ minutes }}" +``` + +Here, the `systemdate` neuron generates variables that haven been passed to the template like described in the previous section and to the memory of Kalliope. + +Those parameters can now be used in a next call +```yml +- name: "synapse-name-2" + signals: + - order: "a what time I've asked the time?" + neurons: + - say: + message: + - "at {{ kalliope_memory['hours_when_asked']}} hours and {{ kalliope_memory['minutes_when_asked']}} minutes" +``` + +As it's based on a template, the value can be modified by adding a string +```yml +kalliope_memory: + my_saved_key: "{{ neuron_parameter_name }} with a word" +``` + +Multiple parameters can be used and concatenated in the same memorized key +```yml +kalliope_memory: + my_saved_key: "{{ neuron_parameter_name1 }} and {{ neuron_parameter_name2 }}" +``` + +### Store parameters captured from orders + +Syntax +```yml +- name: "synapse-name" + signals: + - order: "my order with {{ variable }}" + neurons: + - neuron_name: + kalliope_memory: + key_name_in_memory: "{{ variable }}" +``` + +The syntax to reuse memorized parameters in another synapse is the same as the one used with neuron parameters +```yml +- name: "synapse-name" + signals: + - order: "an order" + neurons: + - neuron_name: + parameter1: {{ kalliope_memory['key_name_in_memory']}} +``` + +Example +```yml +- name: "synapse-id" + signals: + - order: "say hello to {{ name }}" + neurons: + - say: + message: + - "Hello {{ name }}" + kalliope_memory: + friend: "{{ name }}" +``` + +Here, the variable "name" has been used directly into the template and also saved in memory behind the key "friend". +The value can now be used in a next call like the following +```yml +- name: "synapse-id" + signals: + - order: "what is the name of my friend?" + neurons: + - say: + message: + - "It's {{ kalliope_memory['friend'] }} +``` + +Here is another example brain whit use the `neurotimer` neuron. In this scenario, you want to remember to do something + +> **You:** remind me to call mom in 15 minutes
+**Kalliope:** I'll notify you in 15 minutes
+15 minutes later..
+**Kalliope:** You asked me to remind you to call mom 15 minutes ago + +```yml +- name: "remember-synapse" + signals: + - order: "remind me to {{ remember }} in {{ time }} minutes" + neurons: + - neurotimer: + seconds: "{{ time }}" + synapse: "remember-todo" + kalliope_memory: + remember: "{{ remember }}" + seconds: "{{ time }}" + - say: + message: + - "I'll remind you in {{ time }} minutes" + +- name: "remember-todo" + signals: {} + neurons: + - say: + message: + - "You asked me to remind you to {{ kalliope_memory['remember'] }} {{ kalliope_memory['time'] }} minutes ago" +``` + ## Overridable parameters @@ -172,4 +326,3 @@ You can override all parameter for each neurons like in the example bellow ``` >**Note:** The TTS must has been configured with its required parameters in the settings.yml file to be overridden. See [TTS documentation](tts.md). - diff --git a/Docs/rest_api.md b/Docs/rest_api.md index 17e3f56c..e49e13ea 100644 --- a/Docs/rest_api.md +++ b/Docs/rest_api.md @@ -12,6 +12,8 @@ Kalliope provides the REST API to manage the synapses. For configuring the API r | POST | /synapses/start/id/ | Run a synapse by its name | | POST | /synapses/start/order | Run a synapse from a text order | | POST | /synapses/start/audio | Run a synapse from an audio sample | +| GET | /mute | Get the current mute status | +| POST | /mute | Switch the mute status | ## Curl examples @@ -306,6 +308,39 @@ Curl command: curl -i --user admin:secret -X POST http://localhost:5000/synapses/start/audio -F "file=@path/to/file.wav" -F no_voice="true" ``` +### 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 +} +``` + +### Switch 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 +} +``` ## No voice flag diff --git a/Docs/settings.md b/Docs/settings.md index ba924aea..3070d8bc 100644 --- a/Docs/settings.md +++ b/Docs/settings.md @@ -4,7 +4,7 @@ This part of the documentation explains the main configuration of Kalliope place ## Triggers configuration -#### default_trigger +### default_trigger The trigger is the module detecting the hotword that will wake up Kalliope. Common usage of hotword include Alexa on Amazon Echo, OK Google on some Android devices and Hey Siri on iPhones. @@ -17,7 +17,7 @@ default_trigger: "trigger_name" Available triggers for Kalliope are: - snowboy -#### triggers +### triggers The hotword (also called a wake word or trigger word) detector is the engine in charge of waking up Kalliope. Each Trigger has it own configuration. This configuration is passed as argument following the syntax bellow @@ -38,7 +38,7 @@ See the complete list of [available triggers here](trigger.md). ## Players configuration -#### default_player +### default_player The player is the module managing the sound in Kalliope. @@ -51,7 +51,7 @@ E.g default_player: "mplayer" ``` -#### players +### players The player is the engine in charge of running sounds in Kalliope. Each Players has it own configuration. @@ -91,7 +91,7 @@ Core players are already packaged with the installation of Kalliope an can be us ## Speech to text configuration -#### default_speech_to_text +### default_speech_to_text A Speech To Text(STT) is an engine used to translate what you say into a text that can be processed by Kalliope core. By default, Kalliope uses google STT engine. @@ -108,7 +108,7 @@ default_speech_to_text: "google" Get the full list of [SST engine here](stt.md). -#### speech_to_text +### speech_to_text Each STT has it own configuration. This configuration is passed as argument as shown bellow ```yml speech_to_text: @@ -126,9 +126,58 @@ speech_to_text: Some arguments are required, some others are optional, please refer to the [STT documentation](stt.md) to know available parameters for each supported STT. +### recognition_options + +Represents a collection of speech recognition settings and functionality. +```yml +recognition_options: + option_name: option_value + option_name2: option_value2 +``` + +E.g +```yml +recognition_options: + energy_threshold: 3000 +``` + +#### energy_threshold + +Represents the energy level threshold for sounds. By default set to **4000**. +Values below this threshold are considered silence, and values above this threshold are considered speech. +This is adjusted automatically if dynamic thresholds are enabled with `adjust_for_ambient_noise_second` parameter. + +This threshold is associated with the perceived loudness of the sound, but it is a nonlinear relationship. +The actual energy threshold you will need depends on your microphone sensitivity or audio data. +Typical values for a silent room are 0 to 100, and typical values for speaking are between 150 and 3500. +Ambient (non-speaking) noise has a significant impact on what values will work best. + +If you're having trouble with the recognizer trying to recognize words even when you're not speaking, try tweaking this to a higher value. +If you're having trouble with the recognizer not recognizing your words when you are speaking, try tweaking this to a lower value. +For example, a sensitive microphone or microphones in louder rooms might have a ambient energy level of up to 4000. +```yml +recognition_options: + energy_threshold: 4000 +``` + +>**Note:** The default value is 4000 if not set + +#### adjust_for_ambient_noise_second + +If defined, will adjusts the energy threshold dynamically by capturing the current ambient noise of the room during the number of second set in the parameter. +When set, the `energy_threshold` parameter is overridden by the returned value of the noise calibration. +This value should be at least 0.5 in order to get a representative sample of the ambient noise. + +```yml +recognition_options: + adjust_for_ambient_noise_second: 1 +``` + +>**Note:** The number of second here represents the time between kalliope's awakening and the moment when you can give her your order. + ## Text to speech configuration -#### default_text_to_speech +### default_text_to_speech A Text To Speech is an engine used to translate written text into a speech, into an audio stream. By default, Kalliope use Pico2wave TTS engine. @@ -144,7 +193,7 @@ default_text_to_speech: "pico2wave" Get the full list of [TTS engine here](tts.md). -#### text_to_speech +### text_to_speech Each TTS has it own configuration. This configuration is passed as argument following the syntax bellow ```yml text_to_speech: @@ -165,7 +214,7 @@ Some arguments are required, some other optional, please refer to the [TTS docum ## Wake up answers configuration -#### random_wake_up_answers +### random_wake_up_answers When Kalliope detects your trigger/hotword/magic word, it lets you know that it's operational and now waiting for order. It's done by answering randomly one of the sentences provided in the variable random_wake_up_answers. @@ -187,7 +236,7 @@ random_wake_up_answers: - "Yes?" ``` -#### random_wake_up_sounds +### random_wake_up_sounds You can play a sound when Kalliope detects the hotword/trigger instead of saying something from the `random_wake_up_answers`. Place here a list of full paths of the sound files you want to use. Otherwise, you can use some default sounds provided by Kalliope which you can find in `/usr/lib/kalliope/sounds`. @@ -215,7 +264,7 @@ E.g: `# random_wake_up_answers:` ## On ready notification This section is used to notify the user when Kalliope is waiting for a trigger detection by playing a sound or speak a sentence out loud -#### play_on_ready_notification +### play_on_ready_notification This parameter define if you play the on ready notification: - `always`: every time Kalliope is ready to be awaken - `never`: never play a sound or sentences when kalliope is ready @@ -225,7 +274,7 @@ E.g: ```yml play_on_ready_notification: always ``` -#### on_ready_answers +### on_ready_answers The on ready notification can be a sentence. Place here a sentence or a list of sentence. If you set a list, one sentence will be picked up randomly E.g: @@ -235,7 +284,7 @@ on_ready_answers: - "Waiting for order" ``` -#### on_ready_sounds +### on_ready_sounds You can play a sound instead of a sentence. Remove the `on_ready_answers` parameters by commenting it out and use this one instead. Place here the path of the sound file. Files must be .wav or .mp3 format. @@ -272,22 +321,22 @@ rest_api: allowed_cors_origin: "*" ``` -#### active +### active To enable the rest api server. -#### port +### port The listening port of the web server. Must be an integer in range 1024-65535. -#### password_protected +### password_protected If `True`, the whole api will be password protected. #### Login Login used by the basic HTTP authentication. Must be provided if `password_protected` is `True` -#### Password +### Password Password used by the basic HTTP authentication. Must be provided if `password_protected` is `True` -#### Cors request +### Cors request If you want to allow request from external application, you'll need to enable the CORS requests settings by defining authorized origins. To do so, just indicated the origins that are allowed to leverage the API. The authorize values are: @@ -354,7 +403,7 @@ var_files: - variables.yml - variables2.yml ``` -/!\ If a variable is defined in different files, the last file defines the value. +> **Note:** If a variable is defined in different files, the last file defines the value. In the files the variables are defined by key/value: ```yml @@ -364,7 +413,6 @@ password: "secret" ``` And use variables in your neurons: -/!\ Because YAML format does no allow double braces not surrounded by quotes: you must use the variable between double quotes. ```yml - name: "run-simple-sleep" signals: @@ -376,6 +424,26 @@ And use variables in your neurons: password: "{{password}}" ``` +> **Note:** Because YAML format does no allow double braces not surrounded by quotes: you must use the variable between double quotes. + +A global variable can be a dictionary. Example: +```yml +contacts: + nico: "1234" + tibo: "5678" +``` + +And a synapse that use this dict: +```yml +- name: "test-var" + signals: + - order: "give me the number of {{ contact_to_search }}" + neurons: + - say: + message: + - "the number is {{ contacts[contact_to_search] }}" +``` + ## Raspberry LED and mute button LEDs connected to GPIO port of your Raspberry can be used to know current status of Kalliope. A button can also be added in order to pause the trigger process. Kalliope does not listen for the hotword anymore when pressed. diff --git a/Docs/signals.md b/Docs/signals.md index dc7ddbaa..1ed9a009 100644 --- a/Docs/signals.md +++ b/Docs/signals.md @@ -1,6 +1,6 @@ # Signals -A signal is an input event triggered by a synapse. When a signal is caught, Kalliope runs attached neurons of the synapse. The signal can be of two types: order and event. +A signal is an input event triggered by a synapse. When a signal is caught, Kalliope runs attached neurons of the synapse. The syntax is the following ```yml @@ -8,173 +8,22 @@ signals: - signal_type: parameter ``` -## Order - -### Simple order -An **order** signal is a word, or a sentence caught by the microphone and processed by the STT engine. - -Syntax: -```yml -signals: - - order: "" -``` - -Example: -```yml -signals: - - order: "please do this action" -``` - -> **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. - -> **Important note:** STT engines don't know the context. Sometime they will return an unexpected word. -For example, "the operation to perform is 2 minus 2" can return "two", "too", "to" or "2" in english. - -> **Important note:** Kalliope will try to match the order in each synapse of its brain. So, if an order of one synapse is included in another order of another synapse, then both synapses tasks will be started by Kalliope. - -> For example, you have "test my umbrella" in a synapse A and "test" in a synapse B. When you'll say "test my umbrella", both synapse A and B -will be started by Kalliope. So keep in mind that the best practice is to use really different sentences with more than one word for your order. - -### Order with arguments -You can add one or more arguments to an order by adding bracket to the sentence. - -Syntax: -```yml -signals: - - order: " {{ arg_name }}" - - order: " {{ arg_name }} " - - order: " {{ arg_name }} {{ arg_name }}" -``` - -Example: -```yml -signals: - - order: "I want to listen {{ artist_name }}" - - order: "start the {{ episode_number }} episode" - - order: "give me the weather at {{ location }} for {{ date }}" -``` - -Here, an example order would be speaking out loud the order: "I want to listen Amy Winehouse" -In this example, both word "Amy" and "Winehouse" will be passed as an unique argument called `artist_name` to the neuron. - -If you want to send more than one argument, you must split your argument with a word that Kalliope will use to recognise the start and the end of each arguments. -For example: "give me the weather at {{ location }} for {{ date }}" -And the order would be: "give me the weather at Paris for tomorrow" -And so, it will work too with: "give me the weather at St-Pierre de Chartreuse for tomorrow" - -See the **input values** section of the [neuron documentation](neurons) to know how to send arguments to a neuron. - ->**Important note:** The following syntax cannot be used: " {{ arg_name }} {{ arg_name2 }}" as Kalliope cannot know when a block starts and when it finishes. - -## Event - -An event is a way to schedule the launching of a synapse periodically at fixed times, dates, or intervals. - -The event system is based on [APScheduler](http://apscheduler.readthedocs.io/en/latest/modules/triggers/cron.html) which it is itself based on [Linux crontab](https://en.wikipedia.org/wiki/Cron). -When you declare an event in the signal, Kalliope will schedule the launching of the target synapse. - -The syntax of an event declaration in a synapse is the following +Or ```yml signals: - - event: - parameter1: "value1" - parameter2: "value2" + - signal_type: + parameter_key1: parameter_value1 + parameter_key2: parameter_value2 ``` -For example, if we want Kalliope to run the synapse every day a 8:30, the event will be declared like this: -```yml -- event: - hour: "8" - minute: "30" -``` - -### Parameters -Parameters are keyword you can use to build your event - -List of available parameter: - -| parameter | required | default | choices | comment | -|-------------|----------|---------|-----------------------------------------------------------------|-----------| -| year | no | * | 4 digit | E.g: 2016 | -| month | no | * | month (1-12) | | -| day | no | * | day of the (1-31) | | -| week | no | * | ISO week (1-53) | | -| day_of_week | no | * | number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun) | 6=Sunday | -| hour | no | * | hour (0-23) | | -| minute | no | * | minute (0-59) | | -| second | no | * | second (0-59) | | - -> **Note:** You must set at least one parameter from the list of parameter - -### Expression -Expressions can be used in value of each parameter. Multiple expression can be given in a single field, separated by commas. - -| Expression | Field | Description | -|------------|-------|-----------------------------------------------------------------------------------------| -| * | any | Fire on every value | -| */a | any | Fire every `a` values, starting from the minimum | -| a-b | any | Fire on any value within the `a-b` range (a must be smaller than b) | -| a-b/c | any | Fire every c values within the `a-b` range | -| xrd y | day | Fire on the `x` -rd occurrence of weekday `y` within the month | -| last x | day | Fire on the last occurrence of weekday `x` within the month | -| last x | day | Fire on the last day within the month | -| x,y,z | day | Fire on any matching expression; can combine any number of any of the above expressions | -### Examples +## Available signals -#### Web clock radio +Here is a list of core signal that are installed natively with Kalliope -Let's make a complete example. We want Kalliope to wake us up each morning of working day (Monday to friday) at 7:30 AM and: -- Wish us good morning -- Give us the time -- Play our favourite web radio - -The synapse in the brain would be -```yml - - name: "wake-up" - signals: - - event: - hour: "7" - minute: "30" - day_of_week: "1,2,3,4,5" - neurons: - - say: - message: - - "Good morning" - - systemdate: - say_template: - - "It is {{ hours }} hours and {{ minutes }} minutes" - - shell: - cmd: "mplayer http://192.99.17.12:6410/" - async: True -``` - -After setting up an event, you must restart Kalliope -```bash -python kalliope.py start -``` - -If the syntax is ok, Kalliope will show you each synapse that it has loaded in the crontab -``` -Add synapse name "wake-up" to the scheduler: cron[day_of_week='1,2,3,4,5', hour='7', minute='30'] -Event loaded -``` - -That's it, the synapse is now scheduled and will be started automatically. - - -#### Make Kalliope say something on the third Friday of June, July, August, November and December at 00:00, 01:00, 02:00 and 03:00 -```yml -- name: "wake-up" - signals: - - event: - day: "3rd fri" - month: "6-8,11-12" - hour: "0-3" - neurons: - - say: - message: - - "This is a schedulled sentence" -``` \ No newline at end of file +| Name | Description | +|--------------------------------------------------------|-------------------------------------------------------------------| +| [event](../kalliope/signals/event) | Launch synapses periodically at fixed times, dates, or intervals. | +| [mqtt_subscriber](../kalliope/signals/mqtt_subscriber) | Launch synapse from when receive a message from a MQTT broker | +| [order](../kalliope/signals/order) | Launch synapses from captured vocal order from the microphone | diff --git a/Docs/trigger.md b/Docs/trigger.md index 8db2bb24..ce79001b 100644 --- a/Docs/trigger.md +++ b/Docs/trigger.md @@ -28,6 +28,7 @@ kalliope-FR kalliope-EN kalliope-RU kalliope-DE +kalliope-IT ``` Then, open an issue or create a pull request to add the model to the list bellow. @@ -35,9 +36,10 @@ Then, open an issue or create a pull request to add the model to the list bellow > **Important note:** Do not enhance a model in the wrong language. Check the pronunciation before recording your voice! -| Name | language | Pronounced | -|-----------------------------------------------------|----------|----------------| -| [kalliope-FR](https://snowboy.kitt.ai/hotword/1363) | French | Ka-lio-pé | -| [kalliope-EN](https://snowboy.kitt.ai/hotword/2540) | English | kə-LIE-ə-pee | -| [kalliope-RU](https://snowboy.kitt.ai/hotword/2964) | Russian | каллиопа | -| [kalliope-DE](https://snowboy.kitt.ai/hotword/4324) | German | Ka-lio-pe | +| Name | language | Pronounced | +|------------------------------------------------------|----------|--------------| +| [kalliope-FR](https://snowboy.kitt.ai/hotword/1363) | French | Ka-lio-pé | +| [kalliope-EN](https://snowboy.kitt.ai/hotword/2540) | English | kə-LIE-ə-pee | +| [kalliope-RU](https://snowboy.kitt.ai/hotword/2964) | Russian | каллиопа | +| [kalliope-DE](https://snowboy.kitt.ai/hotword/4324) | German | Ka-lio-pe | +| [kalliope-IT](https://snowboy.kitt.ai/hotword/10650) | Italian | Ka-lljo-pe | diff --git a/Docs/tts_list.md b/Docs/tts_list.md index 46416637..2c981cc8 100644 --- a/Docs/tts_list.md +++ b/Docs/tts_list.md @@ -13,6 +13,7 @@ Core TTSs are already packaged with the installation of Kalliope an can be used | VoiceRSS | [VoiceRSS](../kalliope/tts/voicerss/README.md) | Cloud based | | Pico2wave | [Pico2wave](../kalliope/tts/pico2wave/README.md) | Self hosted | | ~~Voxygen~~ | ~~[Voxygen](../kalliope/tts/voxygen/README.md)~~ |~~Cloud based~~| +| Espeak | [Espeak](../kalliope/tts/espeak/README.md) | Self hosted | ## Community TTS Community TTSs need to be installed manually. @@ -21,7 +22,7 @@ To know how to install a community TTS, read the "Installation" section of the [ | Name | Description | Type | |--------|------------------------------------------------------|-------------| -| Espeak | [Espeak](https://github.com/Ultchad/kalliope-espeak) | Self hosted | + Wanna add your TTS in the list? Open [an issue](../../issues) with the link of your TTS or send a pull request to update the list directly. diff --git a/README.md b/README.md index 5c91f925..d2780b09 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,13 @@ Kalliope is easy-peasy to use, see the hello world message: "Hello world!" ``` +If you want an idea of what you can do with Kalliope, click on the image below +[![ENGLISH DEMO](https://img.youtube.com/vi/PcLzo4H18S4/0.jpg)](https://www.youtube.com/watch?v=PcLzo4H18S4) + ## Installation - [Kalliope installation documentation](Docs/installation.md) (Ubuntu/Debian/Raspbian) -- [Pre-compiled disk image for Raspberry](Docs/installation/raspbian_jessie.md) +- [Pre-compiled disk image for Raspberry](Docs/installation/raspbian.md) ## Quick start @@ -43,6 +46,7 @@ Once installed, you can start learning basics of Kalliope from a [quick start co - [Configure default settings](Docs/settings.md) - [Create the brain of your Kalliope](Docs/brain.md) - [Run Kalliope with CLI](Docs/kalliope_cli.md) +- See the list of [available neurons](https://kalliope-project.github.io/neurons_marketplace.html) with examples of usage ## Documentation summary @@ -62,30 +66,33 @@ Once installed, you can start learning basics of Kalliope from a [quick start co ## Contributing -If you'd like to contribute to Kalliope, please read our [Contributing Guide](Docs/contributing.md), which contains the philosophies to preserve, tests to run, and more. +If you'd like to contribute to Kalliope, please read our [Contributing Guide](Docs/contributing.md), which contains the philosophies to preserve, tests to run, and more. Reading through this guide before writing any code is recommended. - Read the [contributing guide](Docs/contributing.md) - Add [issues and feature requests](../../issues) - -You liked kalliope? **Star us!** +- [Chat](https://gitter.im/kalliope-project/Lobby) with the community or developers +- You liked kalliope? **Star us!** +- [Improve the Snowboy models](Docs/trigger.md) ## Credits > **Meaning of Kalliope** Kalliope means "beautiful voice" from Greek καλλος (kallos) "beauty" and οψ (ops) "voice". In Greek mythology she was a goddess of epic poetry and eloquence, one of the nine Muses. -kə-LIE-ə-pee (English) -Ka-li-o-pé (French) -каллиопа (Russian) - -[Improve models with Snowboy](Docs/trigger.md) +- kə-LIE-ə-pee (English) +- Ka-li-o-pé (French) +- каллиопа (Russian) ## Links -Demo French : [video](https://www.youtube.com/watch?v=t4J42yO2rkM) -Demo English : [video](https://www.youtube.com/watch?v=PcLzo4H18S4) -Android app : [Playstore](https://play.google.com/store/apps/details?id=kalliope.project) +- [Kalliope website](https://kalliope-project.github.io/) +- [Android app](https://play.google.com/store/apps/details?id=kalliope.project) +- [Chat](https://gitter.im/kalliope-project/Lobby) + +

+ +

## License diff --git a/Tests/backup_test_order_analyser.py b/Tests/backup_test_order_analyser.py deleted file mode 100644 index 7b59e0cf..00000000 --- a/Tests/backup_test_order_analyser.py +++ /dev/null @@ -1,587 +0,0 @@ -import unittest -import mock - -from kalliope.core.OrderAnalyser import OrderAnalyser -from kalliope.core.Models.Neuron import Neuron -from kalliope.core.Models.Synapse import Synapse -from kalliope.core.Models.Brain import Brain -from kalliope.core.Models.Settings import Settings -from kalliope.core.Models.Order import Order - - -class TestOrderAnalyser(unittest.TestCase): - - """Test case for the OrderAnalyser Class""" - - def setUp(self): - pass - - def test_start(self): - """ - Testing if the matches from the incoming messages and the signals/order sentences. - Scenarii : - - Order matchs a synapse and the synapse has been launched. - - Order does not match but have a default synapse. - - Order does not match and does not have default synapse. - - Provide synapse without any external orders - - Provide synapse with any external orders - """ - # Init - neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'}) - neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2'}) - neuron3 = Neuron(name='neurone3', parameters={'var3': 'val3'}) - neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4'}) - - signal1 = Order(sentence="this is the sentence") - signal2 = Order(sentence="this is the second sentence") - signal3 = Order(sentence="that is part of the third sentence") - - 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]) - - all_synapse_list = [synapse1, - synapse2, - synapse3] - - br = Brain(synapses=all_synapse_list) - - with mock.patch("kalliope.core.OrderAnalyser._start_neuron") as mock_start_neuron_method: - # assert synapses have been launched - order_to_match = "this is the sentence" - oa = OrderAnalyser(order=order_to_match, - brain=br) - expected_result = [synapse1] - - self.assertEquals(oa.start(), - expected_result, - "Fail to run the expected Synapse matching the order") - - calls = [mock.call(neuron1, {}), mock.call(neuron2, {})] - mock_start_neuron_method.assert_has_calls(calls=calls) - mock_start_neuron_method.reset_mock() - - # No order matching Default Synapse to run - order_to_match = "random sentence" - oa = OrderAnalyser(order=order_to_match, - brain=br) - oa.settings = mock.MagicMock(default_synapse="Synapse3") - expected_result = [synapse3] - self.assertEquals(oa.start(), - expected_result, - "Fail to run the default Synapse because no other synapses match the order") - - # No order matching no Default Synapse - order_to_match = "random sentence" - oa = OrderAnalyser(order=order_to_match, - brain=br) - oa.settings = mock.MagicMock() - expected_result = [] - self.assertEquals(oa.start(), - expected_result, - "Fail to no synapse because no synapse matchs and no default defined") - - # Provide synapse to run - order_to_match = "this is the sentence" - oa = OrderAnalyser(order=order_to_match, - brain=br) - expected_result = [synapse1] - synapses_to_run = [synapse1] - - self.assertEquals(oa.start(synapses_to_run=synapses_to_run), - expected_result, - "Fail to run the provided synapse to run") - calls = [mock.call(neuron1, {}), mock.call(neuron2, {})] - mock_start_neuron_method.assert_has_calls(calls=calls) - mock_start_neuron_method.reset_mock() - - # Provide synapse and external orders - order_to_match = "this is an external sentence" - oa = OrderAnalyser(order=order_to_match, - brain=br) - external_orders = "this is an external {{ order }}" - synapses_to_run = [synapse2] - expected_result = [synapse2] - - self.assertEquals(oa.start(synapses_to_run=synapses_to_run, external_order=external_orders), - expected_result, - "Fail to run a provided synapse with external order") - calls = [mock.call(neuron3, {"order":u"sentence"}), mock.call(neuron4, {"order":u"sentence"})] - mock_start_neuron_method.assert_has_calls(calls=calls) - mock_start_neuron_method.reset_mock() - - def test_start_neuron(self): - """ - Testing params association and starting a Neuron - """ - - neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4'}) - - with mock.patch("kalliope.core.NeuronLauncher.NeuronLauncher.start_neuron") as mock_start_neuron_method: - # Assert to the neuron is launched - neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'}) - params = { - 'param1':'parval1' - } - OrderAnalyser._start_neuron(neuron=neuron1,params=params) - mock_start_neuron_method.assert_called_with(neuron1) - mock_start_neuron_method.reset_mock() - - # Assert the params are well passed to the neuron - neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2', 'args': ['arg1', 'arg2']}) - params = { - 'arg1':'argval1', - 'arg2':'argval2' - } - OrderAnalyser._start_neuron(neuron=neuron2, params=params) - neuron2_params = Neuron(name='neurone2', - parameters={'var2': 'val2', - 'args': ['arg1', 'arg2'], - 'arg1':'argval1', - 'arg2':'argval2'} - ) - mock_start_neuron_method.assert_called_with(neuron2_params) - mock_start_neuron_method.reset_mock() - - # Assert the Neuron is not started when missing args - neuron3 = Neuron(name='neurone3', parameters={'var3': 'val3', 'args': ['arg3', 'arg4']}) - params = { - 'arg1': 'argval1', - 'arg2': 'argval2' - } - OrderAnalyser._start_neuron(neuron=neuron3, params=params) - mock_start_neuron_method.assert_not_called() - mock_start_neuron_method.reset_mock() - - # Assert no neuron is launched when waiting for args and none are given - neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4', 'args': ['arg5', 'arg6']}) - params = {} - OrderAnalyser._start_neuron(neuron=neuron4, params=params) - mock_start_neuron_method.assert_not_called() - mock_start_neuron_method.reset_mock() - - def test_spelt_order_match_brain_order_via_table(self): - order_to_test = "this is the order" - sentence_to_test = "this is the order" - - # Success - self.assertTrue(OrderAnalyser.spelt_order_match_brain_order_via_table(order_to_test, sentence_to_test), - "Fail matching order with the expected sentence") - - # Failure - sentence_to_test = "unexpected sentence" - self.assertFalse(OrderAnalyser.spelt_order_match_brain_order_via_table(order_to_test, sentence_to_test), - "Fail to ensure the expected sentence is not matching the order") - - # 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), - "Fail matching Upper/lower cases") - - def test_format_sentences_to_analyse(self): - # First capital in sentence - order_to_test = "this is the order" - sentence_to_test = "This is the order" - expected_result = "this is the order", "this is the order" - self.assertEqual(OrderAnalyser._format_sentences_to_analyse(order_to_analyse=order_to_test, - user_said=sentence_to_test), - expected_result, - "Fails formatting the sentences with first capital in sentence") - - # random uppercase in sentence - order_to_test = "this is the order" - sentence_to_test = "This IS the ordeR" - expected_result = "this is the order", "this is the order" - self.assertEqual(OrderAnalyser._format_sentences_to_analyse(order_to_analyse=order_to_test, - user_said=sentence_to_test), - expected_result, - "Fails formatting the sentences with random in sentence") - - # random uppercase in order - order_to_test = "thiS is THE orDer" - sentence_to_test = "this is the order" - expected_result = "this is the order", "this is the order" - self.assertEqual(OrderAnalyser._format_sentences_to_analyse(order_to_analyse=order_to_test, - user_said=sentence_to_test), - expected_result, - "Fails formatting the sentences with random in order") - - # random uppercase in both order and sentence - order_to_test = "thiS is THE orDer" - sentence_to_test = "THIS is the Order" - expected_result = "this is the order", "this is the order" - self.assertEqual(OrderAnalyser._format_sentences_to_analyse(order_to_analyse=order_to_test, - user_said=sentence_to_test), - expected_result, - "Fails formatting the sentences with random in both order and sentence") - - def test_get_split_order_without_bracket(self): - # Success - order_to_test = "this is the order" - expected_result = ["this", "is", "the", "order"] - self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result, - "No brackets Fails to return the expected list") - - order_to_test = "this is the {{ order }}" - expected_result = ["this", "is", "the"] - self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result, - "With spaced brackets Fails to return the expected list") - - order_to_test = "this is the {{order }}" # left bracket without space - expected_result = ["this", "is", "the"] - self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result, - "Left brackets Fails to return the expected list") - - order_to_test = "this is the {{ order}}" # right bracket without space - expected_result = ["this", "is", "the"] - self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result, - "Right brackets Fails to return the expected list") - - order_to_test = "this is the {{order}}" # bracket without space - expected_result = ["this", "is", "the"] - self.assertEqual(OrderAnalyser._get_split_order_without_bracket(order_to_test), expected_result, - "No space brackets Fails to return the expected list") - - def test_associate_order_params_to_values(self): - ## - # Testing the brackets position behaviour - ## - - # Success - order_brain = "This is the {{ variable }}" - order_user = "This is the value" - expected_result = {'variable': 'value'} - self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result, - "Fail to match the order_brain {{ variable }} to the 'value'") - - # Success - order_brain = "This is the {{variable }}" - order_user = "This is the value" - expected_result = {'variable': 'value'} - self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result, - "Fail to match the order_brain {{variable }} to the 'value'") - - # Success - order_brain = "This is the {{ variable}}" - order_user = "This is the value" - expected_result = {'variable': 'value'} - self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result, - "Fail to match the order_brain {{ variable}} to the 'value'") - - # Success - order_brain = "This is the {{variable}}" - order_user = "This is the value" - expected_result = {'variable': 'value'} - self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result, - "Fail to match the order_brain {{variable}} to the 'value'") - - # Fail - order_brain = "This is the {variable}" - order_user = "This is the value" - expected_result = {'variable': 'value'} - self.assertNotEquals(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result, - "Should not match the order_brain {variable} to the 'value'") - - # Fail - order_brain = "This is the { variable}}" - order_user = "This is the value" - expected_result = {'variable': 'value'} - self.assertNotEquals(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result, - "Should not match the order_brain { variable}} to the 'value'") - - ## - # Testing the brackets position in the sentence - ## - - # Success - order_brain = "{{ variable }} This is the" - order_user = "value This is the" - expected_result = {'variable': 'value'} - self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result, - "Fail to match the order_brain {{ variable }} in first position " - "ins the sentence to the 'value'") - - # Success - order_brain = "This is {{ variable }} the" - order_user = " This is value the" - expected_result = {'variable': 'value'} - self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result, - "Fail to match the order_brain {{ variable }} in middle position ins " - "the sentence to the 'value'") - - ## - # Testing multi variables - ## - - # Success - order_brain = "This is {{ variable }} the {{ variable2 }}" - order_user = "This is value the value2" - expected_result = {'variable': 'value', - 'variable2': 'value2'} - self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result, - "Fail to match the order_brain multi variable to the multi values") - - ## - # Testing multi words in variable - ## - - # Success - order_brain = "This is the {{ variable }}" - order_user = "This is the value with multiple words" - expected_result = {'variable': 'value with multiple words'} - self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result, - "Fail to match the order_brain {{ variable }} to the 'value with multiple words'") - - # Success - order_brain = "This is the {{ variable }} and {{ variable2 }}" - order_user = "This is the value with multiple words and second value multiple" - expected_result = {'variable': 'value with multiple words', - 'variable2': 'second value multiple'} - self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result, - "Fail to match the order_brain multiple variables with multiple words as values'") - - ## - # Specific Behaviour - ## - - # Upper/Lower case - order_brain = "This Is The {{ variable }}" - order_user = "ThiS is tHe VAlue" - expected_result = {'variable': 'VAlue'} - self.assertEqual(OrderAnalyser._associate_order_params_to_values(order_user, order_brain), expected_result, - "Fail to match the order_brain when using Upper/Lower cases") - - def test_get_matching_synapse_list(self): - # Init - neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'}) - neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2'}) - neuron3 = Neuron(name='neurone3', parameters={'var3': 'val3'}) - neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4'}) - - signal1 = Order(sentence="this is the sentence") - signal2 = Order(sentence="this is the second sentence") - signal3 = Order(sentence="that is part of the third sentence") - - 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]) - - order_to_match = "this is the sentence" - all_synapse_list = [synapse1, - synapse2, - synapse3] - - - - # Success - expected_result = synapse1 - oa_tuple_list = OrderAnalyser._get_matching_synapse_list(all_synapses_list=all_synapse_list, - order_to_match=order_to_match) - self.assertEquals(oa_tuple_list[0].synapse, - expected_result, - "Fail matching 'the expected synapse' from the complete synapse list and the order") - - # Multiple Matching synapses - signal2 = Order(sentence="this is the sentence") - - synapse2 = Synapse(name="Synapse2", neurons=[neuron3, neuron4], signals=[signal2]) - order_to_match = "this is the sentence" - - all_synapse_list = [synapse1, - synapse2, - synapse3] - - expected_result = [synapse1, - synapse2] - oa_tuple_list = OrderAnalyser._get_matching_synapse_list(all_synapses_list=all_synapse_list, - order_to_match=order_to_match) - self.assertEquals(oa_tuple_list[0].synapse, - expected_result[0], - "Fail 'Multiple Matching synapses' from the complete synapse list and the order (first element)") - self.assertEquals(oa_tuple_list[1].synapse, - expected_result[1], - "Fail 'Multiple Matching synapses' from the complete synapse list and the order (second element)") - - # matching no synapses - order_to_match = "this is not the correct word" - - all_synapse_list = [synapse1, - synapse2, - synapse3] - - expected_result = [] - - self.assertEquals(OrderAnalyser._get_matching_synapse_list(all_synapses_list=all_synapse_list, - order_to_match=order_to_match), - expected_result, - "Fail matching 'no synapses' from the complete synapse list and the order") - - # matching synapse with all key worlds - # /!\ Some words in the order are matching all words in synapses signals ! - order_to_match = "this is not the correct sentence" - all_synapse_list = [synapse1, - synapse2, - synapse3] - - expected_result = [synapse1, - synapse2] - - oa_tuple_list = OrderAnalyser._get_matching_synapse_list(all_synapses_list=all_synapse_list, - order_to_match=order_to_match) - - self.assertEquals(oa_tuple_list[0].synapse, - expected_result[0], - "Fail matching 'synapse with all key worlds' from the complete synapse list and the order (first element)") - self.assertEquals(oa_tuple_list[1].synapse, - expected_result[1], - "Fail matching 'synapse with all key worlds' from the complete synapse list and the order (second element)") - - def test_get_params_from_order(self): - - string_order = "this is the {{ sentence }}" - order_to_check = "this is the value" - expected_result = {'sentence': 'value'} - - self.assertEquals(OrderAnalyser._get_params_from_order(string_order=string_order, order_to_check=order_to_check), - expected_result, - "Fail to retrieve 'the params' of the string_order from the order") - - # Multiple match - string_order = "this is the {{ sentence }}" - - order_to_check = "this is the value with multiple words" - expected_result = {'sentence': 'value with multiple words'} - - self.assertEqual(OrderAnalyser._get_params_from_order(string_order=string_order, order_to_check=order_to_check), - expected_result, - "Fail to retrieve the 'multiple words params' of the string_order from the order") - - # Multiple params - string_order = "this is the {{ sentence }} with multiple {{ params }}" - - order_to_check = "this is the value with multiple words" - expected_result = {'sentence': 'value', - 'params':'words'} - - self.assertEqual(OrderAnalyser._get_params_from_order(string_order=string_order, order_to_check=order_to_check), - expected_result, - "Fail to retrieve the 'multiple params' of the string_order from the order") - - # Multiple params with multiple words - string_order = "this is the {{ sentence }} with multiple {{ params }}" - - order_to_check = "this is the multiple values with multiple values as words" - expected_result = {'sentence': 'multiple values', - 'params': 'values as words'} - - self.assertEqual(OrderAnalyser._get_params_from_order(string_order=string_order, order_to_check=order_to_check), - expected_result, - "Fail to retrieve the 'multiple params with multiple words' of the string_order from the order") - - # params at the begining of the sentence - string_order = "{{ sentence }} this is the sentence" - - order_to_check = "hello world this is the multiple values with multiple values as words" - expected_result = {'sentence': 'hello world'} - - self.assertEqual(OrderAnalyser._get_params_from_order(string_order=string_order, order_to_check=order_to_check), - expected_result, - "Fail to retrieve the 'params at the begining of the sentence' of the string_order from the order") - - # all of the sentence is a variable - string_order = "{{ sentence }}" - - order_to_check = "this is the all sentence is a variable" - expected_result = {'sentence': 'this is the all sentence is a variable'} - - self.assertEqual(OrderAnalyser._get_params_from_order(string_order=string_order, order_to_check=order_to_check), - expected_result, - "Fail to retrieve the 'all of the sentence is a variable' of the string_order from the order") - - def test_get_default_synapse_from_sysnapses_list(self): - # Init - neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'}) - neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2'}) - neuron3 = Neuron(name='neurone3', parameters={'var3': 'val3'}) - neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4'}) - - signal1 = Order(sentence="this is the sentence") - signal2 = Order(sentence="this is the second sentence") - signal3 = Order(sentence="that is part of the third sentence") - - 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]) - - default_synapse_name = "Synapse2" - all_synapse_list = [synapse1, - synapse2, - synapse3] - expected_result = synapse2 - - # Assert equals - self.assertEquals(OrderAnalyser._get_default_synapse_from_sysnapses_list(all_synapses_list=all_synapse_list, - default_synapse_name=default_synapse_name), - expected_result, - "Fail to match the expected default Synapse") - - def test_find_synapse_to_run(self): - """ - Test to find the good synapse to run - Scenarii: - - 1/ Find the synapse - - 2/ No synpase found, no default synapse - - 3/ No synapse found, run the default synapse - """ - # Init - neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'}) - neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2'}) - neuron3 = Neuron(name='neurone3', parameters={'var3': 'val3'}) - neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4'}) - - signal1 = Order(sentence="this is the sentence") - signal2 = Order(sentence="this is the second sentence") - signal3 = Order(sentence="that is part of the third sentence") - - 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]) - - all_synapse_list = [synapse1, - synapse2, - synapse3] - - br = Brain(synapses=all_synapse_list) - st = Settings() - # 1/ Find synapse - order = "this is the sentence" - expected_result = synapse1 - oa_tuple_list = OrderAnalyser._find_synapse_to_run(brain=br,settings=st, order=order) - self.assertEquals(oa_tuple_list[0].synapse, - expected_result, - "Fail to run the proper synapse matching the order") - - expected_result = signal1.sentence - self.assertEquals(oa_tuple_list[0].order, - expected_result, - "Fail to run the proper synapse matching the order") - - # 2/ No Default synapse - order = "No default synapse" - expected_result = [] - self.assertEquals(OrderAnalyser._find_synapse_to_run(brain=br,settings=st, order=order), - expected_result, - "Fail to run no synapse, when no default is defined") - - # 3/ Default synapse - st = Settings(default_synapse="Synapse2") - order = "default synapse" - expected_result = synapse2 - oa_tuple_list = OrderAnalyser._find_synapse_to_run(brain=br, settings=st, order=order) - self.assertEquals(oa_tuple_list[0].synapse, - expected_result, - "Fail to run the default synapse") - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_brain_loader.py b/Tests/test_brain_loader.py index f2b80edc..11cb89bb 100644 --- a/Tests/test_brain_loader.py +++ b/Tests/test_brain_loader.py @@ -2,13 +2,11 @@ import os import unittest -from kalliope.core.Models import Singleton +from kalliope.core.Models import Singleton, Signal from kalliope.core.ConfigurationManager import BrainLoader -from kalliope.core.Models import Event from kalliope.core.Models import Neuron from kalliope.core.Models import Synapse -from kalliope.core.Models import Order from kalliope.core.Models.Brain import Brain from kalliope.core.Models.Settings import Settings @@ -57,10 +55,10 @@ def test_get_brain(self): neuron = Neuron(name='say', parameters={'message': ['test message']}) neuron2 = Neuron(name='sleep', parameters={'seconds': 60}) - signal1 = Order(sentence="test_order") - signal2 = Order(sentence="test_order_2") - signal3 = Order(sentence="test_order_3") - signal4 = Order(sentence="order_for_int") + signal1 = Signal(name="order", parameters="test_order") + signal2 = Signal(name="order", parameters="test_order_2") + signal3 = Signal(name="order", parameters="test_order_3") + signal4 = Signal(name="order", parameters="order_for_int") synapse1 = Synapse(name="test", neurons=[neuron], signals=[signal1]) synapse2 = Synapse(name="test2", neurons=[neuron], signals=[signal2]) @@ -127,28 +125,13 @@ def test_get_neurons(self): def test_get_signals(self): signals = [{'order': 'test_order'}] - signal = Order(sentence='test_order') + signal = Signal(name="order", parameters="test_order") bl = BrainLoader(file_path=self.brain_to_test) signals_from_brain_loader = bl._get_signals(signals) self.assertEqual([signal], signals_from_brain_loader) - def test_get_event_or_order_from_dict(self): - - order_object = Order(sentence="test_order") - event_object = Event(hour="7") - - dict_order = {'order': 'test_order'} - dict_event = {'event': {'hour': '7'}} - - bl = BrainLoader(file_path=self.brain_to_test) - order_from_bl = bl._get_event_or_order_from_dict(dict_order) - event_from_bl = bl._get_event_or_order_from_dict(dict_event) - - self.assertEqual(order_from_bl, order_object) - self.assertEqual(event_from_bl, event_object) - def test_singleton(self): bl1 = BrainLoader(file_path=self.brain_to_test) bl2 = BrainLoader(file_path=self.brain_to_test) @@ -184,7 +167,7 @@ def test_replace_global_variables(self): } self.assertEqual(BrainLoader._replace_global_variables(parameter=parameters, - settings=st), + settings=st), expected_parameters, "Fail to assign a single global variable to parameters") @@ -203,7 +186,7 @@ def test_replace_global_variables(self): } self.assertEqual(BrainLoader._replace_global_variables(parameter=parameters, - settings=st), + settings=st), expected_parameters, "Fail to assign a global variable with string after to parameters") @@ -222,7 +205,7 @@ def test_replace_global_variables(self): } self.assertEqual(BrainLoader._replace_global_variables(parameter=parameters, - settings=st), + settings=st), expected_parameters, "Fail to assign global variable with int after to parameters") @@ -241,7 +224,7 @@ def test_replace_global_variables(self): } self.assertEqual(BrainLoader._replace_global_variables(parameter=parameters, - settings=st), + settings=st), expected_parameters, "Fail to assign multiple global variables to parameters") @@ -260,7 +243,7 @@ def test_replace_global_variables(self): } self.assertEqual(BrainLoader._replace_global_variables(parameter=parameters, - settings=st), + settings=st), expected_parameters, "Fail to assign a single global when parameter value is a list to neuron") @@ -280,7 +263,7 @@ def test_replace_global_variables(self): } self.assertEqual(BrainLoader._replace_global_variables(parameter=parameters, - settings=st), + settings=st), expected_parameters, "Fail to assign a single global when parameter value is a list to neuron") @@ -300,7 +283,7 @@ def test_get_global_variable(self): expected_result = "i am kalliope" self.assertEqual(BrainLoader._get_global_variable(sentence=sentence, - settings=st), + settings=st), expected_result) # test with accent @@ -308,7 +291,7 @@ def test_get_global_variable(self): expected_result = u"i am kalliopé" self.assertEqual(BrainLoader._get_global_variable(sentence=sentence, - settings=st), + settings=st), expected_result) # test with int @@ -316,7 +299,7 @@ def test_get_global_variable(self): expected_result = "i am 1" self.assertEqual(BrainLoader._get_global_variable(sentence=sentence, - settings=st), + settings=st), expected_result) diff --git a/Tests/test_configuration_checker.py b/Tests/test_configuration_checker.py index 79f3bd21..2a3d47b3 100644 --- a/Tests/test_configuration_checker.py +++ b/Tests/test_configuration_checker.py @@ -1,7 +1,7 @@ import unittest -from kalliope.core.ConfigurationManager.ConfigurationChecker import ConfigurationChecker, NoSynapeName, NoSynapeNeurons, \ - NoSynapeSignals, NoValidSignal, NoEventPeriod, NoValidOrder, MultipleSameSynapseName +from kalliope.core.ConfigurationManager.ConfigurationChecker import ConfigurationChecker, NoSynapeName, \ + NoSynapeNeurons, NoSynapeSignals, NoValidSignal, MultipleSameSynapseName from kalliope.core.Models import Synapse from kalliope.core.Utils.Utils import ModuleNotFoundError @@ -57,48 +57,14 @@ def test_check_neuron_dict(self): ConfigurationChecker.check_neuron_dict(invalid_neuron) def test_check_signal_dict(self): - valid_signal_with_order = {'order': 'test_order'} - valid_signal_with_event = {'event': '0 * * * *'} - invalid_signal = {'invalid_option': 'test_order'} + valid_signal = {'event': {'parameter_1': ['value1']}} + invalid_signal = {'non_existing_signal_name': {'parameter_2': ['value2']}} - self.assertTrue(ConfigurationChecker.check_signal_dict(valid_signal_with_order)) - self.assertTrue(ConfigurationChecker.check_signal_dict(valid_signal_with_event)) + self.assertTrue(ConfigurationChecker.check_signal_dict(valid_signal)) - with self.assertRaises(NoValidSignal): + with self.assertRaises(ModuleNotFoundError): ConfigurationChecker.check_signal_dict(invalid_signal) - def test_check_event_dict(self): - valid_event = { - "hour": "18", - "minute": "16" - } - invalid_event = None - invalid_event2 = "" - invalid_event3 = { - "notexisting": "12" - } - - self.assertTrue(ConfigurationChecker.check_event_dict(valid_event)) - - with self.assertRaises(NoEventPeriod): - ConfigurationChecker.check_event_dict(invalid_event) - with self.assertRaises(NoEventPeriod): - ConfigurationChecker.check_event_dict(invalid_event2) - with self.assertRaises(NoEventPeriod): - ConfigurationChecker.check_event_dict(invalid_event3) - - def test_check_order_dict(self): - valid_order = 'test_order' - invalid_order = '' - invalid_order2 = None - - self.assertTrue(ConfigurationChecker.check_order_dict(valid_order)) - - with self.assertRaises(NoValidOrder): - ConfigurationChecker.check_order_dict(invalid_order) - with self.assertRaises(NoValidOrder): - ConfigurationChecker.check_order_dict(invalid_order2) - def test_check_synapes(self): synapse_1 = Synapse(name="test") synapse_2 = Synapse(name="test2") diff --git a/Tests/test_cortex.py b/Tests/test_cortex.py new file mode 100644 index 00000000..9f58b563 --- /dev/null +++ b/Tests/test_cortex.py @@ -0,0 +1,163 @@ +import unittest + +from kalliope.core.Cortex import Cortex + + +class TestCortex(unittest.TestCase): + + def setUp(self): + # cleanup the cortex memory + Cortex.memory = dict() + Cortex.temp = dict() + + def test_get_memory(self): + test_memory = { + "key1": "value1", + "key2": "value2" + } + + Cortex.memory = test_memory + self.assertDictEqual(test_memory, Cortex.get_memory()) + + def test_save(self): + key_to_save = "key1" + value_to_save = "value1" + + expected_memory = { + "key1": "value1" + } + + Cortex.save(key=key_to_save, value=value_to_save) + self.assertDictEqual(expected_memory, Cortex.memory) + + def test_get_from_key(self): + test_memory = { + "key1": "value1", + "key2": "value2" + } + + Cortex.memory = test_memory + expected_value = "value2" + self.assertEqual(expected_value, Cortex.get_from_key("key2")) + + def test_add_parameters_from_order(self): + + order_parameters = { + "key1": "value1", + "key2": "value2" + } + + expected_temp_dict = { + "key1": "value1", + "key2": "value2" + } + + Cortex.add_parameters_from_order(order_parameters) + self.assertDictEqual(Cortex.temp, expected_temp_dict) + + def test_clean_parameter_from_order(self): + Cortex.temp = { + "key1": "value1", + "key2": "value2" + } + + Cortex.clean_parameter_from_order() + expected_temp_dict = dict() + self.assertDictEqual(expected_temp_dict, Cortex.memory) + + def test_save_neuron_parameter_in_memory(self): + + # test with a list of parameter with bracket + + neuron1_parameters = { + "key1": "value1", + "key2": "value2" + } + + dict_val_to_save = {"my_key_in_memory": "{{key1}}"} + + expected_dict = {"my_key_in_memory": "value1"} + + Cortex.save_neuron_parameter_in_memory(kalliope_memory_dict=dict_val_to_save, + neuron_parameters=neuron1_parameters) + + self.assertDictEqual(expected_dict, Cortex.memory) + + # test with a list of parameter with brackets and string + self.setUp() # clean + neuron1_parameters = { + "key1": "value1", + "key2": "value2" + } + + dict_val_to_save = {"my_key_in_memory": "string {{key1}}"} + + expected_dict = {"my_key_in_memory": "string value1"} + + Cortex.save_neuron_parameter_in_memory(kalliope_memory_dict=dict_val_to_save, + neuron_parameters=neuron1_parameters) + + self.assertDictEqual(expected_dict, Cortex.memory) + + # test with a list of parameter with only a string. Neuron parameters are not used + self.setUp() # clean + neuron1_parameters = { + "key1": "value1", + "key2": "value2" + } + + dict_val_to_save = {"my_key_in_memory": "string"} + + expected_dict = {"my_key_in_memory": "string"} + + Cortex.save_neuron_parameter_in_memory(kalliope_memory_dict=dict_val_to_save, + neuron_parameters=neuron1_parameters) + + self.assertDictEqual(expected_dict, Cortex.memory) + + # test with an empty list of parameter to save (no kalliope_memory set) + self.setUp() # clean + + neuron1_parameters = { + "key1": "value1", + "key2": "value2" + } + + dict_val_to_save = None + + Cortex.save_neuron_parameter_in_memory(kalliope_memory_dict=dict_val_to_save, + neuron_parameters=neuron1_parameters) + + self.assertDictEqual(dict(), Cortex.memory) + + def test_save_parameter_from_order_in_memory(self): + # Test with a value that exist in the temp memory + order_parameters = { + "key1": "value1", + "key2": "value2" + } + + Cortex.temp = order_parameters + + dict_val_to_save = {"my_key_in_memory": "{{key1}}"} + + expected_dict = {"my_key_in_memory": "value1"} + + Cortex.save_parameter_from_order_in_memory(dict_val_to_save) + + self.assertDictEqual(expected_dict, Cortex.memory) + + # test with a value that does not exsit + order_parameters = { + "key1": "value1", + "key2": "value2" + } + + Cortex.temp = order_parameters + dict_val_to_save = {"my_key_in_memory": "{{key3}}"} + + self.assertFalse(Cortex.save_parameter_from_order_in_memory(dict_val_to_save)) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_init.py b/Tests/test_init.py index 137699c1..10ba710c 100644 --- a/Tests/test_init.py +++ b/Tests/test_init.py @@ -43,10 +43,12 @@ def test_configure_logging(self): def test_main(self): # test start kalliope sys.argv = ['kalliope.py', 'start'] - with mock.patch('kalliope.core.MainController.__init__') as mock_maincontroller: - mock_maincontroller.return_value = None - main() - mock_maincontroller.assert_called() + with mock.patch('kalliope.start_rest_api') as mock_rest_api: + with mock.patch('kalliope.start_kalliope') as mock_start_kalliope: + mock_rest_api.return_value = None + main() + mock_rest_api.assert_called() + mock_start_kalliope.assert_called() # test start gui sys.argv = ['kalliope.py', 'gui'] diff --git a/Tests/test_lifo_buffer.py b/Tests/test_lifo_buffer.py index 5fd3ef80..957064b4 100644 --- a/Tests/test_lifo_buffer.py +++ b/Tests/test_lifo_buffer.py @@ -82,7 +82,7 @@ def test_execute(self): 'neuron_module_list': [ { 'neuron_name': 'Say', - 'generated_message': 'question in synapse 1' + 'generated_message': 'question in synapse 1' }, { 'neuron_name': 'Neurotransmitter', @@ -99,7 +99,7 @@ def test_execute(self): 'generated_message': 'enter synapse 2' } ], - 'synapse_name': 'synapse2' + 'synapse_name': 'synapse2' } ], 'user_order': None @@ -139,7 +139,7 @@ def test_execute(self): }, { 'matched_order': 'enter in synapse 1', - 'neuron_module_list':[ + 'neuron_module_list': [ { 'neuron_name': 'Say', 'generated_message': 'question in synapse 1' @@ -345,8 +345,8 @@ def test_process_neuron_list(self): self.lifo_buffer.set_api_call(True) self.lifo_buffer.set_answer("synapse 6 answer") with mock.patch("kalliope.core.TTS.TTSModule.generate_and_play"): - with self.assertRaises(SynapseListAddedToLIFO): - self.lifo_buffer._process_neuron_list(matched_synapse=matched_synapse) + self.assertRaises(SynapseListAddedToLIFO, + self.lifo_buffer._process_neuron_list(matched_synapse=matched_synapse)) if __name__ == '__main__': @@ -355,4 +355,4 @@ def test_process_neuron_list(self): # suite = unittest.TestSuite() # suite.addTest(TestLIFOBuffer("test_process_neuron_list")) # runner = unittest.TextTestRunner() - # runner.run(suite) \ No newline at end of file + # runner.run(suite) diff --git a/Tests/test_models.py b/Tests/test_models.py index 7fc70d72..29af9797 100644 --- a/Tests/test_models.py +++ b/Tests/test_models.py @@ -3,6 +3,8 @@ import mock from kalliope.core.Models.Player import Player +from kalliope.core.Models.Signal import Signal +from kalliope.core.Models.RecognitionOptions import RecognitionOptions from kalliope.core.Models.Tts import Tts from kalliope.core.Models.Trigger import Trigger @@ -16,7 +18,7 @@ from kalliope.core import LIFOBuffer from kalliope.core.Models.Settings import Settings -from kalliope.core.Models import Neuron, Order, Synapse, Brain, Event, Resources, Singleton +from kalliope.core.Models import Neuron, Synapse, Brain, Resources, Singleton from kalliope.core.Models.APIResponse import APIResponse from kalliope.core.Models.MatchedSynapse import MatchedSynapse @@ -34,9 +36,9 @@ def setUp(self): neuron3 = Neuron(name='neurone3', parameters={'var3': 'val3'}) neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4'}) - signal1 = Order(sentence="this is the sentence") - signal2 = Order(sentence="this is the second sentence") - signal3 = Order(sentence="that is part of the third sentence") + 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") self.synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) self.synapse2 = Synapse(name="Synapse2", neurons=[neuron3, neuron4], signals=[signal2]) @@ -119,35 +121,6 @@ def test_Dna(self): self.assertTrue(dna1.__eq__(dna3)) self.assertFalse(dna1.__eq__(dna2)) - def test_Event(self): - event1 = Event(year=2017, month=12, day=31, week=53, day_of_week=2, - hour=8, minute=30, second=0) - - event2 = Event(year=2018, month=11, day=30, week=25, day_of_week=4, - hour=9, minute=40, second=0) - - # same as the event1 - event3 = Event(year=2017, month=12, day=31, week=53, day_of_week=2, - hour=8, minute=30, second=0) - - expected_result_serialize = { - 'event': { - 'week': 53, - 'second': 0, - 'minute': 30, - 'hour': 8, - 'year': 2017, - 'day': 31, - 'day_of_week': 2, - 'month': 12 - } - } - - self.assertDictEqual(expected_result_serialize, event1.serialize()) - - self.assertTrue(event1.__eq__(event3)) - self.assertFalse(event1.__eq__(event2)) - def test_MatchedSynapse(self): user_order = "user order" matched_synapse1 = MatchedSynapse(matched_synapse=self.synapse1, matched_order=user_order) @@ -215,20 +188,6 @@ def test_Neuron(self): self.assertDictEqual(ast.literal_eval(neuron.__str__()), ast.literal_eval(expected_result_str)) - def test_Order(self): - order1 = Order(sentence="this is an order") - order2 = Order(sentence="this is an other order") - order3 = Order(sentence="this is an order") - - expected_result_serialize = {'order': 'this is an order'} - expected_result_str = "{'order': 'this is an order'}" - - self.assertEqual(expected_result_serialize, order1.serialize()) - self.assertEqual(expected_result_str, order1.__str__()) - - self.assertTrue(order1.__eq__(order3)) - self.assertFalse(order1.__eq__(order2)) - def test_Resources(self): resource1 = Resources(neuron_folder="/path/neuron", stt_folder="/path/stt", tts_folder="/path/tts", trigger_folder="/path/trigger") @@ -243,7 +202,8 @@ def test_Resources(self): 'tts_folder': '/path/tts', 'neuron_folder': '/path/neuron', 'stt_folder': '/path/stt', - 'trigger_folder': '/path/trigger' + 'trigger_folder': '/path/trigger', + 'signal_folder': None } self.assertDictEqual(expected_result_serialize, resource1.serialize()) @@ -284,6 +244,8 @@ def test_Settings(self): active=True, port=5000, allowed_cors_origin="*") + recognition_options = RecognitionOptions() + setting1 = Settings(default_tts_name="pico2wav", default_stt_name="google", default_trigger_name="swoyboy", @@ -301,7 +263,8 @@ def test_Settings(self): cache_path="/tmp/kalliope", default_synapse="default_synapse", resources=None, - variables={"key1": "val1"}) + variables={"key1": "val1"}, + recognition_options=recognition_options) setting1.kalliope_version = "0.4.5" setting2 = Settings(default_tts_name="accapela", @@ -320,7 +283,8 @@ def test_Settings(self): cache_path="/tmp/kalliope_tmp", default_synapse="my_default_synapse", resources=None, - variables={"key1": "val1"}) + variables={"key1": "val1"}, + recognition_options=recognition_options) setting2.kalliope_version = "0.4.5" setting3 = Settings(default_tts_name="pico2wav", @@ -340,7 +304,8 @@ def test_Settings(self): cache_path="/tmp/kalliope", default_synapse="default_synapse", resources=None, - variables={"key1": "val1"}) + variables={"key1": "val1"}, + recognition_options=recognition_options) setting3.kalliope_version = "0.4.5" expected_result_serialize = { @@ -372,7 +337,8 @@ def test_Settings(self): 'resources': None, 'triggers': ['snowboy'], 'rpi_settings': None, - 'players': ['mplayer'] + 'players': ['mplayer'], + 'recognition_options': {'energy_threshold': 4000, 'adjust_for_ambient_noise_second': 0} } self.assertDictEqual(expected_result_serialize, setting1.serialize()) @@ -398,8 +364,8 @@ def test_Synapse(self): neuron3 = Neuron(name='neurone3', parameters={'var3': 'val3'}) neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4'}) - signal1 = Order(sentence="this is the sentence") - signal2 = Order(sentence="this is the second sentence") + signal1 = Signal(name="order", parameters="this is the sentence") + signal2 = Signal(name="order", parameters="this is the second sentence") synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) synapse2 = Synapse(name="Synapse2", neurons=[neuron3, neuron4], signals=[signal2]) @@ -408,13 +374,14 @@ def test_Synapse(self): expected_result_serialize = { 'signals': [ { - 'order': 'this is the sentence' + 'name': 'order', + 'parameters': 'this is the sentence' } ], 'neurons': [ { 'name': 'neurone1', - 'parameters': { + 'parameters': { 'var1': 'val1' } }, @@ -471,3 +438,10 @@ def test_Tts(self): self.assertFalse(tts1.__eq__(tts2)) +if __name__ == '__main__': + unittest.main() + + # suite = unittest.TestSuite() + # suite.addTest(TestLIFOBuffer("test_process_neuron_list")) + # runner = unittest.TextTestRunner() + # runner.run(suite) diff --git a/Tests/test_neuron_launcher.py b/Tests/test_neuron_launcher.py index 48dd769e..434712d1 100644 --- a/Tests/test_neuron_launcher.py +++ b/Tests/test_neuron_launcher.py @@ -2,6 +2,7 @@ import unittest import mock +from kalliope.core.Models import Singleton from kalliope.core.Models.Resources import Resources from kalliope.core.NeuronLauncher import NeuronLauncher, NeuronParameterNotAvailable @@ -16,7 +17,11 @@ class TestNeuronLauncher(unittest.TestCase): """ def setUp(self): - pass + # clean settings + Singleton._instances = dict() + + def tearDown(self): + Singleton._instances = dict() #### # Neurons Launcher @@ -181,47 +186,87 @@ def test_replace_brackets_by_loaded_parameter(self): self.assertEqual(expected_result, NeuronLauncher._replace_brackets_by_loaded_parameter(neuron_parameters, loaded_parameters)) + # replacing with variable + sl = SettingLoader() + sl.settings.variables = { + "replaced": { + "name": u'replaced successfully' + } + } + + neuron_parameters = { + "param1": "this is a value {{ replaced['name'] }}" + } + + loaded_parameters = { + "name": "replaced successfully" + } + + expected_result = { + "param1": "this is a value replaced successfully" + } + + self.assertEqual(expected_result, NeuronLauncher._replace_brackets_by_loaded_parameter(neuron_parameters, + loaded_parameters)) + + # the parameter is a reserved key. for example from_answer_link from the neurotransmitter + list_reserved_keys = ["say_template", "file_template", "kalliope_memory", "from_answer_link"] + + for reserved_key in list_reserved_keys: + neuron_parameters = { + reserved_key: "this is a value with {{ 'brackets '}}" + } + + loaded_parameters = dict() + + expected_result = { + reserved_key: "this is a value with {{ 'brackets '}}" + } + + self.assertEqual(expected_result, NeuronLauncher._replace_brackets_by_loaded_parameter(neuron_parameters, + loaded_parameters)) + def test_parameters_are_available_in_loaded_parameters(self): # the parameter in bracket is available in the dict string_parameters = "this is a {{ parameter1 }}" loaded_parameters = {"parameter1": "value"} self.assertTrue(NeuronLauncher._neuron_parameters_are_available_in_loaded_parameters(string_parameters, - loaded_parameters)) + loaded_parameters)) # the parameter in bracket is NOT available in the dict string_parameters = "this is a {{ parameter1 }}" loaded_parameters = {"parameter2": "value"} self.assertFalse(NeuronLauncher._neuron_parameters_are_available_in_loaded_parameters(string_parameters, - loaded_parameters)) + loaded_parameters)) # the string_parameters doesn't contains bracket in bracket is available in the dict string_parameters = "this is a {{ parameter1 }}" loaded_parameters = {"parameter1": "value"} self.assertTrue(NeuronLauncher._neuron_parameters_are_available_in_loaded_parameters(string_parameters, - loaded_parameters)) + loaded_parameters)) # the string_parameters contains 2 parameters available in the dict string_parameters = "this is a {{ parameter1 }} and this is {{ parameter2 }}" loaded_parameters = {"parameter1": "value", "parameter2": "other value"} self.assertTrue(NeuronLauncher._neuron_parameters_are_available_in_loaded_parameters(string_parameters, - loaded_parameters)) + loaded_parameters)) # the string_parameters contains 2 parameters and one of them is not available in the dict string_parameters = "this is a {{ parameter1 }} and this is {{ parameter2 }}" loaded_parameters = {"parameter1": "value", "parameter3": "other value"} self.assertFalse(NeuronLauncher._neuron_parameters_are_available_in_loaded_parameters(string_parameters, - loaded_parameters)) + loaded_parameters)) if __name__ == '__main__': unittest.main() # suite = unittest.TestSuite() - # suite.addTest(TestNeuronLauncher("test_start_neuron")) + # suite.addTest(TestNeuronLauncher("test_replace_brackets_by_loaded_parameter")) # runner = unittest.TextTestRunner() # runner.run(suite) diff --git a/Tests/test_neuron_parameter_loader.py b/Tests/test_neuron_parameter_loader.py index bc060332..73c83fd1 100644 --- a/Tests/test_neuron_parameter_loader.py +++ b/Tests/test_neuron_parameter_loader.py @@ -171,5 +171,29 @@ def test_associate_order_params_to_values(self): self.assertEqual(NeuronParameterLoader._associate_order_params_to_values(order_user, order_brain), expected_result) + # Upper/Lower case between multiple variables + order_brain = "This Is The {{ variable }} And The {{ variable2 }}" + order_user = "ThiS is tHe VAlue aND tHE vAlUe2" + expected_result = {'variable': 'VAlue', + 'variable2':'vAlUe2'} + self.assertEqual(NeuronParameterLoader._associate_order_params_to_values(order_user, order_brain), + expected_result) + + # Upper/Lower case between multiple variables and at the End + order_brain = "This Is The {{ variable }} And The {{ variable2 }} And Again" + order_user = "ThiS is tHe VAlue aND tHE vAlUe2 and aGAIN" + expected_result = {'variable': 'VAlue', + 'variable2': 'vAlUe2'} + self.assertEqual(NeuronParameterLoader._associate_order_params_to_values(order_user, order_brain), + expected_result) + + # integers variables + order_brain = "This Is The {{ variable }} And The {{ variable2 }}" + order_user = "ThiS is tHe 1 aND tHE 2" + expected_result = {'variable': '1', + 'variable2': '2'} + self.assertEqual(NeuronParameterLoader._associate_order_params_to_values(order_user, order_brain), + expected_result) + if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/Tests/test_order_analyser.py b/Tests/test_order_analyser.py index 80edd412..3ffe19b7 100644 --- a/Tests/test_order_analyser.py +++ b/Tests/test_order_analyser.py @@ -3,9 +3,9 @@ from kalliope.core.Models import Brain from kalliope.core.Models import Neuron -from kalliope.core.Models import Order from kalliope.core.Models import Synapse from kalliope.core.Models.MatchedSynapse import MatchedSynapse +from kalliope.core.Models.Signal import Signal from kalliope.core.OrderAnalyser import OrderAnalyser @@ -23,9 +23,9 @@ def test_get_matching_synapse(self): neuron3 = Neuron(name='neurone3', parameters={'var3': 'val3'}) neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4'}) - signal1 = Order(sentence="this is the sentence") - signal2 = Order(sentence="this is the second sentence") - signal3 = Order(sentence="that is part of the third sentence") + 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") synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) synapse2 = Synapse(name="Synapse2", neurons=[neuron3, neuron4], signals=[signal2]) diff --git a/Tests/test_resources_manager.py b/Tests/test_resources_manager.py index 9af6f84f..3c5ee1f9 100644 --- a/Tests/test_resources_manager.py +++ b/Tests/test_resources_manager.py @@ -47,6 +47,13 @@ def test_is_settings_ok(self): dna.module_type = "trigger" self.assertTrue(ResourcesManager.is_settings_ok(valid_resource, dna)) + # valid signal + valid_resource = Resources() + valid_resource.signal_folder = "/path" + dna = Dna() + dna.module_type = "signal" + self.assertTrue(ResourcesManager.is_settings_ok(valid_resource, dna)) + # ----------------- # invalid resource # ----------------- @@ -78,6 +85,13 @@ def test_is_settings_ok(self): dna.module_type = "trigger" self.assertFalse(ResourcesManager.is_settings_ok(valid_resource, dna)) + # valid signal + valid_resource = Resources() + valid_resource.signal_folder = None + dna = Dna() + dna.module_type = "signal" + self.assertFalse(ResourcesManager.is_settings_ok(valid_resource, dna)) + def test_is_repo_ok(self): # valid repo if "/Tests" in os.getcwd(): @@ -127,6 +141,11 @@ def test_get_target_folder(self): resources.trigger_folder = '/var/tmp/test/resources' self.assertEqual(ResourcesManager._get_target_folder(resources, "trigger"), "/var/tmp/test/resources") + # test get signal folder + resources = Resources() + resources.signal_folder = '/var/tmp/test/resources' + self.assertEqual(ResourcesManager._get_target_folder(resources, "signal"), "/var/tmp/test/resources") + # test get non existing resource resources = Resources() self.assertIsNone(ResourcesManager._get_target_folder(resources, "not_existing")) diff --git a/Tests/test_rest_api.py b/Tests/test_rest_api.py index dd8f5b6e..b986dab5 100644 --- a/Tests/test_rest_api.py +++ b/Tests/test_rest_api.py @@ -73,51 +73,84 @@ def test_get_all_synapses(self): response = self.client.get(url) expected_content = { - "synapses": [{ - "name": "test", - "neurons": [{ - "name": "say", - "parameters": { - "message": ["test message"] - } - }], - "signals": [{ - "order": "test_order" - }] - }, { - "name": "test2", - "neurons": [{ - "name": "say", - "parameters": { - "message": ["test message"] - } - }], - "signals": [{ - "order": "bonjour" - }] - }, { - "name": "test4", - "neurons": [{ - "name": "say", - "parameters": { - "message": ["test message {{parameter1}}"] - } - }], - "signals": [{ - "order": "test_order_with_parameter" - }] - }, { - "name": "test3", - "neurons": [{ - "name": "say", - "parameters": { - "message": ["test message"] - } - }], - "signals": [{ - "order": "test_order_3" - }] - }] + "synapses": [ + { + "name": "test", + "neurons": [ + { + "name": "say", + "parameters": { + "message": [ + "test message" + ] + } + } + ], + "signals": [ + { + "name": "order", + "parameters": "test_order" + } + ] + }, + { + "name": "test2", + "neurons": [ + { + "name": "say", + "parameters": { + "message": [ + "test message" + ] + } + } + ], + "signals": [ + { + "name": "order", + "parameters": "bonjour" + } + ] + }, + { + "name": "test4", + "neurons": [ + { + "name": "say", + "parameters": { + "message": [ + "test message {{parameter1}}" + ] + } + } + ], + "signals": [ + { + "name": "order", + "parameters": "test_order_with_parameter" + } + ] + }, + { + "name": "test3", + "neurons": [ + { + "name": "say", + "parameters": { + "message": [ + "test message" + ] + } + } + ], + "signals": [ + { + "name": "order", + "parameters": "test_order_3" + } + ] + } + ] } # a lot of char ti process self.maxDiff = None @@ -129,25 +162,26 @@ def test_get_one_synapse(self): url = self.get_server_url() + "/synapses/test" response = self.client.get(url) - expected_content = { - "synapses": { - "name": "test", - "neurons": [ - { - "name": "say", - "parameters": { - "message": [ - "test message" - ] - } - } - ], - "signals": [ - { - "order": "test_order" - } - ] - } + expected_content ={ + "synapses": { + "name": "test", + "neurons": [ + { + "name": "say", + "parameters": { + "message": [ + "test message" + ] + } + } + ], + "signals": [ + { + "name": "order", + "parameters": "test_order" + } + ] + } } self.assertEqual(json.dumps(expected_content, sort_keys=True), json.dumps(json.loads(response.get_data().decode('utf-8')), sort_keys=True)) diff --git a/Tests/test_settings_loader.py b/Tests/test_settings_loader.py index e8096db3..4494da45 100644 --- a/Tests/test_settings_loader.py +++ b/Tests/test_settings_loader.py @@ -11,6 +11,7 @@ from kalliope.core.Models.RestAPI import RestAPI from kalliope.core.Models.Settings import Settings from kalliope.core.Models.Stt import Stt +from kalliope.core.Models.RecognitionOptions import RecognitionOptions from kalliope.core.Models.Trigger import Trigger from kalliope.core.Models.Tts import Tts @@ -119,6 +120,7 @@ def test_get_settings(self): "test": "kalliope" } settings_object.machine = platform.machine() + settings_object.recognition_options = RecognitionOptions() sl = SettingLoader(file_path=self.settings_file_to_test) diff --git a/Tests/test_synapse_launcher.py b/Tests/test_synapse_launcher.py index 9d327826..fa482835 100644 --- a/Tests/test_synapse_launcher.py +++ b/Tests/test_synapse_launcher.py @@ -3,13 +3,12 @@ import mock from kalliope.core import LIFOBuffer -from kalliope.core.Models import Brain +from kalliope.core.Models import Brain, Signal, Singleton from kalliope.core.Models.MatchedSynapse import MatchedSynapse from kalliope.core.Models.Settings import Settings from kalliope.core.SynapseLauncher import SynapseLauncher, SynapseNameNotFound from kalliope.core.Models import Neuron -from kalliope.core.Models import Order from kalliope.core.Models import Synapse @@ -25,9 +24,9 @@ def setUp(self): neuron3 = Neuron(name='neurone3', parameters={'var3': 'val3'}) neuron4 = Neuron(name='neurone4', parameters={'var4': 'val4'}) - signal1 = Order(sentence="this is the sentence") - signal2 = Order(sentence="this is the second sentence") - signal3 = Order(sentence="that is part of the third sentence") + 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") self.synapse1 = Synapse(name="Synapse1", neurons=[neuron1, neuron2], signals=[signal1]) self.synapse2 = Synapse(name="Synapse2", neurons=[neuron3, neuron4], signals=[signal2]) @@ -41,7 +40,7 @@ def setUp(self): self.settings_test = Settings(default_synapse="Synapse3") # clean the LiFO - LIFOBuffer.lifo_list = list() + Singleton._instances = dict() def test_start_synapse_by_name(self): # existing synapse in the brain @@ -50,7 +49,22 @@ def test_start_synapse_by_name(self): SynapseLauncher.start_synapse_by_name("Synapse1", brain=self.brain_test) # we expect that the lifo has been loaded with the synapse to run expected_result = [[should_be_created_matched_synapse]] - self.assertEqual(expected_result, LIFOBuffer.lifo_list) + lifo_buffer = LIFOBuffer() + self.assertEqual(expected_result, lifo_buffer.lifo_list) + + # we expect that the lifo has been loaded with the synapse to run and overwritten parameters + Singleton._instances = dict() + lifo_buffer = LIFOBuffer() + overriding_param = { + "val1": "val" + } + SynapseLauncher.start_synapse_by_name("Synapse1", brain=self.brain_test, + overriding_parameter_dict=overriding_param) + should_be_created_matched_synapse = MatchedSynapse(matched_synapse=self.synapse1, + overriding_parameter=overriding_param) + # we expect that the lifo has been loaded with the synapse to run + expected_result = [[should_be_created_matched_synapse]] + self.assertEqual(expected_result, lifo_buffer.lifo_list) # non existing synapse in the brain with self.assertRaises(SynapseNameNotFound): @@ -71,13 +85,14 @@ def test_run_matching_synapse_from_order(self): brain=self.brain_test, settings=self.settings_test) - self.assertEqual(expected_result, LIFOBuffer.lifo_list) + lifo_buffer = LIFOBuffer() + self.assertEqual(expected_result, lifo_buffer.lifo_list) # ------------------------- # test_match_synapse1_and_2 # ------------------------- # clean LIFO - LIFOBuffer.lifo_list = list() + Singleton._instances = dict() with mock.patch("kalliope.core.LIFOBuffer.execute"): order_to_match = "this is the second sentence" should_be_created_matched_synapse1 = MatchedSynapse(matched_synapse=self.synapse1, @@ -91,13 +106,14 @@ def test_run_matching_synapse_from_order(self): SynapseLauncher.run_matching_synapse_from_order(order_to_match, brain=self.brain_test, settings=self.settings_test) - self.assertEqual(expected_result, LIFOBuffer.lifo_list) + lifo_buffer = LIFOBuffer() + self.assertEqual(expected_result, lifo_buffer.lifo_list) # ------------------------- # test_match_default_synapse # ------------------------- # clean LIFO - LIFOBuffer.lifo_list = list() + Singleton._instances = dict() with mock.patch("kalliope.core.LIFOBuffer.execute"): order_to_match = "not existing sentence" should_be_created_matched_synapse = MatchedSynapse(matched_synapse=self.synapse3, @@ -108,13 +124,14 @@ def test_run_matching_synapse_from_order(self): SynapseLauncher.run_matching_synapse_from_order(order_to_match, brain=self.brain_test, settings=self.settings_test) - self.assertEqual(expected_result, LIFOBuffer.lifo_list) + lifo_buffer = LIFOBuffer() + self.assertEqual(expected_result, lifo_buffer.lifo_list) # ------------------------- # test_no_match_and_no_default_synapse # ------------------------- # clean LIFO - LIFOBuffer.lifo_list = list() + Singleton._instances = dict() with mock.patch("kalliope.core.LIFOBuffer.execute"): order_to_match = "not existing sentence" new_settings = Settings() @@ -122,4 +139,14 @@ def test_run_matching_synapse_from_order(self): SynapseLauncher.run_matching_synapse_from_order(order_to_match, brain=self.brain_test, settings=new_settings) - self.assertEqual(expected_result, LIFOBuffer.lifo_list) + lifo_buffer = LIFOBuffer() + self.assertEqual(expected_result, lifo_buffer.lifo_list) + + +if __name__ == '__main__': + unittest.main() + + # suite = unittest.TestSuite() + # suite.addTest(TestSynapseLauncher("test_run_matching_synapse_from_order")) + # runner = unittest.TextTestRunner() + # runner.run(suite) diff --git a/brain_examples/kalliope_memory.yml b/brain_examples/kalliope_memory.yml new file mode 100644 index 00000000..55c089ac --- /dev/null +++ b/brain_examples/kalliope_memory.yml @@ -0,0 +1,33 @@ +# test with a value to save from the order +- name: "cortex-1" + signals: + - order: "dis bonjour à {{ friend }}" + neurons: + - say: + message: + - "Bonjour {{ friend }}" + kalliope_memory: + friend: "{{ friend }}" + + +- name: "remember-synapse222" + signals: + - order: "remind me {{ remember }} in {{ time }} seconds" + neurons: + - neurotimer: + seconds: "{{ time }}" + synapse: "remember-todo22" + kalliope_memory: + remember: "{{ remember }}" + minutes: "{{ time }}" + - say: + message: + - "Ok {{ time }} seconds" + +- name: "remember-todo22" + signals: + - order: "no-order-for-this-synapse111" + neurons: + - say: + message: + - "does this work? {{ kalliope_memory['remember'] }} and the time? {{ kalliope_memory['minutes'] }}" diff --git a/brain_examples/mute.yml b/brain_examples/mute.yml new file mode 100644 index 00000000..ea35c6ce --- /dev/null +++ b/brain_examples/mute.yml @@ -0,0 +1,10 @@ + +- name: "mute-synapse" + signals: + - order: "stop listening" + neurons: + - say: + message: + - "I stop hearing you, sir" + - mute: + status: True diff --git a/brain_examples/neurotimer.yml b/brain_examples/neurotimer.yml new file mode 100644 index 00000000..bc84e944 --- /dev/null +++ b/brain_examples/neurotimer.yml @@ -0,0 +1,54 @@ + - name: "timer" + signals: + - order: "je lance un thé" + - order: "je lance un T" + neurons: + - neurotimer: + seconds: 10 + synapse: "time-over" + - say: + message: + - "je vous préviens dans 5 secondes" + + - name: "time-over" + signals: + - order: "time-is-over" + neurons: + - say: + message: + - "c'est terminé" + + - name: "timer2" + signals: + - order: "préviens moi dans {{ time }} secondes" + - order: "préviens-moi dans {{ time }} secondes" + neurons: + - neurotimer: + seconds: "{{ time }}" + synapse: "time-over" + - say: + message: + - "je vous préviens dans {{ time }} secondes" + + - name: "remember-synapse" + signals: + - order: "rappel-moi de {{ remember }} dans {{ time }} secondes" + - order: "rappel moi de {{ remember }} dans {{ time }} secondes" + - order: "rappelle-moi de {{ remember }} dans {{ time }} secondes" + neurons: + - neurotimer: + seconds: "{{ time }}" + synapse: "remember-todo" + forwarded_parameters: + remember: "{{ remember }}" + time: "{{ time }}" + - say: + message: + - "je vous le rappel dans {{ time }} secondes" + + - name: "remember-todo" + signals: {} + neurons: + - say: + message: + - "Vous m'avez demandé de vous rappeler de {{ remember }} il y a {{ time }} secondes" \ No newline at end of file diff --git a/brain_examples/test_lifo.yml b/brain_examples/test_lifo.yml new file mode 100644 index 00000000..362b9817 --- /dev/null +++ b/brain_examples/test_lifo.yml @@ -0,0 +1,32 @@ +- name: "remember-synapse" + signals: + - order: "rappel moi de {{ remember }} dans {{ time }} secondes" + neurons: + - neurotimer: + seconds: "{{ time }}" + synapse: "remember-todo" + forwarded_parameters: + remember: "{{ remember }}" + time: "{{ time }}" + - say: + message: + - "je vous le rappel dans {{ time }} secondes" + +- name: "remember-todo" + signals: {} + neurons: + - say: + message: + - "vous m'avez demandé de vous rappeler de {{ remember }} il a {{ time }} secondes" + - neurotransmitter: + from_answer_link: + - synapse: "your-welcome" + answers: + - "merci" + default: "synapse3" + +- name: "your-welcome" + signals: {} + neurons: + - say: + message: "de rien" diff --git a/docker/clone_and_test_python3.sh b/docker/clone_and_test_python3.sh new file mode 100755 index 00000000..51b5320e --- /dev/null +++ b/docker/clone_and_test_python3.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# install dev version +git clone https://github.com/kalliope-project/kalliope.git kalliope; +cd kalliope; +git checkout dev; + +# install +sudo python3 setup.py install + +# tests +export LANG=C.UTF-8 +python3 -m unittest discover diff --git a/docker/compile_snowboy.dockerfile b/docker/compile_snowboy.dockerfile new file mode 100644 index 00000000..13176294 --- /dev/null +++ b/docker/compile_snowboy.dockerfile @@ -0,0 +1,39 @@ +# build last version +# docker build --force-rm=true -t compile-snowboy-python3 -f compile_snowboy.dockerfile . + +# build specified version +# docker build --force-rm=true --build-arg SNOWBOY_VERSION=1.1.1 -t compile-snowboy-python3 -f compile_snowboy.dockerfile . + +# compile into local /tmp/snowboy +# docker run -it --rm --mount type=bind,source=/tmp/snowboy,target=/data compile-snowboy-python3 + +FROM ubuntu:xenial + +RUN apt-get update +RUN apt-get install -y git make g++ python3-dev libatlas3-base libblas-dev gfortran vim wget libpcre3-dev + +# get the last version of swig +RUN wget https://downloads.sourceforge.net/swig/swig-3.0.12.tar.gz && tar xzf swig-3.0.12.tar.gz +RUN cd swig-3.0.12 && \ + ./configure --prefix=/usr \ + --without-clisp \ + --without-maximum-compile-warnings && \ + make + +RUN cd swig-3.0.12 && \ + make install && \ + install -v -m755 -d /usr/share/doc/swig-3.0.12 && \ + cp -v -R Doc/* /usr/share/doc/swig-3.0.12 + +# version can be 1.2.0, 1.1.1, 1.1.0, 1.0.4 +ARG SNOWBOY_VERSION="1.2.0" + +RUN wget https://github.com/Kitt-AI/snowboy/archive/v${SNOWBOY_VERSION}.tar.gz && tar xzf v${SNOWBOY_VERSION}.tar.gz + +RUN sed -i "s|python-config|python3-config|g" snowboy-${SNOWBOY_VERSION}/swig/Python/Makefile +RUN sed -i "s|-lf77blas -lcblas -llapack_atlas -latlas|-lquadmath -lgfortran -lblas /usr/lib/libcblas.so.3|g" snowboy-${SNOWBOY_VERSION}/swig/Python/Makefile +RUN cd /snowboy-${SNOWBOY_VERSION}/swig/Python && make +RUN cd /snowboy-${SNOWBOY_VERSION}/swig/Python && python3 -c "import _snowboydetect; print('OK')" +# compiled binary will be placed into data folder +RUN mkdir /data +CMD cp /snowboy-*/swig/Python/*.so /data diff --git a/docker/compile_snowboy_python34.dockerfile b/docker/compile_snowboy_python34.dockerfile new file mode 100644 index 00000000..de206589 --- /dev/null +++ b/docker/compile_snowboy_python34.dockerfile @@ -0,0 +1,39 @@ +# build last version +# docker build --force-rm=true -t compile-snowboy-python34 -f compile_snowboy_python34.dockerfile . + +# build specified version +# docker build --force-rm=true --build-arg SNOWBOY_VERSION=1.1.1 -t compile-snowboy-python34 -f compile_snowboy_python34.dockerfile . + +# compile into local /tmp/snowboy +# docker run -it --rm --mount type=bind,source=/tmp/snowboy,target=/data compile-snowboy-python34 + +FROM ubuntu:trusty + +RUN apt-get update +RUN apt-get install -y git make g++ python3-dev libatlas3-base libblas-dev gfortran vim wget libpcre3-dev + +# get the last version of swig +RUN wget https://downloads.sourceforge.net/swig/swig-3.0.12.tar.gz && tar xzf swig-3.0.12.tar.gz +RUN cd swig-3.0.12 && \ + ./configure --prefix=/usr \ + --without-clisp \ + --without-maximum-compile-warnings && \ + make + +RUN cd swig-3.0.12 && \ + make install && \ + install -v -m755 -d /usr/share/doc/swig-3.0.12 && \ + cp -v -R Doc/* /usr/share/doc/swig-3.0.12 + +# version can be 1.2.0, 1.1.1, 1.1.0, 1.0.4 +ARG SNOWBOY_VERSION="1.2.0" + +RUN wget https://github.com/Kitt-AI/snowboy/archive/v${SNOWBOY_VERSION}.tar.gz && tar xzf v${SNOWBOY_VERSION}.tar.gz + +RUN sed -i "s|python-config|python3-config|g" snowboy-${SNOWBOY_VERSION}/swig/Python/Makefile +RUN sed -i "s|-lf77blas -lcblas -llapack_atlas -latlas|-lquadmath -lgfortran -lblas /usr/lib/libcblas.so.3|g" snowboy-${SNOWBOY_VERSION}/swig/Python/Makefile +RUN cd /snowboy-${SNOWBOY_VERSION}/swig/Python && make +RUN cd /snowboy-${SNOWBOY_VERSION}/swig/Python && python3 -c "import _snowboydetect; print('OK')" +# compiled binary will be placed into data folder +RUN mkdir /data +CMD cp /snowboy-*/swig/Python/*.so /data diff --git a/docker/ubuntu_16_04.dockerfile b/docker/ubuntu_16_04.dockerfile index 87286da0..6faec658 100644 --- a/docker/ubuntu_16_04.dockerfile +++ b/docker/ubuntu_16_04.dockerfile @@ -21,6 +21,7 @@ RUN pip install --upgrade pip six RUN pip install --upgrade pip pyyaml RUN pip install --upgrade pip SpeechRecognition RUN pip install --upgrade pip Werkzeug +RUN pip install --upgrade pip gitdb # add a standart user. tests must not be ran as root RUN useradd -m -u 1000 tester diff --git a/docker/ubuntu_16_04_python3.dockerfile b/docker/ubuntu_16_04_python3.dockerfile new file mode 100644 index 00000000..172b1bf7 --- /dev/null +++ b/docker/ubuntu_16_04_python3.dockerfile @@ -0,0 +1,38 @@ +# used to build the last kalliope dev version with python 3 +# docker build --force-rm=true -t kalliope-ubuntu1604-python3 -f docker/ubuntu_16_04_python3.dockerfile . +# docker run -it --rm kalliope-ubuntu1604-python3 + +FROM ubuntu:16.04 + +ENV no_proxy="127.0.0.1,localhost,kalliope.fr" + +# set UTF-8 to the terminal +ENV LANG en_US.UTF-8 + +# pico2wav is a multiverse package +RUN echo "deb http://us.archive.ubuntu.com/ubuntu/ xenial multiverse" >> /etc/apt/sources.list + +# install packages +RUN apt-get update && apt-get install -y \ + git python3-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog \ + libffi-dev libffi-dev libssl-dev portaudio19-dev build-essential \ + sox libatlas3-base mplayer wget vim sudo\ + && rm -rf /var/lib/apt/lists/* + +# Install the last PIP +RUN wget https://bootstrap.pypa.io/get-pip.py +RUN python3 get-pip.py + +# add a standart user. tests must not be ran as root +RUN useradd -m -u 1000 tester +RUN usermod -aG sudo tester +RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + +ADD docker/clone_and_test_python3.sh /home/tester/clone_and_test_python3.sh +RUN chown tester /home/tester/clone_and_test_python3.sh + +USER tester +WORKDIR /home/tester + +# run tests +CMD ./clone_and_test_python3.sh \ No newline at end of file diff --git a/images/kalliope_app.png b/images/kalliope_app.png new file mode 100755 index 00000000..5081ace9 Binary files /dev/null and b/images/kalliope_app.png differ diff --git a/install/files/python_requirements.txt b/install/files/python_requirements.txt index 00b0c748..679663b7 100644 --- a/install/files/python_requirements.txt +++ b/install/files/python_requirements.txt @@ -3,7 +3,7 @@ SpeechRecognition>=3.7.1 markupsafe==0.23 pyaudio==0.2.11 pyasn1>=0.1.8 -ansible==2.2.0.0 +ansible==2.3.0.0 #python2-pythondialog==3.4.0 jinja2==2.8 cffi==1.9.1 @@ -24,3 +24,4 @@ SoundFile>=0.9.0 pyalsaaudio>=0.8.4 RPi.GPIO>=0.6.3 sox>=1.3.0 +paho-mqtt>=1.3.0 diff --git a/kalliope/__init__.py b/kalliope/__init__.py index 8a821dca..36b63f8d 100644 --- a/kalliope/__init__.py +++ b/kalliope/__init__.py @@ -3,13 +3,16 @@ import argparse import logging +import time + from kalliope.core import ShellGui from kalliope.core import Utils from kalliope.core.ConfigurationManager import SettingLoader from kalliope.core.ConfigurationManager.BrainLoader import BrainLoader -from kalliope.core.EventManager import EventManager -from kalliope.core.MainController import MainController +from kalliope.core.SignalLauncher import SignalLauncher from kalliope.core.Utils.RpiUtils import RpiUtils +from flask import Flask +from kalliope.core.RestAPI.FlaskAPI import FlaskAPI from ._version import version_str import signal @@ -59,6 +62,7 @@ def parse_args(args): parser.add_argument("--stt-name", help="STT name to uninstall") parser.add_argument("--tts-name", help="TTS name to uninstall") parser.add_argument("--trigger-name", help="Trigger name to uninstall") + parser.add_argument("--signal-name", help="Signal name to uninstall") parser.add_argument('-v', '--version', action='version', version='Kalliope ' + version_str) @@ -107,14 +111,25 @@ def main(): # uninstall modules if parser.action == "uninstall": - if not parser.neuron_name and not parser.stt_name and not parser.tts_name and not parser.trigger_name: - Utils.print_danger("You must specify a module name with --neuron-name or --stt-name or --tts-name " - "or --trigger-name") + if not parser.neuron_name \ + and not parser.stt_name \ + and not parser.tts_name \ + and not parser.trigger_name\ + and not parser.signal_name: + Utils.print_danger("You must specify a module name with " + "--neuron-name " + "or --stt-name " + "or --tts-name " + "or --trigger-name " + "or --signal-name") sys.exit(1) else: res_manager = ResourcesManager() - res_manager.uninstall(neuron_name=parser.neuron_name, stt_name=parser.stt_name, - tts_name=parser.tts_name, trigger_name=parser.trigger_name) + res_manager.uninstall(neuron_name=parser.neuron_name, + stt_name=parser.stt_name, + tts_name=parser.tts_name, + trigger_name=parser.trigger_name, + signal_name=parser.signal_name) return # load the brain once @@ -128,10 +143,6 @@ def main(): if parser.action == "start": - if settings.rpi_settings: - # init GPIO once - RpiUtils(settings.rpi_settings) - # user set a synapse to start if parser.run_synapse is not None: SynapseLauncher.start_synapse_by_name(parser.run_synapse, @@ -144,25 +155,9 @@ def main(): is_api_call=False) if (parser.run_synapse is None) and (parser.run_order is None): - # first, load events in event manager - EventManager(brain.synapses) - Utils.print_success("Events loaded") - # then start kalliope - Utils.print_success("Starting Kalliope") - Utils.print_info("Press Ctrl+C for stopping") - # catch signal for killing on Ctrl+C pressed - signal.signal(signal.SIGINT, signal_handler) - # start the state machine - try: - MainController(brain=brain) - except (KeyboardInterrupt, SystemExit): - Utils.print_info("Ctrl+C pressed. Killing Kalliope") - finally: - # we need to switch GPIO pin to default status if we are using a Rpi - if settings.rpi_settings: - logger.debug("Clean GPIO") - import RPi.GPIO as GPIO - GPIO.cleanup() + # start rest api + start_rest_api(settings, brain) + start_kalliope(settings, brain) if parser.action == "gui": try: @@ -172,6 +167,15 @@ def main(): sys.exit(0) +class AppFilter(logging.Filter): + """ + Class used to add a custom entry into the logger + """ + def filter(self, record): + record.app_version = "kalliope-%s" % version_str + return True + + def configure_logging(debug=None): """ Prepare log folder in current home directory. @@ -180,18 +184,90 @@ def configure_logging(debug=None): """ logger = logging.getLogger("kalliope") + logger.addFilter(AppFilter()) logger.propagate = False - ch = logging.StreamHandler() - ch.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(message)s') - ch.setFormatter(formatter) + syslog = logging.StreamHandler() + syslog .setLevel(logging.DEBUG) - # add the handlers to logger - logger.addHandler(ch) + formatter = logging.Formatter('%(asctime)s :: %(app_version)s :: %(message)s', "%Y-%m-%d %H:%M:%S") + syslog .setFormatter(formatter) if debug: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) + # add the handlers to logger + logger.addHandler(syslog) + logger.debug("Logger ready") + + +def get_list_signal_class_to_load(brain): + """ + Return a list of signal class name + For all synapse, each signal type is added to a list only if the signal is not yet present in the list + :param brain: Brain object + :type brain: Brain + :return: set of signal class + """ + list_signal_class_name = set() + + for synapse in brain.synapses: + for signal_object in synapse.signals: + list_signal_class_name.add(signal_object.name) + logger.debug("[Kalliope entrypoint] List of signal class to load: %s" % list_signal_class_name) + return list_signal_class_name + + +def start_rest_api(settings, brain): + """ + Start the Rest API if asked in the user settings + """ + # run the api if the user want it + if settings.rest_api.active: + Utils.print_info("Starting REST API Listening port: %s" % settings.rest_api.port) + app = Flask(__name__) + flask_api = FlaskAPI(app=app, + port=settings.rest_api.port, + brain=brain, + allowed_cors_origin=settings.rest_api.allowed_cors_origin) + flask_api.daemon = True + flask_api.start() + + +def start_kalliope(settings, brain): + """ + Start all signals declared in the brain + """ + # start kalliope + Utils.print_success("Starting Kalliope") + Utils.print_info("Press Ctrl+C for stopping") + # catch signal for killing on Ctrl+C pressed + signal.signal(signal.SIGINT, signal_handler) + + # get a list of signal class to load from declared synapse in the brain + # this list will contain string of signal class type. + # For example, if the brain contains multiple time the signal type "order", the list will be ["order"] + # If the brain contains some synapse with "order" and "event", the list will be ["order", "event"] + list_signals_class_to_load = get_list_signal_class_to_load(brain) + + # start each class name + try: + for signal_class_name in list_signals_class_to_load: + signal_instance = SignalLauncher.launch_signal_class_by_name(signal_name=signal_class_name, + settings=settings) + if signal_instance is not None: + signal_instance.daemon = True + signal_instance.start() + + while True: # keep main thread alive + time.sleep(0.1) + + except (KeyboardInterrupt, SystemExit): + # we need to switch GPIO pin to default status if we are using a Rpi + if settings.rpi_settings: + Utils.print_info("GPIO cleaned") + logger.debug("Clean GPIO") + import RPi.GPIO as GPIO + GPIO.cleanup() diff --git a/kalliope/_version.py b/kalliope/_version.py index 3a8156bb..233f4985 100644 --- a/kalliope/_version.py +++ b/kalliope/_version.py @@ -1,2 +1,2 @@ # https://www.python.org/dev/peps/pep-0440/ -version_str = "0.4.5" +version_str = "0.4.6" diff --git a/kalliope/core/ConfigurationManager/BrainLoader.py b/kalliope/core/ConfigurationManager/BrainLoader.py index 9d3477d4..4eacab5f 100644 --- a/kalliope/core/ConfigurationManager/BrainLoader.py +++ b/kalliope/core/ConfigurationManager/BrainLoader.py @@ -4,15 +4,14 @@ from six import with_metaclass import six +from kalliope.core.Models.Signal import Signal from .YAMLLoader import YAMLLoader from kalliope.core.Utils import Utils from kalliope.core.ConfigurationManager import SettingLoader from kalliope.core.ConfigurationManager.ConfigurationChecker import ConfigurationChecker from kalliope.core.Models import Singleton from kalliope.core.Models.Brain import Brain -from kalliope.core.Models.Event import Event from kalliope.core.Models.Neuron import Neuron -from kalliope.core.Models.Order import Order from kalliope.core.Models.Synapse import Synapse logging.basicConfig() @@ -166,38 +165,12 @@ def _get_signals(cls, signals_dict): signals = list() for signal_dict in signals_dict: if ConfigurationChecker().check_signal_dict(signal_dict): - event_or_order = cls._get_event_or_order_from_dict(signal_dict) - signals.append(event_or_order) + for signal_name in signal_dict: + new_signal = Signal(name=signal_name, parameters=signal_dict[signal_name]) + signals.append(new_signal) return signals - @classmethod - def _get_event_or_order_from_dict(cls, signal_or_event_dict): - """ - The signal is either an Event or an Order - - :param signal_or_event_dict: A dict of event or signal - :type signal_or_event_dict: dict - :return: The object corresponding to An Order or an Event - :rtype: An Order or an Event - - :Example: - - event_or_order = cls._get_event_or_order_from_dict(signal_dict) - - .. seealso:: Event, Order - .. warnings:: Static method and Private - """ - if 'event' in signal_or_event_dict: - event = signal_or_event_dict["event"] - if ConfigurationChecker.check_event_dict(event): - return cls._get_event_object(event) - - if 'order' in signal_or_event_dict: - order = signal_or_event_dict["order"] - if ConfigurationChecker.check_order_dict(order): - return Order(sentence=order) - @staticmethod def _get_root_brain_path(): """ @@ -221,26 +194,6 @@ def _get_root_brain_path(): return brain_path raise IOError("Default brain.yml file not found") - @classmethod - def _get_event_object(cls, event_dict): - def get_key(key_name): - try: - return event_dict[key_name] - except KeyError: - return None - - year = get_key("year") - month = get_key("month") - day = get_key("day") - week = get_key("week") - day_of_week = get_key("day_of_week") - hour = get_key("hour") - minute = get_key("minute") - second = get_key("second") - - return Event(year=year, month=month, day=day, week=week, - day_of_week=day_of_week, hour=hour, minute=minute, second=second) - @classmethod def _replace_global_variables(cls, parameter, settings): """ diff --git a/kalliope/core/ConfigurationManager/ConfigurationChecker.py b/kalliope/core/ConfigurationManager/ConfigurationChecker.py index 7e3eb81a..ebd8ae7b 100644 --- a/kalliope/core/ConfigurationManager/ConfigurationChecker.py +++ b/kalliope/core/ConfigurationManager/ConfigurationChecker.py @@ -5,6 +5,7 @@ from kalliope.core.Utils.Utils import ModuleNotFoundError from kalliope.core.ConfigurationManager.SettingLoader import SettingLoader + class InvalidSynapeName(Exception): """ The name of the synapse is not correct. It should only contains alphanumerics at the beginning and the end of @@ -48,15 +49,6 @@ class NoValidSignal(Exception): pass -class NoEventPeriod(Exception): - """ - An Event must contains a period corresponding to its execution - - .. seealso:: Event - """ - pass - - class MultipleSameSynapseName(Exception): """ A synapse name must be unique @@ -144,7 +136,7 @@ def check_neuron_exist(neuron_module_name): sl = SettingLoader() settings = sl.settings package_name = "kalliope.neurons" + "." + neuron_module_name.lower() + "." + neuron_module_name.lower() - if settings.resources is not None: + if settings.resources.neuron_folder is not None: neuron_resource_path = settings.resources.neuron_folder + \ os.sep + neuron_module_name.lower() + os.sep + \ neuron_module_name.lower()+".py" @@ -172,72 +164,43 @@ def check_neuron_exist(neuron_module_name): @staticmethod def check_signal_dict(signal_dict): - """ - Check received signal dictionary is valid: - - :param signal_dict: The signal Dictionary - :type signal_dict: Dict - :return: True if signal are ok - :rtype: Boolean - :Example: - - ConfigurationChecker().check_signal_dict(signal_dict): - - .. seealso:: Order, Event - .. raises:: NoValidSignal - .. warnings:: Static and Public - """ - - if ('event' not in signal_dict) and ('order' not in signal_dict): - raise NoValidSignal("The signal is not an event or an order %s" % signal_dict) - return True - - @staticmethod - def check_event_dict(event_dict): - """ - Check received event dictionary is valid: - - :param event_dict: The event Dictionary - :type event_dict: Dict - :return: True if event are ok - :rtype: Boolean - - :Example: - - ConfigurationChecker().check_event_dict(event_dict): + def check_signal_exist(signal_name): + """ + Return True if the signal_name python Class exist in signals package + :param signal_name: Name of the neuron module to check + :type signal_name: str + :return: + """ + sl = SettingLoader() + settings = sl.settings + package_name = "kalliope.signals" + "." + signal_name.lower() + "." + signal_name.lower() + if settings.resources.signal_folder is not None: + neuron_resource_path = settings.resources.neuron_folder + \ + os.sep + signal_name.lower() + os.sep + \ + signal_name.lower() + ".py" + if os.path.exists(neuron_resource_path): + imp.load_source(signal_name.capitalize(), neuron_resource_path) + package_name = signal_name.capitalize() - .. seealso:: Event - .. raises:: NoEventPeriod - .. warnings:: Static and Public - """ - def get_key(key_name): try: - return event_dict[key_name] - except KeyError: - return None - - if event_dict is None or event_dict == "": - raise NoEventPeriod("Event must contain at least one of those elements: " - "year, month, day, week, day_of_week, hour, minute, second") - - # check content as at least on key - year = get_key("year") - month = get_key("month") - day = get_key("day") - week = get_key("week") - day_of_week = get_key("day_of_week") - hour = get_key("hour") - minute = get_key("minute") - second = get_key("second") - - list_to_check = [year, month, day, week, day_of_week, hour, minute, second] - number_of_none_object = list_to_check.count(None) - list_size = len(list_to_check) - if number_of_none_object >= list_size: - raise NoEventPeriod("Event must contain at least one of those elements: " - "year, month, day, week, day_of_week, hour, minute, second") + mod = __import__(package_name, fromlist=[signal_name.capitalize()]) + getattr(mod, signal_name.capitalize()) + except AttributeError: + raise ModuleNotFoundError( + "[AttributeError] The module %s does not exist in the package %s " % (signal_name.capitalize(), + package_name)) + except ImportError: + raise ModuleNotFoundError( + "[ImportError] The module %s does not exist in the package %s " % (signal_name.capitalize(), + package_name)) + return True + if isinstance(signal_dict, dict): + for signal_name in signal_dict: + check_signal_exist(signal_name) + else: + check_signal_exist(signal_dict) return True @staticmethod diff --git a/kalliope/core/ConfigurationManager/SettingLoader.py b/kalliope/core/ConfigurationManager/SettingLoader.py index 35dc1eaa..d09a74ae 100644 --- a/kalliope/core/ConfigurationManager/SettingLoader.py +++ b/kalliope/core/ConfigurationManager/SettingLoader.py @@ -3,6 +3,7 @@ from six import with_metaclass from kalliope.core.Models.RpiSettings import RpiSettings +from kalliope.core.Models.RecognitionOptions import RecognitionOptions from .YAMLLoader import YAMLLoader from kalliope.core.Models.Resources import Resources from kalliope.core.Utils.Utils import Utils @@ -118,6 +119,7 @@ def _get_settings(self): resources = self._get_resources(settings) variables = self._get_variables(settings) rpi_settings = self._get_rpi_settings(settings) + recognition_options = self._get_recognition_options(settings) # Load the setting singleton with the parameters setting_object.default_tts_name = default_tts_name @@ -139,6 +141,7 @@ def _get_settings(self): setting_object.resources = resources setting_object.variables = variables setting_object.rpi_settings = rpi_settings + setting_object.recognition_options = recognition_options return setting_object @@ -615,6 +618,8 @@ def _get_resources(settings): .. raises:: SettingNotFound, NullSettingException, SettingInvalidException .. warnings:: Class Method and Private """ + # return an empty resource object anyway + resource_object = Resources() try: resource_dir = settings["resource_directory"] logger.debug("Resource directory synapse: %s" % resource_dir) @@ -623,40 +628,60 @@ def _get_resources(settings): stt_folder = None tts_folder = None trigger_folder = None + signal_folder = None + if "neuron" in resource_dir: neuron_folder = resource_dir["neuron"] - if not os.path.exists(neuron_folder): + if os.path.exists(neuron_folder): + logger.debug("[SettingLoader] Neuron resource folder path loaded: %s" % neuron_folder) + resource_object.neuron_folder = neuron_folder + else: raise SettingInvalidException("The path %s does not exist on the system" % neuron_folder) if "stt" in resource_dir: stt_folder = resource_dir["stt"] - if not os.path.exists(stt_folder): + if os.path.exists(stt_folder): + logger.debug("[SettingLoader] STT resource folder path loaded: %s" % stt_folder) + resource_object.stt_folder = stt_folder + else: raise SettingInvalidException("The path %s does not exist on the system" % stt_folder) if "tts" in resource_dir: tts_folder = resource_dir["tts"] - if not os.path.exists(tts_folder): + if os.path.exists(tts_folder): + logger.debug("[SettingLoader] TTS resource folder path loaded: %s" % tts_folder) + resource_object.tts_folder = tts_folder + else: raise SettingInvalidException("The path %s does not exist on the system" % tts_folder) if "trigger" in resource_dir: trigger_folder = resource_dir["trigger"] - if not os.path.exists(trigger_folder): + if os.path.exists(trigger_folder): + logger.debug("[SettingLoader] Trigger resource folder path loaded: %s" % trigger_folder) + resource_object.trigger_folder = trigger_folder + else: raise SettingInvalidException("The path %s does not exist on the system" % trigger_folder) + if "signal" in resource_dir: + signal_folder = resource_dir["signal"] + if os.path.exists(signal_folder): + logger.debug("[SettingLoader] Signal resource folder path loaded: %s" % signal_folder) + resource_object.signal_folder = signal_folder + else: + raise SettingInvalidException("The path %s does not exist on the system" % signal_folder) + if neuron_folder is None \ and stt_folder is None \ and tts_folder is None \ - and trigger_folder is None: + and trigger_folder is None \ + and signal_folder is None: raise SettingInvalidException("No required folder has been provided in the setting resource_directory. " - "Define : \'neuron\' or/and \'stt\' or/and \'tts\' or/and \'trigger\'") + "Define : \'neuron\' or/and \'stt\' or/and \'tts\' or/and \'trigger\' " + "or/and \'signal\'") - resource_object = Resources(neuron_folder=neuron_folder, - stt_folder=stt_folder, - tts_folder=tts_folder, - trigger_folder=trigger_folder) except KeyError: logger.debug("Resource directory not found in settings") - resource_object = None + return resource_object return resource_object @@ -762,3 +787,30 @@ def _get_rpi_settings(settings): except KeyError: logger.debug("[SettingsLoader] No Rpi config") return None + + @staticmethod + def _get_recognition_options(settings): + """ + return the value of stt_threshold + :param settings: The loaded YAML settings file + :return: integer or 1200 by default if not set + """ + recognition_options = RecognitionOptions() + + try: + recognition_options_dict = settings["RecognitionOptions"] + + if "energy_threshold" in recognition_options_dict: + recognition_options.energy_threshold = recognition_options_dict["energy_threshold"] + logger.debug("[SettingsLoader] energy_threshold set to %s" % recognition_options.energy_threshold) + if "adjust_for_ambient_noise_second" in recognition_options_dict: + recognition_options.adjust_for_ambient_noise_second = recognition_options_dict["adjust_for_ambient_noise_second"] + logger.debug("[SettingsLoader] adjust_for_ambient_noise_second set to %s" + % recognition_options.adjust_for_ambient_noise_second) + return recognition_options + + except KeyError: + logger.debug("[SettingsLoader] no recognition_options defined. Set to default") + + logger.debug("[SettingsLoader] recognition_options: %s" % str(recognition_options)) + return recognition_options diff --git a/kalliope/core/Cortex.py b/kalliope/core/Cortex.py new file mode 100644 index 00000000..133eb80d --- /dev/null +++ b/kalliope/core/Cortex.py @@ -0,0 +1,120 @@ +import logging + +import jinja2 +from kalliope.core.Utils.Utils import Utils + +from kalliope.core.Models import Singleton +from six import with_metaclass + +logging.basicConfig() +logger = logging.getLogger("kalliope") + + +class Cortex(with_metaclass(Singleton, object)): + """ + short-term memories of kalliope. Used to store object with a "key" "value" + """ + # this dict contains the short term memory of kalliope. + # all keys present in this dict has been saved from a user demand + memory = dict() + # this is a temp dict that allow us to store temporary parameters that as been loaded from the user order + # if the user want to a key from this dict, the key and its value will be added o the memory dict + temp = dict() + + def __init__(self): + logger.debug("[Cortex] New memory created") + + @classmethod + def get_memory(cls): + """ + Get the current dict of parameters saved in memory + :return: dict memory + """ + return cls.memory + + @classmethod + def save(cls, key, value): + """ + Save a new value in the memory + :param key: key to save + :param value: value to save into the key + """ + if key in cls.memory: + logger.debug("[Cortex] key %s already present in memory with value %s. Will be overridden" + % (key, cls.memory[key])) + logger.debug("[Cortex] key saved in memory. key: %s, value: %s" % (key, value)) + cls.memory[key] = value + + @classmethod + def get_from_key(cls, key): + try: + return cls.memory[key] + except KeyError: + logger.debug("[Cortex] key %s does not exist in memory" % key) + return None + + @classmethod + def add_parameters_from_order(cls, dict_parameter): + logger.debug("[Cortex] place parameters in temp list: %s" % dict_parameter) + cls.temp.update(dict_parameter) + + @classmethod + def clean_parameter_from_order(cls): + """ + Clean the temps memory that store parameters loaded from vocal order + """ + logger.debug("[Cortex] Clean temp memory") + cls.temp = dict() + + @classmethod + def save_neuron_parameter_in_memory(cls, kalliope_memory_dict, neuron_parameters): + """ + receive a dict of value send by the child neuron + save in kalliope memory all value + + E.g + dict_parameter_to_save = {"my_key_to_save_in_memory": "{{ output_val_from_neuron }}"} + neuron_parameter = {"output_val_from_neuron": "this_is_a_value" } + + then the cortex will save in memory the key "my_key_to_save_in_memory" and attach the value "this_is_a_value" + + :param neuron_parameters: dict of parameter the neuron has processed and send to the neurone module to + be processed by the TTS engine + :param kalliope_memory_dict: a dict of key value the user want to save from the dict_neuron_parameter + """ + + if kalliope_memory_dict is not None: + logger.debug("[Cortex] save_memory - User want to save: %s" % kalliope_memory_dict) + logger.debug("[Cortex] save_memory - Available parameters in the neuron: %s" % neuron_parameters) + + for key, value in kalliope_memory_dict.items(): + # ask the cortex to save in memory the target "key" if it was in parameters of the neuron + if isinstance(neuron_parameters, dict): + if Utils.is_containing_bracket(value): + value = jinja2.Template(value).render(neuron_parameters) + Cortex.save(key, value) + + @classmethod + def save_parameter_from_order_in_memory(cls, order_parameters): + """ + Save key from the temp dict (where parameters loaded from the voice order where placed temporary) + into the memory dict + :param order_parameters: dict of key to save. {'key_name_in_memory': 'key_name_in_temp_dict'} + :return True if a value has been saved in the kalliope memory + """ + order_saved = False + if order_parameters is not None: + logger.debug("[Cortex] save_parameter_from_order_in_memory - User want to save: %s" % order_parameters) + logger.debug("[Cortex] save_parameter_from_order_in_memory - Available parameters in orders: %s" + % cls.temp) + + for key, value in order_parameters.items(): + # ask the cortex to save in memory the target "key" if it was in the order + if Utils.is_containing_bracket(value): + # if the key exist in the temp dict we can load it with jinja + value = jinja2.Template(value).render(Cortex.temp) + if value: + Cortex.save(key, value) + order_saved = True + + return order_saved diff --git a/kalliope/core/EventManager.py b/kalliope/core/EventManager.py deleted file mode 100644 index 6521a65a..00000000 --- a/kalliope/core/EventManager.py +++ /dev/null @@ -1,49 +0,0 @@ -from apscheduler.schedulers.background import BackgroundScheduler -from apscheduler.triggers.cron import CronTrigger - -from kalliope.core.ConfigurationManager import BrainLoader -from kalliope.core.SynapseLauncher import SynapseLauncher -from kalliope.core import Utils -from kalliope.core.Models import Event - - -class EventManager(object): - - def __init__(self, synapses): - Utils.print_info('Starting event manager') - self.scheduler = BackgroundScheduler() - self.synapses = synapses - self.load_events() - self.scheduler.start() - - def load_events(self): - """ - For each received synapse that have an event as signal, we add a new job scheduled - to launch the synapse - :return: - """ - for synapse in self.synapses: - for signal in synapse.signals: - # if the signal is an event we add it to the task list - if type(signal) == Event: - my_cron = CronTrigger(year=signal.year, - month=signal.month, - day=signal.day, - week=signal.week, - day_of_week=signal.day_of_week, - hour=signal.hour, - minute=signal.minute, - second=signal.second) - Utils.print_info("Add synapse name \"%s\" to the scheduler: %s" % (synapse.name, my_cron)) - self.scheduler.add_job(self.run_synapse_by_name, my_cron, args=[synapse.name]) - - @staticmethod - def run_synapse_by_name(synapse_name): - """ - This method will run the synapse - """ - Utils.print_info("Event triggered, running synapse: %s" % synapse_name) - # get a brain - brain_loader = BrainLoader() - brain = brain_loader.brain - SynapseLauncher.start_synapse_by_name(synapse_name, brain=brain) diff --git a/kalliope/core/LIFOBuffer.py b/kalliope/core/LIFOBuffer.py index 5f5080c9..2d72500d 100644 --- a/kalliope/core/LIFOBuffer.py +++ b/kalliope/core/LIFOBuffer.py @@ -1,5 +1,7 @@ import logging +from six import with_metaclass +from kalliope.core.Cortex import Cortex from kalliope.core.NeuronLauncher import NeuronLauncher from kalliope.core.Models import Singleton from kalliope.core.Models.APIResponse import APIResponse @@ -23,7 +25,7 @@ class SynapseListAddedToLIFO(Exception): pass -class LIFOBuffer(object): +class LIFOBuffer(with_metaclass(Singleton, object)): """ This class is a LIFO list of synapse to process where the last synapse list to enter will be the first synapse list to be processed. @@ -32,56 +34,55 @@ class LIFOBuffer(object): like with the Neurotransmitter neuron. """ - __metaclass__ = Singleton - api_response = APIResponse() - lifo_list = list() - logger.debug("[LIFOBuffer] LIFO buffer created") - answer = None - is_api_call = False - no_voice = False + def __init__(self): + logger.debug("[LIFOBuffer] LIFO buffer created") + self.api_response = APIResponse() + self.lifo_list = list() + self.answer = None + self.is_api_call = False + self.no_voice = False + self.is_running = False + self.reset_lifo = False - @classmethod - def set_answer(cls, value): - cls.answer = value + def set_answer(self, value): + self.answer = value - @classmethod - def set_api_call(cls, value): - cls.is_api_call = value + def set_api_call(self, value): + self.is_api_call = value - @classmethod - def add_synapse_list_to_lifo(cls, matched_synapse_list): + def add_synapse_list_to_lifo(self, matched_synapse_list, high_priority=False): """ Add a synapse list to process to the lifo :param matched_synapse_list: List of Matched Synapse + :param high_priority: If True, the synapse list added is executed directly :return: """ logger.debug("[LIFOBuffer] Add a new synapse list to process to the LIFO") - cls.lifo_list.append(matched_synapse_list) + self.lifo_list.append(matched_synapse_list) + if high_priority: + self.reset_lifo = True - @classmethod - def clean(cls): + def clean(self): """ Clean the LIFO by creating a new list """ - cls.lifo_list = list() - cls.api_response = APIResponse() + self.lifo_list = list() + self.api_response = APIResponse() - @classmethod - def _return_serialized_api_response(cls): + def _return_serialized_api_response(self): """ Serialize Exception has been raised by the execute process somewhere, return the serialized API response to the caller. Clean up the APIResponse object for the next call :return: """ # we prepare a json response - returned_api_response = cls.api_response.serialize() + returned_api_response = self.api_response.serialize() # we clean up the API response object for the next call - cls.api_response = APIResponse() + self.api_response = APIResponse() return returned_api_response - @classmethod - def execute(cls, answer=None, is_api_call=False, no_voice=False): + def execute(self, answer=None, is_api_call=False, no_voice=False): """ Process the LIFO list. @@ -97,29 +98,34 @@ def execute(cls, answer=None, is_api_call=False, no_voice=False): :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 - while cls.lifo_list: - logger.debug("[LIFOBuffer] number of synapse list to process: %s" % len(cls.lifo_list)) - try: - # get the last list of matched synapse in the LIFO - last_synapse_fifo_list = cls.lifo_list[-1] - cls._process_synapse_list(last_synapse_fifo_list) - except SynapseListAddedToLIFO: - continue - # remove the synapse list from the LIFO - cls.lifo_list.remove(last_synapse_fifo_list) - raise Serialize - - except Serialize: - return cls._return_serialized_api_response() - - @classmethod - def _process_synapse_list(cls, synapse_list): + self.answer = answer + self.is_api_call = is_api_call + self.no_voice = no_voice + + if not self.is_running: + self.is_running = True + + try: + # we keep looping over the LIFO til we have synapse list to process in it + while self.lifo_list: + logger.debug("[LIFOBuffer] number of synapse list to process: %s" % len(self.lifo_list)) + try: + # get the last list of matched synapse in the LIFO + last_synapse_fifo_list = self.lifo_list[-1] + self._process_synapse_list(last_synapse_fifo_list) + except SynapseListAddedToLIFO: + continue + # remove the synapse list from the LIFO + self.lifo_list.remove(last_synapse_fifo_list) + # clean the cortex from value loaded from order as all synapses have been processed + Cortex.clean_parameter_from_order() + self.is_running = False + raise Serialize + + except Serialize: + return self._return_serialized_api_response() + + def _process_synapse_list(self, synapse_list): """ Process a list of matched synapse. Execute each neuron list for each synapse. @@ -133,16 +139,15 @@ def _process_synapse_list(cls, synapse_list): matched_synapse = synapse_list[0] # add the synapse to the API response so the user will get the status if the synapse was not already # in the response - if matched_synapse not in cls.api_response.list_processed_matched_synapse: - cls.api_response.list_processed_matched_synapse.append(matched_synapse) + if matched_synapse not in self.api_response.list_processed_matched_synapse: + self.api_response.list_processed_matched_synapse.append(matched_synapse) - cls._process_neuron_list(matched_synapse=matched_synapse) + self._process_neuron_list(matched_synapse=matched_synapse) # The synapse has been processed we can remove it from the list. synapse_list.remove(matched_synapse) - @classmethod - def _process_neuron_list(cls, matched_synapse): + def _process_neuron_list(self, matched_synapse): """ Process the neuron list of the matched_synapse Execute the Neuron @@ -162,25 +167,26 @@ def _process_neuron_list(cls, matched_synapse): # get the first neuron in the FIFO neuron list neuron = matched_synapse.neuron_fifo_list[0] # from here, we are back into the last neuron we were processing. - if cls.answer is not None: # we give the answer if exist to the first neuron - neuron.parameters["answer"] = cls.answer + if self.answer is not None: # we give the answer if exist to the first neuron + neuron.parameters["answer"] = self.answer # the next neuron should not get this answer - cls.answer = None + 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"] = 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)) + 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)) # execute the neuron instantiated_neuron = NeuronLauncher.start_neuron(neuron=neuron, parameters_dict=matched_synapse.parameters) # the status of an execution is "complete" if no neuron are waiting for an answer - cls.api_response.status = "complete" + self.api_response.status = "complete" if instantiated_neuron is not None: if instantiated_neuron.is_waiting_for_answer: # the neuron is waiting for an answer logger.debug("[LIFOBuffer] Wait for answer mode") - cls.api_response.status = "waiting_for_answer" + self.api_response.status = "waiting_for_answer" + self.is_running = False raise Serialize else: logger.debug("[LIFOBuffer] complete mode") @@ -190,12 +196,11 @@ def _process_neuron_list(cls, matched_synapse): # the neuron is fully processed we can remove it from the list matched_synapse.neuron_fifo_list.remove(neuron) - if instantiated_neuron.pending_synapse: # the last executed neuron want to run a synapse + if self.reset_lifo: # the last executed neuron want to run a synapse logger.debug("[LIFOBuffer] Last executed neuron want to run a synapse. Restart the LIFO") - # add the synapse to the lifo (inside a list as expected by the lifo) - cls.add_synapse_list_to_lifo([instantiated_neuron.pending_synapse]) # we have added a list of synapse to the LIFO ! this one must start over. # break all while loop until the execution is back to the LIFO loop + self.reset_lifo = False raise SynapseListAddedToLIFO else: raise Serialize diff --git a/kalliope/core/Models/Event.py b/kalliope/core/Models/Event.py deleted file mode 100644 index b482dd1c..00000000 --- a/kalliope/core/Models/Event.py +++ /dev/null @@ -1,49 +0,0 @@ -class Event(object): - """ - This Class is representing an Event which is raised by when the System at some defined time. - - .. note:: Events are based on the system crontab - """ - - def __init__(self, year=None, month=None, day=None, week=None, day_of_week=None, - hour=None, minute=None, second=None): - self.year = year - self.month = month - self.day = day - self.week = week - self.day_of_week = day_of_week - self.hour = hour - self.minute = minute - self.second = second - - def __str__(self): - return str(self.serialize()) - - def serialize(self): - """ - This method allows to serialize in a proper way this object - - :return: A dict of name / period - :rtype: Dict - """ - - return { - 'event': { - "year": self.year, - "month": self.month, - "day": self.day, - "week": self.week, - "day_of_week": self.day_of_week, - "hour": self.hour, - "minute": self.minute, - "second": self.second, - } - } - - def __eq__(self, other): - """ - This is used to compare 2 objects - :param other: - :return: - """ - return self.__dict__ == other.__dict__ diff --git a/kalliope/core/Models/MatchedSynapse.py b/kalliope/core/Models/MatchedSynapse.py index 9980c2c0..bbdc1fb2 100644 --- a/kalliope/core/Models/MatchedSynapse.py +++ b/kalliope/core/Models/MatchedSynapse.py @@ -28,9 +28,8 @@ def __init__(self, matched_synapse=None, matched_order=None, user_order=None, ov self.parameters = NeuronParameterLoader.get_parameters(synapse_order=self.matched_order, user_order=user_order) if overriding_parameter is not None: - # we suppose that we don't have any parameters. - # We replace the current parameter object with the received one - self.parameters = overriding_parameter + # merge dict of parameters with overriding + self.parameters.update(overriding_parameter) # list of Neuron Module self.neuron_module_list = list() diff --git a/kalliope/core/Models/Order.py b/kalliope/core/Models/Order.py deleted file mode 100644 index 353768b7..00000000 --- a/kalliope/core/Models/Order.py +++ /dev/null @@ -1,32 +0,0 @@ -class Order(object): - """ - This Class is representing an Order which is raised by when an entry (Vocal/REST/ anything ...) is matching it. - - .. note:: Order are defined in the brain file for each synapse. - """ - - def __init__(self, sentence): - self.sentence = sentence - - def __str__(self): - return str(self.serialize()) - - def serialize(self): - """ - This method allows to serialize in a proper way this object - - :return: A dict of order - :rtype: Dict - """ - - return { - 'order': self.sentence - } - - def __eq__(self, other): - """ - This is used to compare 2 objects - :param other: - :return: - """ - return self.__dict__ == other.__dict__ diff --git a/kalliope/core/Models/RecognitionOptions.py b/kalliope/core/Models/RecognitionOptions.py new file mode 100644 index 00000000..452e9f71 --- /dev/null +++ b/kalliope/core/Models/RecognitionOptions.py @@ -0,0 +1,27 @@ +class RecognitionOptions(object): + """ + This Class is representing a Speech To Text (STT) Recognition elements with name and parameters + + .. note:: must be defined in the settings.yml + """ + + def __init__(self, energy_threshold=4000, adjust_for_ambient_noise_second=0): + self.energy_threshold = energy_threshold + self.adjust_for_ambient_noise_second = adjust_for_ambient_noise_second + + def __str__(self): + return str(self.serialize()) + + def serialize(self): + return { + 'energy_threshold': self.energy_threshold, + 'adjust_for_ambient_noise_second': self.adjust_for_ambient_noise_second + } + + def __eq__(self, other): + """ + This is used to compare 2 objects + :param other: + :return: + """ + return self.__dict__ == other.__dict__ diff --git a/kalliope/core/Models/Resources.py b/kalliope/core/Models/Resources.py index c16e0451..da5f8e1e 100644 --- a/kalliope/core/Models/Resources.py +++ b/kalliope/core/Models/Resources.py @@ -4,11 +4,12 @@ class Resources(object): """ """ - def __init__(self, neuron_folder=None, stt_folder=None, tts_folder=None, trigger_folder=None): + def __init__(self, neuron_folder=None, stt_folder=None, tts_folder=None, trigger_folder=None, signal_folder=None): self.neuron_folder = neuron_folder self.stt_folder = stt_folder self.tts_folder = tts_folder self.trigger_folder = trigger_folder + self.signal_folder = signal_folder def __str__(self): return str(self.serialize()) @@ -25,7 +26,8 @@ def serialize(self): 'neuron_folder': self.neuron_folder, 'stt_folder': self.stt_folder, 'tts_folder': self.tts_folder, - 'trigger_folder': self.trigger_folder + 'trigger_folder': self.trigger_folder, + 'signal_folder': self.signal_folder } def __eq__(self, other): diff --git a/kalliope/core/Models/Settings.py b/kalliope/core/Models/Settings.py index 84bfce14..33d93721 100644 --- a/kalliope/core/Models/Settings.py +++ b/kalliope/core/Models/Settings.py @@ -27,7 +27,8 @@ def __init__(self, default_synapse=None, resources=None, variables=None, - rpi_settings=None): + rpi_settings=None, + recognition_options=None): self.default_tts_name = default_tts_name self.default_stt_name = default_stt_name @@ -50,6 +51,7 @@ def __init__(self, self.machine = platform.machine() # can be x86_64 or armv7l self.kalliope_version = current_kalliope_version self.rpi_settings = rpi_settings + self.recognition_options = recognition_options def serialize(self): """ @@ -80,7 +82,8 @@ def serialize(self): 'variables': self.variables, 'machine': self.machine, 'kalliope_version': self.kalliope_version, - 'rpi_settings': self.rpi_settings.serialize() if self.rpi_settings is not None else None + 'rpi_settings': self.rpi_settings.serialize() if self.rpi_settings is not None else None, + 'recognition_options': self.recognition_options.serialize() if self.recognition_options is not None else None, } def __str__(self): diff --git a/kalliope/core/Models/Signal.py b/kalliope/core/Models/Signal.py new file mode 100644 index 00000000..6e9cadea --- /dev/null +++ b/kalliope/core/Models/Signal.py @@ -0,0 +1,51 @@ +class Signal(object): + """ + This Class is representing a Signal which is corresponding to an input action that should start executing neuron + list when triggered + """ + + def __init__(self, name=None, parameters=None): + self.name = name + self.parameters = parameters + + def serialize(self): + """ + This method allows to serialize in a proper way this object + + :return: A dict of name and parameters + :rtype: Dict + """ + return { + 'name': self.name, + 'parameters': self.parameters + } + + def __str__(self): + """ + Return a string that describe the signal. If a parameter contains the word "password", + the output of this parameter will be masked in order to not appears in clean in the console + :return: string description of the neuron + """ + returned_dict = { + 'name': self.name, + 'parameters': self.parameters + } + + cleaned_parameters = dict() + if isinstance(self.parameters, dict): + for key, value in self.parameters.items(): + if "password" in key: + cleaned_parameters[key] = "*****" + else: + cleaned_parameters[key] = value + returned_dict["parameters"] = cleaned_parameters + + return str(returned_dict) + + def __eq__(self, other): + """ + This is used to compare 2 objects + :param other: + :return: + """ + return self.__dict__ == other.__dict__ diff --git a/kalliope/core/Models/__init__.py b/kalliope/core/Models/__init__.py index 0c1ea5b4..fde6b7db 100644 --- a/kalliope/core/Models/__init__.py +++ b/kalliope/core/Models/__init__.py @@ -1,8 +1,7 @@ from .Singleton import Singleton -from .Event import Event from .Resources import Resources from .Brain import Brain -from .Order import Order from .Synapse import Synapse from .Neuron import Neuron from .RpiSettings import RpiSettings +from .Signal import Signal diff --git a/kalliope/core/NeuronExceptions.py b/kalliope/core/NeuronExceptions.py new file mode 100644 index 00000000..7ed094d5 --- /dev/null +++ b/kalliope/core/NeuronExceptions.py @@ -0,0 +1,2 @@ +class NeuronExceptions(Exception): + pass diff --git a/kalliope/core/NeuronLauncher.py b/kalliope/core/NeuronLauncher.py index ce0d9663..947e9336 100644 --- a/kalliope/core/NeuronLauncher.py +++ b/kalliope/core/NeuronLauncher.py @@ -1,10 +1,12 @@ import logging -import six + import jinja2 -import sys +import six -from kalliope.core.Utils.Utils import Utils from kalliope.core.ConfigurationManager.SettingLoader import SettingLoader +from kalliope.core.Cortex import Cortex +from kalliope.core.NeuronExceptions import NeuronExceptions +from kalliope.core.Utils.Utils import Utils logging.basicConfig() logger = logging.getLogger("kalliope") @@ -28,8 +30,7 @@ def launch_neuron(cls, neuron): :return: """ logger.debug("Run neuron: \"%s\"" % (neuron.__str__())) - sl = SettingLoader() - settings = sl.settings + settings = cls.load_settings() neuron_folder = None if settings.resources: neuron_folder = settings.resources.neuron_folder @@ -52,9 +53,14 @@ def start_neuron(cls, neuron, parameters_dict=None): try: neuron.parameters = cls._replace_brackets_by_loaded_parameter(neuron.parameters, parameters_dict) except NeuronParameterNotAvailable: - Utils.print_danger("The neuron %s cannot be launched" % neuron.name) + Utils.print_danger("Missing parameter in neuron %s. Execution skipped" % neuron.name) return None - instantiated_neuron = NeuronLauncher.launch_neuron(neuron) + try: + instantiated_neuron = NeuronLauncher.launch_neuron(neuron) + except NeuronExceptions as e: + Utils.print_danger("ERROR: Fail to execute neuron '%s'. " + '%s' ". -> Execution skipped" % (neuron.name, e.message)) + return None return instantiated_neuron @classmethod @@ -67,11 +73,21 @@ def _replace_brackets_by_loaded_parameter(cls, neuron_parameters, loaded_paramet :param loaded_parameters: dict of parameters """ logger.debug("[NeuronLauncher] replacing brackets from %s, using %s" % (neuron_parameters, loaded_parameters)) + # add variables from the short term memory to the list of loaded parameters that can be used in a template + # the final dict is added into a key "kalliope_memory" to not override existing keys loaded form the order + memory_dict = dict() + memory_dict["kalliope_memory"] = Cortex.get_memory() + if loaded_parameters is None: + loaded_parameters = dict() # instantiate an empty dict in order to be able to add memory in it + loaded_parameters.update(memory_dict) if isinstance(neuron_parameters, str) or isinstance(neuron_parameters, six.text_type): # replace bracket parameter only if the str contains brackets if Utils.is_containing_bracket(neuron_parameters): # check that the parameter to replace is available in the loaded_parameters dict if cls._neuron_parameters_are_available_in_loaded_parameters(neuron_parameters, loaded_parameters): + # add parameters from global variable into the final loaded parameter dict + settings = cls.load_settings() + loaded_parameters.update(settings.variables) neuron_parameters = jinja2.Template(neuron_parameters).render(loaded_parameters) neuron_parameters = Utils.encode_text_utf8(neuron_parameters) return str(neuron_parameters) @@ -82,7 +98,9 @@ def _replace_brackets_by_loaded_parameter(cls, neuron_parameters, loaded_paramet if isinstance(neuron_parameters, dict): returned_dict = dict() for key, value in neuron_parameters.items(): - if key in "say_template" or key in "file_template": # those keys are reserved for the TTS. + # following keys are reserved by kalliope core + if key in "say_template" or key in "file_template" or key in "kalliope_memory" \ + or key in "from_answer_link": returned_dict[key] = value else: returned_dict[key] = cls._replace_brackets_by_loaded_parameter(value, loaded_parameters) @@ -121,3 +139,12 @@ def _neuron_parameters_are_available_in_loaded_parameters(string_parameters, loa Utils.print_danger("The parameter %s is not available in the order" % str(parameter)) return False return True + + @staticmethod + def load_settings(): + """ + Return loaded kalliope settings + :return: setting object + """ + sl = SettingLoader() + return sl.settings diff --git a/kalliope/core/NeuronModule.py b/kalliope/core/NeuronModule.py index 4a3a60d8..5ced5a36 100644 --- a/kalliope/core/NeuronModule.py +++ b/kalliope/core/NeuronModule.py @@ -2,13 +2,16 @@ import logging import random import sys -import six +import six from jinja2 import Template from kalliope.core import OrderListener from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader +from kalliope.core.Cortex import Cortex +from kalliope.core.LIFOBuffer import LIFOBuffer from kalliope.core.Models.MatchedSynapse import MatchedSynapse +from kalliope.core.NeuronExceptions import NeuronExceptions from kalliope.core.OrderAnalyser import OrderAnalyser from kalliope.core.Utils.RpiUtils import RpiUtils from kalliope.core.Utils.Utils import Utils @@ -17,18 +20,23 @@ logger = logging.getLogger("kalliope") -class InvalidParameterException(Exception): +class InvalidParameterException(NeuronExceptions): """ Some Neuron parameters are invalid. """ - pass + def __init__(self, message): + # Call the base class constructor with the parameters it needs + super(InvalidParameterException, self).__init__(message) -class MissingParameterException(Exception): +class MissingParameterException(NeuronExceptions): """ Some Neuron parameters are missing. """ - pass + + def __init__(self, message): + # Call the base class constructor with the parameters it needs + super(MissingParameterException, self).__init__(message) class NoTemplateException(Exception): @@ -103,6 +111,10 @@ def __init__(self, **kwargs): self.is_waiting_for_answer = False # the synapse name to add the the buffer self.pending_synapse = None + # a dict of parameters the user ask to save in short term memory + self.kalliope_memory = kwargs.get('kalliope_memory', None) + # parameters loaded from the order can be save now + Cortex.save_parameter_from_order_in_memory(self.kalliope_memory) def __str__(self): retuned_string = "" @@ -137,6 +149,9 @@ def say(self, message): tts_message = None + # we can save parameters from the neuron in memory + Cortex.save_neuron_parameter_in_memory(self.kalliope_memory, message) + if isinstance(message, str) or isinstance(message, six.text_type): logger.debug("[NeuronModule] message is string") tts_message = message @@ -185,7 +200,6 @@ def _get_message_from_dict(self, message_dict): .. raises:: TemplateFileNotFoundException """ returned_message = None - # the user chooses a say_template option if self.say_template is not None: returned_message = self._get_say_template(self.say_template, message_dict) @@ -222,18 +236,30 @@ def _get_file_template(cls, file_template, message_dict): return returned_message - def run_synapse_by_name(self, synapse_name, user_order=None, synapse_order=None): + @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): """ 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 :param user_order: The user order :param synapse_order: The synapse order + :param high_priority: If True, the synapse is executed before the end of the current synapse list + :param is_api_call: If true, the current call comes from the api + :param overriding_parameter_dict: dict of value to add to neuron parameters """ synapse = BrainLoader().get_brain().get_synapse_by_name(synapse_name) matched_synapse = MatchedSynapse(matched_synapse=synapse, matched_order=synapse_order, - user_order=user_order) - self.pending_synapse = matched_synapse + user_order=user_order, + overriding_parameter=overriding_parameter_dict) + + list_synapse_to_process = list() + list_synapse_to_process.append(matched_synapse) + # get the singleton + lifo_buffer = LIFOBuffer() + lifo_buffer.add_synapse_list_to_lifo(list_synapse_to_process, high_priority=high_priority) + lifo_buffer.execute(is_api_call=is_api_call) @staticmethod def is_order_matching(order_said, order_match): diff --git a/kalliope/core/NeuronParameterLoader.py b/kalliope/core/NeuronParameterLoader.py index ae526e42..999b905e 100644 --- a/kalliope/core/NeuronParameterLoader.py +++ b/kalliope/core/NeuronParameterLoader.py @@ -1,3 +1,4 @@ +from kalliope.core.Cortex import Cortex from kalliope.core.Utils import Utils import logging @@ -16,7 +17,9 @@ def get_parameters(cls, synapse_order, user_order): params = dict() if Utils.is_containing_bracket(synapse_order): params = cls._associate_order_params_to_values(user_order, synapse_order) - logger.debug("Parameters for order: %s" % params) + logger.debug("[NeuronParameterLoader.get_parameters]Parameters for order: %s" % params) + # we place the dict of parameters load from order into a cache in Cortex so the user can save it later + Cortex.add_parameters_from_order(params) return params @classmethod @@ -29,7 +32,7 @@ def _associate_order_params_to_values(cls, order, order_to_check): :type order: str :return: the dict corresponding to the key / value of the params """ - logger.debug("[OrderAnalyser._associate_order_params_to_values] user order: %s, " + logger.debug("[NeuronParameterLoader._associate_order_params_to_values] user order: %s, " "order from synapse: %s" % (order, order_to_check)) list_word_in_order = Utils.remove_spaces_in_brackets(order_to_check).split() @@ -54,7 +57,7 @@ def _associate_order_params_to_values(cls, order, order_to_check): dict_var[var_name] = " ".join(truncate_list_word_said) break for word_said in truncate_list_word_said: - if word_said == stop_value: + if word_said.lower() == stop_value.lower(): # Do not consider the case break if var_name in dict_var: dict_var[var_name] += " " + word_said diff --git a/kalliope/core/OrderAnalyser.py b/kalliope/core/OrderAnalyser.py index b54ed526..91b56fc3 100644 --- a/kalliope/core/OrderAnalyser.py +++ b/kalliope/core/OrderAnalyser.py @@ -1,13 +1,11 @@ # coding: utf8 import collections from collections import Counter -import sys import six from kalliope.core.Models.MatchedSynapse import MatchedSynapse from kalliope.core.Utils.Utils import Utils from kalliope.core.ConfigurationManager import SettingLoader -from kalliope.core.Models import Order import logging @@ -54,12 +52,13 @@ def get_matching_synapse(cls, order, brain=None): for synapse in cls.brain.synapses: # we are only concerned by synapse with a order type of signal for signal in synapse.signals: - if type(signal) == Order: - if cls.spelt_order_match_brain_order_via_table(signal.sentence, order): + + if signal.name == "order": + if cls.spelt_order_match_brain_order_via_table(signal.parameters, order): # 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.sentence)) + list_match_synapse.append(synapse_order_tuple(synapse=synapse, order=signal.parameters)) # create a list of MatchedSynapse from the tuple list list_synapse_to_process = list() diff --git a/kalliope/core/ResourcesManager.py b/kalliope/core/ResourcesManager.py index c72d0c59..9caef22a 100644 --- a/kalliope/core/ResourcesManager.py +++ b/kalliope/core/ResourcesManager.py @@ -32,7 +32,7 @@ TYPE_TTS = "tts" TYPE_STT = "stt" TYPE_TRIGGER = "trigger" - +TYPE_SIGNAL = "signal" class ResourcesManagerException(Exception): pass @@ -103,7 +103,11 @@ def install(self): % str(self.tmp_path)) shutil.rmtree(self.tmp_path) - def uninstall(self, neuron_name=None, tts_name=None, stt_name=None, trigger_name=None): + def uninstall(self, neuron_name=None, + tts_name=None, + stt_name=None, + trigger_name=None, + signal_name= None): """ Uninstall a community resource """ @@ -111,21 +115,26 @@ def uninstall(self, neuron_name=None, tts_name=None, stt_name=None, trigger_name module_name = "" if neuron_name is not None: target_path_to_delete = self._get_target_folder(resources=self.settings.resources, - module_type="neuron") + module_type=TYPE_NEURON) module_name = neuron_name if tts_name is not None: target_path_to_delete = self._get_target_folder(resources=self.settings.resources, - module_type="neuron") + module_type=TYPE_TTS) module_name = tts_name if stt_name is not None: target_path_to_delete = self._get_target_folder(resources=self.settings.resources, - module_type="neuron") + module_type=TYPE_STT) module_name = stt_name if trigger_name is not None: target_path_to_delete = self._get_target_folder(resources=self.settings.resources, - module_type="neuron") + module_type=TYPE_TRIGGER) module_name = trigger_name + if signal_name is not None: + target_path_to_delete = self._get_target_folder(resources=self.settings.resources, + module_type=TYPE_SIGNAL) + module_name = signal_name + if target_path_to_delete is not None: try: shutil.rmtree(target_path_to_delete + os.sep + module_name.lower()) @@ -173,6 +182,11 @@ def is_settings_ok(resources, dna): logger.debug(message) Utils.print_danger(message) settings_ok = False + if dna.module_type == "signal" and resources.signal_folder is None: + message = "Resources folder for signal installation not set in settings, cannot install." + logger.debug(message) + Utils.print_danger(message) + settings_ok = False return settings_ok @@ -201,7 +215,7 @@ def _get_target_folder(resources, module_type): Return the folder from the resources and given a module type :param resources: Resource object :type resources: Resources - :param module_type: type of the module (TYPE_NEURON, TYPE_STT, TYPE_TTS, TYPE_TRIGGER) + :param module_type: type of the module (TYPE_NEURON, TYPE_STT, TYPE_TTS, TYPE_TRIGGER, TYPE_SIGNAL) :return: path of the folder """ module_type_converter = dict() @@ -211,7 +225,8 @@ def _get_target_folder(resources, module_type): TYPE_NEURON: resources.neuron_folder, TYPE_STT: resources.stt_folder, TYPE_TTS: resources.tts_folder, - TYPE_TRIGGER: resources.trigger_folder + TYPE_TRIGGER: resources.trigger_folder, + TYPE_SIGNAL: resources.signal_folder } except AttributeError: # will be raised if the resource folder is not set in settings diff --git a/kalliope/core/RestAPI/FlaskAPI.py b/kalliope/core/RestAPI/FlaskAPI.py index 49ae0318..2cba79b3 100644 --- a/kalliope/core/RestAPI/FlaskAPI.py +++ b/kalliope/core/RestAPI/FlaskAPI.py @@ -9,6 +9,7 @@ from flask_restful import abort from werkzeug.utils import secure_filename +from kalliope import SignalLauncher from kalliope._version import version_str from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader from kalliope.core.LIFOBuffer import LIFOBuffer @@ -17,6 +18,7 @@ from kalliope.core.RestAPI.utils import requires_auth from kalliope.core.SynapseLauncher import SynapseLauncher from kalliope.core.Utils.FileManager import FileManager +from kalliope.signals.order import Order logging.basicConfig() logger = logging.getLogger("kalliope") @@ -72,6 +74,8 @@ def __init__(self, app, port=5000, brain=None, allowed_cors_origin=False): self.app.add_url_rule('/synapses/start/order', view_func=self.run_synapse_by_order, methods=['POST']) self.app.add_url_rule('/synapses/start/audio', view_func=self.run_synapse_by_audio, methods=['POST']) self.app.add_url_rule('/shutdown/', view_func=self.shutdown_server, 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="%s" % int(self.port), debug=True, threaded=True, use_reloader=False) @@ -157,7 +161,7 @@ def run_synapse_by_name(self, 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) + no_voice = self.get_boolean_flag_from_request(request, boolean_flag_to_find="no_voice") # get parameters parameters = self.get_parameters_from_request(request) @@ -204,7 +208,7 @@ def run_synapse_by_order(self): order = request.get_json('order') # get no_voice_flag if present - no_voice = self.get_no_voice_flag_from_request(request) + no_voice = self.get_boolean_flag_from_request(request, boolean_flag_to_find="no_voice") if order is not None: # get the order order_to_run = order["order"] @@ -307,6 +311,59 @@ def shutdown_server(self): func() return "Shutting down..." + @requires_auth + def get_mute(self): + """ + Return the current trigger status + + Curl test + curl -i --user admin:secret -X GET http://127.0.0.1:5000/mute + """ + + # find the order signal and call the mute method + signal_order = SignalLauncher.get_order_instance() + if signal_order is not None: + data = { + "mute": signal_order.get_mute_status() + } + 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 trigger status (muted 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") + + # find the order signal and call the mute method + signal_order = SignalLauncher.get_order_instance() + if signal_order is not None: + signal_order.set_mute_status(mute) + data = { + "mute": signal_order.get_mute_status() + } + return jsonify(data), 200 + + data = { + "error": "Cannot switch mute status" + } + return jsonify(error=data), 400 + def audio_analyser_callback(self, order): """ Callback of the OrderListener. Called after the processing of the audio file @@ -329,23 +386,23 @@ def audio_analyser_callback(self, order): # 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): + def get_boolean_flag_from_request(self, http_request, boolean_flag_to_find): """ - Get the no_voice flag from the request if exist + Get the boolean flag from the request if exist :param http_request: - :return: + :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 """ - - no_voice = False + boolean_flag = False try: received_json = http_request.get_json(force=True, silent=True, cache=True) - if 'no_voice' in received_json: - no_voice = self.str_to_bool(received_json['no_voice']) + if boolean_flag_to_find in received_json: + boolean_flag = self.str_to_bool(received_json[boolean_flag_to_find]) except TypeError: # no json received pass - logger.debug("[FlaskAPI] no_voice: %s" % no_voice) - return no_voice + logger.debug("[FlaskAPI] Boolean %s : %s" % (boolean_flag_to_find, boolean_flag)) + return boolean_flag @staticmethod def str_to_bool(s): diff --git a/kalliope/core/ShellGui.py b/kalliope/core/ShellGui.py index 2719dcf2..696b17ed 100644 --- a/kalliope/core/ShellGui.py +++ b/kalliope/core/ShellGui.py @@ -2,15 +2,12 @@ import locale import logging -import signal -import sys from dialog import Dialog from kalliope.core import OrderListener from kalliope.core.ConfigurationManager import SettingLoader from kalliope.core.SynapseLauncher import SynapseLauncher -from kalliope.core.Utils.Utils import Utils from kalliope.neurons.say.say import Say logging.basicConfig() diff --git a/kalliope/core/SignalLauncher.py b/kalliope/core/SignalLauncher.py new file mode 100644 index 00000000..3484cfbb --- /dev/null +++ b/kalliope/core/SignalLauncher.py @@ -0,0 +1,54 @@ +import logging + +from kalliope import Utils +from kalliope.signals.order import Order + +logging.basicConfig() +logger = logging.getLogger("kalliope") + + +class SignalLauncher: + + # keep a list of instantiated signals + list_launched_signals = list() + + def __init__(self): + pass + + @classmethod + def launch_signal_class_by_name(cls, signal_name, settings=None): + """ + load the signal class from the given name, pass the brain and settings to the signal + :param signal_name: name of the signal class to load + :param settings: Settings Object + """ + signal_folder = None + if settings.resources: + signal_folder = settings.resources.signal_folder + + launched_signal = Utils.get_dynamic_class_instantiation(package_name="signals", + module_name=signal_name, + resources_dir=signal_folder) + + cls.add_launched_signals_to_list(launched_signal) + + return launched_signal + + @classmethod + def add_launched_signals_to_list(cls, signal): + cls.list_launched_signals.append(signal) + + @classmethod + def get_launched_signals_list(cls): + return cls.list_launched_signals + + @classmethod + def get_order_instance(cls): + """ + Return the Order instance from the list of launched signals if exist + :return: + """ + for signal in cls.list_launched_signals: + if isinstance(signal, Order): + return signal + return None diff --git a/kalliope/core/SynapseLauncher.py b/kalliope/core/SynapseLauncher.py index c208470a..0557da57 100644 --- a/kalliope/core/SynapseLauncher.py +++ b/kalliope/core/SynapseLauncher.py @@ -23,11 +23,12 @@ class SynapseNameNotFound(Exception): class SynapseLauncher(object): @classmethod - def start_synapse_by_name(cls, name, brain=None): + def start_synapse_by_name(cls, name, brain=None, overriding_parameter_dict=None): """ Start a synapse by it's name :param name: Name (Unique ID) of the synapse to launch :param brain: Brain instance + :param overriding_parameter_dict: parameter to pass to neurons """ logger.debug("[SynapseLauncher] start_synapse_by_name called with synapse name: %s " % name) # check if we have found and launched the synapse @@ -41,7 +42,8 @@ def start_synapse_by_name(cls, name, brain=None): list_synapse_to_process = list() new_matching_synapse = MatchedSynapse(matched_synapse=synapse, matched_order=None, - user_order=None) + user_order=None, + overriding_parameter=overriding_parameter_dict) list_synapse_to_process.append(new_matching_synapse) lifo_buffer.add_synapse_list_to_lifo(list_synapse_to_process) return lifo_buffer.execute(is_api_call=True) diff --git a/kalliope/core/Utils/Utils.py b/kalliope/core/Utils/Utils.py index 916a5a95..ac582257 100644 --- a/kalliope/core/Utils/Utils.py +++ b/kalliope/core/Utils/Utils.py @@ -44,42 +44,42 @@ class Utils(object): @classmethod def print_info(cls, text_to_print): pipe_print(cls.color_list["BLUE"] + text_to_print + cls.color_list["ENDLINE"]) - logger.info(text_to_print) + logger.debug(text_to_print) @classmethod def print_success(cls, text_to_print): pipe_print(cls.color_list["GREEN"] + text_to_print + cls.color_list["ENDLINE"]) - logger.info(text_to_print) + logger.debug(text_to_print) @classmethod def print_warning(cls, text_to_print): pipe_print(cls.color_list["YELLOW"] + text_to_print + cls.color_list["ENDLINE"]) - logger.info(text_to_print) + logger.debug(text_to_print) @classmethod def print_danger(cls, text_to_print): pipe_print(cls.color_list["RED"] + text_to_print + cls.color_list["ENDLINE"]) - logger.info(text_to_print) + logger.debug(text_to_print) @classmethod def print_header(cls, text_to_print): pipe_print(cls.color_list["HEADER"] + text_to_print + cls.color_list["ENDLINE"]) - logger.info(text_to_print) + logger.debug(text_to_print) @classmethod def print_purple(cls, text_to_print): pipe_print(cls.color_list["PURPLE"] + text_to_print + cls.color_list["ENDLINE"]) - logger.info(text_to_print) + logger.debug(text_to_print) @classmethod def print_bold(cls, text_to_print): pipe_print(cls.color_list["BOLD"] + text_to_print + cls.color_list["ENDLINE"]) - logger.info(text_to_print) + logger.debug(text_to_print) @classmethod def print_underline(cls, text_to_print): pipe_print(cls.color_list["UNDERLINE"] + text_to_print + cls.color_list["ENDLINE"]) - logger.info(text_to_print) + logger.debug(text_to_print) @staticmethod def print_yaml_nicely(to_print): diff --git a/kalliope/core/__init__.py b/kalliope/core/__init__.py index 4b6b7b0c..50f0bb09 100755 --- a/kalliope/core/__init__.py +++ b/kalliope/core/__init__.py @@ -10,5 +10,3 @@ from kalliope.core.NeuronParameterLoader import NeuronParameterLoader from kalliope.core.NeuronModule import NeuronModule from kalliope.core.PlayerModule import PlayerModule -from kalliope.core.MainController import MainController -from kalliope.core.EventManager import EventManager diff --git a/kalliope/neurons/mqtt_publisher/README.md b/kalliope/neurons/mqtt_publisher/README.md new file mode 100644 index 00000000..b5a122b5 --- /dev/null +++ b/kalliope/neurons/mqtt_publisher/README.md @@ -0,0 +1,145 @@ +# MQTT Publisher + +## Synopsis + +Publish a message to a MQTT broker server + +## Installation + +This is a core neuron, no installation required. + +## Options + +| parameter | required | type | default | choices | comment | +|--------------|----------|---------|----------|---------------------|--------------------------------------------------------------------------------------------------| +| broker_ip | YES | string | | | IP address of the MQTT broker server | +| port | NO | int | 1883 | | Port of the broker. By default 1883. 8883 when TLS is activated. | +| topic | YES | string | | | Topic name where the message will be published | +| payload | YES | string | | | Message to publish on the topic | +| qos | NO | int | 0 | 0 or 1 or 2 | The quality of service level to use | +| retain | NO | Boolean | FALSE | True, False | if set to True, the message will be set as the “last known good”/retained message for the topic. | +| client_id | NO | string | kalliope | | The MQTT client id to use. If not set, the name will be set to "kalliope" | +| keepalive | NO | int | 60 | | The keepalive timeout value for the client | +| username | NO | string | | | username for authenticating the client | +| password | NO | string | | | password for authenticating the client | +| ca_cert | NO | string | | | Path to the remote server CA certificate used for securing the transport | +| certfile | NO | string | | | Path to the client certificate file used for authentication | +| keyfile | NO | string | | | Path to the client key file attached to the client certificate | +| protocol | NO | string | MQTTv311 | MQTTv31 or MQTTv311 | Can be either MQTTv31 or MQTTv311 | +| tls_insecure | NO | string | FALSE | | Set the verification of the server hostname in the server certificate | + +## Return Values + +No returned values + +## Synapses example + +Publish a message to the topic "my/topic" with minimal configuration +```yml +- name: "mqtt-publisher-1" + signals: + - order: "this is my order" + neurons: + - mqtt_publisher: + broker_ip: "127.0.0.1" + topic: "my/topic" + payload: "my message" +``` + +Publish a json formatted message. Note that anti-slashes must be escaped. +```yml +- name: "mqtt-publisher-2" + signals: + - order: "this is my order" + neurons: + - mqtt_publisher: + broker_ip: "127.0.0.1" + topic: "mytopic" + payload: "{\"mykey\": \"myvalue\"}" +``` + +The broker require authentication +```yml +- name: "mqtt-publisher-3" + signals: + - order: "this is my order" + neurons: + - mqtt_publisher: + broker_ip: "127.0.0.1" + topic: "my/topic" + payload: "my message" + username: "guest" + password: "guest" +``` + +The broker require a secure TLS connection +```yml +- name: "mqtt-publisher-4" + signals: + - order: "this is my order" + neurons: + - mqtt_publisher: + broker_ip: "127.0.0.1" + topic: "my/topic" + payload: "my message" + ca_cert: "/path/to/ca.cert" +``` + +The broker require a secure TLS connection and authentication based on client certificate +```yml +- name: "mqtt-publisher-5" + signals: + - order: "this is my order" + neurons: + - mqtt_publisher: + broker_ip: "127.0.0.1" + topic: "my/topic" + payload: "my message" + ca_cert: "/path/to/ca.cert" + certfile: "path/to/client.crt" + keyfile: "path/to/client.key" +``` + +The broker require a secure TLS connection, an authentication based on client certificate and the CA is a self signed certificate +```yml +- name: "mqtt-publisher-6" + signals: + - order: "this is my order" + neurons: + - mqtt_publisher: + broker_ip: "127.0.0.1" + topic: "my/topic" + payload: "my message" + ca_cert: "/path/to/ca.cert" + certfile: "path/to/client.crt" + keyfile: "path/to/client.key" + tls_insecure: True +``` + + +## Test with CLI + +The following part of the documentation can help you to configure your synapse with right options. +From here we suppose that you have already a running broker server on your local machine. If it's not the case, please refer to the documentation of the [signal mqtt_subscriber](../../signals/mqtt_subscriber) to install a testing broker server. + +Install a CLI mqtt client +```bash +sudo apt-get install mosquitto-clients +``` + +Run a subscriber +```bash +mosquitto_sub -t 'this/is/a/topic' +``` + +Then use your neuron. E.g +```yml +- name: "test-mqtt-publisher" + signals: + - order: "this is my order" + neurons: + - mqtt_publisher: + broker_ip: "127.0.0.1" + topic: "this/is/a/topic" + payload: "info" +``` diff --git a/kalliope/neurons/mqtt_publisher/__init__.py b/kalliope/neurons/mqtt_publisher/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kalliope/neurons/mqtt_publisher/mqtt_publisher.py b/kalliope/neurons/mqtt_publisher/mqtt_publisher.py new file mode 100644 index 00000000..04bcbffc --- /dev/null +++ b/kalliope/neurons/mqtt_publisher/mqtt_publisher.py @@ -0,0 +1,143 @@ +import logging +import socket + +import paho +import paho.mqtt.client as mqtt + +from kalliope.core.NeuronModule import NeuronModule + +logging.basicConfig() +logger = logging.getLogger("kalliope") + + +class Mqtt_publisher(NeuronModule): + def __init__(self, **kwargs): + super(Mqtt_publisher, self).__init__(**kwargs) + + logger.debug("[mqtt_publisher] neuron called with parameters: %s" % kwargs) + + # get parameters + self.broker_ip = kwargs.get('broker_ip', None) + self.port = kwargs.get('port', 1883) + self.topic = kwargs.get('topic', None) + self.payload = kwargs.get('payload', None) + self.qos = kwargs.get('qos', 0) + self.retain = kwargs.get('retain', False) + self.client_id = kwargs.get('client_id', 'kalliope') + self.keepalive = kwargs.get('keepalive', 60) + self.username = kwargs.get('username', None) + self.password = kwargs.get('password', None) + self.ca_cert = kwargs.get('ca_cert', None) + self.certfile = kwargs.get('certfile', None) + self.keyfile = kwargs.get('keyfile', None) + self.protocol = kwargs.get('protocol', 'MQTTv311') + self.tls_insecure = kwargs.get('tls_insecure', False) + + if not self._is_parameters_ok(): + logger.debug("[mqtt_publisher] One or more invalid parameters, neuron will not be launched") + else: + # string must be converted + self.protocol = self._get_protocol(self.protocol) + + self.client = mqtt.Client(client_id=self.broker_ip, protocol=self.protocol) + + if self.username is not None and self.password is not None: + logger.debug("[mqtt_publisher] Username and password are set") + self.client.username_pw_set(self.username, self.password) + + if self.ca_cert is not None and self.certfile is not None and self.keyfile is not None: + logger.debug("[mqtt_publisher] Active TLS with client certificate authentication") + self.client.tls_set(ca_certs=self.ca_cert, + certfile=self.certfile, + keyfile=self.keyfile) + self.client.tls_insecure_set(self.tls_insecure) + + elif self.ca_cert is not None: + logger.debug("[mqtt_publisher] Active TLS with server CA certificate only") + self.client.tls_set(ca_certs=self.ca_cert) + self.client.tls_insecure_set(self.tls_insecure) + + try: + self.client.connect(self.broker_ip, port=self.port, keepalive=self.keepalive) + self.client.publish(topic=self.topic, payload=self.payload, qos=int(self.qos), retain=self.retain) + logger.debug("[mqtt_publisher] Message published to topic %s: %s" % (self.topic, self.payload)) + self.client.disconnect() + except socket.error: + logger.debug("[mqtt_publisher] Unable to connect to broker %s" % self.broker_ip) + + def _is_parameters_ok(self): + if self.broker_ip is None: + print("[mqtt_publisher] ERROR: broker_ip is not set") + return False + + if self.port is not None: + if not isinstance(self.port, int): + try: + self.port = int(self.port) + except ValueError: + print("[mqtt_publisher] ERROR: port must be an integer") + return False + + if self.topic is None: + print("[mqtt_publisher] ERROR: topic is not set") + return False + + if self.payload is None: + print("[mqtt_publisher] ERROR: payload is not set") + return False + + if self.qos: + if not isinstance(self.qos, int): + try: + self.qos = int(self.qos) + except ValueError: + print("[mqtt_publisher] ERROR: qos must be an integer") + return False + if self.qos not in [0, 1, 2]: + print("[mqtt_publisher] ERROR: qos must be 0,1 or 2") + return False + + if self.keepalive: + if not isinstance(self.keepalive, int): + try: + self.keepalive = int(self.keepalive) + except ValueError: + print("[mqtt_publisher] ERROR: keepalive must be an integer") + return False + + if self.username is not None and self.password is None: + print("[mqtt_publisher] ERROR: password must be set when using username") + return False + if self.username is None and self.password is not None: + print("[mqtt_publisher] ERROR: username must be set when using password") + return False + + if self.protocol: + if self.protocol not in ["MQTTv31", "MQTTv311"]: + print("[mqtt_publisher] Invalid protocol value, fallback to MQTTv311") + self.protocol = "MQTTv311" + + # if the user set a certfile, the key and ca cert must be set to + if self.certfile is not None and self.keyfile is None: + print("[mqtt_publisher] ERROR: keyfile must be set when using certfile") + return False + if self.certfile is None and self.keyfile is not None: + print("[mqtt_publisher] ERROR: certfile must be set when using keyfile") + return False + + if self.certfile is not None and self.keyfile is not None: + if self.ca_cert is None: + print("[mqtt_publisher] ERROR: ca_cert must be set when using keyfile and certfile") + return False + + return True + + def _get_protocol(self, protocol): + """ + Return the right code depending on the given string protocol name + :param protocol: string name of the protocol to use. + :return: integer + """ + if protocol == "MQTTv31": + return paho.mqtt.client.MQTTv31 + return paho.mqtt.client.MQTTv311 diff --git a/kalliope/neurons/mute/README.md b/kalliope/neurons/mute/README.md new file mode 100644 index 00000000..3744952b --- /dev/null +++ b/kalliope/neurons/mute/README.md @@ -0,0 +1,52 @@ +# Mute + +## Synopsis + +Mute control of kalliope. If set to True the trigger process will be stopped. + +Once this neuron is used, and Kalliope muted, the hotword is deactivated. Only ways to unmute are: +- by calling the API (see [mute section](../../../Docs/rest_api.md#switch-mute-status)) +- If running on Raspberry, by using the unmute button. (See the section [Raspberry LED and mute button](../../../Docs/settings.md#raspberry-led-and-mute-button)) +- by using another signals than a "vocal order" that call back this neuron with a status set to "False" +- Restarting Kalliope + +## Options + +| parameter | required | type | default | choices | comment | +|-----------|----------|---------|---------|-------------|---------------------------------------------------| +| status | YES | Boolean | | True, False | If "True" Kalliope will stop the hotword process | + + +## Return Values + +Not returned values + +## Synapses example + +Mute Kalliope from a vocal order +```yml +- name: "mute-synapse" + signals: + - order: "stop listening" + neurons: + - say: + message: + - "I stop hearing you, sir" + - mute: + status: True +``` + +Unmute Kalliope from another signals. In the following example, a MQTT message is received +```yml +- name: "unmute-synapse" + signals: + - mqtt_subscriber: + broker_ip: "127.0.0.1" + topic: "/my/sensor" + neurons: + - mute: + status: False + - say: + message: + - "Waiting for orders, sir" +``` diff --git a/kalliope/neurons/mute/__init__.py b/kalliope/neurons/mute/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kalliope/neurons/mute/mute.py b/kalliope/neurons/mute/mute.py new file mode 100644 index 00000000..8c30b260 --- /dev/null +++ b/kalliope/neurons/mute/mute.py @@ -0,0 +1,33 @@ +import logging + +from kalliope import SignalLauncher +from kalliope.core.NeuronModule import NeuronModule + +logging.basicConfig() +logger = logging.getLogger("kalliope") + + +class Mute(NeuronModule): + + def __init__(self, **kwargs): + super(Mute, self).__init__(**kwargs) + + self.status = kwargs.get('status', None) + + # check if parameters have been provided + if self._is_parameters_ok(): + signal_order = SignalLauncher.get_order_instance() + if signal_order is not None: + signal_order.set_mute_status(self.status) + + def _is_parameters_ok(self): + """ + Check if received parameters are ok to perform operations in the neuron + :return: true if parameters are ok, raise an exception otherwise + + .. raises:: MissingParameterException + """ + if self.status is None: + logger.debug("[Mute] You must specify a status with a boolean") + return False + return True diff --git a/kalliope/neurons/neurotimer/README.md b/kalliope/neurons/neurotimer/README.md new file mode 100644 index 00000000..6b83b03b --- /dev/null +++ b/kalliope/neurons/neurotimer/README.md @@ -0,0 +1,120 @@ +# Neurotimer + +## Synopsis + +Run a synapse after a delay. + +## Installation + +CORE NEURON : No installation needed. + +## Options + +| parameter | required | type | default | choices | comment | +|----------------------|----------|--------|---------|-----------|--------------------------------------------------------------| +| seconds | NO | int | | value > 0 | Number of second to wait before running the synapse | +| minutes | NO | int | | value > 0 | Number of minutes to wait before running the synapse | +| hours | NO | int | | value > 0 | Number of hours to wait before running the synapse | +| synapse | YES | string | | | Name of the synapse to run after the selected delay | +| forwarded_parameters | NO | dict | | | dict of parameters that will be passed to the called synapse | + +## Return Values + +None + +## Synapses example + + +**Scenario:** You are used to make a tea and want to know when it's time to remove the bag. +> **You:** remember me to remove the bag of my tea
+**Kalliope:** Alright
+3 minutes later..
+**Kalliope:** your tea is ready + +```yml +- name: "tea-bag" + signals: + - order: "remember me to remove the bag of my tea" + neurons: + - neurotimer: + minutes: 3 + synapse: "time-over" + - say: + message: + - "Alright" + +- name: "time-over" + signals: + - order: "no-order-for-this-synapse" + neurons: + - say: + message: + - "your tea is ready" +``` + +If your STT engine return integer when capturing a spoken order, you can set the time on the fly. +**Scenario:** You are starting to cook something +> **You:** notify me in 10 minutes
+**Kalliope:** I'll notify you in 10 minutes
+10 minutes later..
+**Kalliope:** You asked me to notify you + +```yml +- name: "timer2" + signals: + - order: "notify me in {{ time }} minutes" + neurons: + - neurotimer: + minutes: "{{ time }}" + synapse: "notify" + - say: + message: + - "I'll notify you in {{ time }} minutes" + +- name: "notify" + signals: + - order: "no-order-for-this-synapse" + neurons: + - say: + message: + - "You asked me to notify you" +``` + +Passing argument to the called synapse with the `forwarded_parameters`. +**Scenario:** You want to remember to do something +> **You:** remind me to call mom in 15 minutes
+**Kalliope:** I'll notify you in 15 minutes
+15 minutes later..
+**Kalliope:** You asked me to remind you to call mom 15 minutes ago +```yml +- name: "remember-synapse" + signals: + - order: "remind me to {{ remember }} in {{ time }} minutes" + neurons: + - neurotimer: + seconds: "{{ time }}" + synapse: "remember-todo" + forwarded_parameters: + remember: "{{ remember }}" + seconds: "{{ time }}" + - say: + message: + - "I'll remind you in {{ time }} minutes" + +- name: "remember-todo" + signals: + - order: "no-order-for-this-synapse" + neurons: + - say: + message: + - "You asked me to remind you to {{ remember }} {{ time }} minutes ago" +``` +> **Note:** You can still use the **kalliope_memory** instead of **forwarded_parameters** but your value will be overridden if you call the same synapse a multiple time. + +## Notes + +> **Note:** When used from the API, returned value from the launched synapse are lost + +> **Note:** Not all STT engine return integer. + +> **Note:** You must set at least one timer parameter (seconds or minutes or hours). You can also set them all. \ No newline at end of file diff --git a/kalliope/neurons/neurotimer/__init__.py b/kalliope/neurons/neurotimer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kalliope/neurons/neurotimer/neurotimer.py b/kalliope/neurons/neurotimer/neurotimer.py new file mode 100644 index 00000000..9b932d4b --- /dev/null +++ b/kalliope/neurons/neurotimer/neurotimer.py @@ -0,0 +1,126 @@ +import logging +import sys +import threading +import time + +from kalliope.core import NeuronModule +from kalliope.core.NeuronModule import MissingParameterException, InvalidParameterException + +logging.basicConfig() +logger = logging.getLogger("kalliope") + + +class TimerThread(threading.Thread): + def __init__(self, time_to_wait_seconds, callback): + """ + A Thread that will call the given callback method after waiting time_to_wait_seconds + :param time_to_wait_seconds: number of second to wait before call the callback method + :param callback: callback method + """ + threading.Thread.__init__(self) + self.time_to_wait_seconds = time_to_wait_seconds + self.callback = callback + + def run(self): + # wait the amount of seconds + logger.debug("[Neurotimer] wait %s seconds" % self.time_to_wait_seconds) + time.sleep(self.time_to_wait_seconds) + # then run the callback method + self.callback() + + +class Neurotimer(NeuronModule): + def __init__(self, **kwargs): + super(Neurotimer, self).__init__(**kwargs) + + # get parameters + self.seconds = kwargs.get('seconds', None) + self.minutes = kwargs.get('minutes', None) + self.hours = kwargs.get('hours', None) + self.synapse = kwargs.get('synapse', None) + self.forwarded_parameter = kwargs.get('forwarded_parameters', None) + + # do some check + if self._is_parameters_ok(): + # make the sum of all time parameter in seconds + retarding_time_seconds = self._get_retarding_time_seconds() + + # now wait before running the target synapse + ds = TimerThread(time_to_wait_seconds=retarding_time_seconds, callback=self.callback_run_synapse) + # ds.daemon = True + ds.start() + + def _is_parameters_ok(self): + """ + Check given neuron parameters are valid + :return: True if the neuron has been well configured + """ + + # at least one time parameter must be set + if self.seconds is None and self.minutes is None and self.hours is None: + raise MissingParameterException("Neurotimer must have at least one time " + "parameter: seconds, minutes, hours") + + self.seconds = self.get_integer_time_parameter(self.seconds) + self.minutes = self.get_integer_time_parameter(self.minutes) + self.hours = self.get_integer_time_parameter(self.hours) + if self.synapse is None: + raise MissingParameterException("Neurotimer must have a synapse name parameter") + + return True + + @staticmethod + def get_integer_time_parameter(time_parameter): + """ + Check if a given time parameter is a valid integer: + - must be > 0 + - if type no an integer, must be convertible to integer + + :param time_parameter: string or integer + :return: integer + """ + if time_parameter is not None: + if not isinstance(time_parameter, int): + # try to convert into integer + try: + time_parameter = int(time_parameter) + except ValueError: + raise InvalidParameterException("[Neurotimer] %s is not a valid integer" % time_parameter) + # check if positive + if time_parameter < 0: + raise InvalidParameterException("[Neurotimer] %s must be > 0" % time_parameter) + + return time_parameter + + def _get_retarding_time_seconds(self): + """ + Return the sum of given time parameters + seconds + minutes + hours + :return: integer, number of total seconds + """ + returned_time = 0 + + if self.seconds is not None: + returned_time += self.seconds + if self.minutes is not None: + returned_time += self.minutes * 60 + if self.hours is not None: + returned_time += self.hours * 3600 + + logger.debug("[Neurotimer] get_retarding_time_seconds: %s" % returned_time) + return returned_time + + def callback_run_synapse(self): + """ + Callback method which will be started by the timer thread once the time is over + :return: + """ + logger.debug("[Neurotimer] waiting time is over, start the synapse %s" % self.synapse) + # trick to remove unicode problem when loading jinja template with non ascii char + if sys.version_info[0] == 2: + reload(sys) + sys.setdefaultencoding('utf-8') + + self.run_synapse_by_name(synapse_name=self.synapse, + high_priority=False, + overriding_parameter_dict=self.forwarded_parameter) diff --git a/kalliope/neurons/neurotransmitter/neurotransmitter.py b/kalliope/neurons/neurotransmitter/neurotransmitter.py index eaa6fc47..153d8d2d 100644 --- a/kalliope/neurons/neurotransmitter/neurotransmitter.py +++ b/kalliope/neurons/neurotransmitter/neurotransmitter.py @@ -21,7 +21,7 @@ def __init__(self, **kwargs): if self._is_parameters_ok(): if self.direct_link is not None: logger.debug("Neurotransmitter directly call to the synapse name: %s" % self.direct_link) - self.run_synapse_by_name(self.direct_link) + self.run_synapse_by_name(self.direct_link, high_priority=True) else: if self.is_api_call: if self.answer is not None: @@ -42,7 +42,7 @@ def callback(self, audio): # print self.links # set a bool to know if we have found a valid answer if audio is None: - self.run_synapse_by_name(self.default) + self.run_synapse_by_name(self.default, high_priority=True, is_api_call=self.is_api_call) else: found = False for el in self.from_answer_link: @@ -51,11 +51,13 @@ def callback(self, audio): logger.debug("Neurotransmitter: match answer: %s" % answer) self.run_synapse_by_name(synapse_name=el["synapse"], user_order=audio, - synapse_order=answer) + synapse_order=answer, + high_priority=True, + 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 - self.run_synapse_by_name(self.default) + self.run_synapse_by_name(self.default, high_priority=True, is_api_call=self.is_api_call) def _is_parameters_ok(self): """ diff --git a/kalliope/neurons/neurotransmitter/tests/test_neurotransmitter.py b/kalliope/neurons/neurotransmitter/tests/test_neurotransmitter.py index 9307ecb9..25dd343d 100644 --- a/kalliope/neurons/neurotransmitter/tests/test_neurotransmitter.py +++ b/kalliope/neurons/neurotransmitter/tests/test_neurotransmitter.py @@ -108,13 +108,13 @@ def testCallback(self): # testing running the default when audio None audio_text = None nt.callback(audio=audio_text) - mock_run_synapse_by_name.assert_called_once_with(self.default) + mock_run_synapse_by_name.assert_called_once_with(self.default, high_priority=True, is_api_call=False) mock_run_synapse_by_name.reset_mock() # testing running the default when no order matching audio_text = "try test audio " nt.callback(audio=audio_text) - mock_run_synapse_by_name.assert_called_once_with(self.default) + mock_run_synapse_by_name.assert_called_once_with(self.default, high_priority=True, is_api_call=False) mock_run_synapse_by_name.reset_mock() # Testing calling the right synapse @@ -122,7 +122,9 @@ def testCallback(self): nt.callback(audio=audio_text) mock_run_synapse_by_name.assert_called_once_with(synapse_name="synapse2", user_order=audio_text, - synapse_order="answer one") + synapse_order="answer one", + high_priority=True, + is_api_call=False) def testInit(self): """ @@ -136,7 +138,7 @@ def testInit(self): "direct_link": self.direct_link } Neurotransmitter(**parameters) - mock_run_synapse_by_name.assert_called_once_with(self.direct_link) + mock_run_synapse_by_name.assert_called_once_with(self.direct_link, high_priority=True) with mock.patch("kalliope.core.NeuronModule.get_audio_from_stt") as mock_get_audio_from_stt: # Test get_audio_from_stt @@ -146,3 +148,7 @@ def testInit(self): } Neurotransmitter(**parameters) mock_get_audio_from_stt.assert_called_once() + + +if __name__ == '__main__': + unittest.main() diff --git a/kalliope/neurons/shell/README.md b/kalliope/neurons/shell/README.md index b71c2d7c..fc3283ed 100644 --- a/kalliope/neurons/shell/README.md +++ b/kalliope/neurons/shell/README.md @@ -100,7 +100,7 @@ If you want to add argument to your shell command, you can use an input value fr - order: "remove file {{ query }}" neurons: - shell: - cmd: "rm { query }}" + cmd: "rm {{ query }}" file_template: remove_file.j2 ``` In the example above, kalliope will remove the file you asked for in the query. diff --git a/kalliope/settings.yml b/kalliope/settings.yml index 6f974517..0a09f157 100644 --- a/kalliope/settings.yml +++ b/kalliope/settings.yml @@ -15,7 +15,7 @@ default_trigger: "snowboy" # - snowboy triggers: - snowboy: - pmdl_file: "trigger/snowboy/resources/kalliope-FR-13samples.pmdl" + pmdl_file: "trigger/snowboy/resources/kalliope-FR-40samples.pmdl" # --------------------------- @@ -25,6 +25,11 @@ triggers: # This is the STT that will be used by default default_speech_to_text: "google" +# Speech to text options +#recognition_options: +# energy_threshold: 4000 +# adjust_for_ambient_noise_second: 1 + # Speech to Text engines configuration # Available engine are: # - google (via SpeechRecognition) @@ -172,13 +177,14 @@ default_synapse: "default-synapse" # - stt # - tts # - trigger /!\ we do not manage trigger properly yet... +# - signal # --------------------------- #resource_directory: # neuron: "/var/tmp/resources/neurons" # stt: "resources/stt" # tts: "resources/tts" # trigger: "resources/trigger" - +# signal: "resources/signal" # --------------------------- # Global files variables diff --git a/kalliope/signals/__init__.py b/kalliope/signals/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kalliope/signals/event/README.md b/kalliope/signals/event/README.md new file mode 100644 index 00000000..652c5d34 --- /dev/null +++ b/kalliope/signals/event/README.md @@ -0,0 +1,114 @@ +# Event + +## Synopsis + +An **event** is a way to schedule the launching of a synapse periodically at fixed times, dates, or intervals. + +The event system is based on [APScheduler](http://apscheduler.readthedocs.io/en/latest/modules/triggers/cron.html) which it is itself based on [Linux crontab](https://en.wikipedia.org/wiki/Cron). +When you declare an event in the signal, Kalliope will schedule the launching of the target synapse. + +The syntax of an event declaration in a synapse is the following +```yml +signals: + - event: + parameter1: "value1" + parameter2: "value2" +``` + +For example, if we want Kalliope to run the synapse every day a 8:30, the event will be declared like this: +```yml +- event: + hour: "8" + minute: "30" +``` + +## Options + +Parameters are keyword you can use to build your event + +List of available parameter: + +| parameter | required | default | choices | comment | +|-------------|----------|---------|-----------------------------------------------------------------|-----------| +| year | no | * | 4 digit | E.g: 2016 | +| month | no | * | month (1-12) | | +| day | no | * | day of the (1-31) | | +| week | no | * | ISO week (1-53) | | +| day_of_week | no | * | number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun) | 6=Sunday | +| hour | no | * | hour (0-23) | | +| minute | no | * | minute (0-59) | | +| second | no | * | second (0-59) | | + +> **Note:** You must set at least one parameter from the list of parameter + +Expressions can be used in value of each parameter. Multiple expression can be given in a single field, separated by commas. + +| Expression | Field | Description | +|------------|-------|-----------------------------------------------------------------------------------------| +| * | any | Fire on every value | +| */a | any | Fire every `a` values, starting from the minimum | +| a-b | any | Fire on any value within the `a-b` range (a must be smaller than b) | +| a-b/c | any | Fire every c values within the `a-b` range | +| xrd y | day | Fire on the `x` -rd occurrence of weekday `y` within the month | +| last x | day | Fire on the last occurrence of weekday `x` within the month | +| last x | day | Fire on the last day within the month | +| x,y,z | day | Fire on any matching expression; can combine any number of any of the above expressions | + + +## Synapses example + +### Web clock radio + +Let's make a complete example. We want Kalliope to wake us up each morning of working day (Monday to friday) at 7:30 AM and: +- Wish us good morning +- Give us the time +- Play our favourite web radio + +The synapse in the brain would be +```yml + - name: "wake-up" + signals: + - event: + hour: "7" + minute: "30" + day_of_week: "1,2,3,4,5" + neurons: + - say: + message: + - "Good morning" + - systemdate: + say_template: + - "It is {{ hours }} hours and {{ minutes }} minutes" + - shell: + cmd: "mplayer http://192.99.17.12:6410/" + async: True +``` + +After setting up an event, you must restart Kalliope +```bash +python kalliope.py start +``` + +If the syntax is ok, Kalliope will show you each synapse that it has loaded in the crontab +``` +Add synapse name "wake-up" to the scheduler: cron[day_of_week='1,2,3,4,5', hour='7', minute='30'] +Event loaded +``` + +That's it, the synapse is now scheduled and will be started automatically. + + +### Make Kalliope say something on the third Friday of June, July, August, November and December at 00:00, 01:00, 02:00 and 03:00 + +```yml +- name: "wake-up" + signals: + - event: + day: "3rd fri" + month: "6-8,11-12" + hour: "0-3" + neurons: + - say: + message: + - "This is a schedulled sentence" +``` diff --git a/kalliope/signals/event/__init__.py b/kalliope/signals/event/__init__.py new file mode 100644 index 00000000..1387ac96 --- /dev/null +++ b/kalliope/signals/event/__init__.py @@ -0,0 +1 @@ +from .event import Event diff --git a/kalliope/signals/event/event.py b/kalliope/signals/event/event.py new file mode 100644 index 00000000..448b11e4 --- /dev/null +++ b/kalliope/signals/event/event.py @@ -0,0 +1,116 @@ +from threading import Thread + +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger + +from kalliope.core.ConfigurationManager import BrainLoader +from kalliope.core.SynapseLauncher import SynapseLauncher +from kalliope.core import Utils + + +class NoEventPeriod(Exception): + """ + An Event must contains a period corresponding to its execution + + .. seealso:: Event + """ + pass + + +class Event(Thread): + def __init__(self): + super(Event, self).__init__() + Utils.print_info('Starting event manager') + self.scheduler = BackgroundScheduler() + self.brain = BrainLoader().get_brain() + self.synapses = self.brain.synapses + self.load_events() + + def run(self): + self.scheduler.start() + + def load_events(self): + """ + For each received synapse that have an event as signal, we add a new job scheduled + to launch the synapse + :return: + """ + for synapse in self.synapses: + for signal in synapse.signals: + # if the signal is an event we add it to the task list + if signal.name == "event": + if self.check_event_dict(signal.parameters): + my_cron = CronTrigger(year=self.get_parameter_from_dict("year", signal.parameters), + month=self.get_parameter_from_dict("month", signal.parameters), + day=self.get_parameter_from_dict("day", signal.parameters), + week=self.get_parameter_from_dict("week", signal.parameters), + day_of_week=self.get_parameter_from_dict("day_of_week", signal.parameters), + hour=self.get_parameter_from_dict("hour", signal.parameters), + minute=self.get_parameter_from_dict("minute", signal.parameters), + second=self.get_parameter_from_dict("second", signal.parameters),) + Utils.print_info("Add synapse name \"%s\" to the scheduler: %s" % (synapse.name, my_cron)) + self.scheduler.add_job(self.run_synapse_by_name, my_cron, args=[synapse.name]) + + @staticmethod + def run_synapse_by_name(synapse_name): + """ + This method will run the synapse + """ + Utils.print_info("Event triggered, running synapse: %s" % synapse_name) + # get a brain + brain_loader = BrainLoader() + brain = brain_loader.brain + SynapseLauncher.start_synapse_by_name(synapse_name, brain=brain) + + @staticmethod + def get_parameter_from_dict(parameter_name, parameters_dict): + """ + return the value in the dict parameters_dict frm the key parameter_name + return None if the key does not exist + :param parameter_name: name of the key + :param parameters_dict: dict + :return: string + """ + try: + return parameters_dict[parameter_name] + except KeyError: + return None + + @staticmethod + def check_event_dict(event_dict): + """ + Check received event dictionary of parameter is valid: + + :param event_dict: The event Dictionary + :type event_dict: Dict + :return: True if event are ok + :rtype: Boolean + """ + def get_key(key_name): + try: + return event_dict[key_name] + except KeyError: + return None + + if event_dict is None or event_dict == "": + raise NoEventPeriod("Event must contain at least one of those elements: " + "year, month, day, week, day_of_week, hour, minute, second") + + # check content as at least on key + year = get_key("year") + month = get_key("month") + day = get_key("day") + week = get_key("week") + day_of_week = get_key("day_of_week") + hour = get_key("hour") + minute = get_key("minute") + second = get_key("second") + + list_to_check = [year, month, day, week, day_of_week, hour, minute, second] + number_of_none_object = list_to_check.count(None) + list_size = len(list_to_check) + if number_of_none_object >= list_size: + raise NoEventPeriod("Event must contain at least one of those elements: " + "year, month, day, week, day_of_week, hour, minute, second") + + return True diff --git a/kalliope/signals/event/tests/__init__.py b/kalliope/signals/event/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kalliope/signals/event/tests/test_event.py b/kalliope/signals/event/tests/test_event.py new file mode 100644 index 00000000..4ae29509 --- /dev/null +++ b/kalliope/signals/event/tests/test_event.py @@ -0,0 +1,20 @@ +# +# def test_check_event_dict(self): +# valid_event = { +# "hour": "18", +# "minute": "16" +# } +# invalid_event = None +# invalid_event2 = "" +# invalid_event3 = { +# "notexisting": "12" +# } +# +# self.assertTrue(ConfigurationChecker.check_event_dict(valid_event)) +# +# with self.assertRaises(NoEventPeriod): +# ConfigurationChecker.check_event_dict(invalid_event) +# with self.assertRaises(NoEventPeriod): +# ConfigurationChecker.check_event_dict(invalid_event2) +# with self.assertRaises(NoEventPeriod): +# ConfigurationChecker.check_event_dict(invalid_event3) \ No newline at end of file diff --git a/kalliope/signals/mqtt_subscriber/MqttClient.py b/kalliope/signals/mqtt_subscriber/MqttClient.py new file mode 100644 index 00000000..ed83917e --- /dev/null +++ b/kalliope/signals/mqtt_subscriber/MqttClient.py @@ -0,0 +1,124 @@ +import json +import logging +import socket +from threading import Thread + +import paho +import paho.mqtt.client as mqtt +from kalliope.core.SynapseLauncher import SynapseLauncher + +logging.basicConfig() +logger = logging.getLogger("kalliope") + + +class MqttClient(Thread): + + def __init__(self, broker=None, brain=None): + """ + Class used to instantiate mqtt client + Thread used to be non blocking when called from parent class + :param broker: broker object + :type broker: Broker + """ + super(MqttClient, self).__init__() + self.broker = broker + self.brain = brain + + self.client = mqtt.Client(client_id=self.broker.client_id, protocol=self._get_protocol(self.broker.protocol)) + self.client.on_connect = self.on_connect + self.client.on_message = self.on_message + self.client.on_subscribe = self.on_subscribe + + if self.broker.username is not None and self.broker.password is not None: + logger.debug("[MqttClient] Username and password are set") + self.client.username_pw_set(self.broker.username, self.broker.password) + + if self.broker.ca_cert is not None and self.broker.certfile is not None and self.broker.keyfile is not None: + logger.debug("[MqttClient] Active TLS with client certificate authentication") + self.client.tls_set(ca_certs=self.broker.ca_cert, + certfile=self.broker.certfile, + keyfile=self.broker.keyfile) + self.client.tls_insecure_set(self.broker.tls_insecure) + + elif self.broker.ca_cert is not None: + logger.debug("[MqttClient] Active TLS with server CA certificate only") + self.client.tls_set(ca_certs=self.broker.ca_cert) + self.client.tls_insecure_set(self.broker.tls_insecure) + + def run(self): + logger.debug("[MqttClient] Try to connect to broker: %s, port: %s, " + "keepalive: %s, protocol: %s" % (self.broker.broker_ip, + self.broker.port, + self.broker.keepalive, + self.broker.protocol)) + try: + self.client.connect(self.broker.broker_ip, self.broker.port, self.broker.keepalive) + self.client.loop_forever() + except socket.error: + logger.debug("[MqttClient] Unable to connect to broker %s" % self.broker.broker_ip) + + def on_connect(self, client, userdata, flags, rc): + """ + The callback for when the client receives a CONNACK response from the server. + """ + logger.debug("[MqttClient] Broker %s connection result code %s" % (self.broker.broker_ip, str(rc))) + + if rc == 0: # success connection + logger.debug("[MqttClient] Successfully connected to broker %s" % self.broker.broker_ip) + # Subscribing in on_connect() means that if we lose the connection and + # reconnect then subscriptions will be renewed. + for topic in self.broker.topics: + logger.debug("[MqttClient] Trying to subscribe to topic %s" % topic.name) + client.subscribe(topic.name) + else: + logger.debug("[MqttClient] Broker %s connection failled. Disconnect" % self.broker.broker_ip) + self.client.disconnect() + + def on_message(self, client, userdata, msg): + """ + The callback for when a PUBLISH message is received from the server + """ + logger.debug("[MqttClient] " + msg.topic + ": " + str(msg.payload)) + + self.call_concerned_synapses(msg.topic, msg.payload) + + def on_subscribe(self, mqttc, obj, mid, granted_qos): + """ + The callback for when the client successfully subscribe to a topic on the server + """ + logger.debug("[MqttClient] Successfully subscribed to topic") + + def call_concerned_synapses(self, topic_name, message): + """ + Call synapse launcher class for each synapse concerned by the subscribed topic + convert the message to json if needed before. + The synapse is loaded with a parameter called "mqtt_subscriber_message" that can be used in neurons + :param topic_name: name of the topic that received a message from the broker + :param message: string message received from the broker + """ + # find concerned topic by name + target_topic = next(topic for topic in self.broker.topics if topic.name == topic_name) + # convert payload to a dict if necessary + if target_topic.is_json: + message = json.loads(message) + logger.debug("[MqttClient] Payload message converted to JSON dict: %s" % message) + else: + logger.debug("[MqttClient] Payload message is plain text: %s" % message) + + # run each synapse + for synapse in target_topic.synapses: + logger.debug("[MqttClient] start synapse name %s" % synapse.name) + overriding_parameter_dict = dict() + overriding_parameter_dict["mqtt_subscriber_message"] = message + SynapseLauncher.start_synapse_by_name(synapse.name, + brain=self.brain, + overriding_parameter_dict=overriding_parameter_dict) + + @staticmethod + def _get_protocol(protocol): + """ + return the right protocol version number from the lib depending on the string protocol + """ + if protocol == "MQTTv31": + return paho.mqtt.client.MQTTv31 + return paho.mqtt.client.MQTTv311 diff --git a/kalliope/signals/mqtt_subscriber/README.md b/kalliope/signals/mqtt_subscriber/README.md new file mode 100644 index 00000000..181fdba0 --- /dev/null +++ b/kalliope/signals/mqtt_subscriber/README.md @@ -0,0 +1,283 @@ +# MQTT Subscriber + +## Synopsis + +Launch synapses when receiving a message on a topic from a MQTT messaging broker server. + +>MQTT is a Client Server publish/subscribe messaging transport protocol. +It is mostly used in communication in Machine to Machine (M2M) and Internet of Things (IoT) contexts. +The main concept is that a client will publish a message attached to a "topic" to a server called a "broker", and other clients which are interested by the topic will subscribe to it. +The broker filters all incoming messages and distributes them accordingly. + +## Options + +| parameter | required | default | choices | comment | +|--------------|----------|----------|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| +| broker_ip | YES | | | IP address of the MQTT broker server | +| topic | YES | | | topic name to subscribe | +| is_json | NO | FALSE | True, False | if true, all received message will be converted into a dict | +| broker_port | NO | 1883 | | Port of the broker. By default 1883. 8883 when TLS is activated. | +| client_id | NO | kalliope | | The client identifier is an identifier of each MQTT client and used by the broker server for identifying the client. Should be unique per broker | +| keepalive | NO | 60 | | A time interval in seconds where the clients commits to by sending regular PING Request messages to the broker. | +| username | NO | | | username for authenticating the client | +| password | NO | | | password for authenticating the client | +| protocol | NO | MQTTv311 | MQTTv31, MQTTv311 | Can be either MQTTv31 or MQTTv311 | +| ca_cert | NO | | | Path to the remote server CA certificate used for securing the transport | +| certfile | NO | | | Path to the client certificate file used for authentication | +| keyfile | NO | | | Path to the client key file attached to the client certificate | +| tls_insecure | NO | FALSE | True, False | Set the verification of the server hostname in the server certificate | + + +## Values sent to the synapse + +| Name | Description | Type | sample | +|-------------------------|----------------------------------|-------------|------------------------------------------------------| +| mqtt_subscriber_message | message received from the broker | string/dict | "on", "off", {"temperature": "25", "humidity": "30"} | + + +## Synapses example + +### Topic with plain text message + +The topic send the status of a light. The received message would be "on" or off" +```yml +- name: "test-mqtt-1" + signals: + - mqtt_subscriber: + broker_ip: "127.0.0.1" + topic: "topic1" + neurons: + - say: + message: + - "The light is now {{ mqtt_subscriber_message }}" +``` + +Kalliope output example: +``` +The light is now on +``` + +### Topic with json message + +In this example, the topic send a json payload that contain multiple information. E.g: `{"temperature": "25", "humidity": "30"}` +```yml +- name: "test-mqtt-2" + signals: + - mqtt_subscriber: + broker_ip: "127.0.0.1" + topic: "topic2" + is_json: True + neurons: + - say: + message: + - "The temperature is now {{ mqtt_subscriber_message['temperature'] }}, humidity {{ mqtt_subscriber_message['humidity'] percents }}" +``` + +Kalliope output example: +``` +The temperature is now 25 degrees, humidity 30% +``` + +### The broker require authentication + +Password authentication +```yml +- name: "test-mqtt-3" + signals: + - mqtt_subscriber: + broker_ip: "127.0.0.1" + topic: "topic 3" + username: "guest" + password: "guest" + neurons: + - say: + message: + - "Message received on topic 3" +``` + +It's better to use TLS when using password authentication for securing the transport +```yml +- name: "test-mqtt-4" + signals: + - mqtt_subscriber: + broker_ip: "127.0.0.1" + broker_port: 8883 + topic: "topic 4" + username: "guest" + password: "guest" + ca_cert: "/path/to/ca.cert" + tls_insecure: True + neurons: + - say: + message: + - "Message received on topic 3" +``` + +Authentication based on client certificate +```yml +- name: "test-mqtt-5" + signals: + - mqtt_subscriber: + broker_ip: "127.0.0.1" + broker_port: 8883 + topic: "topic 5" + ca_cert: "/path/to/ca.cert" + tls_insecure: True + certfile: "/path/to/client.crt" + keyfile: "/path/to/client.key" + neurons: + - say: + message: + - "Message received on topic 5" +``` + +## Notes + +When you want to use the same broker within your multiple synapses in your brain, you must keep in mind that the configuration must be the same +It means that you cannot declare a synapse that use a broker ip with TLS activated, and another synapse that use the same broker ip but without TLS activated. +When you declare a "broker_ip", a unique object is created once, then topic are added following all synapses + +On the other hand, you can subscribe to multiple topic that use json or not within the same broker ip. +```yml +- name: "synapse-mqtt-1" + signals: + - mqtt_subscriber: + broker_ip: "127.0.0.1" + topic: "topic1" + is_json: False + neurons: + - say: + message: + - "I'm started when message on topic 1" + +- name: "synapse-mqtt-2" + signals: + - mqtt_subscriber: + broker_ip: "127.0.0.1" + topic: "topic2" + is_json: True + neurons: + - say: + message: + - "I'm started when message on topic 2" +``` + + + +## Test with rabbitmq-server broker + +This part can help you to configure your brain by sending message to a local broker + +### Install rabbitmq + +``` +sudo apt-get install rabbitmq-server mosquitto-clients +``` + +Enable mqtt plugin +``` +sudo rabbitmq-plugins enable rabbitmq_mqtt +sudo systemctl restart rabbitmq-server +``` + +Active web ui (optional) +```bash +sudo rabbitmq-plugins enable rabbitmq_management +``` + +Get the cli and make it available to use +``` +wget http://127.0.0.1:15672/cli/rabbitmqadmin +sudo mv rabbitmqadmin /etc/rabbitmqadmin +sudo chmod 755 /etc/rabbitmqadmin +``` + +Create admin account (when UI installed) +```bash +sudo rabbitmqctl add_user admin p@ssw0rd +sudo rabbitmqctl set_user_tags admin administrator +sudo rabbitmqctl set_permissions -p / admin ".*" ".*" ".*" +``` + +### Publish message from CLI + +Publish a plain text message +``` +mosquitto_pub -t 'my_topic' -m 'message' +``` + +Test publish a json message +``` +mosquitto_pub -t 'my_topic' -m '{"test" : "message"}' +``` + +### Add TLS to rabbitmq + +#### Create root CA + +Install openssl +```bash +apt-get install openssl +``` + +Create PKI structure +```bash +mkdir testca +cd testca +echo 01 > serial +``` + +Create private key and CA certificate +```bash +openssl req -out ca.key -new -x509 +``` + +Generate server/key pair +```bash +openssl genrsa -out server.key 2048 +openssl req -key server.key -new -out server.req +openssl x509 -req -in server.req -CA ca.crt -CAkey privkey.pem -CAserial serial -out server.crt +``` + +#### Create client certificate/key pair + +Create private key +```bash +openssl genrsa -out client.key 2048 +``` + +Create a certificate request +```bash +openssl req -key client.key -new -out client.req +``` + +Sign the client request with the CA +```bash +openssl x509 -req -in client.req -CA ca.cert -CAkey privkey.pem -CAserial serial -out client.crt +``` + +#### Update rabbitmq configuration + +Edit (or create if the file is not present) a config file `/etc/rabbitmq/rabbitmq.config` +``` +[ + {rabbit, [ + {ssl_listeners, [5671]}, + {ssl_options, [{cacertfile,"/path/to/ca.cert"}, + {certfile,"/path/to/server.crt"}, + {keyfile,"/path/to/server.key"}, + {verify,verify_peer}, + {fail_if_no_peer_cert,false}]} + ]}, + {rabbitmq_mqtt, [ + {ssl_listeners, [8883]}, + {tcp_listeners, [1883]} + ]} + +]. +``` + +Restart rabbitmq server to take care of changes +```bash +sudo systemctl restart rabbitmq-server +``` diff --git a/kalliope/signals/mqtt_subscriber/__init__.py b/kalliope/signals/mqtt_subscriber/__init__.py new file mode 100644 index 00000000..d3449ac6 --- /dev/null +++ b/kalliope/signals/mqtt_subscriber/__init__.py @@ -0,0 +1 @@ +from .mqtt_subscriber import Mqtt_subscriber diff --git a/kalliope/signals/mqtt_subscriber/models.py b/kalliope/signals/mqtt_subscriber/models.py new file mode 100644 index 00000000..c3cd5b16 --- /dev/null +++ b/kalliope/signals/mqtt_subscriber/models.py @@ -0,0 +1,160 @@ +import logging + +logging.basicConfig() +logger = logging.getLogger("kalliope") + + +class Topic(object): + def __init__(self, name=None, synapses=None, is_json=False): + self.name = name + self.synapses = synapses + self.is_json = is_json + + def serialize(self): + """ + This method allows to serialize in a proper way this object + + :return: A dict of name and parameters + :rtype: Dict + """ + return { + 'name': self.name, + 'is_json': self.is_json, + 'synapses': [e.serialize() for e in self.synapses], + } + + def __str__(self): + return str(self.serialize()) + + def __eq__(self, other): + """ + This is used to compare 2 objects + :param other: + :return: + """ + return self.__dict__ == other.__dict__ + + +class Broker(object): + def __init__(self, broker_ip=None, topics=None, port=1883, client_id="kalliope", keepalive=60, + username=None, password=None, protocol="MQTTv311", ca_cert=None, certfile=None, keyfile=None, + tls_insecure=False): + self.broker_ip = broker_ip + self.topics = topics + if self.topics is None: + self.topics = list() + + # optional value + self.port = port + self.client_id = client_id + self.keepalive = keepalive + self.username = username + self.password = password + self.protocol = protocol + self.ca_cert = ca_cert + self.certfile = certfile + self.keyfile = keyfile + self.tls_insecure = tls_insecure + + def serialize(self): + """ + This method allows to serialize in a proper way this object + + :return: A dict of name and parameters + :rtype: Dict + """ + return { + 'broker_ip': self.broker_ip, + 'port': self.port, + 'client_id': self.client_id, + 'keepalive': self.keepalive, + 'username': self.username, + 'password': self.password, + 'protocol': self.protocol, + 'ca_cert': self.ca_cert, + 'certfile': self.certfile, + 'keyfile': self.keyfile, + 'tls_insecure': self.tls_insecure, + 'topics': [e.serialize() for e in self.topics], + } + + def __str__(self): + return str(self.serialize()) + + def build_from_signal_dict(self, dict_parameters): + """ + Build the Broker object from a received dict of parameters + :param dict_parameters: dict of parameters used to build the Broker object + """ + logger.debug("[Broker] Build broker object from received parameters: %s" % dict_parameters) + + self.broker_ip = dict_parameters["broker_ip"] + + if "broker_port" in dict_parameters: + self.port = dict_parameters["broker_port"] + # keep alive must be an integer + if not isinstance(self.keepalive, int): + try: + self.port = int(self.port) + except ValueError: + logger.debug("[Broker] Invalid port value, fallback to 1883") + self.port = 1883 + else: + # set default port + self.port = 1883 + + if "client_id" in dict_parameters: + self.client_id = dict_parameters["client_id"] + else: + self.client_id = "kalliope" + + if "username" in dict_parameters: + self.username = dict_parameters["username"] + + if "password" in dict_parameters: + self.password = dict_parameters["password"] + + if "keepalive" in dict_parameters: + self.keepalive = dict_parameters["keepalive"] + # keep alive must be an integer + if not isinstance(self.keepalive, int): + try: + self.keepalive = int(self.keepalive) + except ValueError: + logger.debug("[Broker] Invalid keepalive value, fallback to 60") + self.keepalive = 60 + else: + # set default value + self.keepalive = 60 + + if "protocol" in dict_parameters: + if dict_parameters["protocol"] not in ["MQTTv31", "MQTTv311"]: + logger.debug("[Broker] Invalid protocol value, fallback to MQTTv311") + self.protocol = "MQTTv311" + else: + self.protocol = dict_parameters["protocol"] + else: + self.protocol = "MQTTv311" + + if "ca_cert" in dict_parameters: + self.ca_cert = dict_parameters["ca_cert"] + + if "certfile" in dict_parameters: + self.certfile = dict_parameters["certfile"] + + if "keyfile" in dict_parameters: + self.keyfile = dict_parameters["keyfile"] + + if "tls_insecure" in dict_parameters: + self.tls_insecure = dict_parameters["tls_insecure"] + self.tls_insecure = bool(self.tls_insecure) + else: + self.tls_insecure = False + + def __eq__(self, other): + """ + This is used to compare 2 objects + :param other: + :return: + """ + return self.__dict__ == other.__dict__ diff --git a/kalliope/signals/mqtt_subscriber/mqtt_subscriber.py b/kalliope/signals/mqtt_subscriber/mqtt_subscriber.py new file mode 100644 index 00000000..6c0cd061 --- /dev/null +++ b/kalliope/signals/mqtt_subscriber/mqtt_subscriber.py @@ -0,0 +1,145 @@ +import logging +from threading import Thread + +from kalliope.core.ConfigurationManager import BrainLoader +from kalliope.signals.mqtt_subscriber.MqttClient import MqttClient +from kalliope.signals.mqtt_subscriber.models import Broker, Topic + +CLIENT_ID = "kalliope" + +logging.basicConfig() +logger = logging.getLogger("kalliope") + + +class Mqtt_subscriber(Thread): + + def __init__(self, brain=None): + super(Mqtt_subscriber, self).__init__() + logger.debug("[Mqtt_subscriber] Mqtt_subscriber class created") + # variables + self.broker_ip = None + self.topic = None + self.json_message = False + + self.brain = brain + if self.brain is None: + self.brain = BrainLoader().get_brain() + + def run(self): + logger.debug("[Mqtt_subscriber] Starting Mqtt_subscriber") + # get the list of synapse that use Mqtt_subscriber as signal + list_synapse_with_mqtt_subscriber = self.get_list_synapse_with_mqtt_subscriber(brain=self.brain) + + # we need to sort broker URL by ip, then for each broker, we sort by topic and attach synapses name to run to it + list_broker_to_instantiate = self.get_list_broker_to_instantiate(list_synapse_with_mqtt_subscriber) + + # now instantiate a MQTT client for each broker object + self.instantiate_mqtt_client(list_broker_to_instantiate) + + def get_list_synapse_with_mqtt_subscriber(self, brain): + """ + return the list of synapse that use Mqtt_subscriber as signal in the provided brain + :param brain: Brain object that contain all synapses loaded + :type brain: Brain + :return: list of synapse that use Mqtt_subscriber as signal + """ + for synapse in brain.synapses: + for signal in synapse.signals: + # if the signal is an event we add it to the task list + if signal.name == "mqtt_subscriber": + if self.check_mqtt_dict(signal.parameters): + yield synapse + + @staticmethod + def check_mqtt_dict(mqtt_signal_parameters): + """ + receive a dict of parameter from a mqtt_subscriber signal and them + :param mqtt_signal_parameters: dict of parameters + :return: True if parameters are valid + """ + # check mandatory parameters + mandatory_parameters = ["broker_ip", "topic"] + if not all(key in mqtt_signal_parameters for key in mandatory_parameters): + return False + + return True + + @staticmethod + def get_list_broker_to_instantiate(list_synapse_with_mqtt_subscriber): + """ + return a list of Broker object from the given list of synapse + :param list_synapse_with_mqtt_subscriber: list of Synapse object + :return: list of Broker + """ + returned_list_of_broker = list() + + for synapse in list_synapse_with_mqtt_subscriber: + for signal in synapse.signals: + # check if the broker exist in the list + if not any(x.broker_ip == signal.parameters["broker_ip"] for x in returned_list_of_broker): + logger.debug("[Mqtt_subscriber] Create new broker: %s" % signal.parameters["broker_ip"]) + # create a new broker object + new_broker = Broker() + new_broker.build_from_signal_dict(signal.parameters) + # add the current topic + logger.debug("[Mqtt_subscriber] Add new topic to broker %s: %s" % (new_broker.broker_ip, + signal.parameters["topic"])) + new_topic = Topic() + new_topic.name = signal.parameters["topic"] + if "is_json" in signal.parameters: + logger.debug("[Mqtt_subscriber] Message for the topic %s will be json converted" + % new_topic.name) + new_topic.is_json = bool(signal.parameters["is_json"]) + else: + new_topic.is_json = False + # add the current synapse to the topic + new_topic.synapses = list() + new_topic.synapses.append(synapse) + new_broker.topics.append(new_topic) + + logger.debug("[Mqtt_subscriber] Add new synapse to topic %s :%s" % (new_topic.name, synapse.name)) + returned_list_of_broker.append(new_broker) + else: + # the broker exist. get it from the list of broker + broker_to_edit = next((broker for broker in returned_list_of_broker + if signal.parameters["broker_ip"] == broker.broker_ip)) + # check if the topic already exist + if not any(topic.name == signal.parameters["topic"] for topic in broker_to_edit.topics): + new_topic = Topic() + new_topic.name = signal.parameters["topic"] + if "is_json" in signal.parameters: + logger.debug("[Mqtt_subscriber] Message for the topic %s will be json converted" + % new_topic.name) + new_topic.is_json = bool(signal.parameters["is_json"]) + else: + new_topic.is_json = False + logger.debug("[Mqtt_subscriber] Add new topic to existing broker " + "%s: %s" % (broker_to_edit.broker_ip, signal.parameters["topic"])) + # add the current synapse to the topic + logger.debug("[Mqtt_subscriber] Add new synapse " + "to topic %s :%s" % (new_topic.name, synapse.name)) + new_topic.synapses = list() + new_topic.synapses.append(synapse) + # add the topic to the broker + broker_to_edit.topics.append(new_topic) + else: + # the topic already exist, get it from the list + topic_to_edit = next((topic for topic in broker_to_edit.topics + if topic.name == signal.parameters["topic"])) + # add the synapse + logger.debug("[Mqtt_subscriber] Add synapse %s to existing topic %s " + "in existing broker %s" % (synapse.name, + topic_to_edit.name, + broker_to_edit.broker_ip)) + topic_to_edit.synapses.append(synapse) + + return returned_list_of_broker + + def instantiate_mqtt_client(self, list_broker_to_instantiate): + """ + Instantiate a MqttClient thread for each broker + :param list_broker_to_instantiate: list of broker to run + """ + for broker in list_broker_to_instantiate: + mqtt_client = MqttClient(broker=broker, brain=self.brain) + mqtt_client.start() diff --git a/kalliope/signals/mqtt_subscriber/test_mqtt_subscriber.py b/kalliope/signals/mqtt_subscriber/test_mqtt_subscriber.py new file mode 100644 index 00000000..63af2d56 --- /dev/null +++ b/kalliope/signals/mqtt_subscriber/test_mqtt_subscriber.py @@ -0,0 +1,210 @@ +import unittest + +from kalliope.core.Models import Neuron, Signal, Synapse, Brain +from kalliope.signals.mqtt_subscriber import Mqtt_subscriber +from kalliope.signals.mqtt_subscriber.models import Broker, Topic + + +class TestMqtt_subscriber(unittest.TestCase): + + def test_check_mqtt_dict(self): + + valid_dict_of_parameters = { + "topic": "my_topic", + "broker_ip": "192.168.0.1" + } + + invalid_dict_of_parameters = { + "topic": "my_topic" + } + + self.assertTrue(Mqtt_subscriber.check_mqtt_dict(valid_dict_of_parameters)) + self.assertFalse(Mqtt_subscriber.check_mqtt_dict(invalid_dict_of_parameters)) + + def test_get_list_synapse_with_mqtt_subscriber(self): + + # test with one signal mqtt + neuron = Neuron(name='say', parameters={'message': ['test message']}) + signal1 = Signal(name="mqtt_subscriber", parameters={"topic": "test", "broker_ip": "192.168.0.1"}) + synapse1 = Synapse(name="synapse1", neurons=[neuron], signals=[signal1]) + synapses = [synapse1] + brain = Brain() + brain.synapses = synapses + + expected_result = synapses + + mq = Mqtt_subscriber(brain=brain) + + generator = mq.get_list_synapse_with_mqtt_subscriber(brain) + + self.assertEqual(expected_result, list(generator)) + + # test with two synapse + neuron = Neuron(name='say', parameters={'message': ['test message']}) + signal1 = Signal(name="order", parameters="test_order") + signal2 = Signal(name="mqtt_subscriber", parameters={"topic": "test", "broker_ip": "192.168.0.1"}) + synapse1 = Synapse(name="synapse1", neurons=[neuron], signals=[signal1]) + synapse2 = Synapse(name="synapse2", neurons=[neuron], signals=[signal1, signal2]) + + synapses = [synapse1, synapse2] + brain = Brain() + brain.synapses = synapses + + expected_result = [synapse2] + + mq = Mqtt_subscriber(brain=brain) + generator = mq.get_list_synapse_with_mqtt_subscriber(brain) + + self.assertEqual(expected_result, list(generator)) + + def test_get_list_broker_to_instantiate(self): + # ---------------- + # only one synapse + # ---------------- + neuron = Neuron(name='say', parameters={'message': ['test message']}) + signal1 = Signal(name="mqtt_subscriber", parameters={"topic": "topic1", "broker_ip": "192.168.0.1"}) + synapse1 = Synapse(name="synapse1", neurons=[neuron], signals=[signal1]) + brain = Brain() + brain.synapses = [synapse1] + + list_synapse_with_mqtt_subscriber = [synapse1] + + expected_broker = Broker() + expected_broker.broker_ip = "192.168.0.1" + expected_broker.topics = list() + expected_topic = Topic() + expected_topic.name = "topic1" + # add the current synapse to the topic + expected_topic.synapses = list() + expected_topic.synapses.append(synapse1) + expected_broker.topics.append(expected_topic) + + expected_retuned_list = [expected_broker] + + mq = Mqtt_subscriber(brain=brain) + + self.assertListEqual(expected_retuned_list, + mq.get_list_broker_to_instantiate(list_synapse_with_mqtt_subscriber)) + + # ---------------- + # one synapse, two different broker + # ---------------- + neuron = Neuron(name='say', parameters={'message': ['test message']}) + signal1 = Signal(name="mqtt_subscriber", parameters={"topic": "topic1", + "broker_ip": "192.168.0.1", + "is_json": False}) + signal2 = Signal(name="mqtt_subscriber", parameters={"topic": "topic2", + "broker_ip": "172.16.0.1", + "is_json": False}) + synapse1 = Synapse(name="synapse1", neurons=[neuron], signals=[signal1, signal2]) + brain = Brain() + brain.synapses = [synapse1] + + list_synapse_with_mqtt_subscriber = [synapse1] + + expected_broker1 = Broker() + expected_broker1.broker_ip = "192.168.0.1" + expected_broker1.topics = list() + expected_topic = Topic() + expected_topic.name = "topic1" + # add the current synapse to the topic + expected_topic.synapses = list() + expected_topic.synapses.append(synapse1) + expected_broker1.topics.append(expected_topic) + + expected_broker2 = Broker() + expected_broker2.broker_ip = "172.16.0.1" + expected_broker2.topics = list() + expected_topic = Topic() + expected_topic.name = "topic2" + # add the current synapse to the topic + expected_topic.synapses = list() + expected_topic.synapses.append(synapse1) + expected_broker2.topics.append(expected_topic) + + expected_retuned_list = [expected_broker1, expected_broker2] + + mq = Mqtt_subscriber(brain=brain) + + self.assertEqual(expected_retuned_list, mq.get_list_broker_to_instantiate(list_synapse_with_mqtt_subscriber)) + + # ---------------- + # two synapse, same broker, different topics + # ---------------- + # synapse 1 + neuron1 = Neuron(name='say', parameters={'message': ['test message']}) + signal1 = Signal(name="mqtt_subscriber", parameters={"topic": "topic1", "broker_ip": "192.168.0.1"}) + synapse1 = Synapse(name="synapse1", neurons=[neuron1], signals=[signal1]) + + # synapse 2 + neuron2 = Neuron(name='say', parameters={'message': ['test message']}) + signal2 = Signal(name="mqtt_subscriber", parameters={"topic": "topic2", "broker_ip": "192.168.0.1"}) + synapse2 = Synapse(name="synapse2", neurons=[neuron2], signals=[signal2]) + + brain = Brain() + brain.synapses = [synapse1, synapse2] + + list_synapse_with_mqtt_subscriber = [synapse1, synapse2] + + expected_broker1 = Broker() + expected_broker1.broker_ip = "192.168.0.1" + expected_broker1.topics = list() + expected_topic1 = Topic() + expected_topic1.name = "topic1" + expected_topic2 = Topic() + expected_topic2.name = "topic2" + # add the current synapse to the topic + expected_topic1.synapses = [synapse1] + expected_topic2.synapses = [synapse2] + # add both topic to the broker + expected_broker1.topics.append(expected_topic1) + expected_broker1.topics.append(expected_topic2) + + expected_retuned_list = [expected_broker1] + + mq = Mqtt_subscriber(brain=brain) + + self.assertEqual(expected_retuned_list, mq.get_list_broker_to_instantiate(list_synapse_with_mqtt_subscriber)) + + # ---------------- + # two synapse, same broker, same topic + # ---------------- + # synapse 1 + neuron1 = Neuron(name='say', parameters={'message': ['test message']}) + signal1 = Signal(name="mqtt_subscriber", parameters={"topic": "topic1", "broker_ip": "192.168.0.1"}) + synapse1 = Synapse(name="synapse1", neurons=[neuron1], signals=[signal1]) + + # synapse 2 + neuron2 = Neuron(name='say', parameters={'message': ['test message']}) + signal2 = Signal(name="mqtt_subscriber", parameters={"topic": "topic1", "broker_ip": "192.168.0.1"}) + synapse2 = Synapse(name="synapse2", neurons=[neuron2], signals=[signal2]) + + brain = Brain() + brain.synapses = [synapse1, synapse2] + + list_synapse_with_mqtt_subscriber = [synapse1, synapse2] + + expected_broker1 = Broker() + expected_broker1.broker_ip = "192.168.0.1" + expected_broker1.topics = list() + expected_topic1 = Topic() + expected_topic1.name = "topic1" + # add both synapses to the topic + expected_topic1.synapses = [synapse1, synapse2] + # add the topic to the broker + expected_broker1.topics.append(expected_topic1) + + expected_retuned_list = [expected_broker1] + + mq = Mqtt_subscriber(brain=brain) + + self.assertEqual(expected_retuned_list, mq.get_list_broker_to_instantiate(list_synapse_with_mqtt_subscriber)) + + +if __name__ == '__main__': + unittest.main() + + # suite = unittest.TestSuite() + # suite.addTest(TestMqtt_subscriber("test_get_list_broker_to_instantiate")) + # runner = unittest.TextTestRunner() + # runner.run(suite) diff --git a/kalliope/signals/order/README.md b/kalliope/signals/order/README.md new file mode 100644 index 00000000..2c410959 --- /dev/null +++ b/kalliope/signals/order/README.md @@ -0,0 +1,73 @@ +# Order + +## Synopsis + +An **order** signal is a word, or a sentence caught by the microphone and processed by the STT engine. + +## Options + +| parameter | required | default | choices | comment | +|-----------|----------|---------|---------|-----------------------------------------------------| +| order | YES | | | The order is passed directly without any parameters | + +## Values sent to the synapse + +None + +## Synapses example + +### Simple order + +Syntax: +```yml +signals: + - order: "" +``` + +Example: +```yml +signals: + - order: "please do this action" +``` + +> **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. + +> **Important note:** STT engines don't know the context. Sometime they will return an unexpected word. +For example, "the operation to perform is 2 minus 2" can return "two", "too", "to" or "2" in english. + +> **Important note:** Kalliope will try to match the order in each synapse of its brain. So, if an order of one synapse is included in another order of another synapse, then both synapses tasks will be started by Kalliope. + +> For example, you have "test my umbrella" in a synapse A and "test" in a synapse B. When you'll say "test my umbrella", both synapse A and B +will be started by Kalliope. So keep in mind that the best practice is to use really different sentences with more than one word for your order. + +### Order with arguments +You can add one or more arguments to an order by adding bracket to the sentence. + +Syntax: +```yml +signals: + - order: " {{ arg_name }}" + - order: " {{ arg_name }} " + - order: " {{ arg_name }} {{ arg_name }}" +``` + +Example: +```yml +signals: + - order: "I want to listen {{ artist_name }}" + - order: "start the {{ episode_number }} episode" + - order: "give me the weather at {{ location }} for {{ date }}" +``` + +Here, an example order would be speaking out loud the order: "I want to listen Amy Winehouse" +In this example, both word "Amy" and "Winehouse" will be passed as an unique argument called `artist_name` to the neuron. + +If you want to send more than one argument, you must split your argument with a word that Kalliope will use to recognise the start and the end of each arguments. +For example: "give me the weather at {{ location }} for {{ date }}" +And the order would be: "give me the weather at Paris for tomorrow" +And so, it will work too with: "give me the weather at St-Pierre de Chartreuse for tomorrow" + +See the **input values** section of the [neuron documentation](neurons) to know how to send arguments to a neuron. + +>**Important note:** The following syntax cannot be used: " {{ arg_name }} {{ arg_name2 }}" as Kalliope cannot know when a block starts and when it finishes. diff --git a/kalliope/signals/order/__init__.py b/kalliope/signals/order/__init__.py new file mode 100644 index 00000000..a611692f --- /dev/null +++ b/kalliope/signals/order/__init__.py @@ -0,0 +1 @@ +from .order import Order diff --git a/kalliope/core/MainController.py b/kalliope/signals/order/order.py similarity index 84% rename from kalliope/core/MainController.py rename to kalliope/signals/order/order.py index 796f3ace..4ea057f1 100644 --- a/kalliope/core/MainController.py +++ b/kalliope/signals/order/order.py @@ -1,75 +1,57 @@ import logging import random +from threading import Thread from time import sleep -from transitions import Machine -from kalliope.core import Utils -from kalliope.core.ConfigurationManager import SettingLoader +from kalliope.core.Utils.RpiUtils import RpiUtils + +from kalliope.core.SynapseLauncher import SynapseLauncher + from kalliope.core.OrderListener import OrderListener -# API -from flask import Flask -from kalliope.core.RestAPI.FlaskAPI import FlaskAPI +from kalliope import Utils, BrainLoader +from kalliope.neurons.say import Say -# Launchers -from kalliope.core.SynapseLauncher import SynapseLauncher from kalliope.core.TriggerLauncher import TriggerLauncher -from kalliope.core.Utils.RpiUtils import RpiUtils +from transitions import Machine + from kalliope.core.PlayerLauncher import PlayerLauncher -# Neurons -from kalliope.neurons.say.say import Say +from kalliope.core.ConfigurationManager import SettingLoader logging.basicConfig() logger = logging.getLogger("kalliope") -class MainController: - """ - This Class is the global controller of the application. - """ +class Order(Thread): states = ['init', 'starting_trigger', 'playing_ready_sound', 'waiting_for_trigger_callback', 'stopping_trigger', - 'start_order_listener', 'playing_wake_up_answer', + 'start_order_listener', 'waiting_for_order_listener_callback', 'analysing_order'] - def __init__(self, brain=None): - self.brain = brain - # get global configuration + def __init__(self): + super(Order, self).__init__() + Utils.print_info('Starting voice order manager') + # load settings and brain from singleton sl = SettingLoader() self.settings = sl.settings + self.brain = BrainLoader().get_brain() + # keep in memory the order to process self.order_to_process = None - # Starting the rest API - self._start_rest_api() - - # rpi setting for led and mute button - self.rpi_utils = None - if self.settings.rpi_settings: - # the useer set GPIO pin, we need to instantiate the RpiUtils class in order to setup GPIO - self.rpi_utils = RpiUtils(self.settings.rpi_settings, self.muted_button_pressed) - if self.settings.rpi_settings.pin_mute_button: - # start the listening for button pressed thread only if the user set a pin - self.rpi_utils.daemon = True - self.rpi_utils.start() - # switch high the start led, as kalliope is started. Only if the setting exist - if self.settings.rpi_settings: - if self.settings.rpi_settings.pin_led_started: - logger.debug("[MainController] Switching pin_led_started to ON") - RpiUtils.switch_pin_to_on(self.settings.rpi_settings.pin_led_started) - # get the player instance self.player_instance = PlayerLauncher.get_player(settings=self.settings) # save an instance of the trigger self.trigger_instance = None self.trigger_callback_called = False + self.is_trigger_muted = False # save the current order listener self.order_listener = None @@ -78,17 +60,20 @@ def __init__(self, brain=None): # boolean used to know id we played the on ready notification at least one time self.on_ready_notification_played_once = False + # rpi setting for led and mute button + self.init_rpi_utils() + # Initialize the state machine - self.machine = Machine(model=self, states=MainController.states, initial='init', queued=True) + self.machine = Machine(model=self, states=Order.states, initial='init', queued=True) # define transitions self.machine.add_transition('start_trigger', ['init', 'analysing_order'], 'starting_trigger') self.machine.add_transition('play_ready_sound', 'starting_trigger', 'playing_ready_sound') self.machine.add_transition('wait_trigger_callback', 'playing_ready_sound', 'waiting_for_trigger_callback') self.machine.add_transition('stop_trigger', 'waiting_for_trigger_callback', 'stopping_trigger') - self.machine.add_transition('play_wake_up_answer', 'waiting_for_trigger_callback', 'playing_wake_up_answer') + self.machine.add_transition('play_wake_up_answer', 'stopping_trigger', 'playing_wake_up_answer') self.machine.add_transition('wait_for_order', 'playing_wake_up_answer', 'waiting_for_order_listener_callback') - self.machine.add_transition('analyse_order', 'waiting_for_order_listener_callback', 'analysing_order') + self.machine.add_transition('analyse_order', 'playing_wake_up_answer', 'analysing_order') self.machine.add_ordered_transitions() @@ -102,6 +87,7 @@ def __init__(self, brain=None): self.machine.on_enter_waiting_for_order_listener_callback('waiting_for_order_listener_callback_thread') self.machine.on_enter_analysing_order('analysing_order_thread') + def run(self): self.start_trigger() def start_trigger_process(self): @@ -138,7 +124,11 @@ def waiting_for_trigger_callback_thread(self): Method to print in debug that the main process is waiting for a trigger detection """ logger.debug("[MainController] Entering state: %s" % self.state) - Utils.print_info("Waiting for trigger detection") + if self.is_trigger_muted: # the user asked to mute inside the mute neuron + Utils.print_info("Kalliope is muted") + self.trigger_instance.pause() + else: + Utils.print_info("Waiting for trigger detection") # this loop is used to keep the main thread alive while not self.trigger_callback_called: sleep(0.1) @@ -168,7 +158,7 @@ def trigger_callback(self): def stop_trigger_process(self): """ The trigger has been awaken, we don't needed it anymore - :return: + :return: """ logger.debug("[MainController] Entering state: %s" % self.state) self.trigger_instance.stop() @@ -237,26 +227,41 @@ def _get_random_sound(random_wake_up_sounds): logger.debug("[MainController] Selected sound: %s" % random_path) return Utils.get_real_file_path(random_path) - def _start_rest_api(self): + def set_mute_status(self, muted=False): """ - Start the Rest API if asked in the user settings + Define is the trigger is listening or not + :param muted: Boolean. If true, kalliope is muted """ - # 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) - app = Flask(__name__) - flask_api = FlaskAPI(app=app, - port=self.settings.rest_api.port, - brain=self.brain, - allowed_cors_origin=self.settings.rest_api.allowed_cors_origin) - flask_api.daemon = True - flask_api.start() - - def muted_button_pressed(self, muted=False): logger.debug("[MainController] Mute button pressed. Switch trigger process to muted: %s" % muted) if muted: self.trigger_instance.pause() + self.is_trigger_muted = True Utils.print_info("Kalliope now muted") else: self.trigger_instance.unpause() + self.is_trigger_muted = False Utils.print_info("Kalliope now listening for trigger detection") + + def get_mute_status(self): + """ + return the current state of the trigger (muted or not) + :return: Boolean + """ + return self.is_trigger_muted + + def init_rpi_utils(self): + """ + Start listening on GPIO if defined in settings + """ + if self.settings.rpi_settings: + # the user set GPIO pin, we need to instantiate the RpiUtils class in order to setup GPIO + rpi_utils = RpiUtils(self.settings.rpi_settings, self.set_mute_status) + if self.settings.rpi_settings.pin_mute_button: + # start the listening for button pressed thread only if the user set a pin + rpi_utils.daemon = True + rpi_utils.start() + # switch high the start led, as kalliope is started. Only if the setting exist + if self.settings.rpi_settings: + if self.settings.rpi_settings.pin_led_started: + logger.debug("[MainController] Switching pin_led_started to ON") + RpiUtils.switch_pin_to_on(self.settings.rpi_settings.pin_led_started) diff --git a/kalliope/signals/order/tests/__init__.py b/kalliope/signals/order/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kalliope/signals/order/tests/test_order.py b/kalliope/signals/order/tests/test_order.py new file mode 100644 index 00000000..fc49186c --- /dev/null +++ b/kalliope/signals/order/tests/test_order.py @@ -0,0 +1,11 @@ +# def test_check_order_dict(self): +# valid_order = 'test_order' +# invalid_order = '' +# invalid_order2 = None +# +# self.assertTrue(ConfigurationChecker.check_order_dict(valid_order)) +# +# with self.assertRaises(NoValidOrder): +# ConfigurationChecker.check_order_dict(invalid_order) +# with self.assertRaises(NoValidOrder): +# ConfigurationChecker.check_order_dict(invalid_order2) \ No newline at end of file diff --git a/kalliope/stt/Utils.py b/kalliope/stt/Utils.py index 29fdeebf..bf2f25d9 100644 --- a/kalliope/stt/Utils.py +++ b/kalliope/stt/Utils.py @@ -32,8 +32,23 @@ def __init__(self, audio_file=None): if audio_file is None: # audio file not set, we need to capture a sample from the microphone with self.microphone as source: - # we only need to calibrate once, before we start listening - self.recognizer.adjust_for_ambient_noise(source) + if self.settings.recognition_options.adjust_for_ambient_noise_second > 0: + # threshold is calculated from capturing ambient sound + logger.debug("[SpeechRecognition] threshold calculated by " + "capturing ambient noise during %s seconds" % + self.settings.recognition_options.adjust_for_ambient_noise_second) + Utils.print_info("[SpeechRecognition] capturing ambient sound during %s seconds" % + self.settings.recognition_options.adjust_for_ambient_noise_second) + self.recognizer.adjust_for_ambient_noise(source, + duration=self.settings. + recognition_options.adjust_for_ambient_noise_second) + else: + # threshold is defined manually + logger.debug("[SpeechRecognition] threshold defined by settings: %s" % + self.settings.recognition_options.energy_threshold) + self.recognizer.energy_threshold = self.settings.recognition_options.energy_threshold + + Utils.print_info("Threshold set to: %s" % self.recognizer.energy_threshold) else: # audio file provided with sr.AudioFile(audio_file) as source: diff --git a/kalliope/trigger/snowboy/resources/kalliope-FR-40samples.pmdl b/kalliope/trigger/snowboy/resources/kalliope-FR-40samples.pmdl new file mode 100644 index 00000000..6aac0ee3 Binary files /dev/null and b/kalliope/trigger/snowboy/resources/kalliope-FR-40samples.pmdl differ diff --git a/kalliope/trigger/snowboy/snowboydecoder.py b/kalliope/trigger/snowboy/snowboydecoder.py index e6ef5a04..3e725e9c 100644 --- a/kalliope/trigger/snowboy/snowboydecoder.py +++ b/kalliope/trigger/snowboy/snowboydecoder.py @@ -147,7 +147,7 @@ def run(self): message = "Keyword " + str(ans) + " detected at time: " message += time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) - logger.info(message) + logger.debug(message) callback = self.detected_callback[ans-1] if callback is not None: callback() diff --git a/kalliope/trigger/snowboy/x86_64/python34/_snowboydetect.so b/kalliope/trigger/snowboy/x86_64/python34/_snowboydetect.so index 516010cc..34c7fd22 100755 Binary files a/kalliope/trigger/snowboy/x86_64/python34/_snowboydetect.so and b/kalliope/trigger/snowboy/x86_64/python34/_snowboydetect.so differ diff --git a/kalliope/trigger/snowboy/x86_64/python35/_snowboydetect.so b/kalliope/trigger/snowboy/x86_64/python35/_snowboydetect.so index bd848b7b..70d1f87e 100755 Binary files a/kalliope/trigger/snowboy/x86_64/python35/_snowboydetect.so and b/kalliope/trigger/snowboy/x86_64/python35/_snowboydetect.so differ diff --git a/kalliope/trigger/snowboy/x86_64/python36/_snowboydetect.so b/kalliope/trigger/snowboy/x86_64/python36/_snowboydetect.so index b39e2f79..70d1f87e 100755 Binary files a/kalliope/trigger/snowboy/x86_64/python36/_snowboydetect.so and b/kalliope/trigger/snowboy/x86_64/python36/_snowboydetect.so differ diff --git a/kalliope/tts/espeak/README.md b/kalliope/tts/espeak/README.md new file mode 100644 index 00000000..d1e04ccd --- /dev/null +++ b/kalliope/tts/espeak/README.md @@ -0,0 +1,81 @@ +# eSpeak + +## Synopsis + +This TTS is based on the eSpeak engine + +## Installation + + kalliope install --git-url "https://github.com/Ultchad/kalliope-espeak.git" + +## Options + +| Parameters | Required | Default | Choices | Comment | +|------------|----------|-----------------|------------------------|-----------------------------------------------------------| +| voice | yes | | all voice installed | see the full list with command "espeak --voices=LANGUAGE" | +| variant | no | | all language installed | see the full list with command "espeak --voices=variant" | +| speed | no | 160 | 80 to 450 | Speed in words per minute | +| amplitude | no | 100 | 0 to 200 | Amplitude | +| pitch | no | 50 | 0 to 99 | Pitch adjustment | +| path | no | /usr/bin/espeak | 0 to 99 | Path of espeak | +| cache | no | TRUE | | True if you want to use the cache with this TTS | + +## Notes : + +Espeak package need to be installed +```bash +sudo apt-get install espeak +``` + +To see the full list of language and voices: +```bash +espeak --voices +``` + +To see the full list of voices: +```bash +espeak --voices=LANGUAGE +``` + +Example: +``` +espeak --voices=fr +Pty Language Age/Gender VoiceName File Other Languages + 5 fr-fr M french fr (fr 5) + 7 fr M french-mbrola-1 mb/mb-fr1 + 7 fr F french-mbrola-4 mb/mb-fr4 + 5 fr-be M french-Belgium europe/fr-be (fr 8) +``` + +Configuration for "7 fr M french-mbrola-1 mb/mb-fr1" +```yaml +voice: "mb-fr1" +``` + + +To see the full list of variant: +```bash +espeak --voices=variant +``` + +Example: +``` +espeak --voices=variant +Pty Language Age/Gender VoiceName File Other Languages + 5 variant F female2 !v/f2 + 5 variant F female3 !v/f3 + 5 variant F female4 !v/f4 + 5 variant F female5 !v/f5 + 5 variant F female_whisper !v/whisperf + 5 variant - klatt !v/klatt + 5 variant - klatt2 !v/klatt2 + [...] +``` + +Configuration for "5 variant F female3 !v/f3". +```yaml +voice: "fr" +variant: "f3" +``` + + diff --git a/kalliope/tts/espeak/__init__.py b/kalliope/tts/espeak/__init__.py new file mode 100644 index 00000000..b3ca19a2 --- /dev/null +++ b/kalliope/tts/espeak/__init__.py @@ -0,0 +1 @@ +from .espeak import Espeak diff --git a/kalliope/tts/espeak/espeak.py b/kalliope/tts/espeak/espeak.py new file mode 100644 index 00000000..f44a953a --- /dev/null +++ b/kalliope/tts/espeak/espeak.py @@ -0,0 +1,58 @@ +from kalliope.core.TTS.TTSModule import TTSModule, MissingTTSParameter +import logging +import sys +import subprocess + +logging.basicConfig() +logger = logging.getLogger("kalliope") + + +class Espeak(TTSModule): + + def __init__(self, **kwargs): + super(Espeak, self).__init__(language="any", **kwargs) + + # set parameter from what we receive from the settings + self.variant = kwargs.get('variant', None) + self.speed = str(kwargs.get('speed', '160')) + self.amplitude = str(kwargs.get('amplitude', '100')) + self.pitch = str(kwargs.get('pitch', '50')) + self.espeak_exec_path = kwargs.get('path', r'/usr/bin/espeak') + + if self.voice == 'Default': + raise MissingTTSParameter("voice parameter is required by the eSpeak TTS") + + # if voice = default, don't add voice option to espeak + if self.variant is None: + self.voice_and_variant = self.voice + else: + self.voice_and_variant = self.voice + '+' + self.variant + + def say(self, words): + """ + :param words: The sentence to say + """ + + self.generate_and_play(words, self._generate_audio_file) + + def _generate_audio_file(self): + """ + Generic method used as a Callback in TTSModule + - must provided the audio file and write it on the disk + + .. raises:: FailToLoadSoundFile + """ + + options = { + 'v': '-v' + self.voice_and_variant, + 's': '-s' + self.speed, + 'a': '-a' + self.amplitude, + 'p': '-p' + self.pitch, + 'w': '-w' + self.file_path + } + + final_command = [self.espeak_exec_path, options['v'], options['s'], options['a'], + options['p'], options['w'], self.words] + + # generate the file with eSpeak + subprocess.call(final_command, stderr=sys.stderr) diff --git a/kalliope/tts/pico2wave/README.md b/kalliope/tts/pico2wave/README.md index 592fdeb1..c0ae0be6 100644 --- a/kalliope/tts/pico2wave/README.md +++ b/kalliope/tts/pico2wave/README.md @@ -2,11 +2,12 @@ This TTS is based on the SVOX picoTTS engine -| Parameters | Required | Default | Choices | Comment | -|------------|----------|---------|--------------|-------------------------------------------------| -| language | YES | | 6 languages | List of supported languages in the Note section | -| cache | No | TRUE | True / False | True if you want to use the cache with this TTS | -| samplerate | No | None | int | Pico2wave creates 16 khz files but not all USB devices support this. Set a value to convert to a specific samplerate. For Example: 44100| +| Parameters | Required | Default | Choices | Comment | +|------------|----------|--------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------| +| language | yes | | 6 languages | List of supported languages in the Note section | +| cache | no | TRUE | True, False | True if you want to use the cache with this TTS | +| samplerate | no | | int | Pico2wave creates 16 khz files but not all USB devices support this. Set a value to convert to a specific samplerate. For Example: 44100 | +| path | no | /usr/bin/pico2wave | | Path to the Pico2wave binary. If not set, Kalliope will try to load it from the environment | convert to a specific samplerate. For Example: 44100| #### Notes : diff --git a/kalliope/tts/pico2wave/pico2wave.py b/kalliope/tts/pico2wave/pico2wave.py index ecaf0d35..bb47ca92 100644 --- a/kalliope/tts/pico2wave/pico2wave.py +++ b/kalliope/tts/pico2wave/pico2wave.py @@ -16,6 +16,7 @@ class Pico2wave(TTSModule): def __init__(self, **kwargs): super(Pico2wave, self).__init__(**kwargs) self.samplerate = kwargs.get('samplerate', None) + self.path = kwargs.get('path', None) def say(self, words): """ @@ -31,15 +32,19 @@ def _generate_audio_file(self): .. raises:: FailToLoadSoundFile """ - - pico2wave_exec_path = ["/usr/bin/pico2wave"] + if self.path is None: + # we try to get the path from the env + self.path = self._get_pico_path() + # if still None, we set a default value + if self.path is None: + self.path = "/usr/bin/pico2wave" # pico2wave needs that the file path ends with .wav tmp_path = self.file_path+".wav" pico2wave_options = ["-l=%s" % self.language, "-w=%s" % tmp_path] final_command = list() - final_command.extend(pico2wave_exec_path) + final_command.extend([self.path]) final_command.extend(pico2wave_options) final_command.append(self.words) @@ -52,8 +57,18 @@ def _generate_audio_file(self): if self.samplerate is not None: tfm = sox.Transformer() tfm.rate(samplerate=self.samplerate) - tfm.build(str(tmp_path), str(tmp_path)+("tmp_name.wav")) - os.rename(str(tmp_path)+("tmp_name.wav"), tmp_path) + tfm.build(str(tmp_path), str(tmp_path) + "tmp_name.wav") + os.rename(str(tmp_path) + "tmp_name.wav", tmp_path) # remove the extension .wav os.rename(tmp_path, self.file_path) + + @staticmethod + def _get_pico_path(): + prog = "pico2wave" + for path in os.environ["PATH"].split(os.pathsep): + path = path.strip('"') + exe_file = os.path.join(path, prog) + if os.path.isfile(exe_file): + return exe_file + return None diff --git a/setup.py b/setup.py index 32e17a08..a6273e0a 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,7 @@ def read_version_py(file_name): 'markupsafe>=1.0', 'pyaudio>=0.2.11', 'pyasn1>=0.2.3', - 'ansible>=2.3', + 'ansible>=2.3,<2.4', py2_prefix + 'pythondialog>=3.4.0', 'jinja2>=2.8,<=2.9.6', 'cffi>=1.9.1', @@ -91,7 +91,8 @@ def read_version_py(file_name): 'SoundFile>=0.9.0', 'pyalsaaudio>=0.8.4', 'RPi.GPIO>=0.6.3', - 'sox>=1.3.0' + 'sox>=1.3.0', + 'paho-mqtt>=1.3.0' ],