FlaskAPI.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import logging
  2. import os
  3. import threading
  4. import time
  5. from kalliope.core.LIFOBuffer import LIFOBuffer
  6. from kalliope.core.Models.MatchedSynapse import MatchedSynapse
  7. from kalliope.core.Utils.FileManager import FileManager
  8. from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
  9. from kalliope.core.OrderListener import OrderListener
  10. from werkzeug.utils import secure_filename
  11. from flask import jsonify
  12. from flask import request
  13. from flask_restful import abort
  14. from flask_cors import CORS
  15. from kalliope.core.RestAPI.utils import requires_auth
  16. from kalliope.core.SynapseLauncher import SynapseLauncher
  17. from kalliope._version import version_str
  18. logging.basicConfig()
  19. logger = logging.getLogger("kalliope")
  20. UPLOAD_FOLDER = '/tmp/kalliope/tmp_uploaded_audio'
  21. ALLOWED_EXTENSIONS = {'mp3', 'wav'}
  22. class FlaskAPI(threading.Thread):
  23. def __init__(self, app, port=5000, brain=None, allowed_cors_origin=False):
  24. """
  25. :param app: Flask API
  26. :param port: Port to listen
  27. :param brain: Brain object
  28. :type brain: Brain
  29. """
  30. super(FlaskAPI, self).__init__()
  31. self.app = app
  32. self.port = port
  33. self.brain = brain
  34. self.allowed_cors_origin = allowed_cors_origin
  35. # get current settings
  36. sl = SettingLoader()
  37. self.settings = sl.settings
  38. # api_response sent by the Order Analyser when using the /synapses/start/audio URL
  39. self.api_response = None
  40. # boolean used to notify the main process that we get the list of returned synapse
  41. self.order_analyser_return = False
  42. # configure the upload folder
  43. app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
  44. # create the temp folder
  45. FileManager.create_directory(UPLOAD_FOLDER)
  46. # Flask configuration remove default Flask behaviour to encode to ASCII
  47. self.app.url_map.strict_slashes = False
  48. self.app.config['JSON_AS_ASCII'] = False
  49. if self.allowed_cors_origin is not False:
  50. cors = CORS(app, resources={r"/*": {"origins": allowed_cors_origin}}, supports_credentials=True)
  51. # Add routing rules
  52. self.app.add_url_rule('/', view_func=self.get_main_page, methods=['GET'])
  53. self.app.add_url_rule('/synapses', view_func=self.get_synapses, methods=['GET'])
  54. self.app.add_url_rule('/synapses/<synapse_name>', view_func=self.get_synapse, methods=['GET'])
  55. self.app.add_url_rule('/synapses/start/id/<synapse_name>', view_func=self.run_synapse_by_name, methods=['POST'])
  56. self.app.add_url_rule('/synapses/start/order', view_func=self.run_synapse_by_order, methods=['POST'])
  57. self.app.add_url_rule('/synapses/start/audio', view_func=self.run_synapse_by_audio, methods=['POST'])
  58. self.app.add_url_rule('/shutdown/', view_func=self.shutdown_server, methods=['POST'])
  59. def run(self):
  60. self.app.run(host='0.0.0.0', port="%s" % int(self.port), debug=True, threaded=True, use_reloader=False)
  61. def get_main_page(self):
  62. data = {
  63. "Kalliope version": "%s" % version_str
  64. }
  65. return jsonify(data), 200
  66. @staticmethod
  67. def allowed_file(filename):
  68. return '.' in filename and \
  69. filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
  70. def _get_synapse_by_name(self, synapse_name):
  71. """
  72. Find a synapse in the brain by its name
  73. :param synapse_name:
  74. :return:
  75. """
  76. all_synapse = self.brain.synapses
  77. for synapse in all_synapse:
  78. try:
  79. if synapse.name == synapse_name:
  80. return synapse
  81. except KeyError:
  82. pass
  83. return None
  84. @requires_auth
  85. def get_synapses(self):
  86. """
  87. get all synapses.
  88. test with curl:
  89. curl -i --user admin:secret -X GET http://127.0.0.1:5000/synapses
  90. """
  91. data = jsonify(synapses=[e.serialize() for e in self.brain.synapses])
  92. return data, 200
  93. @requires_auth
  94. def get_synapse(self, synapse_name):
  95. """
  96. get a synapse by its name
  97. test with curl:
  98. curl --user admin:secret -i -X GET http://127.0.0.1:5000/synapses/say-hello-en
  99. """
  100. synapse_target = self._get_synapse_by_name(synapse_name)
  101. if synapse_target is not None:
  102. data = jsonify(synapses=synapse_target.serialize())
  103. return data, 200
  104. data = {
  105. "synapse name not found": "%s" % synapse_name
  106. }
  107. return jsonify(error=data), 404
  108. @requires_auth
  109. def run_synapse_by_name(self, synapse_name):
  110. """
  111. Run a synapse by its name
  112. test with curl:
  113. curl -i --user admin:secret -X POST http://127.0.0.1:5000/synapses/start/id/say-hello-fr
  114. :param synapse_name:
  115. :return:
  116. """
  117. # get a synapse object from the name
  118. synapse_target = BrainLoader().get_brain().get_synapse_by_name(synapse_name=synapse_name)
  119. if synapse_target is None:
  120. data = {
  121. "synapse name not found": "%s" % synapse_name
  122. }
  123. return jsonify(error=data), 404
  124. else:
  125. # generate a MatchedSynapse from the synapse
  126. matched_synapse = MatchedSynapse(matched_synapse=synapse_target)
  127. # get the current LIFO buffer
  128. lifo_buffer = LIFOBuffer()
  129. # this is a new call we clean up the LIFO
  130. lifo_buffer.clean()
  131. lifo_buffer.add_synapse_list_to_lifo([matched_synapse])
  132. response = lifo_buffer.execute(is_api_call=True)
  133. data = jsonify(response)
  134. return data, 201
  135. @requires_auth
  136. def run_synapse_by_order(self):
  137. """
  138. Give an order to Kalliope via API like it was from a spoken one
  139. Test with curl
  140. curl -i --user admin:secret -H "Content-Type: application/json" -X POST -d '{"order":"my order"}' http://localhost:5000/synapses/start/order
  141. In case of quotes in the order or accents, use a file
  142. cat post.json:
  143. {"order":"j'aime"}
  144. curl -i --user admin:secret -H "Content-Type: application/json" -X POST --data @post.json http://localhost:5000/order/
  145. :return:
  146. """
  147. if not request.get_json() or 'order' not in request.get_json():
  148. abort(400)
  149. order = request.get_json('order')
  150. if order is not None:
  151. # get the order
  152. order_to_run = order["order"]
  153. api_response = SynapseLauncher.run_matching_synapse_from_order(order_to_run,
  154. self.brain,
  155. self.settings,
  156. is_api_call=True)
  157. if api_response:
  158. # if the list is not empty, we have launched one or more synapses
  159. data = jsonify(api_response)
  160. return data, 201
  161. else:
  162. data = {
  163. "error": "The given order doesn't match any synapses"
  164. }
  165. return jsonify(error=data), 400
  166. else:
  167. data = {
  168. "error": "order cannot be null"
  169. }
  170. return jsonify(error=data), 400
  171. def run_synapse_by_audio(self):
  172. """
  173. Give an order to Kalliope with an audio file
  174. Test with curl
  175. curl -i --user admin:secret -X POST http://localhost:5000/synapses/start/audio -F "file=@/path/to/input.wav"
  176. :return:
  177. """
  178. # check if the post request has the file part
  179. if 'file' not in request.files:
  180. data = {
  181. "error": "No file provided"
  182. }
  183. return jsonify(error=data), 400
  184. file = request.files['file']
  185. # if user does not select file, browser also
  186. # submit a empty part without filename
  187. if file.filename == '':
  188. data = {
  189. "error": "No file provided"
  190. }
  191. return jsonify(error=data), 400
  192. if file and self.allowed_file(file.filename):
  193. # save the file
  194. filename = secure_filename(file.filename)
  195. base_path = os.path.join(self.app.config['UPLOAD_FOLDER'])
  196. file.save(os.path.join(base_path, filename))
  197. # now start analyse the audio with STT engine
  198. audio_path = base_path + os.sep + filename
  199. ol = OrderListener(callback=self.audio_analyser_callback, audio_file_path=audio_path)
  200. ol.start()
  201. ol.join()
  202. # wait the Order Analyser processing. We need to wait in this thread to keep the context
  203. while not self.order_analyser_return:
  204. time.sleep(0.1)
  205. self.order_analyser_return = False
  206. if self.api_response is not None and self.api_response:
  207. data = jsonify(self.api_response)
  208. self.api_response = None
  209. return data, 201
  210. else:
  211. data = {
  212. "error": "The given order doesn't match any synapses"
  213. }
  214. return jsonify(error=data), 400
  215. @requires_auth
  216. def shutdown_server(self):
  217. func = request.environ.get('werkzeug.server.shutdown')
  218. if func is None:
  219. raise RuntimeError('Not running with the Werkzeug Server')
  220. func()
  221. return "Shutting down..."
  222. def audio_analyser_callback(self, order):
  223. """
  224. Callback of the OrderListener. Called after the processing of the audio file
  225. This method will
  226. - call the Order Analyser to analyse the order and launch corresponding synapse as usual.
  227. - get a list of launched synapse.
  228. - give the list to the main process via self.launched_synapses
  229. - notify that the processing is over via order_analyser_return
  230. :param order: string order to analyse
  231. :return:
  232. """
  233. logger.debug("order to process %s" % order)
  234. api_response = SynapseLauncher.run_matching_synapse_from_order(order,
  235. self.brain,
  236. self.settings,
  237. is_api_call=True)
  238. self.api_response = api_response
  239. # this boolean will notify the main process that the order have been processed
  240. self.order_analyser_return = True