瀏覽代碼

Merge branch '94-mass-media-mode' into 'master'

Resolve "mass media mode"

Closes #94

See merge request troebs/pycs!87
Eric Tröbs 4 年之前
父節點
當前提交
606934eaa2

+ 1 - 1
pycs/frontend/WebServer.py

@@ -68,7 +68,7 @@ class WebServer:
         @self.__flask.route('/projects', methods=['POST'])
         @self.__flask.route('/projects', methods=['POST'])
         def create_project():
         def create_project():
             data = request.get_json(force=True)
             data = request.get_json(force=True)
-            app_status['projects'].create_project(data['name'], data['description'], data['model'])
+            app_status['projects'].create_project(data['name'], data['description'], data['model'], data['unmanaged'])
 
 
             return response()
             return response()
 
 

+ 3 - 3
pycs/projects/ImageFile.py

@@ -6,13 +6,13 @@ from pycs.projects.MediaFile import MediaFile
 
 
 
 
 class ImageFile(MediaFile):
 class ImageFile(MediaFile):
-    def __init__(self, obj, project_id, type='data'):
+    def __init__(self, obj, project, type='data'):
         obj['type'] = 'image'
         obj['type'] = 'image'
-        super().__init__(obj, project_id, type)
+        super().__init__(obj, project, type)
 
 
     def resize(self, maximum_width):
     def resize(self, maximum_width):
         # check if resized file already exists
         # check if resized file already exists
-        resized = ImageFile(self.copy(), self.project_id, 'temp')
+        resized = ImageFile(self.copy(), self.project, 'temp')
         resized['id'] = self['id'] + '-' + maximum_width
         resized['id'] = self['id'] + '-' + maximum_width
 
 
         target_path = resized.path
         target_path = resized.path

+ 6 - 3
pycs/projects/MediaFile.py

@@ -5,18 +5,21 @@ from pycs.observable import ObservableDict
 
 
 
 
 class MediaFile(ObservableDict):
 class MediaFile(ObservableDict):
-    def __init__(self, obj, project_id, type):
+    def __init__(self, obj, project, type):
         if 'predictionResults' not in obj.keys():
         if 'predictionResults' not in obj.keys():
             obj['predictionResults'] = {}
             obj['predictionResults'] = {}
 
 
-        self.project_id = project_id
+        self.project = project
         self.type = type
         self.type = type
 
 
         super().__init__(obj)
         super().__init__(obj)
 
 
     @property
     @property
     def directory(self):
     def directory(self):
-        return join('projects', self.project_id, self.type)
+        if self.project['unmanaged'] is not None and self.type == 'data':
+            return self.project['unmanaged']
+        else:
+            return join('projects', self.project['id'], self.type)
 
 
     @property
     @property
     def full_name(self):
     def full_name(self):

+ 28 - 14
pycs/projects/Project.py

@@ -1,5 +1,6 @@
 from json import load
 from json import load
-from os import path, mkdir
+from os import path, mkdir, listdir
+from os.path import splitext
 from uuid import uuid1
 from uuid import uuid1
 
 
 from eventlet import spawn_after
 from eventlet import spawn_after
@@ -31,9 +32,18 @@ class Project(ObservableDict):
 
 
             obj['model'] = model
             obj['model'] = model
 
 
+        # handle unmanaged files
+        if obj['unmanaged'] is not None:
+            for file in listdir(obj['unmanaged']):
+                if file not in obj['data'].keys():
+                    name, ext = splitext(file)
+                    uuid = name
+
+                    obj['data'][uuid] = self.create_media_file_dict(uuid, name, ext, 0, 0)
+
         # save data as MediaFile objects
         # save data as MediaFile objects
         for key in obj['data'].keys():
         for key in obj['data'].keys():
-            obj['data'][key] = self.create_media_file(obj['data'][key], obj['id'])
+            obj['data'][key] = self.create_media_file(obj['data'][key], self)
 
 
         # initialize super
         # initialize super
         super().__init__(obj, parent)
         super().__init__(obj, parent)
@@ -56,28 +66,32 @@ class Project(ObservableDict):
     def new_media_file_path(self):
     def new_media_file_path(self):
         return path.join('projects', self['id'], 'data'), str(uuid1())
         return path.join('projects', self['id'], 'data'), str(uuid1())
 
 
-    def create_media_file(self, file, project_id=None):
-        if project_id is None:
-            project_id = self['id']
+    @staticmethod
+    def create_media_file_dict(uuid, name, extension, size, created):
+        return {
+            'id': uuid,
+            'name': name,
+            'extension': extension,
+            'size': size,
+            'created': created
+        }
+
+    def create_media_file(self, file, project=None):
+        if project is None:
+            project = self
 
 
         # TODO check file extension
         # TODO check file extension
         # TODO determine type
         # TODO determine type
         # TODO filter supported types
         # TODO filter supported types
         if file['extension'] in ['.jpg', '.png']:
         if file['extension'] in ['.jpg', '.png']:
