6
0

WebServer.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  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 'predict' in data.keys():
  80. app_status['projects'].predict(identifier, data['predict'])
  81. else:
  82. app_status['projects'].update_project(identifier, data)
  83. return response()
  84. @self.__flask.route('/projects/<identifier>/data', methods=['POST'])
  85. def upload_file(identifier):
  86. # abort if project id is not valid
  87. if identifier not in app_status['projects'].keys():
  88. # TODO return 404
  89. return make_response('project does not exist', 500)
  90. # get project and upload path
  91. project = app_status['projects'][identifier]
  92. upload_path, file_uuid = project.new_media_file_path()
  93. # prepare wrapper objects
  94. job = GenericWrapper()
  95. file_name = GenericWrapper()
  96. file_extension = GenericWrapper()
  97. file_size = GenericWrapper(0)
  98. # save upload to file
  99. def custom_stream_factory(total_content_length, filename, content_type, content_length=None):
  100. file_name.value, file_extension.value = path.splitext(filename)
  101. file_path = path.join(upload_path, f'{file_uuid}{file_extension.value}')
  102. # add job to app status
  103. project['jobs'][file_uuid] = {
  104. 'id': file_uuid,
  105. 'type': 'upload',
  106. 'progress': 0,
  107. 'filename': filename,
  108. 'created': int(time()),
  109. 'finished': None
  110. }
  111. job.value = project['jobs'][file_uuid]
  112. # define progress callback
  113. length = content_length if content_length is not None and content_length != 0 else total_content_length
  114. def callback(progress):
  115. file_size.value += progress
  116. relative = progress / length
  117. if relative - job.value['progress'] > 0.02:
  118. job.value['progress'] = relative
  119. # open file handler
  120. return ProgressFileWriter(file_path, 'wb', callback)
  121. stream, form, files = formparser.parse_form_data(request.environ, stream_factory=custom_stream_factory)
  122. if 'file' not in files.keys():
  123. return make_response('no file uploaded', 500)
  124. # set progress to 1 after upload is done
  125. job = job.value
  126. job['progress'] = 1
  127. job['finished'] = int(time())
  128. # add to project files
  129. project.add_media_file(file_uuid, file_name.value, file_extension.value, file_size.value, job['created'])
  130. # return default success response
  131. return response()
  132. @self.__flask.route('/projects/<project_identifier>/unmanaged/<int:file_number>', methods=['GET'])
  133. def get_unmanaged_file(project_identifier, file_number):
  134. # abort if project id is not valid
  135. if project_identifier not in app_status['projects'].keys():
  136. return make_response('project does not exist', 500)
  137. project = app_status['projects'][project_identifier]
  138. # abort if file id is not valid
  139. if file_number >= len(project.unmanaged_files_keys):
  140. return make_response('file does not exist', 500)
  141. file_identifier = project.unmanaged_files_keys[file_number]
  142. if file_identifier not in project.unmanaged_files_keys:
  143. return make_response('file does not exist', 500)
  144. target_object = project.unmanaged_files[file_identifier]
  145. # return element data
  146. return response(target_object.get_data())
  147. @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>', defaults={'size': None}, methods=['GET'])
  148. @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>/<size>', methods=['GET'])
  149. def get_file(project_identifier, file_identifier, size):
  150. # abort if project id is not valid
  151. if project_identifier not in app_status['projects'].keys():
  152. return make_response('project does not exist', 500)
  153. project = app_status['projects'][project_identifier]
  154. # abort if file id is not valid
  155. target_object = project.get_media_file(file_identifier)
  156. if target_object is None:
  157. return make_response('file does not exist', 500)
  158. # resize image to requested size
  159. if size is not None:
  160. target_object = target_object.resize(size)
  161. # construct directory and filename
  162. file_directory = path.join(getcwd(), target_object.directory)
  163. file_name = target_object.full_name
  164. # return data
  165. return send_from_directory(file_directory, file_name)
  166. @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>', methods=['POST'])
  167. def add_result(project_identifier, file_identifier):
  168. # abort if project id is not valid
  169. if project_identifier not in app_status['projects'].keys():
  170. return make_response('project does not exist', 500)
  171. project = app_status['projects'][project_identifier]
  172. # abort if file id is not valid
  173. target_object = project.get_media_file(file_identifier)
  174. if target_object is None:
  175. return make_response('file does not exist', 500)
  176. # add result
  177. result = request.get_json(force=True)
  178. if result:
  179. if 'delete' in result:
  180. project.remove_media_file(file_identifier)
  181. elif 'reset' in result:
  182. target_object.remove_results()
  183. elif 'x' not in result:
  184. if result['label']:
  185. result['type'] = 'labeled-image'
  186. target_object.add_global_result(result)
  187. else:
  188. target_object.remove_global_result()
  189. else:
  190. result['type'] = 'labeled-bounding-box' if 'label' in result else 'bounding-box'
  191. target_object.add_result(result)
  192. # return default success response
  193. return response()
  194. @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>/<result_identifier>', methods=['POST'])
  195. def edit_result(project_identifier, file_identifier, result_identifier):
  196. # abort if project id is not valid
  197. if project_identifier not in app_status['projects'].keys():
  198. return make_response('project does not exist', 500)
  199. project = app_status['projects'][project_identifier]
  200. # abort if file id is not valid
  201. target_object = project.get_media_file(file_identifier)
  202. if target_object is None:
  203. return make_response('file does not exist', 500)
  204. # parse post data
  205. result = request.get_json(force=True)
  206. if result:
  207. # remove result
  208. if 'delete' in result.keys():
  209. target_object.remove_result(result_identifier)
  210. # update result
  211. else:
  212. target_object.update_result(result_identifier, result)
  213. # return default success response
  214. return response()
  215. @self.__flask.route('/projects/<project_identifier>/labels', methods=['POST'])
  216. def create_label(project_identifier):
  217. # abort if project id is not valid
  218. if project_identifier not in app_status['projects'].keys():
  219. return make_response('project does not exist', 500)
  220. project = app_status['projects'][project_identifier]
  221. # add result
  222. result = request.get_json(force=True)
  223. if result:
  224. project.add_label(result['name'])
  225. # return default success response
  226. return response()
  227. @self.__flask.route('/projects/<project_identifier>/labels/<label_identifier>', methods=['POST'])
  228. def edit_label(project_identifier, label_identifier):
  229. # abort if project id is not valid
  230. if project_identifier not in app_status['projects'].keys():
  231. return make_response('project does not exist', 500)
  232. project = app_status['projects'][project_identifier]
  233. # parse post data
  234. result = request.get_json(force=True)
  235. if result:
  236. # remove label
  237. if 'delete' in result.keys():
  238. project.remove_label(label_identifier)
  239. # update label
  240. else:
  241. project.update_label(label_identifier, result['name'])
  242. # return default success response
  243. return response()
  244. @self.__flask.route('/projects/<project_identifier>/predictions', methods=['GET'])
  245. def download_predictions(project_identifier):
  246. # abort if project id is not valid
  247. if project_identifier not in app_status['projects'].keys():
  248. return make_response('project does not exist', 404)
  249. project = app_status['projects'][project_identifier]
  250. # create export
  251. result = []
  252. def mk_obj(type, name, extension, predictions):
  253. data_res = {
  254. 'type': type,
  255. 'filename': name + extension,
  256. 'predictions': []
  257. }
  258. for result_key in predictions:
  259. result_obj = predictions[result_key]
  260. data_res['predictions'].append(result_obj)
  261. return data_res
  262. for data_key in project['data']:
  263. data_obj = project['data'][data_key]
  264. result.append(mk_obj(
  265. data_obj['type'],
  266. data_obj['name'],
  267. data_obj['extension'],
  268. data_obj['predictionResults']
  269. ))
  270. for data_key in project.unmanaged_files:
  271. data_obj = project.unmanaged_files[data_key].get_data()
  272. result.append(mk_obj(
  273. data_obj['type'],
  274. data_obj['id'],
  275. data_obj['extension'],
  276. data_obj['predictionResults']
  277. ))
  278. # send to user
  279. rsp = make_response(dumps(result))
  280. rsp.headers['Content-Type'] = 'text/json;charset=UTF-8'
  281. rsp.headers['Content-Disposition'] = 'attachment;filename=predictions.json'
  282. return rsp
  283. # finally start web server
  284. host = app_status['settings']['frontend']['host']
  285. port = app_status['settings']['frontend']['port']
  286. eventlet.wsgi.server(eventlet.listen((host, port)), self.__app)
  287. def __update_application_status(self, status, keys):
  288. value = status
  289. for key in keys[:-1]:
  290. value = value[key]
  291. self.__status = status
  292. self.__sio.emit('app_status', {
  293. 'keys': keys[:-1],
  294. 'value': value
  295. })