From adf11cbc685a4a6f030e0fc71f5b59103f40d33b Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Thu, 3 Oct 2024 19:20:53 -0400 Subject: [PATCH] updated tests --- server/src/services/metadata.service.spec.ts | 72 ++++----- server/src/services/person.service.spec.ts | 140 ++++++++++++++---- server/src/services/person.service.ts | 40 +++-- server/test/fixtures/face.stub.ts | 28 ++++ .../admin-page/jobs/job-tile.svelte | 10 +- 5 files changed, 195 insertions(+), 95 deletions(-) diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 9447b4c94dd08..5e3c5ba3a57aa 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -247,7 +247,7 @@ describe(MetadataService.name, () => { it('should handle an asset that could not be found', async () => { await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } }); expect(assetMock.upsertExif).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalled(); }); @@ -265,7 +265,7 @@ describe(MetadataService.name, () => { }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id], { faces: { person: false } }); expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate })); expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, @@ -280,7 +280,7 @@ describe(MetadataService.name, () => { metadataMock.readTags.mockResolvedValue({ ISO: [160] }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } }); expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 })); expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, @@ -300,7 +300,7 @@ describe(MetadataService.name, () => { }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } }); expect(assetMock.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ city: 'City', state: 'State', country: 'Country' }), ); @@ -320,7 +320,7 @@ describe(MetadataService.name, () => { }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } }); expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ latitude: null, longitude: null })); }); @@ -482,7 +482,9 @@ describe(MetadataService.name, () => { mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { + faces: { person: false }, + }); expect(storageMock.createOrOverwriteFile).not.toHaveBeenCalled(); expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queueAll).not.toHaveBeenCalled(); @@ -508,7 +510,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.video.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } }); expect(assetMock.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ orientation: Orientation.Rotate270CW.toString() }), ); @@ -536,7 +538,9 @@ describe(MetadataService.name, () => { assetStub.livePhotoWithOriginalFileName.originalPath, 'MotionPhotoVideo', ); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id], { + faces: { person: false }, + }); expect(assetMock.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), deviceAssetId: 'NONE', @@ -579,7 +583,9 @@ describe(MetadataService.name, () => { assetStub.livePhotoWithOriginalFileName.originalPath, 'EmbeddedVideoFile', ); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id], { + faces: { person: false }, + }); expect(assetMock.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), deviceAssetId: 'NONE', @@ -619,7 +625,9 @@ describe(MetadataService.name, () => { storageMock.readFile.mockResolvedValue(video); await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id], { + faces: { person: false }, + }); expect(storageMock.readFile).toHaveBeenCalledWith( assetStub.livePhotoWithOriginalFileName.originalPath, expect.any(Object), @@ -768,7 +776,7 @@ describe(MetadataService.name, () => { metadataMock.readTags.mockResolvedValue(tags); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } }); expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: assetStub.image.id, bitsPerSample: expect.any(Number), @@ -826,7 +834,7 @@ describe(MetadataService.name, () => { metadataMock.readTags.mockResolvedValue(tags); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } }); expect(assetMock.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ timeZone: 'UTC+0', @@ -846,7 +854,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.video.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } }); expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ @@ -867,7 +875,7 @@ describe(MetadataService.name, () => { }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } }); expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ @@ -889,7 +897,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.video.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } }); expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ @@ -911,7 +919,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.video.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } }); expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ @@ -975,11 +983,10 @@ describe(MetadataService.name, () => { metadataMock.readTags.mockResolvedValue(metadataStub.withFaceNoName); personMock.getDistinctNames.mockResolvedValue([]); personMock.createAll.mockResolvedValue([]); - personMock.refreshFaces.mockResolvedValue([]); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(personMock.createAll).toHaveBeenCalledWith([]); - expect(personMock.refreshFaces).toHaveBeenCalledWith(assetStub.primaryImage.id, [], SourceType.EXIF); - expect(personMock.updateAll).toHaveBeenCalledWith([]); + expect(personMock.createAll).not.toHaveBeenCalled(); + expect(personMock.refreshFaces).not.toHaveBeenCalled(); + expect(personMock.updateAll).not.toHaveBeenCalled(); }); it('should skip importing faces with empty name', async () => { @@ -988,11 +995,10 @@ describe(MetadataService.name, () => { metadataMock.readTags.mockResolvedValue(metadataStub.withFaceEmptyName); personMock.getDistinctNames.mockResolvedValue([]); personMock.createAll.mockResolvedValue([]); - personMock.refreshFaces.mockResolvedValue([]); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(personMock.createAll).toHaveBeenCalledWith([]); - expect(personMock.refreshFaces).toHaveBeenCalledWith(assetStub.primaryImage.id, [], SourceType.EXIF); - expect(personMock.updateAll).toHaveBeenCalledWith([]); + expect(personMock.createAll).not.toHaveBeenCalled(); + expect(personMock.refreshFaces).not.toHaveBeenCalled(); + expect(personMock.updateAll).not.toHaveBeenCalled(); }); it('should apply metadata face tags creating new persons', async () => { @@ -1001,14 +1007,12 @@ describe(MetadataService.name, () => { metadataMock.readTags.mockResolvedValue(metadataStub.withFace); personMock.getDistinctNames.mockResolvedValue([]); personMock.createAll.mockResolvedValue([personStub.withName.id]); - personMock.refreshFaces.mockResolvedValue(['face-asset-uuid']); personMock.update.mockResolvedValue(personStub.withName); await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id], { faces: { person: false } }); expect(personMock.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true }); expect(personMock.createAll).toHaveBeenCalledWith([expect.objectContaining({ name: personStub.withName.name })]); expect(personMock.refreshFaces).toHaveBeenCalledWith( - assetStub.primaryImage.id, [ { id: 'random-uuid', @@ -1023,7 +1027,7 @@ describe(MetadataService.name, () => { sourceType: SourceType.EXIF, }, ], - SourceType.EXIF, + [], ); expect(personMock.updateAll).toHaveBeenCalledWith([{ id: 'random-uuid', faceAssetId: 'random-uuid' }]); expect(jobMock.queueAll).toHaveBeenCalledWith([ @@ -1040,14 +1044,12 @@ describe(MetadataService.name, () => { metadataMock.readTags.mockResolvedValue(metadataStub.withFace); personMock.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]); personMock.createAll.mockResolvedValue([]); - personMock.refreshFaces.mockResolvedValue(['face-asset-uuid']); personMock.update.mockResolvedValue(personStub.withName); await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id], { faces: { person: false } }); expect(personMock.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true }); - expect(personMock.createAll).toHaveBeenCalledWith([]); + expect(personMock.createAll).not.toHaveBeenCalled(); expect(personMock.refreshFaces).toHaveBeenCalledWith( - assetStub.primaryImage.id, [ { id: 'random-uuid', @@ -1062,10 +1064,10 @@ describe(MetadataService.name, () => { sourceType: SourceType.EXIF, }, ], - SourceType.EXIF, + [], ); - expect(personMock.updateAll).toHaveBeenCalledWith([]); - expect(jobMock.queueAll).toHaveBeenCalledWith([]); + expect(personMock.updateAll).not.toHaveBeenCalled(); + expect(jobMock.queueAll).not.toHaveBeenCalledWith(); }); it('should handle invalid modify date', async () => { diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 2d2884c83fd62..23de02712bdb2 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -35,21 +35,33 @@ const responseDto: PersonResponseDto = { const statistics = { assets: 3 }; +const faceId = 'face-id'; +const face = { + id: faceId, + assetId: 'asset-id', + boundingBoxX1: 100, + boundingBoxY1: 100, + boundingBoxX2: 200, + boundingBoxY2: 200, + imageHeight: 500, + imageWidth: 400, +}; +const faceSearch = { faceId, embedding: [1, 2, 3, 4] }; const detectFaceMock: DetectedFaces = { faces: [ { boundingBox: { - x1: 100, - y1: 100, - x2: 200, - y2: 200, + x1: face.boundingBoxX1, + y1: face.boundingBoxY1, + x2: face.boundingBoxX2, + y2: face.boundingBoxY2, }, - embedding: [1, 2, 3, 4], + embedding: faceSearch.embedding, score: 0.2, }, ], - imageHeight: 500, - imageWidth: 400, + imageHeight: face.imageHeight, + imageWidth: face.imageWidth, }; describe(PersonService.name, () => { @@ -449,7 +461,7 @@ describe(PersonService.name, () => { hasNextPage: false, }); - await sut.handleQueueDetectFaces({}); + await sut.handleQueueDetectFaces({ force: false }); expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.FACES); expect(jobMock.queueAll).toHaveBeenCalledWith([ @@ -465,14 +477,33 @@ describe(PersonService.name, () => { items: [assetStub.image], hasNextPage: false, }); - personMock.getAll.mockResolvedValue({ - items: [personStub.withName], + personMock.getAllWithoutFaces.mockResolvedValue([personStub.withName]); + + await sut.handleQueueDetectFaces({ force: true }); + + expect(personMock.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING }); + expect(personMock.delete).toHaveBeenCalledWith([personStub.withName]); + expect(storageMock.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath); + expect(assetMock.getAll).toHaveBeenCalled(); + expect(jobMock.queueAll).toHaveBeenCalledWith([ + { + name: JobName.FACE_DETECTION, + data: { id: assetStub.image.id }, + }, + ]); + }); + + it('should refresh all assets', async () => { + assetMock.getAll.mockResolvedValue({ + items: [assetStub.image], hasNextPage: false, }); - personMock.getAllWithoutFaces.mockResolvedValue([]); - await sut.handleQueueDetectFaces({ force: true }); + await sut.handleQueueDetectFaces({ force: undefined }); + expect(personMock.delete).not.toHaveBeenCalled(); + expect(personMock.deleteFaces).not.toHaveBeenCalled(); + expect(storageMock.unlink).not.toHaveBeenCalled(); expect(assetMock.getAll).toHaveBeenCalled(); expect(jobMock.queueAll).toHaveBeenCalledWith([ { @@ -480,6 +511,7 @@ describe(PersonService.name, () => { data: { id: assetStub.image.id }, }, ]); + expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.PERSON_CLEANUP }); }); it('should delete existing people and faces if forced', async () => { @@ -542,7 +574,7 @@ describe(PersonService.name, () => { expect(personMock.getAllFaces).toHaveBeenCalledWith( { skip: 0, take: 1000 }, - { where: { personId: IsNull(), sourceType: IsNull() } }, + { where: { personId: IsNull(), sourceType: SourceType.MACHINE_LEARNING } }, ); expect(jobMock.queueAll).toHaveBeenCalledWith([ { @@ -663,6 +695,10 @@ describe(PersonService.name, () => { }); describe('handleDetectFaces', () => { + beforeEach(() => { + cryptoMock.randomUUID.mockReturnValue(faceId); + }); + it('should skip if machine learning is disabled', async () => { systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled); @@ -719,27 +755,73 @@ describe(PersonService.name, () => { it('should create a face with no person and queue recognition job', async () => { personMock.createFaces.mockResolvedValue([faceStub.face1.id]); machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock); - searchMock.searchFaces.mockResolvedValue([{ face: faceStub.face1, distance: 0.7 }]); assetMock.getByIds.mockResolvedValue([assetStub.image]); - const faceId = 'face-id'; - cryptoMock.randomUUID.mockReturnValue(faceId); - const face = { - id: faceId, - assetId: 'asset-id', - boundingBoxX1: 100, - boundingBoxY1: 100, - boundingBoxX2: 200, - boundingBoxY2: 200, - imageHeight: 500, - imageWidth: 400, - faceSearch: { faceId, embedding: [1, 2, 3, 4] }, - }; await sut.handleDetectFaces({ id: assetStub.image.id }); - expect(personMock.createFaces).toHaveBeenCalledWith([face]); + expect(personMock.refreshFaces).toHaveBeenCalledWith([face], [], [faceSearch]); + expect(jobMock.queueAll).toHaveBeenCalledWith([ + { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }, + { name: JobName.FACIAL_RECOGNITION, data: { id: faceId } }, + ]); + expect(personMock.reassignFace).not.toHaveBeenCalled(); + expect(personMock.reassignFaces).not.toHaveBeenCalled(); + }); + + it('should delete an existing face not among the new detected faces', async () => { + machineLearningMock.detectFaces.mockResolvedValue({ faces: [], imageHeight: 500, imageWidth: 400 }); + assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.primaryFace1] }]); + + await sut.handleDetectFaces({ id: assetStub.image.id }); + + expect(personMock.refreshFaces).toHaveBeenCalledWith([], [faceStub.primaryFace1.id], []); + expect(jobMock.queueAll).not.toHaveBeenCalled(); + expect(personMock.reassignFace).not.toHaveBeenCalled(); + expect(personMock.reassignFaces).not.toHaveBeenCalled(); + }); + + it('should add new face and delete an existing face not among the new detected faces', async () => { + personMock.createFaces.mockResolvedValue([faceStub.face1.id]); + machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock); + assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.primaryFace1] }]); + + await sut.handleDetectFaces({ id: assetStub.image.id }); + + expect(personMock.refreshFaces).toHaveBeenCalledWith([face], [faceStub.primaryFace1.id], [faceSearch]); + expect(jobMock.queueAll).toHaveBeenCalledWith([ + { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }, + { name: JobName.FACIAL_RECOGNITION, data: { id: faceId } }, + ]); + expect(personMock.reassignFace).not.toHaveBeenCalled(); + expect(personMock.reassignFaces).not.toHaveBeenCalled(); + }); + + it('should add embedding to matching metadata face', async () => { + machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock); + assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.fromExif1] }]); + + await sut.handleDetectFaces({ id: assetStub.image.id }); + + expect(personMock.refreshFaces).toHaveBeenCalledWith( + [], + [], + [{ faceId: faceStub.fromExif1.id, embedding: faceSearch.embedding }], + ); + expect(jobMock.queueAll).not.toHaveBeenCalled(); + expect(personMock.reassignFace).not.toHaveBeenCalled(); + expect(personMock.reassignFaces).not.toHaveBeenCalled(); + }); + + it('should not add embedding to non-matching metadata face', async () => { + machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock); + assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.fromExif2] }]); + + await sut.handleDetectFaces({ id: assetStub.image.id }); + + expect(personMock.refreshFaces).toHaveBeenCalledWith([face], [], [faceSearch]); expect(jobMock.queueAll).toHaveBeenCalledWith([ - { name: JobName.FACIAL_RECOGNITION, data: { id: faceStub.face1.id } }, + { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }, + { name: JobName.FACIAL_RECOGNITION, data: { id: faceId } }, ]); expect(personMock.reassignFace).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled(); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 09dd2bbb2c6b6..b827dae01f851 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -310,11 +310,13 @@ export class PersonService extends BaseService { ); this.logger.debug(`${faces.length} faces detected in ${previewFile.path}`); - const facesToAdd: Partial[] = []; - const embeddingsToAdd: FaceSearchEntity[] = []; - const facesToRemove = new Map(); + const facesToAdd: (Partial & { id: string })[] = []; + const embeddings: FaceSearchEntity[] = []; + const mlFaceIds = new Set(); for (const face of asset.faces) { - facesToRemove.set(face.id, face); + if (face.sourceType === SourceType.MACHINE_LEARNING) { + mlFaceIds.add(face.id); + } } const heightScale = imageHeight / (asset.faces[0]?.imageHeight || 1); @@ -328,12 +330,8 @@ export class PersonService extends BaseService { }; const match = asset.faces.find((face) => this.iou(face, scaledBox) > 0.5); - if (match) { - const existing = facesToRemove.get(match.id)!; - facesToRemove.delete(existing.id); - if (existing.sourceType !== SourceType.MACHINE_LEARNING) { - embeddingsToAdd.push({ faceId: existing.id, embedding }); - } + if (match && !mlFaceIds.delete(match.id)) { + embeddings.push({ faceId: match.id, embedding }); } else { const faceId = this.cryptoRepository.randomUUID(); facesToAdd.push({ @@ -346,13 +344,13 @@ export class PersonService extends BaseService { boundingBoxX2: boundingBox.x2, boundingBoxY2: boundingBox.y2, }); - embeddingsToAdd.push({ faceId, embedding }); + embeddings.push({ faceId, embedding }); } } - const faceIdsToRemove = [...facesToRemove.values()].map((face) => face.id); + const faceIdsToRemove = [...mlFaceIds]; - if (facesToAdd.length > 0 || faceIdsToRemove.length > 0 || embeddingsToAdd.length > 0) { - await this.personRepository.refreshFaces(facesToAdd, faceIdsToRemove, embeddingsToAdd); + if (facesToAdd.length > 0 || faceIdsToRemove.length > 0 || embeddings.length > 0) { + await this.personRepository.refreshFaces(facesToAdd, faceIdsToRemove, embeddings); } if (faceIdsToRemove.length > 0) { @@ -361,14 +359,10 @@ export class PersonService extends BaseService { if (facesToAdd.length > 0) { this.logger.log(`Detected ${facesToAdd.length} new faces in asset ${id}`); - await this.jobRepository.queueAll([ - { name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }, - ...facesToAdd.map((face) => ({ name: JobName.FACIAL_RECOGNITION, data: { id: face.id! } }) as const), - ]); - } - - if (embeddingsToAdd.length > 0) { - this.logger.log(`Added ${embeddingsToAdd.length} face embeddings for asset ${id}`); + const jobs = facesToAdd.map((face) => ({ name: JobName.FACIAL_RECOGNITION, data: { id: face.id } }) as const); + await this.jobRepository.queueAll([{ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }, ...jobs]); + } else if (embeddings.length > 0) { + this.logger.log(`Added ${embeddings.length} face embeddings for asset ${id}`); } await this.assetRepository.upsertJobStatus({ assetId: asset.id, facesRecognizedAt: new Date() }); @@ -425,7 +419,7 @@ export class PersonService extends BaseService { const lastRun = new Date().toISOString(); const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => this.personRepository.getAllFaces(pagination, { - where: force ? undefined : { personId: IsNull(), sourceType: IsNull() }, + where: force ? undefined : { personId: IsNull(), sourceType: SourceType.MACHINE_LEARNING }, }), ); diff --git a/server/test/fixtures/face.stub.ts b/server/test/fixtures/face.stub.ts index 27ca2a4356e22..e8c4592b8bac7 100644 --- a/server/test/fixtures/face.stub.ts +++ b/server/test/fixtures/face.stub.ts @@ -141,4 +141,32 @@ export const faceStub = { sourceType: SourceType.MACHINE_LEARNING, faceSearch: { faceId: 'assetFaceId9', embedding: [1, 2, 3, 4] }, }), + fromExif1: Object.freeze({ + id: 'assetFaceId9', + assetId: assetStub.image.id, + asset: assetStub.image, + personId: personStub.randomPerson.id, + person: personStub.randomPerson, + boundingBoxX1: 100, + boundingBoxY1: 100, + boundingBoxX2: 200, + boundingBoxY2: 200, + imageHeight: 500, + imageWidth: 400, + sourceType: SourceType.EXIF, + }), + fromExif2: Object.freeze({ + id: 'assetFaceId9', + assetId: assetStub.image.id, + asset: assetStub.image, + personId: personStub.randomPerson.id, + person: personStub.randomPerson, + boundingBoxX1: 0, + boundingBoxY1: 0, + boundingBoxX2: 1, + boundingBoxY2: 1, + imageHeight: 1024, + imageWidth: 1024, + sourceType: SourceType.EXIF, + }), }; diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte index 3f3b82cee1ab4..b0af3a710f36f 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -149,19 +149,13 @@ {#if !disabled && multipleButtons && isIdle} {#if allText} - onCommand({ command: JobCommand.Start, force: true })} - > + onCommand({ command: JobCommand.Start, force: true })}> {allText} {/if} {#if refreshText} - onCommand({ command: JobCommand.Start, force: undefined })} - > + onCommand({ command: JobCommand.Start, force: undefined })}> {refreshText}