FlaskAPI.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import logging
  2. import os
  3. import threading
  4. import time
  5. from flask import jsonify
  6. from flask import request
  7. from flask_cors import CORS
  8. from flask_restful import abort
  9. from werkzeug.utils import secure_filename
  10. from kalliope import SignalLauncher
  11. from kalliope._version import version_str
  12. from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
  13. from kalliope.core.LIFOBuffer import LIFOBuffer
  14. from kalliope.core.Models.MatchedSynapse import MatchedSynapse
  15. from kalliope.core.OrderListener import OrderListener
  16. from kalliope.core.RestAPI.utils import requires_auth
  17. from kalliope.core.SynapseLauncher import SynapseLauncher
  18. from kalliope.core.Utils.FileManager import FileManager
  19. from kalliope.signals.order import Order
  20. logging.basicConfig()
  21. logger = logging.getLogger("kalliope")
  22. UPLOAD_FOLDER = '/tmp/kalliope/tmp_uploaded_audio'
  23. ALLOWED_EXTENSIONS = {'mp3', 'wav'}
  24. class FlaskAPI(threading.Thread):
  25. def __init__(self, app, port=5000, brain=None, allowed_cors_origin=False):
  26. """
  27. :param app: Flask API
  28. :param port: Port to listen
  29. :param brain: Brain object
  30. :type brain: Brain
  31. """
  32. super(FlaskAPI, self).__init__()
  33. self.app = app
  34. self.port = port
  35. self.brain = brain
  36. self.allowed_cors_origin = allowed_cors_origin
  37. # get current settings
  38. sl = SettingLoader()
  39. self.settings = sl.settings
  40. # api_response sent by the Order Analyser when using the /synapses/start/audio URL
  41. self.api_response = None
  42. # boolean used to notify the main process that we get the list of returned synapse
  43. self.order_analyser_return = False
  44. # configure the upload folder
  45. app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
  46. # create the temp folder
  47. FileManager.create_directory(UPLOAD_FOLDER)
  48. # Flask configuration remove default Flask behaviour to encode to ASCII
  49. self.app.url_map.strict_slashes = False
  50. self.app.config['JSON_AS_ASCII'] = False
  51. if self.allowed_cors_origin is not False:
  52. CORS(app, resources={r"/*": {"origins": allowed_cors_origin}}, supports_credentials=True)
  53. # no voice flag
  54. self.no_voice = False
  55. # Add routing rules
  56. self.app.add_url_rule('/', view_func=self.get_main_page, methods=['GET'])
  57. self.app.add_url_rule('/synapses', view_func=self.get_synapses, methods=['GET'])
  58. self.app.add_url_rule('/synapses/<synapse_name>', view_func=self.get_synapse, methods=['GET'])
  59. self.app.add_url_rule('/synapses/start/id/<synapse_name>', view_func=self.run_synapse_by_name, methods=['POST'])
  60. self.app.add_url_rule('/synapses/start/order', view_func=self.run_synapse_by_order, methods=['POST'])
  61. self.app.add_url_rule('/synapses/start/audio', view_func=self.run_synapse_by_audio, methods=['POST'])
  62. self.app.add_url_rule('/shutdown/', view_func=self.shutdown_server, methods=['POST'])
  63. self.app.add_url_rule('/mute/', view_func=self.get_mute, methods=['GET'])
  64. self.app.add_url_rule('/mute/', view_func=self.set_mute, methods=['POST'])
  65. def run(self):
  66. self.app.run(host='0.0.0.0', port=int(self.port), debug=True, threaded=True, use_reloader=False)
  67. @requires_auth
  68. def get_main_page(self):
  69. logger.debug("[FlaskAPI] get_main_page")
  70. data = {
  71. "Kalliope version": "%s" % version_str
  72. }
  73. return jsonify(data), 200
  74. @staticmethod
  75. def allowed_file(filename):
  76. return '.' in filename and \
  77. filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
  78. def _get_synapse_by_name(self, synapse_name):
  79. """
  80. Find a synapse in the brain by its name
  81. :param synapse_name:
  82. :return:
  83. """
  84. all_synapse = self.brain.synapses
  85. for synapse in all_synapse:
  86. try:
  87. if synapse.name == synapse_name:
  88. return synapse
  89. except KeyError:
  90. pass
  91. return None
  92. @requires_auth
  93. def get_synapses(self):
  94. """
  95. get all synapses.
  96. test with curl:
  97. curl -i --user admin:secret -X GET http://127.0.0.1:5000/synapses
  98. """
  99. logger.debug("[FlaskAPI] get_synapses: all")
  100. data = jsonify(synapses=[e.serialize() for e in self.brain.synapses])
  101. return data, 200
  102. @requires_auth
  103. def get_synapse(self, synapse_name):
  104. """
  105. get a synapse by its name
  106. test with curl:
  107. curl --user admin:secret -i -X GET http://127.0.0.1:5000/synapses/say-hello-en
  108. """
  109. logger.debug("[FlaskAPI] get_synapse: synapse_name -> %s" % synapse_name)
  110. synapse_target = self._get_synapse_by_name(synapse_name)
  111. if synapse_target is not None:
  112. data = jsonify(synapses=synapse_target.serialize())
  113. return data, 200
  114. data = {
  115. "synapse name not found": "%s" % synapse_name
  116. }
  117. return jsonify(error=data), 404
  118. @requires_auth
  119. def run_synapse_by_name(self, synapse_name):
  120. """
  121. Run a synapse by its name
  122. test with curl:
  123. curl -i --user admin:secret -X POST http://127.0.0.1:5000/synapses/start/id/say-hello-fr
  124. run a synapse without making kalliope speaking
  125. curl -i -H "Content-Type: application/json" --user admin:secret -X POST \
  126. -d '{"no_voice":"true"}' http://127.0.0.1:5000/synapses/start/id/say-hello-fr
  127. Run a synapse by its name and pass order's parameters
  128. curl -i -H "Content-Type: application/json" --user admin:secret -X POST \
  129. -d '{"no_voice":"true", "parameters": {"parameter1": "value1" }}' \
  130. http://127.0.0.1:5000/synapses/start/id/say-hello-fr
  131. :param synapse_name: name(id) of the synapse to execute
  132. :return:
  133. """
  134. # get a synapse object from the name
  135. logger.debug("[FlaskAPI] run_synapse_by_name: synapse name -> %s" % synapse_name)
  136. synapse_target = BrainLoader().brain.get_synapse_by_name(synapse_name=synapse_name)
  137. # get no_voice_flag if present
  138. no_voice = self.get_boolean_flag_from_request(request, boolean_flag_to_find="no_voice")
  139. # get parameters
  140. parameters = self.get_parameters_from_request(request)
  141. if synapse_target is None:
  142. data = {
  143. "synapse name not found": "%s" % synapse_name
  144. }
  145. return jsonify(error=data), 404
  146. else:
  147. # generate a MatchedSynapse from the synapse
  148. matched_synapse = MatchedSynapse(matched_synapse=synapse_target, overriding_parameter=parameters)
  149. # get the current LIFO buffer
  150. lifo_buffer = LIFOBuffer()
  151. # this is a new call we clean up the LIFO
  152. lifo_buffer.clean()
  153. lifo_buffer.add_synapse_list_to_lifo([matched_synapse])
  154. response = lifo_buffer.execute(is_api_call=True, no_voice=no_voice)
  155. data = jsonify(response)
  156. return data, 201
  157. @requires_auth
  158. def run_synapse_by_order(self):
  159. """
  160. Give an order to Kalliope via API like it was from a spoken one
  161. Test with curl
  162. curl -i --user admin:secret -H "Content-Type: application/json" -X POST \
  163. -d '{"order":"my order"}' http://localhost:5000/synapses/start/order
  164. In case of quotes in the order or accents, use a file
  165. cat post.json:
  166. {"order":"j'aime"}
  167. curl -i --user admin:secret -H "Content-Type: application/json" -X POST \
  168. --data @post.json http://localhost:5000/order/
  169. Can be used with no_voice flag
  170. curl -i --user admin:secret -H "Content-Type: application/json" -X POST \
  171. -d '{"order":"my order", "no_voice":"true"}' http://localhost:5000/synapses/start/order
  172. :return:
  173. """
  174. if not request.get_json() or 'order' not in request.get_json():
  175. abort(400)
  176. order = request.get_json('order')
  177. # get no_voice_flag if present
  178. no_voice = self.get_boolean_flag_from_request(request, boolean_flag_to_find="no_voice")
  179. if order is not None:
  180. # get the order
  181. order_to_run = order["order"]
  182. logger.debug("[FlaskAPI] run_synapse_by_order: order to run -> %s" % order_to_run)
  183. api_response = SynapseLauncher.run_matching_synapse_from_order(order_to_run,
  184. self.brain,
  185. self.settings,
  186. is_api_call=True,
  187. no_voice=no_voice)
  188. data = jsonify(api_response)
  189. return data, 201
  190. else:
  191. data = {
  192. "error": "order cannot be null"
  193. }
  194. return jsonify(error=data), 400
  195. @requires_auth
  196. def run_synapse_by_audio(self):
  197. """
  198. Give an order to Kalliope with an audio file
  199. Test with curl
  200. curl -i --user admin:secret -X POST http://localhost:5000/synapses/start/audio -F "file=@/path/to/input.wav"
  201. With no_voice flag
  202. curl -i -H "Content-Type: application/json" --user admin:secret -X POST \
  203. http://localhost:5000/synapses/start/audio -F "file=@path/to/file.wav" -F no_voice="true"
  204. :return:
  205. """
  206. # get no_voice_flag if present
  207. self.no_voice = self.str_to_bool(request.form.get("no_voice"))
  208. # check if the post request has the file part
  209. if 'file' not in request.files:
  210. data = {
  211. "error": "No file provided"
  212. }
  213. return jsonify(error=data), 400
  214. uploaded_file = request.files['file']
  215. # if user does not select file, browser also
  216. # submit a empty part without filename
  217. if uploaded_file.filename == '':
  218. data = {
  219. "error": "No file provided"
  220. }
  221. return jsonify(error=data), 400
  222. # save the file
  223. filename = secure_filename(uploaded_file.filename)
  224. base_path = os.path.join(self.app.config['UPLOAD_FOLDER'])
  225. uploaded_file.save(os.path.join(base_path, filename))
  226. # now start analyse the audio with STT engine
  227. audio_path = base_path + os.sep + filename
  228. logger.debug("[FlaskAPI] run_synapse_by_audio: with file path %s" % audio_path)
  229. if not self.allowed_file(audio_path):
  230. audio_path = self._convert_to_wav(audio_file_path=audio_path)
  231. ol = OrderListener(callback=self.audio_analyser_callback, audio_file_path=audio_path)
  232. ol.start()
  233. ol.join()
  234. # wait the Order Analyser processing. We need to wait in this thread to keep the context
  235. while not self.order_analyser_return:
  236. time.sleep(0.1)
  237. self.order_analyser_return = False
  238. if self.api_response is not None and self.api_response:
  239. data = jsonify(self.api_response)
  240. self.api_response = None
  241. logger.debug("[FlaskAPI] run_synapse_by_audio: data %s" % data)
  242. return data, 201
  243. else:
  244. data = {
  245. "error": "The given order doesn't match any synapses"
  246. }
  247. return jsonify(error=data), 400
  248. @staticmethod
  249. def _convert_to_wav(audio_file_path):
  250. """
  251. If not already .wav, convert an incoming audio file to wav format. Using system avconv (raspberry)
  252. :param audio_file_path: the current full file path
  253. :return: Wave file path
  254. """
  255. # Not allowed so convert into wav using avconv (raspberry)
  256. base = os.path.splitext(audio_file_path)[0]
  257. extension = os.path.splitext(audio_file_path)[1]
  258. if extension != ".wav":
  259. current_file_path = audio_file_path
  260. audio_file_path = base + ".wav"
  261. os.system("avconv -y -i " + current_file_path + " " + audio_file_path) # --> deprecated
  262. # subprocess.call(['avconv', '-y', '-i', audio_path, new_file_path], shell=True) # Not working ...
  263. return audio_file_path
  264. @requires_auth
  265. def shutdown_server(self):
  266. func = request.environ.get('werkzeug.server.shutdown')
  267. if func is None:
  268. raise RuntimeError('Not running with the Werkzeug Server')
  269. func()
  270. return "Shutting down..."
  271. @requires_auth
  272. def get_mute(self):
  273. """
  274. Return the current trigger status
  275. Curl test
  276. curl -i --user admin:secret -X GET http://127.0.0.1:5000/mute
  277. """
  278. # find the order signal and call the mute method
  279. signal_order = SignalLauncher.get_order_instance()
  280. if signal_order is not None:
  281. data = {
  282. "mute": signal_order.get_mute_status()
  283. }
  284. return jsonify(data), 200
  285. # if no Order instance
  286. data = {
  287. "error": "Mute status unknow"
  288. }
  289. return jsonify(error=data), 400
  290. @requires_auth
  291. def set_mute(self):
  292. """
  293. Set the trigger status (muted or not)
  294. Curl test:
  295. curl -i -H "Content-Type: application/json" --user admin:secret -X POST \
  296. -d '{"mute": "True"}' http://127.0.0.1:5000/mute
  297. """
  298. if not request.get_json() or 'mute' not in request.get_json():
  299. abort(400)
  300. # get mute if present
  301. mute = self.get_boolean_flag_from_request(request, boolean_flag_to_find="mute")
  302. # find the order signal and call the mute method
  303. signal_order = SignalLauncher.get_order_instance()
  304. if signal_order is not None:
  305. signal_order.set_mute_status(mute)
  306. data = {
  307. "mute": signal_order.get_mute_status()
  308. }
  309. return jsonify(data), 200
  310. data = {
  311. "error": "Cannot switch mute status"
  312. }
  313. return jsonify(error=data), 400
  314. def audio_analyser_callback(self, order):
  315. """
  316. Callback of the OrderListener. Called after the processing of the audio file
  317. This method will
  318. - call the Order Analyser to analyse the order and launch corresponding synapse as usual.
  319. - get a list of launched synapse.
  320. - give the list to the main process via self.launched_synapses
  321. - notify that the processing is over via order_analyser_return
  322. :param order: string order to analyse
  323. :return:
  324. """
  325. logger.debug("[FlaskAPI] audio_analyser_callback: order to process -> %s" % order)
  326. api_response = SynapseLauncher.run_matching_synapse_from_order(order,
  327. self.brain,
  328. self.settings,
  329. is_api_call=True,
  330. no_voice=self.no_voice)
  331. self.api_response = api_response
  332. # this boolean will notify the main process that the order have been processed
  333. self.order_analyser_return = True
  334. def get_boolean_flag_from_request(self, http_request, boolean_flag_to_find):
  335. """
  336. Get the boolean flag from the request if exist
  337. :param http_request:
  338. :param boolean_flag_to_find: json flag to find in the http_request
  339. :return: True or False if the boolean flag has been found in the request
  340. """
  341. boolean_flag = False
  342. try:
  343. received_json = http_request.get_json(force=True, silent=True, cache=True)
  344. if boolean_flag_to_find in received_json:
  345. boolean_flag = self.str_to_bool(received_json[boolean_flag_to_find])
  346. except TypeError:
  347. # no json received
  348. pass
  349. logger.debug("[FlaskAPI] Boolean %s : %s" % (boolean_flag_to_find, boolean_flag))
  350. return boolean_flag
  351. @staticmethod
  352. def str_to_bool(s):
  353. if isinstance(s, bool): # do not convert if already a boolean
  354. return s
  355. else:
  356. if s == 'True' or s == 'true' or s == '1':
  357. return True
  358. elif s == 'False' or s == 'false' or s == '0':
  359. return False
  360. else:
  361. return False
  362. @staticmethod
  363. def get_parameters_from_request(http_request):
  364. """
  365. Get "parameters" object from the
  366. :param http_request:
  367. :return:
  368. """
  369. parameters = None
  370. try:
  371. received_json = http_request.get_json(silent=False, force=True)
  372. if 'parameters' in received_json:
  373. parameters = received_json['parameters']
  374. except TypeError:
  375. pass
  376. logger.debug("[FlaskAPI] Overridden parameters: %s" % parameters)
  377. return parameters