|
@@ -1,25 +1,22 @@
|
|
|
import logging
|
|
|
import os
|
|
|
import threading
|
|
|
-
|
|
|
import time
|
|
|
|
|
|
-from kalliope.core.LIFOBuffer import LIFOBuffer
|
|
|
-from kalliope.core.Models.MatchedSynapse import MatchedSynapse
|
|
|
-from kalliope.core.Utils.FileManager import FileManager
|
|
|
-
|
|
|
-from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
|
|
|
-from kalliope.core.OrderListener import OrderListener
|
|
|
-from werkzeug.utils import secure_filename
|
|
|
-
|
|
|
from flask import jsonify
|
|
|
from flask import request
|
|
|
-from flask_restful import abort
|
|
|
from flask_cors import CORS
|
|
|
+from flask_restful import abort
|
|
|
+from werkzeug.utils import secure_filename
|
|
|
|
|
|
+from kalliope._version import version_str
|
|
|
+from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
|
|
|
+from kalliope.core.LIFOBuffer import LIFOBuffer
|
|
|
+from kalliope.core.Models.MatchedSynapse import MatchedSynapse
|
|
|
+from kalliope.core.OrderListener import OrderListener
|
|
|
from kalliope.core.RestAPI.utils import requires_auth
|
|
|
from kalliope.core.SynapseLauncher import SynapseLauncher
|
|
|
-from kalliope._version import version_str
|
|
|
+from kalliope.core.Utils.FileManager import FileManager
|
|
|
|
|
|
logging.basicConfig()
|
|
|
logger = logging.getLogger("kalliope")
|
|
@@ -62,7 +59,10 @@ class FlaskAPI(threading.Thread):
|
|
|
self.app.config['JSON_AS_ASCII'] = False
|
|
|
|
|
|
if self.allowed_cors_origin is not False:
|
|
|
- cors = CORS(app, resources={r"/*": {"origins": allowed_cors_origin}}, supports_credentials=True)
|
|
|
+ CORS(app, resources={r"/*": {"origins": allowed_cors_origin}}, supports_credentials=True)
|
|
|
+
|
|
|
+ # no voice flag
|
|
|
+ self.no_voice = False
|
|
|
|
|
|
# Add routing rules
|
|
|
self.app.add_url_rule('/', view_func=self.get_main_page, methods=['GET'])
|
|
@@ -76,7 +76,9 @@ class FlaskAPI(threading.Thread):
|
|
|
def run(self):
|
|
|
self.app.run(host='0.0.0.0', port="%s" % int(self.port), debug=True, threaded=True, use_reloader=False)
|
|
|
|
|
|
+ @requires_auth
|
|
|
def get_main_page(self):
|
|
|
+ logger.debug("[FlaskAPI] get_main_page")
|
|
|
data = {
|
|
|
"Kalliope version": "%s" % version_str
|
|
|
}
|
|
@@ -109,6 +111,7 @@ class FlaskAPI(threading.Thread):
|
|
|
test with curl:
|
|
|
curl -i --user admin:secret -X GET http://127.0.0.1:5000/synapses
|
|
|
"""
|
|
|
+ logger.debug("[FlaskAPI] get_synapses: all")
|
|
|
data = jsonify(synapses=[e.serialize() for e in self.brain.synapses])
|
|
|
return data, 200
|
|
|
|
|
@@ -119,6 +122,7 @@ class FlaskAPI(threading.Thread):
|
|
|
test with curl:
|
|
|
curl --user admin:secret -i -X GET http://127.0.0.1:5000/synapses/say-hello-en
|
|
|
"""
|
|
|
+ logger.debug("[FlaskAPI] get_synapse: synapse_name -> %s" % synapse_name)
|
|
|
synapse_target = self._get_synapse_by_name(synapse_name)
|
|
|
if synapse_target is not None:
|
|
|
data = jsonify(synapses=synapse_target.serialize())
|
|
@@ -135,12 +139,29 @@ class FlaskAPI(threading.Thread):
|
|
|
Run a synapse by its name
|
|
|
test with curl:
|
|
|
curl -i --user admin:secret -X POST http://127.0.0.1:5000/synapses/start/id/say-hello-fr
|
|
|
- :param synapse_name:
|
|
|
+
|
|
|
+ run a synapse without making kalliope speaking
|
|
|
+ curl -i -H "Content-Type: application/json" --user admin:secret -X POST \
|
|
|
+ -d '{"no_voice":"true"}' http://127.0.0.1:5000/synapses/start/id/say-hello-fr
|
|
|
+
|
|
|
+ Run a synapse by its name and pass order's parameters
|
|
|
+ curl -i -H "Content-Type: application/json" --user admin:secret -X POST \
|
|
|
+ -d '{"no_voice":"true", "parameters": {"parameter1": "value1" }}' \
|
|
|
+ http://127.0.0.1:5000/synapses/start/id/say-hello-fr
|
|
|
+
|
|
|
+ :param synapse_name: name(id) of the synapse to execute
|
|
|
:return:
|
|
|
"""
|
|
|
# get a synapse object from the name
|
|
|
+ logger.debug("[FlaskAPI] run_synapse_by_name: synapse name -> %s" % synapse_name)
|
|
|
synapse_target = BrainLoader().get_brain().get_synapse_by_name(synapse_name=synapse_name)
|
|
|
|
|
|
+ # get no_voice_flag if present
|
|
|
+ no_voice = self.get_no_voice_flag_from_request(request)
|
|
|
+
|
|
|
+ # get parameters
|
|
|
+ parameters = self.get_parameters_from_request(request)
|
|
|
+
|
|
|
if synapse_target is None:
|
|
|
data = {
|
|
|
"synapse name not found": "%s" % synapse_name
|
|
@@ -148,13 +169,13 @@ class FlaskAPI(threading.Thread):
|
|
|
return jsonify(error=data), 404
|
|
|
else:
|
|
|
# generate a MatchedSynapse from the synapse
|
|
|
- matched_synapse = MatchedSynapse(matched_synapse=synapse_target)
|
|
|
+ matched_synapse = MatchedSynapse(matched_synapse=synapse_target, overriding_parameter=parameters)
|
|
|
# get the current LIFO buffer
|
|
|
lifo_buffer = LIFOBuffer()
|
|
|
# this is a new call we clean up the LIFO
|
|
|
lifo_buffer.clean()
|
|
|
lifo_buffer.add_synapse_list_to_lifo([matched_synapse])
|
|
|
- response = lifo_buffer.execute(is_api_call=True)
|
|
|
+ response = lifo_buffer.execute(is_api_call=True, no_voice=no_voice)
|
|
|
data = jsonify(response)
|
|
|
return data, 201
|
|
|
|
|
@@ -163,25 +184,36 @@ class FlaskAPI(threading.Thread):
|
|
|
"""
|
|
|
Give an order to Kalliope via API like it was from a spoken one
|
|
|
Test with curl
|
|
|
- curl -i --user admin:secret -H "Content-Type: application/json" -X POST -d '{"order":"my order"}' http://localhost:5000/synapses/start/order
|
|
|
+ curl -i --user admin:secret -H "Content-Type: application/json" -X POST \
|
|
|
+ -d '{"order":"my order"}' http://localhost:5000/synapses/start/order
|
|
|
+
|
|
|
In case of quotes in the order or accents, use a file
|
|
|
cat post.json:
|
|
|
{"order":"j'aime"}
|
|
|
- curl -i --user admin:secret -H "Content-Type: application/json" -X POST --data @post.json http://localhost:5000/order/
|
|
|
+ curl -i --user admin:secret -H "Content-Type: application/json" -X POST \
|
|
|
+ --data @post.json http://localhost:5000/order/
|
|
|
+
|
|
|
+ Can be used with no_voice flag
|
|
|
+ curl -i --user admin:secret -H "Content-Type: application/json" -X POST \
|
|
|
+ -d '{"order":"my order", "no_voice":"true"}' http://localhost:5000/synapses/start/order
|
|
|
+
|
|
|
:return:
|
|
|
"""
|
|
|
if not request.get_json() or 'order' not in request.get_json():
|
|
|
abort(400)
|
|
|
|
|
|
order = request.get_json('order')
|
|
|
+ # get no_voice_flag if present
|
|
|
+ no_voice = self.get_no_voice_flag_from_request(request)
|
|
|
if order is not None:
|
|
|
# get the order
|
|
|
order_to_run = order["order"]
|
|
|
-
|
|
|
+ logger.debug("[FlaskAPI] run_synapse_by_order: order to run -> %s" % order_to_run)
|
|
|
api_response = SynapseLauncher.run_matching_synapse_from_order(order_to_run,
|
|
|
self.brain,
|
|
|
self.settings,
|
|
|
- is_api_call=True)
|
|
|
+ is_api_call=True,
|
|
|
+ no_voice=no_voice)
|
|
|
|
|
|
data = jsonify(api_response)
|
|
|
return data, 201
|
|
@@ -191,13 +223,21 @@ class FlaskAPI(threading.Thread):
|
|
|
}
|
|
|
return jsonify(error=data), 400
|
|
|
|
|
|
+ @requires_auth
|
|
|
def run_synapse_by_audio(self):
|
|
|
"""
|
|
|
Give an order to Kalliope with an audio file
|
|
|
Test with curl
|
|
|
curl -i --user admin:secret -X POST http://localhost:5000/synapses/start/audio -F "file=@/path/to/input.wav"
|
|
|
+
|
|
|
+ With no_voice flag
|
|
|
+ curl -i -H "Content-Type: application/json" --user admin:secret -X POST \
|
|
|
+ http://localhost:5000/synapses/start/audio -F "file=@path/to/file.wav" -F no_voice="true"
|
|
|
:return:
|
|
|
"""
|
|
|
+ # get no_voice_flag if present
|
|
|
+ self.no_voice = self.str_to_bool(request.form.get("no_voice"))
|
|
|
+
|
|
|
# check if the post request has the file part
|
|
|
if 'file' not in request.files:
|
|
|
data = {
|
|
@@ -205,38 +245,59 @@ class FlaskAPI(threading.Thread):
|
|
|
}
|
|
|
return jsonify(error=data), 400
|
|
|
|
|
|
- file = request.files['file']
|
|
|
+ uploaded_file = request.files['file']
|
|
|
# if user does not select file, browser also
|
|
|
# submit a empty part without filename
|
|
|
- if file.filename == '':
|
|
|
+ if uploaded_file.filename == '':
|
|
|
data = {
|
|
|
"error": "No file provided"
|
|
|
}
|
|
|
return jsonify(error=data), 400
|
|
|
- if file and self.allowed_file(file.filename):
|
|
|
- # save the file
|
|
|
- filename = secure_filename(file.filename)
|
|
|
- base_path = os.path.join(self.app.config['UPLOAD_FOLDER'])
|
|
|
- file.save(os.path.join(base_path, filename))
|
|
|
-
|
|
|
- # now start analyse the audio with STT engine
|
|
|
- audio_path = base_path + os.sep + filename
|
|
|
- ol = OrderListener(callback=self.audio_analyser_callback, audio_file_path=audio_path)
|
|
|
- ol.start()
|
|
|
- ol.join()
|
|
|
- # wait the Order Analyser processing. We need to wait in this thread to keep the context
|
|
|
- while not self.order_analyser_return:
|
|
|
- time.sleep(0.1)
|
|
|
- self.order_analyser_return = False
|
|
|
- if self.api_response is not None and self.api_response:
|
|
|
- data = jsonify(self.api_response)
|
|
|
- self.api_response = None
|
|
|
- return data, 201
|
|
|
- else:
|
|
|
- data = {
|
|
|
- "error": "The given order doesn't match any synapses"
|
|
|
- }
|
|
|
- return jsonify(error=data), 400
|
|
|
+ # save the file
|
|
|
+ filename = secure_filename(uploaded_file.filename)
|
|
|
+ base_path = os.path.join(self.app.config['UPLOAD_FOLDER'])
|
|
|
+ uploaded_file.save(os.path.join(base_path, filename))
|
|
|
+
|
|
|
+ # now start analyse the audio with STT engine
|
|
|
+ audio_path = base_path + os.sep + filename
|
|
|
+ logger.debug("[FlaskAPI] run_synapse_by_audio: with file path %s" % audio_path)
|
|
|
+ if not self.allowed_file(audio_path):
|
|
|
+ audio_path = self._convert_to_wav(audio_file_path=audio_path)
|
|
|
+ ol = OrderListener(callback=self.audio_analyser_callback, audio_file_path=audio_path)
|
|
|
+ ol.start()
|
|
|
+ ol.join()
|
|
|
+ # wait the Order Analyser processing. We need to wait in this thread to keep the context
|
|
|
+ while not self.order_analyser_return:
|
|
|
+ time.sleep(0.1)
|
|
|
+ self.order_analyser_return = False
|
|
|
+ if self.api_response is not None and self.api_response:
|
|
|
+ data = jsonify(self.api_response)
|
|
|
+ self.api_response = None
|
|
|
+ logger.debug("[FlaskAPI] run_synapse_by_audio: data %s" % data)
|
|
|
+ return data, 201
|
|
|
+ else:
|
|
|
+ data = {
|
|
|
+ "error": "The given order doesn't match any synapses"
|
|
|
+ }
|
|
|
+ return jsonify(error=data), 400
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _convert_to_wav(audio_file_path):
|
|
|
+ """
|
|
|
+ If not already .wav, convert an incoming audio file to wav format. Using system avconv (raspberry)
|
|
|
+ :param audio_file_path: the current full file path
|
|
|
+ :return: Wave file path
|
|
|
+ """
|
|
|
+ # Not allowed so convert into wav using avconv (raspberry)
|
|
|
+ base = os.path.splitext(audio_file_path)[0]
|
|
|
+ extension = os.path.splitext(audio_file_path)[1]
|
|
|
+ if extension != ".wav":
|
|
|
+ current_file_path = audio_file_path
|
|
|
+ audio_file_path = base + ".wav"
|
|
|
+ os.system("avconv -y -i " + current_file_path + " " + audio_file_path) # --> deprecated
|
|
|
+ # subprocess.call(['avconv', '-y', '-i', audio_path, new_file_path], shell=True) # Not working ...
|
|
|
+
|
|
|
+ return audio_file_path
|
|
|
|
|
|
@requires_auth
|
|
|
def shutdown_server(self):
|
|
@@ -257,12 +318,61 @@ class FlaskAPI(threading.Thread):
|
|
|
:param order: string order to analyse
|
|
|
:return:
|
|
|
"""
|
|
|
- logger.debug("order to process %s" % order)
|
|
|
+ logger.debug("[FlaskAPI] audio_analyser_callback: order to process -> %s" % order)
|
|
|
api_response = SynapseLauncher.run_matching_synapse_from_order(order,
|
|
|
self.brain,
|
|
|
self.settings,
|
|
|
- is_api_call=True)
|
|
|
+ is_api_call=True,
|
|
|
+ no_voice=self.no_voice)
|
|
|
self.api_response = api_response
|
|
|
|
|
|
# this boolean will notify the main process that the order have been processed
|
|
|
self.order_analyser_return = True
|
|
|
+
|
|
|
+ def get_no_voice_flag_from_request(self, http_request):
|
|
|
+ """
|
|
|
+ Get the no_voice flag from the request if exist
|
|
|
+ :param http_request:
|
|
|
+ :return:
|
|
|
+ """
|
|
|
+
|
|
|
+ no_voice = False
|
|
|
+ try:
|
|
|
+ received_json = http_request.get_json(force=True, silent=True, cache=True)
|
|
|
+ if 'no_voice' in received_json:
|
|
|
+ no_voice = self.str_to_bool(received_json['no_voice'])
|
|
|
+ except TypeError:
|
|
|
+ # no json received
|
|
|
+ pass
|
|
|
+ logger.debug("[FlaskAPI] no_voice: %s" % no_voice)
|
|
|
+ return no_voice
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def str_to_bool(s):
|
|
|
+ if isinstance(s, bool): # do not convert if already a boolean
|
|
|
+ return s
|
|
|
+ else:
|
|
|
+ if s == 'True' or s == 'true' or s == '1':
|
|
|
+ return True
|
|
|
+ elif s == 'False' or s == 'false' or s == '0':
|
|
|
+ return False
|
|
|
+ else:
|
|
|
+ return False
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def get_parameters_from_request(http_request):
|
|
|
+ """
|
|
|
+ Get "parameters" object from the
|
|
|
+ :param http_request:
|
|
|
+ :return:
|
|
|
+ """
|
|
|
+ parameters = None
|
|
|
+ try:
|
|
|
+ received_json = http_request.get_json(silent=False, force=True)
|
|
|
+ if 'parameters' in received_json:
|
|
|
+ parameters = received_json['parameters']
|
|
|
+ except TypeError:
|
|
|
+ pass
|
|
|
+ logger.debug("[FlaskAPI] Overridden parameters: %s" % parameters)
|
|
|
+
|
|
|
+ return parameters
|