-            return ImageFile(file, project_id)
+            return ImageFile(file, project)
         if file['extension'] in ['.mp4']:
         if file['extension'] in ['.mp4']:
-            return VideoFile(file, project_id)
+            return VideoFile(file, project)
 
 
         raise NotImplementedError
         raise NotImplementedError
 
 
     def add_media_file(self, uuid, name, extension, size, created):
     def add_media_file(self, uuid, name, extension, size, created):
-        file = {
-            'id': uuid,
-            'name': name,
-            'extension': extension,
-            'size': size,
-            'created': created
-        }
+        file = self.create_media_file_dict(uuid, name, extension, size, created)
         self['data'][file['id']] = self.create_media_file(file)
         self['data'][file['id']] = self.create_media_file(file)
 
 
     def remove_media_file(self, file_id):
     def remove_media_file(self, file_id):

+ 2 - 1
pycs/projects/ProjectManager.py

@@ -35,7 +35,7 @@ class ProjectManager(ObservableDict):
         with open(path.join('projects', uuid, 'project.json'), 'w') as file:
         with open(path.join('projects', uuid, 'project.json'), 'w') as file:
             dump(copy, file, indent=4)
             dump(copy, file, indent=4)
 
 
-    def create_project(self, name, description, model):
+    def create_project(self, name, description, model, unmanaged=None):
         # create dict representation
         # create dict representation
         uuid = str(uuid1())
         uuid = str(uuid1())
 
 
@@ -51,6 +51,7 @@ class ProjectManager(ObservableDict):
             'id': uuid,
             'id': uuid,
             'name': name,
             'name': name,
             'description': description,
             'description': description,
+            'unmanaged': unmanaged,
             'created': int(time()),
             'created': int(time()),
             'data': {},
             'data': {},
             'labels': {},
             'labels': {},

+ 3 - 3
pycs/projects/VideoFile.py

@@ -7,12 +7,12 @@ from pycs.projects.MediaFile import MediaFile
 
 
 
 
 class VideoFile(MediaFile):
 class VideoFile(MediaFile):
-    def __init__(self, obj, project_id, type='data'):
+    def __init__(self, obj, project, type='data'):
         # add type to object
         # add type to object
         obj['type'] = 'video'
         obj['type'] = 'video'
 
 
         # call parent constructor
         # call parent constructor
-        super().__init__(obj, project_id, type)
+        super().__init__(obj, project, type)
 
 
         # generate thumbnail
         # generate thumbnail
         self.__thumbnail = self.__read_video()
         self.__thumbnail = self.__read_video()
@@ -22,7 +22,7 @@ class VideoFile(MediaFile):
         copy = self.copy()
         copy = self.copy()
         copy['extension'] = '.jpg'
         copy['extension'] = '.jpg'
 
 
-        resized = ImageFile(copy, self.project_id, 'temp')
+        resized = ImageFile(copy, self.project, 'temp')
 
 
         # open video file
         # open video file
         video = cv2.VideoCapture(self.path)
         video = cv2.VideoCapture(self.path)

+ 1 - 1
webui/src/components/media/annotated-media-view.vue

@@ -23,7 +23,7 @@
                      @extremeClicking="extremeClicking = $event"/>
                      @extremeClicking="extremeClicking = $event"/>
     </div>
     </div>
 
 
-    <div class="selector" v-if="showMediaSelector">
+    <div class="selector" v-if="showMediaSelector && !currentProject.unmanaged">
       <media-selector :projectId="currentProject.id"
       <media-selector :projectId="currentProject.id"
                       :media="media"
                       :media="media"
                       :current="current"
                       :current="current"

+ 2 - 1
webui/src/components/media/media-control.vue

@@ -1,6 +1,7 @@
 <template>
 <template>
   <div class="media-control">
   <div class="media-control">
-    <button-input type="transparent"
+    <button-input v-if="!project.unmanaged"
+                  type="transparent"
                   style="color: var(--on_error)"
                   style="color: var(--on_error)"
                   :class="{disabled: !hasNext}"
                   :class="{disabled: !hasNext}"
                   @click="$emit('control', !control)">
                   @click="$emit('control', !control)">

+ 19 - 2
webui/src/components/projects/project-creation-window.vue

@@ -32,6 +32,17 @@
           </li>
           </li>
         </ul>
         </ul>
       </div>
       </div>
+
+      <text-input placeholder="path to folder (leave empty to disable)"
+                  v-model="unmanaged">
+        Unmanaged Storage
+      </text-input>
+
+      <div class="unmanaged">
+        Unmanaged storage mode disables most of the management features, but allows to manage a
+        much larger number of media files without having to copy them to the PyCS instance via
+        the webui first.
+      </div>
     </div>
     </div>
 
 
     <button-row class="footer">
     <button-row class="footer">
