FlaskAPI.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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. data = jsonify(api_response)
  158. return data, 201
  159. else:
  160. data = {
  161. "error": "order cannot be null"
  162. }
  163. return jsonify(error=data), 400
  164. def run_synapse_by_audio(self):
  165. """
  166. Give an order to Kalliope with an audio file
  167. Test with curl
  168. curl -i --user admin:secret -X POST http://localhost:5000/synapses/start/audio -F "file=@/path/to/input.wav"
  169. :return:
  170. """
  171. # check if the post request has the file part
  172. if 'file' not in request.files:
  173. data = {
  174. "error": "No file provided"
  175. }
  176. return jsonify(error=data), 400
  177. file = request.files['file']
  178. # if user does not select file, browser also
  179. # submit a empty part without filename
  180. if file.filename == '':
  181. data = {
  182. "error": "No file provided"
  183. }
  184. return jsonify(error=data), 400
  185. if file and self.allowed_file(file.filename):
  186. # save the file
  187. filename = secure_filename(file.filename)
  188. base_path = os.path.join(self.app.config['UPLOAD_FOLDER'])
  189. file.save(os.path.join(base_path, filename))
  190. # now start analyse the audio with STT engine
  191. audio_path = base_path + os.sep + filename
  192. ol = OrderListener(callback=self.audio_analyser_callback, audio_file_path=audio_path)
  193. ol.start()
  194. ol.join()
  195. # wait the Order Analyser processing. We need to wait in this thread to keep the context
  196. while not self.order_analyser_return:
  197. time.sleep(0.1)
  198. self.order_analyser_return = False
  199. if self.api_response is not None and self.api_response:
  200. data = jsonify(self.api_response)
  201. self.api_response = None
  202. return data, 201
  203. else:
  204. data = {
  205. "error": "The given order doesn't match any synapses"
  206. }
  207. return jsonify(error=data), 400
  208. @requires_auth
  209. def shutdown_server(self):
  210. func = request.environ.get('werkzeug.server.shutdown')
  211. if func is None:
  212. raise RuntimeError('Not running with the Werkzeug Server')
  213. func()
  214. return "Shutting down..."
  215. def audio_analyser_callback(self, order):
  216. """
  217. Callback of the OrderListener. Called after the processing of the audio file
  218. This method will
  219. - call the Order Analyser to analyse the order and launch corresponding synapse as usual.
  220. - get a list of launched synapse.
  221. - give the list to the main process via self.launched_synapses
  222. - notify that the processing is over via order_analyser_return
  223. :param order: string order to analyse
  224. :return:
  225. """
  226. logger.debug("order to process %s" % order)
  227. api_response = SynapseLauncher.run_matching_synapse_from_order(order,
  228. self.brain,
  229. self.settings,
  230. is_api_call=True)
  231. self.api_response = api_response
  232. # this boolean will notify the main process that the order have been processed
  233. self.order_analyser_return = True