FlaskAPI.py 11 KB

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