From 8ea38425a98b25ba513233fb671ee70802cf1752 Mon Sep 17 00:00:00 2001 From: Manraj Singh Grover Date: Fri, 17 Jul 2020 17:44:21 +0530 Subject: [PATCH] Fixes bugs in adding annotations to segmentations --- .gitignore | 2 +- backend/.vscode/settings.sample.json | 7 + backend/__init__.py | 15 +- backend/app.py | 25 ++ backend/models.py | 8 +- backend/routes/data.py | 229 +++++++----------- backend/routes/projects.py | 6 +- backend/routes/users.py | 3 - examples/upload_data/upload_data.py | 10 +- .../.vscode}/settings.sample.json | 4 - frontend/src/pages/annotate.js | 2 +- frontend/src/pages/labelValues.js | 2 +- frontend/src/pages/labels.js | 2 +- 13 files changed, 142 insertions(+), 173 deletions(-) create mode 100644 backend/.vscode/settings.sample.json rename {.vscode => frontend/.vscode}/settings.sample.json (50%) diff --git a/.gitignore b/.gitignore index 0f2c2bf..2f867f7 100644 --- a/.gitignore +++ b/.gitignore @@ -128,7 +128,7 @@ dmypy.json *.db # vscode settings -.vscode/*.json +**/.vscode/*.json # Sample files !*.sample.json diff --git a/backend/.vscode/settings.sample.json b/backend/.vscode/settings.sample.json new file mode 100644 index 0000000..cb6eccc --- /dev/null +++ b/backend/.vscode/settings.sample.json @@ -0,0 +1,7 @@ +{ + "python.venvPath": "~/.envs", + "python.pythonPath": "", + "python.formatting.provider": "black", + "editor.formatOnSave": true, + "python.linting.enabled": true +} \ No newline at end of file diff --git a/backend/__init__.py b/backend/__init__.py index 197d8d0..d879b22 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -7,16 +7,15 @@ from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy from flask_redis import FlaskRedis -from werkzeug.exceptions import HTTPException, default_exceptions from backend.config import Config -def create_app(test_config=None): +def create_app(): app = Flask(__name__, instance_relative_config=True) app.config.from_object(Config) - app.logger.info(app.config["UPLOAD_FOLDER"]) + Path(app.config["UPLOAD_FOLDER"]).mkdir(parents=True, exist_ok=True) return app @@ -31,16 +30,6 @@ def create_app(test_config=None): from backend import models - -def handle_error(error): - if isinstance(error, HTTPException): - return jsonify(message=error.description, code=error.code), error.code - return jsonify(message="An error occured", code=500), 500 - - -for exc in default_exceptions: - app.register_error_handler(exc, handle_error) - from .routes import auth, api app.register_blueprint(auth) diff --git a/backend/app.py b/backend/app.py index 11becea..c8e516f 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,3 +1,6 @@ +from flask import jsonify +from werkzeug.exceptions import HTTPException, default_exceptions + from backend import app, db @@ -11,3 +14,25 @@ def teardown_request(exception): if exception: db.session.rollback() db.session.remove() + + +@app.errorhandler(Exception) +def handle_invalid_usage(error): + app.logger.error(error) + return jsonify(message="An error occured", code=500), 500 + + +def handle_error(error): + if isinstance(error, HTTPException): + + if error.code == 500: + app.logger.error(error) + else: + app.logger.info(error) + + return jsonify(message=error.description, code=error.code), error.code + return jsonify(message="An error occured", code=500), 500 + + +for exc in default_exceptions: + app.register_error_handler(exc, handle_error) diff --git a/backend/models.py b/backend/models.py index b63bcff..710a880 100644 --- a/backend/models.py +++ b/backend/models.py @@ -86,6 +86,9 @@ class Data(db.Model): def update_marked_review(self, marked_review): self.is_marked_for_review = marked_review + def set_segmentations(self, segmentations): + self.segmentations = segmentations + def to_dict(self): return { "original_filename": self.original_filename, @@ -278,10 +281,7 @@ class Segmentation(db.Model): ) values = db.relationship( - "LabelValue", - secondary=annotation_table, - back_populates="segmentations", - single_parent=True, + "LabelValue", secondary=annotation_table, back_populates="segmentations", ) def set_start_time(self, start_time): diff --git a/backend/routes/data.py b/backend/routes/data.py index 7fb0a12..ba0c5ec 100644 --- a/backend/routes/data.py +++ b/backend/routes/data.py @@ -8,6 +8,7 @@ from flask_jwt_extended import jwt_required, get_jwt_identity from werkzeug.urls import url_parse from werkzeug.utils import secure_filename +from werkzeug.exceptions import BadRequest, NotFound, InternalServerError from backend import app, db from backend.models import Data, Project, User, Segmentation, Label, LabelValue @@ -23,19 +24,10 @@ def send_audio_file(file_name): return send_from_directory(app.config["UPLOAD_FOLDER"], file_name) -class LabelNotFoundError(Exception): - """Exception raised when label or labelvalue is not found - """ - - def __init__(self, value, mapping): - self.message = f"{value} does not exist in current mapping of {mapping}" - super().__init__(self.message) - - def validate_segmentation(segment): """Validate the segmentation before accepting the annotation's upload from users """ - required_key = {"annotations", "start_time", "end_time", "transcription"} + required_key = {"start_time", "end_time", "transcription"} if set(required_key).issubset(segment.keys()): return True @@ -44,22 +36,23 @@ def validate_segmentation(segment): def generate_segmentation( - annotations, transcription, project_id, - start_time, end_time, data_id=None, segmentation_id=None + annotations, + transcription, + project_id, + start_time, + end_time, + data_id, + segmentation_id=None, ): """Generate a Segmentation from the required segment information """ if segmentation_id is None: - # segmentation created for new data - if data_id is None: - segmentation = Segmentation( - start_time=start_time, end_time=end_time - ) - # segmetation created for existing data - else: - segmentation = Segmentation( - data_id=data_id, start_time=start_time, end_time=end_time - ) + segmentation = Segmentation( + data_id=data_id, + start_time=start_time, + end_time=end_time, + transcription=transcription, + ) else: # segmentation updated for existing data segmentation = Segmentation.query.filter_by( @@ -67,53 +60,52 @@ def generate_segmentation( ).first() segmentation.set_start_time(start_time) segmentation.set_end_time(end_time) + segmentation.set_transcription(transcription) + + db.session.add(segmentation) + db.session.flush() - segmentation.set_transcription(transcription) values = [] - if annotations: - for label_name, label_value in annotations.items(): - app.logger.info(label_name) - app.logger.info(label_value) - if isinstance(label_value, list): - label = Label.query.filter_by( - name=label_name, project_id=project_id).first() - if label is None: - raise LabelNotFoundError(value=label_name, mapping="Label") - - for _value in label_value: - value = LabelValue.query.filter_by( - value=_value, label_id=label.id - ).first() - if value is None: - raise LabelNotFoundError(value=_value, mapping="LabelValue") - values.append(value) - - elif isinstance(label_value["values"], list): - for val_id in label_value["values"]: - value = LabelValue.query.filter_by( - id=int(val_id), label_id=label_value["label_id"] - ).first() - values.append(value) - - elif isinstance(label_value["values"], dict): - label = Label.query.filter_by( - name=label_name, project_id=project_id).first() - if label is None: - raise LabelNotFoundError(value=label_name, mapping="Label") + + for label_name, val in annotations.items(): + label = Label.query.filter_by(name=label_name, project_id=project_id).first() + + if label is None: + raise NotFound(description=f"Label not found with name: `{label_name}`") + + if "values" not in val: + raise BadRequest( + description=f"Key: `values` missing in Label: `{label_name}`" + ) + + label_values = val["values"] + + if isinstance(label_values, list): + for val_id in label_values: value = LabelValue.query.filter_by( - id=int(label_value["values"]["id"]), label_id=label_value["id"] + id=int(val_id), label_id=int(label.id) ).first() + if value is None: - raise LabelNotFoundError( - value=label_value["values"]["value"], mapping="LabelValue") + raise BadRequest( + description=f"`{label_name}` does not have label value with id `{val_id}`" + ) values.append(value) - else: - value = LabelValue.query.filter_by( - id=int(label_value["values"]), label_id=label_value["label_id"] - ).first() - values.append(value) + else: + if label_values == "-1": + continue + + value = LabelValue.query.filter_by( + id=int(label_values), label_id=int(label.id) + ).first() + + if value is None: + raise BadRequest( + description=f"`{label_name}` does not have label value with id `{label_values}`" + ) + values.append(value) segmentation.values = values return segmentation @@ -122,110 +114,73 @@ def generate_segmentation( @api.route("/data", methods=["POST"]) def add_data(): api_key = request.headers.get("Authorization", None) - app.logger.info(api_key) if not api_key: - return jsonify(message="API Key missing from `Authorization` Header"), 401 + raise BadRequest(description="API Key missing from `Authorization` Header") + + project = Project.query.filter_by(api_key=api_key).first() - try: - project = Project.query.filter_by(api_key=api_key).first() - except Exception as e: - app.logger.info(e) - return jsonify(message="No project exist with given API Key"), 404 + if not project: + raise NotFound(description="No project exist with given API Key") username = request.form.get("username", None) user = User.query.filter_by(username=username).first() + if not user: - return jsonify(message="No user found with given username"), 404 + raise NotFound(description="No user found with given username") - segmentations = request.form.get("segmentations", []) + segmentations = request.form.get("segmentations", "[]") reference_transcription = request.form.get("reference_transcription", None) - is_marked_for_review = bool( - request.form.get("is_marked_for_review", False)) + is_marked_for_review = bool(request.form.get("is_marked_for_review", False)) audio_file = request.files["audio_file"] original_filename = secure_filename(audio_file.filename) extension = Path(original_filename).suffix.lower() if len(extension) > 1 and extension[1:] not in ALLOWED_EXTENSIONS: - return jsonify(message="File format is not supported"), 400 + raise BadRequest(description="File format is not supported") filename = f"{str(uuid.uuid4().hex)}{extension}" file_path = Path(app.config["UPLOAD_FOLDER"]).joinpath(filename) audio_file.save(file_path.as_posix()) - app.logger.info(filename) - try: - segmentations = json.loads(segmentations) - annotations = [] - for segment in segmentations: - validated = validate_segmentation(segment) - - if not validated: - app.logger.error(f"Error adding segmentation: {segment}") - return ( - jsonify( - message=f"Error adding data to project: {project.name}", - type="DATA_CREATION_FAILED", - ), - 400, - ) + data = Data( + project_id=project.id, + filename=filename, + original_filename=original_filename, + reference_transcription=reference_transcription, + is_marked_for_review=is_marked_for_review, + assigned_user_id=user.id, + ) + db.session.add(data) + db.session.flush() + + segmentations = json.loads(segmentations) + + new_segmentations = [] + + for segment in segmentations: + validated = validate_segmentation(segment) - annotations.append(generate_segmentation( - data_id=None, - project_id=project.id, - end_time=segment['end_time'], - start_time=segment['start_time'], - annotations=segment['annotations'], - transcription=segment['transcription'], - )) + if not validated: + raise BadRequest(description=f"Segmentations have missing keys.") - data = Data( + new_segment = generate_segmentation( + data_id=data.id, project_id=project.id, - filename=filename, - segmentations=annotations, - original_filename=original_filename, - reference_transcription=reference_transcription, - is_marked_for_review=is_marked_for_review, - assigned_user_id=user.id, + end_time=segment["end_time"], + start_time=segment["start_time"], + annotations=segment.get("annotations", {}), + transcription=segment["transcription"], ) - db.session.add(data) - db.session.commit() - db.session.refresh(data) - - except AttributeError as e: - app.logger.error( - f"Error parsing segmentations, please make sure segmentations are passed as a list", e) - return ( - jsonify( - message=f"Error adding data to project: {project.name}", - type="DATA_CREATION_FAILED", - ), - 400, - ) + new_segmentations.append(new_segment) - except LabelNotFoundError as e: - app.logger.error(e) - return ( - jsonify( - message=f"Error adding data to project: {project.name}", - type="DATA_CREATION_FAILED", - ), - 400, - ) + data.set_segmentations(new_segmentations) - except Exception as e: - app.logger.error(f"Error adding data to project: {project.name}") - app.logger.error(e) - return ( - jsonify( - message=f"Error adding data to project: {project.name}", - type="DATA_CREATION_FAILED", - ), - 500, - ) + db.session.commit() + db.session.refresh(data) return ( jsonify( diff --git a/backend/routes/projects.py b/backend/routes/projects.py index 439b625..f89332a 100644 --- a/backend/routes/projects.py +++ b/backend/routes/projects.py @@ -161,8 +161,6 @@ def update_project_users(project_id): 400, ) - app.logger.info(users) - try: project = Project.query.get(project_id) # TODO: Decide whether to give creator of project access @@ -551,8 +549,6 @@ def add_segmentations(project_id, data_id, segmentation_id=None): start_time = round(start_time, 4) end_time = round(end_time, 4) - app.logger.info(annotations) - try: request_user = User.query.filter_by(username=identity["username"]).first() project = Project.query.get(project_id) @@ -572,7 +568,7 @@ def add_segmentations(project_id, data_id, segmentation_id=None): start_time=start_time, annotations=annotations, transcription=transcription, - segmentation_id=segmentation_id + segmentation_id=segmentation_id, ) db.session.add(segmentation) diff --git a/backend/routes/users.py b/backend/routes/users.py index c6167fe..106eb70 100644 --- a/backend/routes/users.py +++ b/backend/routes/users.py @@ -136,9 +136,6 @@ def update_user(user_id): app.logger.error(e) return jsonify(message="No user found!"), 404 - app.logger.info(user.username) - app.logger.info(user.role.role) - app.logger.info(user.role.id) return ( jsonify( username=user.username, diff --git a/examples/upload_data/upload_data.py b/examples/upload_data/upload_data.py index aa24b6d..ff7f07f 100644 --- a/examples/upload_data/upload_data.py +++ b/examples/upload_data/upload_data.py @@ -67,12 +67,16 @@ "segmentations": segmentations, "is_marked_for_review": is_marked_for_review, } + print("Creating datapoint") response = requests.post( f"http://{args.host}:{args.port}/api/data", files=file, data=values, headers=headers ) -if response.status_code == 200: - print("Datapoint created!") + +if response.status_code == 201: + response_json = response.json() + print(f"Message: {response_json['message']}") else: + print(f"Error Code: {response.status_code}") response_json = response.json() - print(response_json["message"]) + print(f"Message: {response_json['message']}") diff --git a/.vscode/settings.sample.json b/frontend/.vscode/settings.sample.json similarity index 50% rename from .vscode/settings.sample.json rename to frontend/.vscode/settings.sample.json index 0aca964..408ae75 100644 --- a/.vscode/settings.sample.json +++ b/frontend/.vscode/settings.sample.json @@ -1,9 +1,5 @@ { - "python.venvPath": "~/.envs", - "python.pythonPath": "", - "python.formatting.provider": "black", "editor.formatOnSave": true, - "python.linting.enabled": true, "eslint.autoFixOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true diff --git a/frontend/src/pages/annotate.js b/frontend/src/pages/annotate.js index f529fcb..b887110 100644 --- a/frontend/src/pages/annotate.js +++ b/frontend/src/pages/annotate.js @@ -510,7 +510,7 @@ class Annotate extends React.Component { selectedSegment.data.annotations && selectedSegment.data.annotations[key] && selectedSegment.data.annotations[key][ - "values" + "values" ]) || (value["type"] === "multiselect" ? [] : "") } diff --git a/frontend/src/pages/labelValues.js b/frontend/src/pages/labelValues.js index 98e3965..bbed176 100644 --- a/frontend/src/pages/labelValues.js +++ b/frontend/src/pages/labelValues.js @@ -125,7 +125,7 @@ class LabelValues extends React.Component { # - LabelValueId + Label Value Id Value Created On Options diff --git a/frontend/src/pages/labels.js b/frontend/src/pages/labels.js index e471d4a..8cc1f64 100644 --- a/frontend/src/pages/labels.js +++ b/frontend/src/pages/labels.js @@ -133,7 +133,7 @@ class Labels extends React.Component { # - LabelId + Label Id Name Type Created On