@@ -63,7 +74,8 @@ export default {
       nameError: false,
       nameError: false,
       description: '',
       description: '',
       descriptionError: false,
       descriptionError: false,
-      model: null
+      model: null,
+      unmanaged: ''
     }
     }
   },
   },
   computed: {
   computed: {
@@ -114,7 +126,8 @@ export default {
       this.socket.post('/projects', {
       this.socket.post('/projects', {
         name: this.name,
         name: this.name,
         description: this.description,
         description: this.description,
-        model: this.model
+        model: this.model,
+        unmanaged: this.unmanaged === '' ? null : this.unmanaged
       });
       });
       this.$emit('cancel', null);
       this.$emit('cancel', null);
     }
     }
@@ -158,6 +171,10 @@ export default {
   margin-top: 0.4rem;
   margin-top: 0.4rem;
 }
 }
 
 
+.unmanaged {
+  font-size: 80%;
+}
+
 .footer {
 .footer {
   flex-grow: 0;
   flex-grow: 0;
   padding: 2rem;
   padding: 2rem;

+ 11 - 8
webui/src/components/window/side-navigation-bar.vue

@@ -16,35 +16,35 @@
 
 
       <div class="item"
       <div class="item"
            :class="{active: window.content === 'settings', inactive: !currentProject}"
            :class="{active: window.content === 'settings', inactive: !currentProject}"
-           @click="ifProjectIsOpened(show, 'settings')">
+           @click="ifProjectIsOpened(show, true,'settings')">
         <img src="@/assets/icons/gear.svg">
         <img src="@/assets/icons/gear.svg">
         <span>Settings</span>
         <span>Settings</span>
       </div>
       </div>
 
 
       <div class="item"
       <div class="item"
-           :class="{active: window.content === 'add_data', inactive: !currentProject}"
-           @click="ifProjectIsOpened(show, 'add_data')">
+           :class="{active: window.content === 'add_data', inactive: !currentProject || !dataEnabled}"
+           @click="ifProjectIsOpened(show, dataEnabled, 'add_data')">
         <img src="@/assets/icons/package-dependencies.svg">
         <img src="@/assets/icons/package-dependencies.svg">
         <span>Data</span>
         <span>Data</span>
       </div>
       </div>
 
 
       <div class="item"
       <div class="item"
            :class="{active: window.content === 'labels', inactive: !currentProject || !labelsEnabled}"
            :class="{active: window.content === 'labels', inactive: !currentProject || !labelsEnabled}"
-           @click="ifProjectIsOpened(show, 'labels')">
+           @click="ifProjectIsOpened(show, labelsEnabled,'labels')">
         <img src="@/assets/icons/tag.svg">
         <img src="@/assets/icons/tag.svg">
         <span>Labels</span>
         <span>Labels</span>
       </div>
       </div>
 
 
       <div class="item"
       <div class="item"
            :class="{active: window.content === 'view_data', inactive: !currentProject || !mediaAvailable}"
            :class="{active: window.content === 'view_data', inactive: !currentProject || !mediaAvailable}"
-           @click="ifProjectIsOpened(show, 'view_data')">
+           @click="ifProjectIsOpened(show, mediaAvailable,'view_data')">
         <img src="@/assets/icons/file-media.svg">
         <img src="@/assets/icons/file-media.svg">
         <span>Session</span>
         <span>Session</span>
       </div>
       </div>
 
 
       <div class="item"
       <div class="item"
            :class="{active: window.content === 'model_interaction', inactive: !currentProject || !mediaAvailable}"
            :class="{active: window.content === 'model_interaction', inactive: !currentProject || !mediaAvailable}"
-           @click="ifProjectIsOpened(show, 'model_interaction')">
+           @click="ifProjectIsOpened(show, mediaAvailable,'model_interaction')">
         <img src="@/assets/icons/cpu.svg">
         <img src="@/assets/icons/cpu.svg">
         <span>Model</span>
         <span>Model</span>
       </div>
       </div>
@@ -86,6 +86,9 @@ export default {
     mediaAvailable: function () {
     mediaAvailable: function () {
       return this.currentProject && 'data' in this.currentProject && Object.keys(this.currentProject.data).length > 0;
       return this.currentProject && 'data' in this.currentProject && Object.keys(this.currentProject.data).length > 0;
     },
     },
+    dataEnabled: function () {
+      return this.currentProject && !this.currentProject.unmanaged;
+    },
     labelsEnabled: function () {
     labelsEnabled: function () {
       return this.currentProject
       return this.currentProject
           && (
           && (
@@ -98,8 +101,8 @@ export default {
     closeSelf: function () {
     closeSelf: function () {
       this.$emit('close', null);
       this.$emit('close', null);
     },
     },
-    ifProjectIsOpened: function (fun, ...args) {
-      if (this.currentProject)
+    ifProjectIsOpened: function (fun, additional, ...args) {
+      if (this.currentProject && additional)
         fun.bind(fun)(...args);
         fun.bind(fun)(...args);
     },
     },
     show: function (value) {
     show: function (value) {