WebServer.py 15 KB

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