diff --git a/app/Http/Controllers/Api/ImageAnnotationController.php b/app/Http/Controllers/Api/ImageAnnotationController.php
index 94caebec5b..b77dbd16af 100644
--- a/app/Http/Controllers/Api/ImageAnnotationController.php
+++ b/app/Http/Controllers/Api/ImageAnnotationController.php
@@ -204,7 +204,7 @@ public function show($id)
* }
*
* @param StoreImageAnnotation $request
- * @return ImageAnnotation
+ * @return ImageAnnotation|\Illuminate\Http\Response
*/
public function store(StoreImageAnnotation $request, LabelBotService $labelBotService)
{
@@ -223,6 +223,9 @@ public function store(StoreImageAnnotation $request, LabelBotService $labelBotSe
// Add labelBOTlabels attribute to the response.
$annotation->append('labelBOTLabels');
$label = array_shift($labels);
+ if (is_null($label)) {
+ return response('', 204);
+ }
if (!empty($labels)) {
// Attach the remaining labels (if any).
$annotation->labelBOTLabels = $labels;
diff --git a/app/Http/Controllers/Api/VideoAnnotationController.php b/app/Http/Controllers/Api/VideoAnnotationController.php
index f3a6a71aee..416947bba5 100644
--- a/app/Http/Controllers/Api/VideoAnnotationController.php
+++ b/app/Http/Controllers/Api/VideoAnnotationController.php
@@ -198,7 +198,7 @@ public function show($id)
* }
*
* @param StoreVideoAnnotation $request
- * @return VideoAnnotation
+ * @return VideoAnnotation|\Illuminate\Http\Response
*/
public function store(StoreVideoAnnotation $request, LabelBotService $labelBotService)
{
@@ -227,6 +227,9 @@ public function store(StoreVideoAnnotation $request, LabelBotService $labelBotSe
// Add labelBOTlabels attribute to the response.
$annotation->append('labelBOTLabels');
$label = array_shift($labels);
+ if (is_null($label)) {
+ return response('', 204);
+ }
if (!empty($labels)) {
// Attach the remaining labels (if any).
$annotation->labelBOTLabels = $labels;
diff --git a/app/Services/LabelBot/LabelBotService.php b/app/Services/LabelBot/LabelBotService.php
index 804504bed6..ca801d5682 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 04aa8e7a03..81144c5cb2 100644
--- a/resources/assets/js/annotations/annotatorContainer.vue
+++ b/resources/assets/js/annotations/annotatorContainer.vue
@@ -439,8 +439,11 @@ export default {
annotation.confidence = 1;
let promise;
-
- if (this.labelbotIsActive) {
+ let pendingAnnotation = false;
+ // We check for label_id in case LabelBOT returns no results
+ // and the user input a label in the typeahead, so we skip this step
+ // and save the annotation with its label.
+ if (!annotation.label_id && this.labelbotIsActive) {
let imageId = this.imageId;
promise = this.saveLabelbotAnnotation(
annotation,
@@ -449,18 +452,26 @@ export default {
promise.then((annotation) => {
if (imageId === this.imageId) {
+ pendingAnnotation = annotation.labels?.length === 0
this.showLabelbotPopup(annotation);
}
});
} else {
- annotation.label_id = this.selectedLabel.id;
+ if (!annotation.label_id) {
+ annotation.label_id = this.selectedLabel.id;
+ }
promise = AnnotationsStore.create(this.imageId, annotation);
}
promise.then(this.setLastCreatedAnnotation)
.catch(handleErrorResponse)
- // Remove the temporary annotation if saving succeeded or failed.
- .finally(removeCallback);
+ // Remove the temporary annotation if saving succeeded or failed,
+ // if it's not a pending annotation
+ .finally(() => {
+ if (!pendingAnnotation) {
+ removeCallback();
+ }
+ });
},
handleAttachLabel(annotation, label) {
label = label || this.selectedLabel;
@@ -545,6 +556,9 @@ export default {
Promise.all(toCache).catch(function () {});
},
setLastCreatedAnnotation(annotation) {
+ if (!annotation.id) {
+ return;
+ }
if (this.lastCreatedAnnotationTimeout) {
window.clearTimeout(this.lastCreatedAnnotationTimeout);
}
diff --git a/resources/assets/js/annotations/components/annotationCanvas.vue b/resources/assets/js/annotations/components/annotationCanvas.vue
index b3d5d3ccbd..953363bfcc 100644
--- a/resources/assets/js/annotations/components/annotationCanvas.vue
+++ b/resources/assets/js/annotations/components/annotationCanvas.vue
@@ -590,6 +590,8 @@ export default {
e.feature.set('color', '5bc0de');
e.feature.setStyle(Styles.editing);
+ newAnnotation.feature = e.feature, // we need this temporarily if LabelBOT returns no results
+
// Move feature to the LabelBOT layer so it has opacity=1 while LabelBOT
// is computing.
this.labelbotSource.addFeature(e.feature);
diff --git a/resources/assets/js/annotations/components/annotationCanvas/labelBot.vue b/resources/assets/js/annotations/components/annotationCanvas/labelBot.vue
index 8a8b1bb765..e454a67187 100644
--- a/resources/assets/js/annotations/components/annotationCanvas/labelBot.vue
+++ b/resources/assets/js/annotations/components/annotationCanvas/labelBot.vue
@@ -16,6 +16,8 @@ export default {
'change-labelbot-focused-popup',
'close-labelbot-popup',
'swap',
+ 'new',
+ 'delete-pending',
],
props: {
labelbotState: {
@@ -55,6 +57,9 @@ export default {
},
},
methods: {
+ createNewLabelBOTAnnotation(event) {
+ this.$emit('new', event.newAnnotation);
+ },
updateLabelbotLabel(event) {
this.$emit('swap', event.annotation, event.label);
},
@@ -67,6 +72,9 @@ export default {
handleDeleteLabelbotAnnotation(annotation) {
this.$emit('delete', [annotation]);
},
+ handleDeleteLabelbotPendingAnnotation(annotation) {
+ this.$emit('delete-pending', annotation);
+ },
getBoundingBox(imageWidth, imageHeight, points) {
let minX = imageWidth;
let minY = imageHeight;
diff --git a/resources/assets/js/annotations/components/labelbotPopup.vue b/resources/assets/js/annotations/components/labelbotPopup.vue
index 18fc6800f3..6066235e6d 100644
--- a/resources/assets/js/annotations/components/labelbotPopup.vue
+++ b/resources/assets/js/annotations/components/labelbotPopup.vue
@@ -10,7 +10,12 @@
-
+ - 0) {
+ return [this.annotation.labels[0].label].concat(this.annotation.labelBOTLabels);
+ } else {
+ return [];
+ }
+ },
+ noLabels() {
+ return this.labels.length === 0;
},
classObject() {
return {
@@ -157,7 +172,7 @@ export default {
};
},
popupKey() {
- return this.annotation.id;
+ return this.annotation.id ?? this.annotation.feature.ol_uid;
},
hasProgressBar() {
return this.isFocused && this.shouldHaveProgressBar;
@@ -182,6 +197,27 @@ export default {
},
},
methods: {
+ createAndClose(label) {
+ this.pendingAnnotation = false;
+
+ const annotation = { ...this.annotation };
+ annotation.label_id = label.id;
+ const feature = annotation.feature;
+
+ let removeCallback = () => {
+ try {
+ this.$parent.labelbotSource.removeFeature(feature);
+ } catch (e) {
+ // ignore
+ }
+ };
+
+ delete annotation.feature;
+ delete annotation.labels;
+
+ this.$emit('new', { newAnnotation: annotation, rmvCallback: removeCallback});
+ this.emitClose();
+ },
updateAndClose(label) {
// Top 1 label is already attached/selected
if (this.selectedLabel.id !== label.id) {
@@ -223,6 +259,13 @@ export default {
handleEsc() {
if (!this.isFocused) return;
+ if (this.noLabels) {
+ this.$parent.labelbotSource.removeFeature(this.annotation.feature);
+ if (this.annotation.pendingAnnotation) {
+ this.$emit('delete-pending', this.annotation.pendingAnnotation)
+ }
+ }
+
if (this.shouldHaveProgressBar) {
this.shouldHaveProgressBar = false;
} else {
@@ -250,7 +293,14 @@ export default {
deleteLabelAnnotation() {
if (!this.isFocused) return;
- this.$emit('delete', this.annotation);
+ if (this.labels.length > 0) {
+ this.$emit('delete', this.annotation);
+ } else {
+ this.$parent.labelbotSource.removeFeature(this.annotation.feature);
+ if (this.annotation.pendingAnnotation) {
+ this.$emit('delete-pending', this.annotation.pendingAnnotation);
+ }
+ }
this.emitClose();
Events.emit('labelbot.dismissed');
},
@@ -294,7 +344,13 @@ export default {
},
createOverlay(annotationCanvas) {
- const annotationFeature = annotationCanvas.annotationSource.getFeatureById(this.annotation.id);
+ let annotationFeature;
+ if (this.noLabels) {
+ annotationFeature = this.annotation.feature;
+ this.pendingAnnotation = true;
+ } else {
+ annotationFeature = annotationCanvas.annotationSource.getFeatureById(this.annotation.id);
+ }
const annotationGeometry = annotationFeature.getGeometry();
const annotationExtent = annotationGeometry.getExtent();
let popupPosition = [
@@ -344,7 +400,11 @@ export default {
const line = new LineString([popupPosition, popupPosition]);
this.lineFeature = markRaw(new Feature(line));
this.lineFeature.set('unselectable', true);
- this.lineFeature.set('color', this.labels[0].color);
+ if (this.noLabels) {
+ this.lineFeature.set('color', '5bc0de');
+ } else {
+ this.lineFeature.set('color', this.labels[0].color);
+ }
this.lineFeature.setStyle(Styles.editing);
this.lineFeature._updateCoordinates = () => {
@@ -383,7 +443,11 @@ export default {
}
},
selectTypeaheadLabel(label) {
- this.updateAndClose(label);
+ if (this.noLabels) {
+ this.createAndClose(label)
+ } else {
+ this.updateAndClose(label);
+ }
Events.emit('labelbot.chose_label_other');
},
selectLabel(index) {
@@ -431,6 +495,11 @@ export default {
Keyboard.off('2', this.selectLabel2, 'labelbot');
Keyboard.off('3', this.selectLabel3, 'labelbot');
}
+ // We can't use a plain else here because the pending annotation state
+ // may change if the annotation gets a label via the typeahead.
+ else if (this.pendingAnnotation) {
+ this.$parent.labelbotSource.removeFeature(this.annotation.feature);
+ }
},
};
diff --git a/resources/assets/js/annotations/mixins/labelbot.vue b/resources/assets/js/annotations/mixins/labelbot.vue
index 9ce6071699..2f35037484 100644
--- a/resources/assets/js/annotations/mixins/labelbot.vue
+++ b/resources/assets/js/annotations/mixins/labelbot.vue
@@ -129,7 +129,7 @@ export default {
},
showLabelbotPopup(annotation) {
this.labelbotOverlays.push(annotation);
- this.focusedPopupKey = annotation.id;
+ this.focusedPopupKey = annotation.id ?? annotation.feature.ol_uid;
Keyboard.setActiveSet('labelbot');
},
updateLabelbotState(labelbotState) {
@@ -139,9 +139,11 @@ export default {
const index = this.labelbotOverlays.indexOf(annotation);
if (index !== -1) {
this.labelbotOverlays.splice(index, 1);
- this.focusedPopupKey = this.labelbotOverlays[this.labelbotOverlays.length - 1]?.id;
+ const lastOverlay = this.labelbotOverlays[this.labelbotOverlays.length - 1]
- if (!this.focusedPopupKey) {
+ if (lastOverlay) {
+ this.focusedPopupKey = lastOverlay.id ?? lastOverlay.feature.ol_uid;
+ } else {
Keyboard.setActiveSet('default');
}
}
@@ -152,7 +154,7 @@ export default {
Keyboard.setActiveSet('default');
},
changeLabelbotFocusedPopup(annotation) {
- this.focusedPopupKey = annotation.id;
+ this.focusedPopupKey = annotation.id ?? annotation.feature.ol_uid;
},
saveLabelbotAnnotation(annotation, saveCallback) {
if (this.labelbotState === LABELBOT_STATES.INITIALIZING) {
@@ -164,12 +166,32 @@ export default {
}
this.updateLabelbotState(LABELBOT_STATES.COMPUTING);
- // Make sure the LabelBOT image is not sent in the API request to create the
+ // Make sure the LabelBOT image, temp feature, pending video annotation and track are not sent in the API request to create the
// annotation.
const labelbotImage = annotation.labelbotImage;
annotation.labelbotImage = undefined;
delete annotation.labelbotImage;
+ const feature = annotation.feature;
+ annotation.feature = undefined;
+ delete annotation.feature;
+
+ let pendingVideoAnnotation;
+ let track;
+ if (annotation.pendingAnnotation) {
+ pendingVideoAnnotation = annotation.pendingAnnotation;
+ annotation.pendingAnnotation = undefined;
+ delete annotation.pendingAnnotation;
+
+ track = annotation.track;
+ annotation.track = undefined;
+ delete annotation.track;
+ }
+
+ // We save the shape in case LabelBOT returns no results
+ // as the shape property is deleted in the create API function
+ const shape = annotation.shape;
+
return this.generateFeatureVector(labelbotImage)
.then((featureVector) => {
this.labelbotRequestsInFlight += 1;
@@ -177,12 +199,30 @@ export default {
return saveCallback(annotation);
})
- .then(annotation => {
+ .then(_annotation => {
if (this.labelbotRequestsInFlight === 1) {
this.updateLabelbotState(LABELBOT_STATES.READY);
}
+ if (!_annotation) {
+ // We need to parse the ol_uid to int to be used as pop key
+ feature.ol_uid = parseInt(feature.ol_uid)
+
+ // We return annotation and not _annotation, because _annotation is null
+ annotation.feature = feature;
+ annotation.labels = [];
+ annotation.shape = shape;
- return annotation;
+ if (pendingVideoAnnotation) {
+ annotation.pendingAnnotation = pendingVideoAnnotation;
+ annotation.track = track;
+ }
+
+ annotation.feature_vector = undefined;
+ delete annotation.feature_vector;
+
+ return annotation
+ }
+ return _annotation;
})
.catch((e) => {
if (e.status === 429) {
diff --git a/resources/assets/js/annotations/stores/annotations.js b/resources/assets/js/annotations/stores/annotations.js
index 2b432ca0ac..893580781c 100644
--- a/resources/assets/js/annotations/stores/annotations.js
+++ b/resources/assets/js/annotations/stores/annotations.js
@@ -79,16 +79,20 @@ class Annotations {
delete annotation.shape;
return ImagesApi.saveAnnotations({id: imageId}, annotation)
- .then(this.parseResponse.bind(this))
- .then(this.resolveShape.bind(this))
- .then(this.setDeselected.bind(this))
- .then((annotation) => {
- this.cache[imageId].then(function (annotations) {
- annotations.unshift(annotation);
- });
-
- return annotation;
- });
+ .then((response) => {
+ if (response.status === 204) return null;
+
+ return Promise.resolve(this.parseResponse(response))
+ .then(this.resolveShape.bind(this))
+ .then(this.setDeselected.bind(this))
+ .then((annotation) => {
+ this.cache[imageId].then(function (annotations) {
+ annotations.unshift(annotation);
+ });
+
+ return annotation;
+ });
+ })
}
update(annotation) {
diff --git a/resources/assets/js/videos/components/videoScreen.vue b/resources/assets/js/videos/components/videoScreen.vue
index 06b68bcd7b..1641b72f66 100644
--- a/resources/assets/js/videos/components/videoScreen.vue
+++ b/resources/assets/js/videos/components/videoScreen.vue
@@ -395,6 +395,8 @@
:focused-popup-key="focusedPopupKey"
:annotation="annotation"
:timeout="labelbotTimeout"
+ @new="createNewLabelBOTAnnotation"
+ @delete-pending="handleDeleteLabelbotPendingAnnotation"
@update="updateLabelbotLabel"
@close="closeLabelbotPopup"
@delete="handleDeleteLabelbotAnnotation"
diff --git a/resources/assets/js/videos/components/videoScreen/drawInteractions.vue b/resources/assets/js/videos/components/videoScreen/drawInteractions.vue
index 3539011641..bfa932bd37 100644
--- a/resources/assets/js/videos/components/videoScreen/drawInteractions.vue
+++ b/resources/assets/js/videos/components/videoScreen/drawInteractions.vue
@@ -238,7 +238,9 @@ export default {
if (lastFrame === undefined) {
return;
}
-
+ // We need to pause the video otherwise no annotation would be
+ // visible as an anchor for the labelbot popup.
+ this.pause();
this.$emit('seek', lastFrame);
},
finishDrawAnnotation() {
@@ -265,6 +267,11 @@ export default {
finishTrackAnnotation() {
if (this.isDrawing) {
if (this.hasPendingAnnotation) {
+ if (this.labelbotIsActive) {
+ // If we don't seek to the last frame, no annotation would be
+ // visible as an anchor for the labelbot popup.
+ this.seekToLastFrame(this.pendingAnnotation);
+ }
this.$emit('track-annotation', this.pendingAnnotation);
this.resetPendingAnnotation(this.pendingAnnotation.shape);
}
@@ -339,6 +346,11 @@ export default {
const points = this.getPointsFromGeometry(e.feature.getGeometry());
this.pendingAnnotation.points.push(points);
+ // We add temporarily the feature to the pending annotation
+ // so in case LabelBOT did not return any results, the unique id of
+ // the feature could be used as LabelBOT popup id
+ this.pendingAnnotation.feature = e.feature;
+
// The LabelBOT image is always created because the user could decide to
// enable LabelBOT while they draw the pending annotation.
if (!this.pendingAnnotation.screenshotPromise) {
diff --git a/resources/assets/js/videos/videoContainer.vue b/resources/assets/js/videos/videoContainer.vue
index 703cfac81f..6d99c0012d 100644
--- a/resources/assets/js/videos/videoContainer.vue
+++ b/resources/assets/js/videos/videoContainer.vue
@@ -238,6 +238,13 @@ export default {
return Promise.resolve();
}
+ if (this.labelbotOverlayCount > 0) {
+ this.labelbotOverlays.forEach((overlay) => {
+ this.removeAnnotation(overlay.pendingAnnotation);
+ })
+ this.closeAllLabelbotPopups();
+ }
+
let promise = new Promise((resolve, reject) => {
this.video.addEventListener('seeked', resolve);
this.video.addEventListener('error', reject);
@@ -329,6 +336,12 @@ export default {
}
},
async createAnnotation(pendingAnnotation) {
+ const pendingAnnotationFeature = pendingAnnotation.feature;
+ pendingAnnotation.feature = undefined;
+ delete pendingAnnotation.feature;
+
+ const track = pendingAnnotation.track
+
this.updatePendingAnnotation(pendingAnnotation);
// Save this because it is still required when there may already be another
// this.pendingAnnotation.
@@ -357,6 +370,11 @@ export default {
try {
newAnnotation.labelbotImage = await pendingAnnotation.screenshotPromise;
+ // We need to add the feature, the pending annotation and track to the new annotation
+ // in case LabelBOT returns no results
+ newAnnotation.feature = pendingAnnotationFeature;
+ newAnnotation.pendingAnnotation = pendingAnnotation;
+ newAnnotation.track = track;
} catch (e) {
Messages.danger(e.message);
this.removeAnnotation(pendingAnnotation);
@@ -380,8 +398,14 @@ export default {
},
saveAnnotation(newAnnotation, pendingAnnotation) {
+ let pendingEmptyLabelBOTAnnotation = false;
return VideoAnnotationApi.save({id: this.videoId}, newAnnotation)
.then((res) => {
+ if (res.status === 204) {
+ pendingEmptyLabelBOTAnnotation = true;
+ return null;
+ }
+
if (pendingAnnotation.track) {
this.disableJobTracking = res.body.trackingJobLimitReached;
}
@@ -391,7 +415,24 @@ export default {
this.disableJobTracking = res.status === 429;
})
.finally(() => {
- this.removeAnnotation(pendingAnnotation);
+ if (!pendingEmptyLabelBOTAnnotation) {
+ this.removeAnnotation(pendingAnnotation);
+ }
+ });
+ },
+ createLabelBOTAnnotation(annotation) {
+ let newAnnotation = {
+ label_id: annotation.label_id,
+ points: annotation.points,
+ frames: annotation.frames,
+ shape_id: annotation.shape_id,
+ };
+
+ this.saveAnnotation(newAnnotation, annotation.pendingAnnotation)
+ .then((savedAnnotation) => {
+ if (annotation.track) {
+ this.setAnnotationTrackingState(savedAnnotation);
+ }
});
},
trackAnnotation(pendingAnnotation) {
@@ -400,7 +441,9 @@ export default {
.then(this.setAnnotationTrackingState);
},
setAnnotationTrackingState(annotation) {
- if (annotation) {
+ // If LabelBOT returns no result, then the annotation has no id
+ // Tracking state will be set later, if a label was assigned to the pending annotation
+ if (annotation && annotation.id) {
annotation.startTracking();
}
},
@@ -727,6 +770,14 @@ export default {
if (!this.hasSiblingVideos || this.labelbotIsComputing) {
return;
}
+
+ if (this.labelbotOverlayCount > 0) {
+ this.labelbotOverlays.forEach((overlay) => {
+ this.removeAnnotation(overlay.pendingAnnotation);
+ })
+ this.closeAllLabelbotPopups();
+ }
+
this.reset();
let length = this.videoIds.length;
let index = (this.videoIds.indexOf(this.videoId) + length - 1) % length;
@@ -737,6 +788,14 @@ export default {
if (!this.hasSiblingVideos || this.labelbotIsComputing) {
return;
}
+
+ if (this.labelbotOverlayCount > 0) {
+ this.labelbotOverlays.forEach((overlay) => {
+ this.removeAnnotation(overlay.pendingAnnotation);
+ })
+ this.closeAllLabelbotPopups();
+ }
+
this.reset();
let length = this.videoIds.length;
let index = (this.videoIds.indexOf(this.videoId) + length + 1) % length;
@@ -873,6 +932,13 @@ export default {
return;
}
+ if (this.labelbotOverlayCount > 0) {
+ this.labelbotOverlays.forEach((overlay) => {
+ this.removeAnnotation(overlay.pendingAnnotation);
+ })
+ this.closeAllLabelbotPopups();
+ }
+
if (this.video.paused) {
if (this.autoPauseTimeout) {
this.cancelAutoPlay();
diff --git a/resources/assets/sass/annotations/components/_labelbot-popup.scss b/resources/assets/sass/annotations/components/_labelbot-popup.scss
index 8c9f16d4f9..d4604d4a95 100644
--- a/resources/assets/sass/annotations/components/_labelbot-popup.scss
+++ b/resources/assets/sass/annotations/components/_labelbot-popup.scss
@@ -28,6 +28,15 @@
border-radius: 50%;
}
+.labelbot-popup__message {
+ list-style-type: none;
+ max-width: 300px;
+ margin: 0;
+ padding: $padding-small-vertical;
+ background-color: $body-bg;
+ color: $text-muted;
+}
+
.labelbot-label {
position: relative;
margin-bottom: 4px;
diff --git a/resources/views/annotations/show/annotationCanvas.blade.php b/resources/views/annotations/show/annotationCanvas.blade.php
index 33ecad8f97..3f3ff95dcc 100644
--- a/resources/views/annotations/show/annotationCanvas.blade.php
+++ b/resources/views/annotations/show/annotationCanvas.blade.php
@@ -211,10 +211,12 @@ class="control-button--danger"
beEditor();
+ // Query an empty DB
+ $response = $this->post("/api/v1/images/{$this->image->id}/annotations", [
+ 'shape_id' => Shape::pointId(),
+ 'feature_vector' => range(1, 384),
+ 'confidence' => 0.5,
+ 'points' => [10, 11],
+ ]);
+ // We expect no content
+ $response->assertStatus(204);
+
// Test label
$label = LabelTest::create();
// Label must be attached to a label tree
diff --git a/tests/php/Http/Controllers/Api/VideoAnnotationControllerTest.php b/tests/php/Http/Controllers/Api/VideoAnnotationControllerTest.php
index 7a7a747188..97db5de4e1 100644
--- a/tests/php/Http/Controllers/Api/VideoAnnotationControllerTest.php
+++ b/tests/php/Http/Controllers/Api/VideoAnnotationControllerTest.php
@@ -864,6 +864,16 @@ public function testStoreWithFeatureVectorWithoutHNSW()
{
$this->beEditor();
+ // Query an empty DB
+ $response = $this->json('POST', "api/v1/videos/{$this->video->id}/annotations", [
+ 'shape_id' => Shape::pointId(),
+ 'feature_vector' => range(1, 384),
+ 'points' => [[10, 11]],
+ 'frames' => [0.0],
+ ]);
+ // We expect no content
+ $response->assertStatus(204);
+
// Test label
$label = LabelTest::create();
// Label must be attached to a label tree