diff --git a/app/Http/Controllers/Api/ImageAnnotationController.php b/app/Http/Controllers/Api/ImageAnnotationController.php index a8b435159..d8a96c9ee 100644 --- a/app/Http/Controllers/Api/ImageAnnotationController.php +++ b/app/Http/Controllers/Api/ImageAnnotationController.php @@ -86,6 +86,13 @@ public function index(Request $request, $id) } else { $yieldAnnotations = function () use ($image, $load): Generator { foreach ($image->annotations()->with($load)->lazy() as $annotation) { + // Delete saved unlabeled LabelBOT annotations + // Such annotations are only created when LabelBOT returns no results + // and the user refreshes/closes the BIIGLE session without interacting with the empty LabelBOT popup. + if ($annotation->labels->isEmpty()) { + $annotation->delete(); + continue; + } yield $annotation; } }; @@ -236,15 +243,19 @@ public function store(StoreImageAnnotation $request, LabelBotService $labelBotSe } } - $this->authorize('attach-label', [$annotation, $label]); + if ($label) { + $this->authorize('attach-label', [$annotation, $label]); + } DB::transaction(function () use ($annotation, $request, $label) { $annotation->save(); - $annotationLabel = new ImageAnnotationLabel; - $annotationLabel->label_id = $label->id; - $annotationLabel->user_id = $request->user()->id; - $annotationLabel->confidence = $request->input('confidence'); - $annotation->labels()->save($annotationLabel); + if ($label) { + $annotationLabel = new ImageAnnotationLabel; + $annotationLabel->label_id = $label->id; + $annotationLabel->user_id = $request->user()->id; + $annotationLabel->confidence = $request->input('confidence'); + $annotation->labels()->save($annotationLabel); + } }); $annotation->load('labels.label', 'labels.user'); diff --git a/app/Http/Controllers/Api/VideoAnnotationController.php b/app/Http/Controllers/Api/VideoAnnotationController.php index 76a4c260f..de9a87642 100644 --- a/app/Http/Controllers/Api/VideoAnnotationController.php +++ b/app/Http/Controllers/Api/VideoAnnotationController.php @@ -241,15 +241,19 @@ public function store(StoreVideoAnnotation $request, LabelBotService $labelBotSe } } - $this->authorize('attach-label', [$annotation, $label]); + if ($label) { + $this->authorize('attach-label', [$annotation, $label]); + } $annotation = DB::transaction(function () use ($annotation, $request, $label) { $annotation->save(); - VideoAnnotationLabel::create([ - 'label_id' => $label->id, - 'user_id' => $request->user()->id, - 'annotation_id' => $annotation->id, - ]); + if ($label) { + VideoAnnotationLabel::create([ + 'label_id' => $label->id, + 'user_id' => $request->user()->id, + 'annotation_id' => $annotation->id, + ]); + } return $annotation; }); diff --git a/app/Services/LabelBot/LabelBotService.php b/app/Services/LabelBot/LabelBotService.php index 804504bed..ca801d568 100644 --- a/app/Services/LabelBot/LabelBotService.php +++ b/app/Services/LabelBot/LabelBotService.php @@ -15,7 +15,6 @@ use Illuminate\Http\Request; use InvalidArgumentException; use Pgvector\Laravel\Vector; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; class LabelBotService @@ -53,7 +52,7 @@ public function getLabelsForAnnotation( } if (empty($topNLabels)) { - throw new NotFoundHttpException("LabelBOT could not find similar annotations."); + return []; } $labelModels = Label::whereIn('id', $topNLabels)->get()->keyBy('id'); diff --git a/resources/assets/js/annotations/annotatorContainer.vue b/resources/assets/js/annotations/annotatorContainer.vue index ff5596dcf..259dc3f13 100644 --- a/resources/assets/js/annotations/annotatorContainer.vue +++ b/resources/assets/js/annotations/annotatorContainer.vue @@ -449,6 +449,10 @@ export default { promise.then((annotation) => { if (imageId === this.imageId) { this.showLabelbotPopup(annotation); + } else if (!annotation.labels || annotation.labels?.length === 0) { + // We delete the unlabeled annotation if the image has been switched + // while LabelBOT is computing + this.handleDeleteAnnotation(annotation); } }); } else { @@ -495,9 +499,19 @@ export default { .then(() => { if (lastLabel) { this.handleDetachAnnotationLabel(annotation, lastLabel); + } else { + // In case LabelBOT returned no results and the user selected a label by themselves, there is no label to detach. + // In this case, we need to refresh the annotation to show the attached label. + this.refreshSingleAnnotation(annotation); } }) - .catch(handleErrorResponse); + .catch((error) => { + if (annotation.labels.length === 0) { + this.handleDeleteAnnotation(annotation); + } + + handleErrorResponse(error); + }); } }, handleForceSwapLabel(annotation, label) { @@ -544,6 +558,8 @@ export default { Promise.all(toCache).catch(function () {}); }, setLastCreatedAnnotation(annotation) { + if (annotation.labels?.length === 0) return; + if (this.lastCreatedAnnotationTimeout) { window.clearTimeout(this.lastCreatedAnnotationTimeout); } diff --git a/resources/assets/js/annotations/components/labelbotPopup.vue b/resources/assets/js/annotations/components/labelbotPopup.vue index 18fc6800f..3ddba5c4d 100644 --- a/resources/assets/js/annotations/components/labelbotPopup.vue +++ b/resources/assets/js/annotations/components/labelbotPopup.vue @@ -10,7 +10,12 @@