mirror of
https://github.com/immich-app/immich.git
synced 2025-12-25 17:24:58 +03:00
refactor(server): remove face, person and face search entities (#17535)
* remove face, person and face search entities update tests and mappers check if face relation exists update sql unused imports * pr feedback generate sql, remove unused imports
This commit is contained in:
@@ -9,11 +9,9 @@ import { constants } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { Exif } from 'src/db';
|
||||
import { AssetFaces, Exif, Person } from 'src/db';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import {
|
||||
AssetType,
|
||||
DatabaseLock,
|
||||
@@ -587,10 +585,10 @@ export class MetadataService extends BaseService {
|
||||
return;
|
||||
}
|
||||
|
||||
const facesToAdd: (Partial<AssetFaceEntity> & { assetId: string })[] = [];
|
||||
const facesToAdd: (Insertable<AssetFaces> & { assetId: string })[] = [];
|
||||
const existingNames = await this.personRepository.getDistinctNames(asset.ownerId, { withHidden: true });
|
||||
const existingNameMap = new Map(existingNames.map(({ id, name }) => [name.toLowerCase(), id]));
|
||||
const missing: (Partial<PersonEntity> & { ownerId: string })[] = [];
|
||||
const missing: (Insertable<Person> & { ownerId: string })[] = [];
|
||||
const missingWithFaceAsset: { id: string; ownerId: string; faceAssetId: string }[] = [];
|
||||
for (const region of tags.RegionInfo.RegionList) {
|
||||
if (!region.Name) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { AssetFace } from 'src/database';
|
||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { mapFaces, mapPerson, PersonResponseDto } from 'src/dtos/person.dto';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { CacheControl, Colorspace, ImageFormat, JobName, JobStatus, SourceType, SystemMetadataKey } from 'src/enum';
|
||||
import { WithoutProperty } from 'src/repositories/asset.repository';
|
||||
import { DetectedFaces } from 'src/repositories/machine-learning.repository';
|
||||
@@ -11,7 +11,7 @@ import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { faceStub } from 'test/fixtures/face.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { personStub, personThumbnailStub } from 'test/fixtures/person.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
@@ -24,6 +24,7 @@ const responseDto: PersonResponseDto = {
|
||||
isHidden: false,
|
||||
updatedAt: expect.any(Date),
|
||||
isFavorite: false,
|
||||
color: expect.any(String),
|
||||
};
|
||||
|
||||
const statistics = { assets: 3 };
|
||||
@@ -90,6 +91,7 @@ describe(PersonService.name, () => {
|
||||
isHidden: true,
|
||||
isFavorite: false,
|
||||
updatedAt: expect.any(Date),
|
||||
color: expect.any(String),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -118,6 +120,7 @@ describe(PersonService.name, () => {
|
||||
isHidden: false,
|
||||
isFavorite: true,
|
||||
updatedAt: expect.any(Date),
|
||||
color: personStub.isFavorite.color,
|
||||
},
|
||||
responseDto,
|
||||
],
|
||||
@@ -137,7 +140,6 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should throw a bad request when person is not found', async () => {
|
||||
mocks.person.getById.mockResolvedValue(null);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
@@ -161,7 +163,6 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should throw an error when personId is invalid', async () => {
|
||||
mocks.person.getById.mockResolvedValue(null);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException);
|
||||
expect(mocks.storage.createReadStream).not.toHaveBeenCalled();
|
||||
@@ -231,6 +232,7 @@ describe(PersonService.name, () => {
|
||||
isHidden: false,
|
||||
isFavorite: false,
|
||||
updatedAt: expect.any(Date),
|
||||
color: expect.any(String),
|
||||
});
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') });
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
@@ -346,7 +348,6 @@ describe(PersonService.name, () => {
|
||||
|
||||
describe('handlePersonMigration', () => {
|
||||
it('should not move person files', async () => {
|
||||
mocks.person.getById.mockResolvedValue(null);
|
||||
await expect(sut.handlePersonMigration(personStub.noName)).resolves.toBe(JobStatus.FAILED);
|
||||
});
|
||||
});
|
||||
@@ -400,6 +401,7 @@ describe(PersonService.name, () => {
|
||||
name: personStub.noName.name,
|
||||
thumbnailPath: personStub.noName.thumbnailPath,
|
||||
updatedAt: expect.any(Date),
|
||||
color: personStub.noName.color,
|
||||
});
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalledWith();
|
||||
@@ -438,7 +440,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
await sut.handlePersonCleanup();
|
||||
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.noName]);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.noName.id]);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.noName.thumbnailPath);
|
||||
});
|
||||
});
|
||||
@@ -480,7 +482,7 @@ describe(PersonService.name, () => {
|
||||
await sut.handleQueueDetectFaces({ force: true });
|
||||
|
||||
expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING });
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.withName]);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.withName.id]);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath);
|
||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
@@ -531,7 +533,7 @@ describe(PersonService.name, () => {
|
||||
data: { id: assetStub.image.id },
|
||||
},
|
||||
]);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson]);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath);
|
||||
});
|
||||
});
|
||||
@@ -698,7 +700,7 @@ describe(PersonService.name, () => {
|
||||
data: { id: faceStub.face1.id, deferred: false },
|
||||
},
|
||||
]);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson]);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath);
|
||||
});
|
||||
});
|
||||
@@ -731,7 +733,7 @@ describe(PersonService.name, () => {
|
||||
id: 'asset-face-1',
|
||||
assetId: assetStub.noResizePath.id,
|
||||
personId: faceStub.face1.personId,
|
||||
} as AssetFaceEntity,
|
||||
} as AssetFace,
|
||||
],
|
||||
},
|
||||
]);
|
||||
@@ -848,8 +850,8 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should fail if face does not have asset', async () => {
|
||||
const face = { ...faceStub.face1, asset: null } as AssetFaceEntity & { asset: null };
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(face);
|
||||
const face = { ...faceStub.face1, asset: null };
|
||||
mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(face);
|
||||
|
||||
expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.FAILED);
|
||||
|
||||
@@ -858,7 +860,7 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip if face already has an assigned person', async () => {
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.face1);
|
||||
mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.face1);
|
||||
|
||||
expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.SKIPPED);
|
||||
|
||||
@@ -880,7 +882,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
||||
mocks.search.searchFaces.mockResolvedValue(faces);
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.create.mockResolvedValue(faceStub.primaryFace1.person);
|
||||
|
||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||
@@ -910,7 +912,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
||||
mocks.search.searchFaces.mockResolvedValue(faces);
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.create.mockResolvedValue(faceStub.primaryFace1.person);
|
||||
|
||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||
@@ -940,7 +942,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
||||
mocks.search.searchFaces.mockResolvedValue(faces);
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.create.mockResolvedValue(faceStub.primaryFace1.person);
|
||||
|
||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||
@@ -965,7 +967,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
||||
mocks.search.searchFaces.mockResolvedValue(faces);
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.create.mockResolvedValue(personStub.withName);
|
||||
|
||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||
@@ -984,7 +986,7 @@ describe(PersonService.name, () => {
|
||||
const faces = [{ ...faceStub.noPerson1, distance: 0 }] as FaceSearchResult[];
|
||||
|
||||
mocks.search.searchFaces.mockResolvedValue(faces);
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.create.mockResolvedValue(personStub.withName);
|
||||
|
||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||
@@ -1003,7 +1005,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
|
||||
mocks.search.searchFaces.mockResolvedValue(faces);
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.create.mockResolvedValue(personStub.withName);
|
||||
|
||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||
@@ -1025,7 +1027,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
|
||||
mocks.search.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]);
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1);
|
||||
mocks.person.create.mockResolvedValue(personStub.withName);
|
||||
|
||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true });
|
||||
@@ -1047,7 +1049,6 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip a person not found', async () => {
|
||||
mocks.person.getById.mockResolvedValue(null);
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1058,30 +1059,18 @@ describe(PersonService.name, () => {
|
||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip a person with a face asset id not found', async () => {
|
||||
mocks.person.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.id });
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.face1);
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip a person with a face asset id without a thumbnail', async () => {
|
||||
mocks.person.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId });
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.face1);
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.noResizePath]);
|
||||
it('should skip a person with face not found', async () => {
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should generate a thumbnail', async () => {
|
||||
mocks.person.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId });
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.middle);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailMiddle);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
|
||||
await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id });
|
||||
|
||||
expect(mocks.asset.getById).toHaveBeenCalledWith(faceStub.middle.assetId, { exifInfo: true, files: true });
|
||||
expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(personStub.primaryPerson.id);
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/thumbs/admin_id/pe/rs');
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
assetStub.primaryImage.originalPath,
|
||||
@@ -1107,9 +1096,7 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should generate a thumbnail without going negative', async () => {
|
||||
mocks.person.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.start.assetId });
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.start);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailStart);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
|
||||
await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id });
|
||||
@@ -1134,10 +1121,8 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should generate a thumbnail without overflowing', async () => {
|
||||
mocks.person.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.end.assetId });
|
||||
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.end);
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailEnd);
|
||||
mocks.person.update.mockResolvedValue(personStub.primaryPerson);
|
||||
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
|
||||
await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id });
|
||||
@@ -1220,7 +1205,6 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should throw an error when the primary person is not found', async () => {
|
||||
mocks.person.getById.mockResolvedValue(null);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
|
||||
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf(
|
||||
@@ -1233,7 +1217,6 @@ describe(PersonService.name, () => {
|
||||
|
||||
it('should handle invalid merge ids', async () => {
|
||||
mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
mocks.person.getById.mockResolvedValueOnce(null);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Insertable, Updateable } from 'kysely';
|
||||
import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { AssetFaces, FaceSearch, Person } from 'src/db';
|
||||
import { Chunked, OnJob } from 'src/decorators';
|
||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -21,10 +23,6 @@ import {
|
||||
PersonStatisticsResponseDto,
|
||||
PersonUpdateDto,
|
||||
} from 'src/dtos/person.dto';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { FaceSearchEntity } from 'src/entities/face-search.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import {
|
||||
AssetFileType,
|
||||
AssetType,
|
||||
@@ -243,9 +241,9 @@ export class PersonService extends BaseService {
|
||||
}
|
||||
|
||||
@Chunked()
|
||||
private async delete(people: PersonEntity[]) {
|
||||
private async delete(people: { id: string; thumbnailPath: string }[]) {
|
||||
await Promise.all(people.map((person) => this.storageRepository.unlink(person.thumbnailPath)));
|
||||
await this.personRepository.delete(people);
|
||||
await this.personRepository.delete(people.map((person) => person.id));
|
||||
this.logger.debug(`Deleted ${people.length} people`);
|
||||
}
|
||||
|
||||
@@ -317,8 +315,8 @@ export class PersonService extends BaseService {
|
||||
);
|
||||
this.logger.debug(`${faces.length} faces detected in ${previewFile.path}`);
|
||||
|
||||
const facesToAdd: (Partial<AssetFaceEntity> & { id: string; assetId: string })[] = [];
|
||||
const embeddings: FaceSearchEntity[] = [];
|
||||
const facesToAdd: (Insertable<AssetFaces> & { id: string })[] = [];
|
||||
const embeddings: FaceSearch[] = [];
|
||||
const mlFaceIds = new Set<string>();
|
||||
for (const face of asset.faces) {
|
||||
if (face.sourceType === SourceType.MACHINE_LEARNING) {
|
||||
@@ -377,7 +375,10 @@ export class PersonService extends BaseService {
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
private iou(face: AssetFaceEntity, newBox: BoundingBox): number {
|
||||
private iou(
|
||||
face: { boundingBoxX1: number; boundingBoxY1: number; boundingBoxX2: number; boundingBoxY2: number },
|
||||
newBox: BoundingBox,
|
||||
): number {
|
||||
const x1 = Math.max(face.boundingBoxX1, newBox.x1);
|
||||
const y1 = Math.max(face.boundingBoxY1, newBox.y1);
|
||||
const x2 = Math.min(face.boundingBoxX2, newBox.x2);
|
||||
@@ -453,11 +454,7 @@ export class PersonService extends BaseService {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const face = await this.personRepository.getFaceByIdWithAssets(id, { faceSearch: true }, [
|
||||
'id',
|
||||
'personId',
|
||||
'sourceType',
|
||||
]);
|
||||
const face = await this.personRepository.getFaceForFacialRecognitionJob(id);
|
||||
if (!face || !face.asset) {
|
||||
this.logger.warn(`Face ${id} not found`);
|
||||
return JobStatus.FAILED;
|
||||
@@ -545,46 +542,23 @@ export class PersonService extends BaseService {
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.GENERATE_PERSON_THUMBNAIL, queue: QueueName.THUMBNAIL_GENERATION })
|
||||
async handleGeneratePersonThumbnail(data: JobOf<JobName.GENERATE_PERSON_THUMBNAIL>): Promise<JobStatus> {
|
||||
async handleGeneratePersonThumbnail({ id }: JobOf<JobName.GENERATE_PERSON_THUMBNAIL>): Promise<JobStatus> {
|
||||
const { machineLearning, metadata, image } = await this.getConfig({ withCache: true });
|
||||
if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const person = await this.personRepository.getById(data.id);
|
||||
if (!person?.faceAssetId) {
|
||||
this.logger.error(`Could not generate person thumbnail: person ${person?.id} has no face asset`);
|
||||
const data = await this.personRepository.getDataForThumbnailGenerationJob(id);
|
||||
if (!data) {
|
||||
this.logger.error(`Could not generate person thumbnail for ${id}: missing data`);
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
const face = await this.personRepository.getFaceByIdWithAssets(person.faceAssetId);
|
||||
if (!face) {
|
||||
this.logger.error(`Could not generate person thumbnail: face ${person.faceAssetId} not found`);
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
const { ownerId, x1, y1, x2, y2, oldWidth, oldHeight } = data;
|
||||
|
||||
const {
|
||||
assetId,
|
||||
boundingBoxX1: x1,
|
||||
boundingBoxX2: x2,
|
||||
boundingBoxY1: y1,
|
||||
boundingBoxY2: y2,
|
||||
imageWidth: oldWidth,
|
||||
imageHeight: oldHeight,
|
||||
} = face;
|
||||
const { width, height, inputPath } = await this.getInputDimensions(data);
|
||||
|
||||
const asset = await this.assetRepository.getById(assetId, {
|
||||
exifInfo: true,
|
||||
files: true,
|
||||
});
|
||||
if (!asset) {
|
||||
this.logger.error(`Could not generate person thumbnail: asset ${assetId} does not exist`);
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
const { width, height, inputPath } = await this.getInputDimensions(asset, { width: oldWidth, height: oldHeight });
|
||||
|
||||
const thumbnailPath = StorageCore.getPersonThumbnailPath(person);
|
||||
const thumbnailPath = StorageCore.getPersonThumbnailPath({ id, ownerId });
|
||||
this.storageCore.ensureFolders(thumbnailPath);
|
||||
|
||||
const thumbnailOptions = {
|
||||
@@ -597,7 +571,7 @@ export class PersonService extends BaseService {
|
||||
};
|
||||
|
||||
await this.mediaRepository.generateThumbnail(inputPath, thumbnailOptions, thumbnailPath);
|
||||
await this.personRepository.update({ id: person.id, thumbnailPath });
|
||||
await this.personRepository.update({ id, thumbnailPath });
|
||||
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
@@ -634,7 +608,7 @@ export class PersonService extends BaseService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const update: Partial<PersonEntity> = {};
|
||||
const update: Updateable<Person> & { id: string } = { id: primaryPerson.id };
|
||||
if (!primaryPerson.name && mergePerson.name) {
|
||||
update.name = mergePerson.name;
|
||||
}
|
||||
@@ -644,7 +618,7 @@ export class PersonService extends BaseService {
|
||||
}
|
||||
|
||||
if (Object.keys(update).length > 0) {
|
||||
primaryPerson = await this.personRepository.update({ id: primaryPerson.id, ...update });
|
||||
primaryPerson = await this.personRepository.update(update);
|
||||
}
|
||||
|
||||
const mergeName = mergePerson.name || mergePerson.id;
|
||||
@@ -672,27 +646,26 @@ export class PersonService extends BaseService {
|
||||
return person;
|
||||
}
|
||||
|
||||
private async getInputDimensions(asset: AssetEntity, oldDims: ImageDimensions): Promise<InputDimensions> {
|
||||
if (!asset.exifInfo?.exifImageHeight || !asset.exifInfo.exifImageWidth) {
|
||||
throw new Error(`Asset ${asset.id} dimensions are unknown`);
|
||||
}
|
||||
|
||||
const previewFile = getAssetFile(asset.files, AssetFileType.PREVIEW);
|
||||
if (!previewFile) {
|
||||
throw new Error(`Asset ${asset.id} has no preview path`);
|
||||
}
|
||||
|
||||
private async getInputDimensions(asset: {
|
||||
type: AssetType;
|
||||
exifImageWidth: number;
|
||||
exifImageHeight: number;
|
||||
previewPath: string;
|
||||
originalPath: string;
|
||||
oldWidth: number;
|
||||
oldHeight: number;
|
||||
}): Promise<InputDimensions> {
|
||||
if (asset.type === AssetType.IMAGE) {
|
||||
let { exifImageWidth: width, exifImageHeight: height } = asset.exifInfo;
|
||||
if (oldDims.height > oldDims.width !== height > width) {
|
||||
let { exifImageWidth: width, exifImageHeight: height } = asset;
|
||||
if (asset.oldHeight > asset.oldWidth !== height > width) {
|
||||
[width, height] = [height, width];
|
||||
}
|
||||
|
||||
return { width, height, inputPath: asset.originalPath };
|
||||
}
|
||||
|
||||
const { width, height } = await this.mediaRepository.getImageDimensions(previewFile.path);
|
||||
return { width, height, inputPath: previewFile.path };
|
||||
const { width, height } = await this.mediaRepository.getImageDimensions(asset.previewPath);
|
||||
return { width, height, inputPath: asset.previewPath };
|
||||
}
|
||||
|
||||
private getCrop(dims: { old: ImageDimensions; new: ImageDimensions }, { x1, y1, x2, y2 }: BoundingBox): CropOptions {
|
||||
|
||||
Reference in New Issue
Block a user