|
@@ -1,6 +1,15 @@
|
|
import logging
|
|
import logging
|
|
|
|
+import os
|
|
import threading
|
|
import threading
|
|
|
|
|
|
|
|
+import time
|
|
|
|
+
|
|
|
|
+from kalliope.core.Utils.FileManager import FileManager
|
|
|
|
+
|
|
|
|
+from kalliope.core.ConfigurationManager import SettingLoader
|
|
|
|
+from kalliope.core.OrderListener import OrderListener
|
|
|
|
+from werkzeug.utils import secure_filename
|
|
|
|
+
|
|
from flask import jsonify
|
|
from flask import jsonify
|
|
from flask import request
|
|
from flask import request
|
|
from flask_restful import abort
|
|
from flask_restful import abort
|
|
@@ -9,10 +18,15 @@ from flask_cors import CORS, cross_origin
|
|
from kalliope.core import OrderAnalyser
|
|
from kalliope.core import OrderAnalyser
|
|
from kalliope.core.RestAPI.utils import requires_auth
|
|
from kalliope.core.RestAPI.utils import requires_auth
|
|
from kalliope.core.SynapseLauncher import SynapseLauncher
|
|
from kalliope.core.SynapseLauncher import SynapseLauncher
|
|
|
|
+from kalliope._version import version_str
|
|
|
|
|
|
logging.basicConfig()
|
|
logging.basicConfig()
|
|
logger = logging.getLogger("kalliope")
|
|
logger = logging.getLogger("kalliope")
|
|
|
|
|
|
|
|
+UPLOAD_FOLDER = '/tmp/kalliope/tmp_uploaded_audio'
|
|
|
|
+ALLOWED_EXTENSIONS = {'mp3', 'wav'}
|
|
|
|
+
|
|
|
|
+
|
|
class FlaskAPI(threading.Thread):
|
|
class FlaskAPI(threading.Thread):
|
|
def __init__(self, app, port=5000, brain=None, allowed_cors_origin=False):
|
|
def __init__(self, app, port=5000, brain=None, allowed_cors_origin=False):
|
|
"""
|
|
"""
|
|
@@ -28,6 +42,20 @@ class FlaskAPI(threading.Thread):
|
|
self.brain = brain
|
|
self.brain = brain
|
|
self.allowed_cors_origin = allowed_cors_origin
|
|
self.allowed_cors_origin = allowed_cors_origin
|
|
|
|
|
|
|
|
+ # get current settings
|
|
|
|
+ sl = SettingLoader()
|
|
|
|
+ self.settings = sl.settings
|
|
|
|
+
|
|
|
|
+ # list of launched synapse by the Order Analyser when using the /synapses/start/audio URL
|
|
|
|
+ self.launched_synapses = None
|
|
|
|
+ # boolean used to notify the main process that we get the list of returned synapse
|
|
|
|
+ self.order_analyser_return = False
|
|
|
|
+
|
|
|
|
+ # configure the upload folder
|
|
|
|
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
|
|
|
+ # create the temp folder
|
|
|
|
+ FileManager.create_directory(UPLOAD_FOLDER)
|
|
|
|
+
|
|
# Flask configuration remove default Flask behaviour to encode to ASCII
|
|
# Flask configuration remove default Flask behaviour to encode to ASCII
|
|
self.app.url_map.strict_slashes = False
|
|
self.app.url_map.strict_slashes = False
|
|
self.app.config['JSON_AS_ASCII'] = False
|
|
self.app.config['JSON_AS_ASCII'] = False
|
|
@@ -36,16 +64,28 @@ class FlaskAPI(threading.Thread):
|
|
cors = CORS(app, resources={r"/*": {"origins": allowed_cors_origin}}, supports_credentials=True)
|
|
cors = CORS(app, resources={r"/*": {"origins": allowed_cors_origin}}, supports_credentials=True)
|
|
|
|
|
|
# Add routing rules
|
|
# Add routing rules
|
|
|
|
+ self.app.add_url_rule('/', view_func=self.get_main_page, methods=['GET'])
|
|
self.app.add_url_rule('/synapses', view_func=self.get_synapses, methods=['GET'])
|
|
self.app.add_url_rule('/synapses', view_func=self.get_synapses, methods=['GET'])
|
|
self.app.add_url_rule('/synapses/<synapse_name>', view_func=self.get_synapse, methods=['GET'])
|
|
self.app.add_url_rule('/synapses/<synapse_name>', view_func=self.get_synapse, methods=['GET'])
|
|
- self.app.add_url_rule('/synapses/<synapse_name>', view_func=self.run_synapse, methods=['POST'])
|
|
|
|
- self.app.add_url_rule('/order/', view_func=self.run_order, methods=['POST'])
|
|
|
|
|
|
+ self.app.add_url_rule('/synapses/start/id/<synapse_name>', view_func=self.run_synapse_by_name, methods=['POST'])
|
|
|
|
+ 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('/shutdown/', view_func=self.shutdown_server, methods=['POST'])
|
|
|
|
|
|
-
|
|
|
|
def run(self):
|
|
def run(self):
|
|
self.app.run(host='0.0.0.0', port="%s" % int(self.port), debug=True, threaded=True, use_reloader=False)
|
|
self.app.run(host='0.0.0.0', port="%s" % int(self.port), debug=True, threaded=True, use_reloader=False)
|
|
|
|
|
|
|
|
+ def get_main_page(self):
|
|
|
|
+ data = {
|
|
|
|
+ "Kalliope version": "%s" % version_str
|
|
|
|
+ }
|
|
|
|
+ return jsonify(data), 200
|
|
|
|
+
|
|
|
|
+ @staticmethod
|
|
|
|
+ def allowed_file(filename):
|
|
|
|
+ return '.' in filename and \
|
|
|
|
+ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
|
|
|
+
|
|
def _get_synapse_by_name(self, synapse_name):
|
|
def _get_synapse_by_name(self, synapse_name):
|
|
"""
|
|
"""
|
|
Find a synapse in the brain by its name
|
|
Find a synapse in the brain by its name
|
|
@@ -65,6 +105,8 @@ class FlaskAPI(threading.Thread):
|
|
def get_synapses(self):
|
|
def get_synapses(self):
|
|
"""
|
|
"""
|
|
get all synapses.
|
|
get all synapses.
|
|
|
|
+ test with curl:
|
|
|
|
+ curl -i --user admin:secret -X GET http://127.0.0.1:5000/synapses
|
|
"""
|
|
"""
|
|
data = jsonify(synapses=[e.serialize() for e in self.brain.synapses])
|
|
data = jsonify(synapses=[e.serialize() for e in self.brain.synapses])
|
|
return data, 200
|
|
return data, 200
|
|
@@ -73,6 +115,8 @@ class FlaskAPI(threading.Thread):
|
|
def get_synapse(self, synapse_name):
|
|
def get_synapse(self, synapse_name):
|
|
"""
|
|
"""
|
|
get a synapse by its name
|
|
get a synapse by its name
|
|
|
|
+ test with curl:
|
|
|
|
+ curl --user admin:secret -i -X GET http://127.0.0.1:5000/synapses/say-hello-en
|
|
"""
|
|
"""
|
|
synapse_target = self._get_synapse_by_name(synapse_name)
|
|
synapse_target = self._get_synapse_by_name(synapse_name)
|
|
if synapse_target is not None:
|
|
if synapse_target is not None:
|
|
@@ -85,11 +129,11 @@ class FlaskAPI(threading.Thread):
|
|
return jsonify(error=data), 404
|
|
return jsonify(error=data), 404
|
|
|
|
|
|
@requires_auth
|
|
@requires_auth
|
|
- def run_synapse(self, synapse_name):
|
|
|
|
|
|
+ def run_synapse_by_name(self, synapse_name):
|
|
"""
|
|
"""
|
|
Run a synapse by its name
|
|
Run a synapse by its name
|
|
test with curl:
|
|
test with curl:
|
|
- curl -i --user admin:secret -X POST http://localhost:5000/synapses/say-hello
|
|
|
|
|
|
+ curl -i --user admin:secret -X POST http://127.0.0.1:5000/synapses/start/id/say-hello-fr
|
|
:param synapse_name:
|
|
:param synapse_name:
|
|
:return:
|
|
:return:
|
|
"""
|
|
"""
|
|
@@ -103,15 +147,15 @@ class FlaskAPI(threading.Thread):
|
|
|
|
|
|
# run the synapse
|
|
# run the synapse
|
|
SynapseLauncher.start_synapse(synapse_name, brain=self.brain)
|
|
SynapseLauncher.start_synapse(synapse_name, brain=self.brain)
|
|
- data = jsonify(synapses=synapse_target)
|
|
|
|
|
|
+ data = jsonify(synapses=synapse_target.serialize())
|
|
return data, 201
|
|
return data, 201
|
|
|
|
|
|
@requires_auth
|
|
@requires_auth
|
|
- def run_order(self):
|
|
|
|
|
|
+ def run_synapse_by_order(self):
|
|
"""
|
|
"""
|
|
Give an order to Kalliope via API like it was from a spoken one
|
|
Give an order to Kalliope via API like it was from a spoken one
|
|
Test with curl
|
|
Test with curl
|
|
- curl -i --user admin:secret -H "Content-Type: application/json" -X POST -d '{"order":"my order"}' http://localhost:5000/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
|
|
In case of quotes in the order or accents, use a file
|
|
cat post.json:
|
|
cat post.json:
|
|
{"order":"j'aime"}
|
|
{"order":"j'aime"}
|
|
@@ -144,6 +188,53 @@ class FlaskAPI(threading.Thread):
|
|
}
|
|
}
|
|
return jsonify(error=data), 400
|
|
return jsonify(error=data), 400
|
|
|
|
|
|
|
|
+ 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"
|
|
|
|
+ :return:
|
|
|
|
+ """
|
|
|
|
+ # check if the post request has the file part
|
|
|
|
+ if 'file' not in request.files:
|
|
|
|
+ data = {
|
|
|
|
+ "error": "No file provided"
|
|
|
|
+ }
|
|
|
|
+ return jsonify(error=data), 400
|
|
|
|
+
|
|
|
|
+ file = request.files['file']
|
|
|
|
+ # if user does not select file, browser also
|
|
|
|
+ # submit a empty part without filename
|
|
|
|
+ if 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.launched_synapses is not None and self.launched_synapses:
|
|
|
|
+ data = jsonify(synapses=[e.serialize() for e in self.launched_synapses])
|
|
|
|
+ self.launched_synapses = None
|
|
|
|
+ return data, 201
|
|
|
|
+ else:
|
|
|
|
+ data = {
|
|
|
|
+ "error": "The given order doesn't match any synapses"
|
|
|
|
+ }
|
|
|
|
+ return jsonify(error=data), 400
|
|
|
|
+
|
|
@requires_auth
|
|
@requires_auth
|
|
def shutdown_server(self):
|
|
def shutdown_server(self):
|
|
func = request.environ.get('werkzeug.server.shutdown')
|
|
func = request.environ.get('werkzeug.server.shutdown')
|
|
@@ -151,3 +242,27 @@ class FlaskAPI(threading.Thread):
|
|
raise RuntimeError('Not running with the Werkzeug Server')
|
|
raise RuntimeError('Not running with the Werkzeug Server')
|
|
func()
|
|
func()
|
|
return "Shutting down..."
|
|
return "Shutting down..."
|
|
|
|
+
|
|
|
|
+ def audio_analyser_callback(self, order):
|
|
|
|
+ """
|
|
|
|
+ Callback of the OrderListener. Called after the processing of the audio file
|
|
|
|
+ This method will
|
|
|
|
+ - call the Order Analyser to analyse the order and launch corresponding synapse as usual.
|
|
|
|
+ - get a list of launched synapse.
|
|
|
|
+ - give the list to the main process via self.launched_synapses
|
|
|
|
+ - notify that the processing is over via order_analyser_return
|
|
|
|
+ :param order: string order to analyse
|
|
|
|
+ :return:
|
|
|
|
+ """
|
|
|
|
+ logger.debug("order to process %s" % order)
|
|
|
|
+ if order is not None: # maybe we have received a null audio from STT engine
|
|
|
|
+ order_analyser = OrderAnalyser(order, brain=self.brain)
|
|
|
|
+ synapses_launched = order_analyser.start()
|
|
|
|
+ self.launched_synapses = synapses_launched
|
|
|
|
+ else:
|
|
|
|
+ if self.settings.default_synapse is not None:
|
|
|
|
+ SynapseLauncher.start_synapse(name=self.settings.default_synapse, brain=self.brain)
|
|
|
|
+ self.launched_synapses = self.brain.get_synapse_by_name(synapse_name=self.settings.default_synapse)
|
|
|
|
+
|
|
|
|
+ # this boolean will notify the main process that the order have been processed
|
|
|
|
+ self.order_analyser_return = True
|