WebServer.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. from glob import glob
  2. from json import dumps
  3. from os import path, getcwd
  4. from os.path import exists
  5. from time import time
  6. import eventlet
  7. import socketio
  8. from flask import Flask, make_response, send_from_directory, request
  9. from werkzeug import formparser
  10. from pycs.ApplicationStatus import ApplicationStatus
  11. from pycs.util.GenericWrapper import GenericWrapper
  12. from pycs.util.ProgressFileWriter import ProgressFileWriter
  13. from pycs.util.RecursiveDictionary import set_recursive
  14. class WebServer:
  15. def __init__(self, app_status: ApplicationStatus):
  16. # initialize web server
  17. if exists('webui/index.html'):
  18. print('production build')
  19. # find svg icons and add them as separate static files to
  20. # set their correct mime type / content_type
  21. static_files = {'/': 'webui/'}
  22. for svg_path in glob('webui/img/*.svg'):
  23. svg_path = svg_path.replace('\\', '/')
  24. static_files[svg_path[5:]] = {'content_type': 'image/svg+xml', 'filename': svg_path}
  25. self.__sio = socketio.Server()
  26. self.__flask = Flask(__name__)
  27. self.__app = socketio.WSGIApp(self.__sio, self.__flask, static_files=static_files)
  28. def response(data=None):
  29. if data is None:
  30. return make_response()
  31. else:
  32. return make_response(data)
  33. else:
  34. print('development build')
  35. self.__sio = socketio.Server(cors_allowed_origins='*')
  36. self.__flask = Flask(__name__)
  37. self.__app = socketio.WSGIApp(self.__sio, self.__flask)
  38. def response(data=None):
  39. if data is None:
  40. rsp = make_response()
  41. else:
  42. rsp = make_response(data)
  43. rsp.headers['Access-Control-Allow-Origin'] = '*'
  44. return rsp
  45. # save every change in application status and send it to the client
  46. app_status.subscribe(self.__update_application_status, immediate=True)
  47. # define events
  48. @self.__sio.event
  49. def connect(id, msg):
  50. self.__sio.emit('app_status', {
  51. 'keys': [],
  52. 'value': self.__status
  53. }, to=id)
  54. @self.__flask.route('/settings', methods=['POST'])
  55. def edit_settings():
  56. data = request.get_json(force=True)
  57. set_recursive(data, app_status['settings'])
  58. return response()
  59. @self.__flask.route('/projects', methods=['POST'])
  60. def create_project():
  61. data = request.get_json(force=True)
  62. app_status['projects'].create_project(data['name'], data['description'], data['model'], data['label'], data['unmanaged'])
  63. return response()
  64. @self.__flask.route('/projects/<identifier>', methods=['POST'])
  65. def edit_project(identifier):
  66. data = request.get_json(force=True)
  67. if 'delete' in data.keys():
  68. app_status['projects'].delete_project(identifier)
  69. elif 'fit' in data.keys():
  70. app_status['projects'].fit(identifier)
  71. elif 'predictAll' in data.keys():
  72. app_status['projects'].predict(identifier)
  73. elif 'predict' in data.keys():
  74. app_status['projects'].predict(identifier, data['predict'])
  75. else:
  76. app_status['projects'].update_project(identifier, data)
  77. return response()
  78. @self.__flask.route('/projects/<identifier>/data', methods=['POST'])
  79. def upload_file(identifier):
  80. # abort if project id is not valid
  81. if identifier not in app_status['projects'].keys():
  82. # TODO return 404
  83. return make_response('project does not exist', 500)
  84. # get project and upload path
  85. project = app_status['projects'][identifier]
  86. upload_path, file_uuid = project.new_media_file_path()
  87. # prepare wrapper objects
  88. job = GenericWrapper()
  89. file_name = GenericWrapper()
  90. file_extension = GenericWrapper()
  91. file_size = GenericWrapper(0)
  92. # save upload to file
  93. def custom_stream_factory(total_content_length, filename, content_type, content_length=None):
  94. file_name.value, file_extension.value = path.splitext(filename)
  95. file_path = path.join(upload_path, f'{file_uuid}{file_extension.value}')
  96. # add job to app status
  97. project['jobs'][file_uuid] = {
  98. 'id': file_uuid,
  99. 'type': 'upload',
  100. 'progress': 0,
  101. 'filename': filename,
  102. 'created': int(time()),
  103. 'finished': None
  104. }
  105. job.value = project['jobs'][file_uuid]
  106. # define progress callback
  107. length = content_length if content_length is not None and content_length != 0 else total_content_length
  108. def callback(progress):
  109. file_size.value += progress
  110. relative = progress / length
  111. if relative - job.value['progress'] > 0.02:
  112. job.value['progress'] = relative
  113. # open file handler
  114. return ProgressFileWriter(file_path, 'wb', callback)
  115. stream, form, files = formparser.parse_form_data(request.environ, stream_factory=custom_stream_factory)
  116. if 'file' not in files.keys():
  117. return make_response('no file uploaded', 500)
  118. # set progress to 1 after upload is done
  119. job = job.value
  120. job['progress'] = 1
  121. job['finished'] = int(time())
  122. # add to project files
  123. project.add_media_file(file_uuid, file_name.value, file_extension.value, file_size.value, job['created'])
  124. # return default success response
  125. return response()
  126. @self.__flask.route('/projects/<project_identifier>/unmanaged/<int:file_number>', methods=['GET'])
  127. def get_unmanaged_file(project_identifier, file_number):
  128. # abort if project id is not valid
  129. if project_identifier not in app_status['projects'].keys():
  130. return make_response('project does not exist', 500)
  131. project = app_status['projects'][project_identifier]
  132. # abort if file id is not valid
  133. if file_number >= len(project.unmanaged_files_keys):
  134. return make_response('file does not exist', 500)
  135. file_identifier = project.unmanaged_files_keys[file_number]
  136. if file_identifier not in project.unmanaged_files_keys:
  137. return make_response('file does not exist', 500)
  138. target_object = project.unmanaged_files[file_identifier]
  139. # return element data
  140. return response(target_object.get_data())
  141. @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>', defaults={'size': None}, methods=['GET'])
  142. @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>/<size>', methods=['GET'])
  143. def get_file(project_identifier, file_identifier, size):
  144. # abort if project id is not valid
  145. if project_identifier not in app_status['projects'].keys():
  146. return make_response('project does not exist', 500)
  147. project = app_status['projects'][project_identifier]
  148. # abort if file id is not valid
  149. target_object = project.get_media_file(file_identifier)
  150. if target_object is None:
  151. return make_response('file does not exist', 500)
  152. # resize image to requested size
  153. if size is not None:
  154. target_object = target_object.resize(size)
  155. # construct directory and filename
  156. file_directory = path.join(getcwd(), target_object.directory)
  157. file_name = target_object.full_name
  158. # return data
  159. return send_from_directory(file_directory, file_name)
  160. @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>', methods=['POST'])
  161. def add_result(project_identifier, file_identifier):
  162. # abort if project id is not valid
  163. if project_identifier not in app_status['projects'].keys():
  164. return make_response('project does not exist', 500)
  165. project = app_status['projects'][project_identifier]
  166. # abort if file id is not valid
  167. target_object = project.get_media_file(file_identifier)
  168. if target_object is None:
  169. return make_response('file does not exist', 500)
  170. # add result
  171. result = request.get_json(force=True)
  172. if result:
  173. if 'delete' in result:
  174. project.remove_media_file(file_identifier)
  175. elif 'reset' in result:
  176. target_object.remove_results()
  177. elif 'x' not in result:
  178. if result['label']:
  179. result['type'] = 'labeled-image'
  180. target_object.add_global_result(result)
  181. else:
  182. target_object.remove_global_result()
  183. else:
  184. result['type'] = 'labeled-bounding-box' if 'label' in result else 'bounding-box'
  185. target_object.add_result(result)
  186. # return default success response
  187. return response()
  188. @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>/<result_identifier>', methods=['POST'])
  189. def edit_result(project_identifier, file_identifier, result_identifier):
  190. # abort if project id is not valid
  191. if project_identifier not in app_status['projects'].keys():
  192. return make_response('project does not exist', 500)
  193. project = app_status['projects'][project_identifier]
  194. # abort if file id is not valid
  195. target_object = project.get_media_file(file_identifier)
  196. if target_object is None:
  197. return make_response('file does not exist', 500)
  198. # parse post data
  199. result = request.get_json(force=True)
  200. if result:
  201. # remove result
  202. if 'delete' in result.keys():
  203. target_object.remove_result(result_identifier)
  204. # update result
  205. else:
  206. target_object.update_result(result_identifier, result)
  207. # return default success response
  208. return response()
  209. @self.__flask.route('/projects/<project_identifier>/labels', methods=['POST'])
  210. def create_label(project_identifier):
  211. # abort if project id is not valid
  212. if project_identifier not in app_status['projects'].keys():
  213. return make_response('project does not exist', 500)
  214. project = app_status['projects'][project_identifier]
  215. # add result
  216. result = request.get_json(force=True)
  217. if result:
  218. project.add_label(result['name'])
  219. # return default success response
  220. return response()
  221. @self.__flask.route('/projects/<project_identifier>/labels/<label_identifier>', methods=['POST'])
  222. def edit_label(project_identifier, label_identifier):
  223. # abort if project id is not valid
  224. if project_identifier not in app_status['projects'].keys():
  225. return make_response('project does not exist', 500)
  226. project = app_status['projects'][project_identifier]
  227. # parse post data
  228. result = request.get_json(force=True)
  229. if result:
  230. # remove label
  231. if 'delete' in result.keys():
  232. project.remove_label(label_identifier)
  233. # update label
  234. else:
  235. project.update_label(label_identifier, result['name'])
  236. # return default success response
  237. return response()
  238. @self.__flask.route('/projects/<project_identifier>/predictions', methods=['GET'])
  239. def download_predictions(project_identifier):
  240. # abort if project id is not valid
  241. if project_identifier not in app_status['projects'].keys():
  242. return make_response('project does not exist', 404)
  243. project = app_status['projects'][project_identifier]
  244. # create export
  245. result = []
  246. def mk_obj(type, name, extension, predictions):
  247. data_res = {
  248. 'type': type,
  249. 'filename': name + extension,
  250. 'predictions': []
  251. }
  252. for result_key in predictions:
  253. result_obj = predictions[result_key]
  254. data_res['predictions'].append(result_obj)
  255. return data_res
  256. for data_key in project['data']:
  257. data_obj = project['data'][data_key]
  258. result.append(mk_obj(
  259. data_obj['type'],
  260. data_obj['name'],
  261. data_obj['extension'],
  262. data_obj['predictionResults']
  263. ))
  264. for data_key in project.unmanaged_files:
  265. data_obj = project.unmanaged_files[data_key].get_data()
  266. result.append(mk_obj(
  267. data_obj['type'],
  268. data_obj['id'],
  269. data_obj['extension'],
  270. data_obj['predictionResults']
  271. ))
  272. # send to user
  273. rsp = make_response(dumps(result))
  274. rsp.headers['Content-Type'] = 'text/json;charset=UTF-8'
  275. rsp.headers['Content-Disposition'] = 'attachment;filename=predictions.json'
  276. return rsp
  277. # finally start web server
  278. host = app_status['settings']['frontend']['host']
  279. port = app_status['settings']['frontend']['port']
  280. eventlet.wsgi.server(eventlet.listen((host, port)), self.__app)
  281. def __update_application_status(self, status, keys):
  282. value = status
  283. for key in keys[:-1]:
  284. value = value[key]
  285. self.__status = status
  286. self.__sio.emit('app_status', {
  287. 'keys': keys[:-1],
  288. 'value': value
  289. })