diff --git a/docs/source/guide/converters.md b/docs/source/guide/converters.md deleted file mode 100644 index c6990747d115..000000000000 --- a/docs/source/guide/converters.md +++ /dev/null @@ -1,230 +0,0 @@ ---- -title: Converters -type: guide -order: 705 ---- - -Label Studio Format Converter helps you to encode labels into the format of your favorite machine learning library. It takes the resulting completions as input and produces encodes that into a specified format. - -### JSON - -Running from the command line: -```bash -python backend/converter/cli.py --input examples/sentiment_analysis/completions/ --config examples/sentiment_analysis/config.xml --output tmp/output.json -``` - -Running from python: -```python -from converter import Converter - -c = Converter('examples/sentiment_analysis/config.xml') -c.convert_to_json('examples/sentiment_analysis/completions/', 'tmp/output.json') -``` - -Getting output file: `tmp/output.json` -```json -[ - { - "reviewText": "Good case, Excellent value.", - "sentiment": "Positive" - }, - { - "reviewText": "What a waste of money and time!", - "sentiment": "Negative" - }, - { - "reviewText": "The goose neck needs a little coaxing", - "sentiment": "Neutral" - } -] -``` - -### CSV - -Running from the command line: -```bash -python backend/converter/cli.py --input examples/sentiment_analysis/completions/ --config examples/sentiment_analysis/config.xml --output tmp/output.tsv --format CSV --csv-separator $'\t' -======= -#### JSON - -Running from the command line: -```bash -python backend/converter/cli.py --input examples/sentiment_analysis/completions/ --config examples/sentiment_analysis/config.xml --output tmp/output.json -``` - -Running from python: -```python -from converter import Converter - -c = Converter('examples/sentiment_analysis/config.xml') -c.convert_to_json('examples/sentiment_analysis/completions/', 'tmp/output.json') -``` - -Getting output file: `tmp/output.json` -```json -[ - { - "reviewText": "Good case, Excellent value.", - "sentiment": "Positive" - }, - { - "reviewText": "What a waste of money and time!", - "sentiment": "Negative" - }, - { - "reviewText": "The goose neck needs a little coaxing", - "sentiment": "Neutral" - } -] -``` - -Use cases: any tasks - -### CoNLL 2003 - -Running from the command line: -```bash -python backend/converter/cli.py --input examples/named_entity/completions/ --config examples/named_entity/config.xml --output tmp/output.conll --format CONLL2003 -``` - -Running from python: -```python -from converter import Converter - -c = Converter('examples/named_entity/config.xml') -c.convert_to_conll2003('examples/named_entity/completions/', 'tmp/output.conll') -``` - -Getting output file `tmp/output.conll` -```text --DOCSTART- -X- O -Showers -X- _ O -continued -X- _ O -throughout -X- _ O -the -X- _ O -week -X- _ O -in -X- _ O -the -X- _ O -Bahia -X- _ B-Location -cocoa -X- _ O -zone, -X- _ O -... -``` - -Use cases: text tagging - - -### COCO -Running from the command line: -```bash -python backend/converter/cli.py --input examples/image_bbox/completions/ --config examples/image_bbox/config.xml --output tmp/output.json --format COCO --image-dir tmp/images -``` - -Running from python: -```python -from converter import Converter - -c = Converter('examples/image_bbox/config.xml') -c.convert_to_coco('examples/image_bbox/completions/', 'tmp/output.conll', output_image_dir='tmp/images') -``` - -Output images can be found in `tmp/images` - -Getting the output file `tmp/output.json` -```json -{ - "images": [ - { - "width": 800, - "height": 501, - "id": 0, - "file_name": "tmp/images/62a623a0d3cef27a51d3689865e7b08a" - } - ], - "categories": [ - { - "id": 0, - "name": "Planet" - }, - { - "id": 1, - "name": "Moonwalker" - } - ], - "annotations": [ - { - "id": 0, - "image_id": 0, - "category_id": 0, - "segmentation": [], - "bbox": [ - 299, - 6, - 377, - 260 - ], - "ignore": 0, - "iscrowd": 0, - "area": 98020 - }, - { - "id": 1, - "image_id": 0, - "category_id": 1, - "segmentation": [], - "bbox": [ - 288, - 300, - 132, - 90 - ], - "ignore": 0, - "iscrowd": 0, - "area": 11880 - } - ], - "info": { - "year": 2019, - "version": "1.0", - "contributor": "Label Studio" - } -} -``` - -Use cases: image object detection - -### Pascal VOC XML - -Running from the command line: -```bash -python backend/converter/cli.py --input examples/image_bbox/completions/ --config examples/image_bbox/config.xml --output tmp/voc-annotations --format VOC --image-dir tmp/images -``` - -Running from python: -```python -from converter import Converter - -======= -c = Converter('examples/named_entity/config.xml') -c.convert_to_conll2003('examples/named_entity/completions/', 'tmp/output.conll') -``` - -Getting output file `tmp/output.conll` -```text --DOCSTART- -X- O -Showers -X- _ O -continued -X- _ O -throughout -X- _ O -the -X- _ O -week -X- _ O -in -X- _ O -the -X- _ O -Bahia -X- _ B-Location -cocoa -X- _ O -zone, -X- _ O -... -``` - -Use cases: text tagging - - diff --git a/docs/source/guide/format.md b/docs/source/guide/format.md index 0d7df77eed03..7d8d9a492a17 100644 --- a/docs/source/guide/format.md +++ b/docs/source/guide/format.md @@ -6,9 +6,9 @@ order: 504 There are two possible ways to import data to your labeling project: -1. Start Label Studio without specifying input path and then import through the web interfaces available at [http://127.0.0.1:8200/import](here) + - Start Label Studio without specifying input path and then import through the web interfaces available at [http://127.0.0.1:8200/import](here) -2. Initialize Label Studio project by directly specifying the paths, e.g. `label-studio init --input-path my_tasks.json --input-format json` + - Initialize Label Studio project by directly specifying the paths, e.g. `label-studio init --input-path my_tasks.json --input-format json` The `--input-path` argument points to a file or a directory where your labeling tasks reside. By default it expects [JSON-formatted tasks](config.html#JSON-file), but you can also specify all other formats listed bellow by using `--input-format` option. @@ -37,6 +37,19 @@ Depending on the object tag type, field values are interpreted differently: - ``: value is taken as a valid URL to an audio file with CORS policy enabled on the server side - ``: is a valid URL to an image file +#### Predefined completions and predictions + +In case you want to import predefined completions and/or predictions for labeling (e.g. after being exported from another Label Studio's project in [JSON format](#Export-data)), +use the following high level task structure +```json +{ + "data": {"my_key": "my_value_1"}, + "completions": [...], + "predictions": [...] +} +``` +where `"completions"` and `"predictions"` are taken from [raw completion format](#Completion-format) + ### Directory with JSON files @@ -99,9 +112,70 @@ label-studio init --input-path=my/audios/dir --input-format=audio-dir You can point to a local directory, which is scanned recursively for image files. Each file is used to create one task. Supported formats are `.wav, .aiff, .mp3, .au, .flac` - + + ## Export data +Your annotation results are stored in [raw completion format](#Completion-format) inside `my_project_name/completions` directory, one file per labeled task named as `task_id.json`. + +You can optionally convert and export raw completions to more common format by doing one of the following: + +- From [/export](http://localhost:8200/export) page by choosing target format; +- Applying [converter tool](https://github.com/heartexlabs/label-studio-converter) to `my_project_name/completions` directory + + +Several _Export formats_ are supported: + +### JSON + +List of items in [raw completion format](#Completion-format) stored in JSON file + +### JSON_MIN + +List of items where only `"from_name", "to_name"` values from [raw completion format](#Completion-format) are kept: + +```json +{ + "image": "https://htx-misc.s3.amazonaws.com/opensource/label-studio/examples/images/nick-owuor-astro-nic-visuals-wDifg5xc9Z4-unsplash.jpg", + "tag": [{ + "height": 10.458911419423693, + "rectanglelabels": [ + "Moonwalker" + ], + "rotation": 0, + "width": 12.4, + "x": 50.8, + "y": 5.869797225186766 + }] +} +``` + +### CSV + +Results are stored in comma-separated tabular file with column names specified by `"from_name"` `"to_name"` values + +### TSV + +Results are stored in tab-separated tabular file with column names specified by `"from_name"` `"to_name"` values + + +### CONLL2003 + +Popular format used for [CoNLL-2003 named entity recognition challenge](https://www.clips.uantwerpen.be/conll2003/ner/) + + +### COCO + +Popular machine learning format used by [COCO dataset](http://cocodataset.org/#home) for object detection and image segmentation tasks + + +### Pascal VOC XML + +Popular XML-formatted task data used for object detection and image segmentation tasks + + +## Completion format + The output data is stored in _completions_ - JSON formatted files, one per each completed task saved in project directory in `completions` folder or in the [`"output_dir"` option](config.html#output_dir) The example structure of _completion_ is the following: ```json @@ -182,13 +256,10 @@ The output data is stored in _completions_ - JSON formatted files, one per each } ] } - ], - "task_path": "../examples/image_bbox/tasks.json" + ] } ``` -For popular machine learning libraries, there is a converter code to transform Label Studio format into an ML library format. [Learn More](/guide/converters.html) about it. - ### completions That's where the list of labeling results per one task is stored. @@ -236,7 +307,3 @@ Task identifier ### predictions Machine learning predictions (aka _pre-labeling results_) - -### task_path - -Path to local file from where the current task was taken diff --git a/label_studio/project.py b/label_studio/project.py index d32fa7d5cadb..125ce3bacb5b 100644 --- a/label_studio/project.py +++ b/label_studio/project.py @@ -23,6 +23,10 @@ logger = logging.getLogger(__name__) +class ProjectNotFound(KeyError): + pass + + class Project(object): _storage = {} @@ -41,12 +45,10 @@ def __init__(self, config, name, context=None): self.context = context or {} self.tasks = None - self.load_tasks() - self.label_config_line, self.label_config_full, self.input_data_tags = None, None, None - self.load_label_config() - self.derived_input_schema, self.derived_output_schema = None, None + self.load_tasks() + self.load_label_config() self.load_derived_schemas() self.analytics = None @@ -59,8 +61,19 @@ def __init__(self, config, name, context=None): self.load_converter() def load_tasks(self): - self.tasks = json_load(self.config['input_path']) - self.tasks = {int(k): v for k, v in self.tasks.items()} + self.tasks = {} + self.derived_input_schema = set() + tasks = json_load(self.config['input_path']) + if len(tasks) == 0: + logger.warning('No tasks loaded from ' + self.config['input_path']) + return + for task_id, task in tasks.items(): + self.tasks[int(task_id)] = task + data_keys = set(task['data'].keys()) + if not self.derived_input_schema: + self.derived_input_schema = data_keys + else: + self.derived_input_schema &= data_keys print(str(len(self.tasks)) + ' tasks loaded from: ' + self.config['input_path']) def load_label_config(self): @@ -69,18 +82,11 @@ def load_label_config(self): self.input_data_tags = self.get_input_data_tags(self.label_config_line) def load_derived_schemas(self): - num_tasks_loaded = len(self.tasks) - self.derived_input_schema = [] + self.derived_output_schema = { 'from_name_to_name_type': set(), 'labels': defaultdict(set) } - if num_tasks_loaded > 0: - for tag in self.input_data_tags: - self.derived_input_schema.append({ - 'type': tag.tag, - 'value': tag.attrib['value'].lstrip('$') - }) # for all already completed tasks we update derived output schema for further label config validation for task_id in self.get_task_ids(): @@ -94,6 +100,7 @@ def load_analytics(self): collect_analytics = os.getenv('collect_analytics') if collect_analytics is None: collect_analytics = self.config.get('collect_analytics', True) + collect_analytics = bool(collect_analytics) self.analytics = Analytics(self.label_config_line, collect_analytics, self.name, self.context) def load_project_ml_backend(self): @@ -180,10 +187,9 @@ def validate_label_config_on_derived_input_schema(self, config_string_or_parsed_ :param config_string_or_parsed_config: label config string or parsed config object :return: True if config match already imported tasks """ - input_schema = self.derived_input_schema # check if schema exists, i.e. at least one task has been uploaded - if not input_schema: + if not self.derived_input_schema: return config = config_string_or_parsed_config @@ -195,24 +201,13 @@ def validate_label_config_on_derived_input_schema(self, config_string_or_parsed_ input_types.add(input_item['type']) input_values.add(input_item['value']) - input_schema_types = set([item['type'] for item in input_schema]) - input_schema_values = set([item['value'] for item in input_schema]) - - # check input data types: they must be in schema - for item in input_types: - if item not in input_schema_types: - raise ValidationError( - 'You have already imported tasks and they are incompatible with a new config. ' - 'Can\'t find type "{item}" among already imported tasks with types {input_schema_types}' - .format(item=item, input_schema_types=list(input_schema_types))) - # check input data values: they must be in schema for item in input_values: - if item not in input_schema_values: + if item not in self.derived_input_schema: raise ValidationError( 'You have already imported tasks and they are incompatible with a new config. ' - 'Can\t find key "{item}" among already imported tasks with keys {input_schema_values}' - .format(item=item, input_schema_values=list(input_schema_types))) + 'You\'ve specified value=${item}, but imported tasks contain only keys: {input_schema_values}' + .format(item=item, input_schema_values=list(self.derived_input_schema))) def validate_label_config_on_derived_output_schema(self, config_string_or_parsed_config): """ @@ -274,18 +269,25 @@ def delete_tasks(self): with io.open(self.config['input_path'], mode='w') as f: json.dump({}, f) + # delete everything on ML backend + self.ml_backend.clear(self) + # reload everything related to tasks self.load_tasks() self.load_derived_schemas() - def iter_tasks(self): + def next_task(self, completed_tasks_ids): + completed_tasks_ids = set(completed_tasks_ids) sampling = self.config.get('sampling', 'sequential') if sampling == 'sequential': - return self.tasks.items() + actual_tasks = (self.tasks[task_id] for task_id in self.tasks if task_id not in completed_tasks_ids) + return next(actual_tasks, None) elif sampling == 'uniform': - keys = list(self.tasks.keys()) - random.shuffle(keys) - return ((k, self.tasks[k]) for k in keys) + actual_tasks_ids = [task_id for task_id in self.tasks if task_id not in completed_tasks_ids] + if not actual_tasks_ids: + return None + random.shuffle(actual_tasks_ids) + return self.tasks[actual_tasks_ids[0]] else: raise NotImplementedError('Unknown sampling method ' + sampling) @@ -345,6 +347,9 @@ def get_task_with_completions(self, task_id): except ValueError: return None + if 'completions' in self.tasks[task_id]: + return self.tasks[task_id] + filename = os.path.join(self.config['output_dir'], str(task_id) + '.json') if os.path.exists(filename): @@ -453,6 +458,8 @@ def already_exists_error(what, path): path=path, what=what )) + input_path = args.input_path or config.get('input_path') + # save label config config_xml = 'config.xml' config_xml_path = os.path.join(dir, config_xml) @@ -463,15 +470,21 @@ def already_exists_error(what, path): else: if os.path.exists(config_xml_path) and not args.force: already_exists_error('label config', config_xml_path) - default_label_config = find_file('examples/image_polygons/config.xml') - copy2(default_label_config, config_xml_path) - print(default_label_config + ' label config copied to ' + config_xml_path) + if not input_path: + # create default config with polygons only if input data is not set + default_label_config = find_file('examples/image_polygons/config.xml') + copy2(default_label_config, config_xml_path) + print(default_label_config + ' label config copied to ' + config_xml_path) + else: + with io.open(config_xml_path, mode='w') as fout: + fout.write('') + print('Empty config has been created in ' + config_xml_path) + config['label_config'] = config_xml # save tasks.json tasks_json = 'tasks.json' tasks_json_path = os.path.join(dir, tasks_json) - input_path = args.input_path or config.get('input_path') if input_path: tasks = cls._load_tasks(input_path, args, config_xml_path) with io.open(tasks_json_path, mode='w') as fout: @@ -571,7 +584,7 @@ def get(cls, project_name, args, context): cls._storage[project_name] = project return project - raise KeyError('Project {p} doesn\'t exist'.format(p=project_name)) + raise ProjectNotFound('Project {p} doesn\'t exist'.format(p=project_name)) @classmethod def create(cls, project_name, args, context): @@ -586,7 +599,7 @@ def get_or_create(cls, project_name, args, context): try: project = cls.get(project_name, args, context) logger.info('Get project "' + project_name + '".') - except KeyError: + except ProjectNotFound: project = cls.create(project_name, args, context) logger.info('Project "' + project_name + '" created.') return project diff --git a/label_studio/server.py b/label_studio/server.py index 37a8dd9cac4a..91390395e871 100644 --- a/label_studio/server.py +++ b/label_studio/server.py @@ -3,7 +3,6 @@ import time import shutil import flask -import hashlib import logging import pandas as pd @@ -128,7 +127,7 @@ def labeling_page(): task_data = project.get_task_with_completions(task_id) or project.get_task(task_id) if project.ml_backend: task_data = deepcopy(task_data) - task_data['predictions'] = project.ml_backend.make_predictions(task_data, project) + task_data['predictions'] = project.ml_backend.make_predictions(task_data, project.project_obj) project.analytics.send(getframeinfo(currentframe()).function) return flask.render_template( @@ -404,7 +403,7 @@ class DjangoRequest: # tasks are all in one file, append it path = project.config['input_path'] old_tasks = json.load(open(path)) - max_id_in_old_tasks = max(old_tasks.keys()) if old_tasks else -1 + max_id_in_old_tasks = int(max(old_tasks.keys())) if old_tasks else -1 new_tasks = Tasks().from_list_of_dicts(new_tasks, max_id_in_old_tasks + 1) old_tasks.update(new_tasks) @@ -447,22 +446,21 @@ def api_export(): def api_generate_next_task(): """ Generate next task to label """ - # try to find task is not presented in completions project = project_get_or_create() - completions = project.get_completions_ids() - for task_id, task in project.iter_tasks(): - if task_id not in completions: - log.info(msg='New task for labeling', extra=task) - project.analytics.send(getframeinfo(currentframe()).function) - # try to use ml backend for predictions - if project.ml_backend: - task = deepcopy(task) - task['predictions'] = project.ml_backend.make_predictions(task, project.project_obj) - return make_response(jsonify(task), 200) - - # no tasks found - project.analytics.send(getframeinfo(currentframe()).function, error=404) - return make_response('', 404) + # try to find task is not presented in completions + completed_tasks_ids = project.get_completions_ids() + task = project.next_task(completed_tasks_ids) + if not task: + # no tasks found + project.analytics.send(getframeinfo(currentframe()).function, error=404) + return make_response('', 404) + + project.analytics.send(getframeinfo(currentframe()).function) + # try to use ml backend for predictions + if project.ml_backend: + task = deepcopy(task) + task['predictions'] = project.ml_backend.make_predictions(task, project.project_obj) + return make_response(jsonify(task), 200) @app.route('/api/project/', methods=['POST', 'GET']) diff --git a/label_studio/static/js/lsb.js b/label_studio/static/js/lsb.js index 7b8a4be833ac..b6730bd44836 100644 --- a/label_studio/static/js/lsb.js +++ b/label_studio/static/js/lsb.js @@ -115,9 +115,16 @@ const loadNext = function(ls) { ls.resetState(); ls.assignTask(response); ls.initializeStore(_convertTask(response)); + let cs = ls.completionStore; + let c; + if (cs.predictions.length > 0) { + c = ls.completionStore.addCompletionFromPrediction(cs.predictions[0]); + } + else { + c = ls.completionStore.addCompletion({ userGenerate: true }); + } - const c = ls.completionStore.addCompletion({ userGenerate: true }); - ls.completionStore.selectCompletion(c.id); + cs.selectCompletion(c.id); ls.setFlags({ isLoading: false }); @@ -170,13 +177,13 @@ const LSB = function(elid, config, task) { user: { pk: 1, firstName: "Awesome", lastName: "User" }, task: _convertTask(task), - interfaces: [ "basic", "panel", // undo, redo, reset panel "controls", // all control buttons: skip, submit, update "submit", // submit button on controls "update", // update button on controls + "predictions", "predictions:menu", // right menu with prediction items "completions:menu", // right menu with completion items "completions:add-new", diff --git a/label_studio/tasks.py b/label_studio/tasks.py index 49ca5980707d..02b8c463e4ea 100644 --- a/label_studio/tasks.py +++ b/label_studio/tasks.py @@ -32,13 +32,16 @@ def _create_task_with_local_uri(self, filepath, data_key, task_id): def from_dict(self, d, task_id=0): task = {} - data = d['data'] if 'data' in d else d - task[task_id] = {'id': task_id, 'data': data} - if 'predictions' in data: - task[task_id]['predictions'] = data['predictions'] - task[task_id]['data'].pop('predictions', None) - if 'predictions' in d: - task[task_id]['predictions'] = d['predictions'] + if 'data' in d and isinstance(d['data'], dict): + # if "data" key is presented, task is considered underneath + task[task_id] = {'id': task_id, 'data': d['data']} + if 'completions' in d: + task[task_id]['completions'] = d['completions'] + if 'predictions' in d: + task[task_id]['predictions'] = d['predictions'] + else: + # all input dict is considered as task data, no completions/predictions is expected + task[task_id] = {'id': task_id, 'data': d} return task def from_list_of_dicts(self, l, start_task_id=0): diff --git a/label_studio/utils/models.py b/label_studio/utils/models.py index cd23e6c8faba..b7558085e1ed 100644 --- a/label_studio/utils/models.py +++ b/label_studio/utils/models.py @@ -351,7 +351,7 @@ def setup(self, project): """ return self._post('setup', request={ 'project': self._create_project_uid(project), - 'schema': project.ml_backend_active_connection.schema + 'schema': project.schema }) def delete(self, project): @@ -445,6 +445,14 @@ def _api_exists(self): def make_predictions(self, task, project): if self._api_exists(): + r = self.api.setup(project) + if not r.is_error: + model_version = r.response['model_version'] + if self.model_version != model_version: + self.model_version = model_version + logger.debug('Model version has changed: ' + model_version) + else: + logger.debug('Model version hasn\'t changed: ' + model_version) response = self.api.predict([task], self.model_version, project) if response.is_error: if response.status_code == 404: @@ -480,3 +488,11 @@ def get_schema(self, label_config, project): logger.warning('ML backend returns multiple schemas for label config ' + label_config + ': ' + schema + '\nWe currently support only one schema, so 0th schema is used.') return schema[0] + + def clear(self, project): + if self._api_exists(): + response = self.api.delete(project) + if response.is_error: + logger.error('Can\'t clear ML backend for project ' + project.name + ': ' + response.error_message) + else: + logger.info('ML backend for project ' + project.name + ' has been cleared.') diff --git a/label_studio/utils/schema/label_config_schema.json b/label_studio/utils/schema/label_config_schema.json index 6cbe6128058d..1b671b339665 100644 --- a/label_studio/utils/schema/label_config_schema.json +++ b/label_studio/utils/schema/label_config_schema.json @@ -102,10 +102,11 @@ "$ref": "#/definitions/$" }, "Label": { - "type": "array", - "items": { - "$ref": "#/definitions/Label" - } + "anyOf": [{ + "type": "array", + "items": { + "$ref": "#/definitions/Label" + }}, {"$ref": "#/definitions/Label"}] } } } @@ -128,6 +129,30 @@ "items": {"$ref": "#/definitions/Labels"} }, {"$ref": "#/definitions/Labels"}] }, + "RectangleLabels": { + "anyOf": [{ + "type": "array", + "items": {"$ref": "#/definitions/Labels"} + }, {"$ref": "#/definitions/Labels"}] + }, + "PolygonLabels": { + "anyOf": [{ + "type": "array", + "items": {"$ref": "#/definitions/Labels"} + }, {"$ref": "#/definitions/Labels"}] + }, + "KeypointLabels": { + "anyOf": [{ + "type": "array", + "items": {"$ref": "#/definitions/Labels"} + }, {"$ref": "#/definitions/Labels"}] + }, + "HyperTextLabels": { + "anyOf": [{ + "type": "array", + "items": {"$ref": "#/definitions/Labels"} + }, {"$ref": "#/definitions/Labels"}] + }, "Image": {"$ref": "#/definitions/tags_with_value_required_name"}, "Text": {"$ref": "#/definitions/tags_with_value_required_name"}, "HyperText": {"$ref": "#/definitions/tags_with_value_required_name"} diff --git a/label_studio/utils/uploader.py b/label_studio/utils/uploader.py index 442667c3935d..1203a171bdd8 100644 --- a/label_studio/utils/uploader.py +++ b/label_studio/utils/uploader.py @@ -92,7 +92,7 @@ def check_max_task_number(tasks): if len(tasks) > settings.TASKS_MAX_NUMBER: raise ValidationError('Maximum task number is {TASKS_MAX_NUMBER}, ' 'current task number is {num_tasks}' - .format(TASKS_MAX_NUMBER=settings.TASKS_MAX_FILE_SIZE, num_tasks=len(tasks))) + .format(TASKS_MAX_NUMBER=settings.TASKS_MAX_NUMBER, num_tasks=len(tasks))) def check_file_sizes_and_number(files): diff --git a/label_studio/utils/validation.py b/label_studio/utils/validation.py index 699336b91dc5..010a19d36326 100644 --- a/label_studio/utils/validation.py +++ b/label_studio/utils/validation.py @@ -37,7 +37,6 @@ def __init__(self, project): def check_data(project, data): """ Validate data from task['data'] """ - if data is None: raise ValidationError('Task is empty (None)') @@ -92,7 +91,7 @@ def project(self): @staticmethod def check_allowed(task): - allowed = ['data', 'completions', 'predictions', 'meta'] + allowed = ['data', 'completions', 'predictions', 'meta', 'id'] # check each task filled for key in task.keys(): diff --git a/requirements.txt b/requirements.txt index 29cd3016a8e1..aa4a64f02c7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,4 +32,4 @@ flask_api>=2.0 pandas>=0.24.0 jsonschema>=3.2.0 xmljson==0.2.0 -label-studio-converter>=0.0.7 +label-studio-converter>=0.0.9 diff --git a/setup.py b/setup.py index 318746efd28d..54f0e9ca763f 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ import setuptools # Package version -version = '0.4.5' +version = '0.4.6' # Readme with open('README.md', 'r') as f: