feat: create new person in face editor (#27364)

* feat: create new person in face editor

* add delay

* fix: test

* i18n

* fix: unit test

* pr feedback
This commit is contained in:
Alex
2026-04-02 10:28:40 -05:00
committed by GitHub
parent b465f2b58f
commit 37823bcd51
6 changed files with 274 additions and 7 deletions
+87 -1
View File
@@ -12,7 +12,13 @@ import { PersonFactory } from 'test/factories/person.factory';
import { UserFactory } from 'test/factories/user.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { getAsDetectedFace, getForAssetFace, getForDetectedFaces, getForFacialRecognitionJob } from 'test/mappers';
import {
getAsDetectedFace,
getForAsset,
getForAssetFace,
getForDetectedFaces,
getForFacialRecognitionJob,
} from 'test/mappers';
import { newDate, newUuid } from 'test/small.factory';
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
@@ -370,6 +376,86 @@ describe(PersonService.name, () => {
});
});
describe('createFace', () => {
it('should create a manual face and initialize the person feature photo creation', async () => {
const auth = AuthFactory.create();
const asset = AssetFactory.create();
const person = PersonFactory.create({ faceAssetId: null });
const featureFace = AssetFaceFactory.create({
assetId: asset.id,
personId: person.id,
sourceType: SourceType.Manual,
});
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
mocks.person.getById.mockResolvedValue(person);
mocks.person.getRandomFace.mockResolvedValue(featureFace);
mocks.person.update.mockResolvedValue({ ...person, faceAssetId: featureFace.id });
await expect(
sut.createFace(auth, {
assetId: asset.id,
personId: person.id,
imageHeight: 500,
imageWidth: 400,
x: 10,
y: 20,
width: 100,
height: 110,
}),
).resolves.toBeUndefined();
expect(mocks.asset.getById).toHaveBeenCalledWith(asset.id, { edits: true, exifInfo: true });
expect(mocks.person.createAssetFace).toHaveBeenCalledWith({
assetId: asset.id,
personId: person.id,
imageHeight: 500,
imageWidth: 400,
boundingBoxX1: 10,
boundingBoxX2: 110,
boundingBoxY1: 20,
boundingBoxY2: 130,
sourceType: SourceType.Manual,
});
expect(mocks.person.getRandomFace).toHaveBeenCalledWith(person.id);
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, faceAssetId: featureFace.id });
expect(mocks.job.queueAll).toHaveBeenCalledWith([
{ name: JobName.PersonGenerateThumbnail, data: { id: person.id } },
]);
});
it('should not update the person feature photo if one already exists', async () => {
const auth = AuthFactory.create();
const asset = AssetFactory.create();
const person = PersonFactory.create({ faceAssetId: newUuid() });
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
mocks.asset.getById.mockResolvedValue(getForAsset(asset));
mocks.person.getById.mockResolvedValue(person);
await expect(
sut.createFace(auth, {
assetId: asset.id,
personId: person.id,
imageHeight: 500,
imageWidth: 400,
x: 10,
y: 20,
width: 100,
height: 110,
}),
).resolves.toBeUndefined();
expect(mocks.person.createAssetFace).toHaveBeenCalledOnce();
expect(mocks.person.getRandomFace).not.toHaveBeenCalled();
expect(mocks.person.update).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled();
});
});
describe('createNewFeaturePhoto', () => {
it('should change person feature photo', async () => {
const person = PersonFactory.create();
+9 -1
View File
@@ -631,7 +631,11 @@ export class PersonService extends BaseService {
this.requireAccess({ auth, permission: Permission.PersonRead, ids: [dto.personId] }),
]);
const asset = await this.assetRepository.getById(dto.assetId, { edits: true, exifInfo: true });
const [asset, person] = await Promise.all([
this.assetRepository.getById(dto.assetId, { edits: true, exifInfo: true }),
this.findOrFail(dto.personId),
]);
if (!asset) {
throw new NotFoundException('Asset not found');
}
@@ -689,6 +693,10 @@ export class PersonService extends BaseService {
boundingBoxY2: Math.round(bottomRight.y),
sourceType: SourceType.Manual,
});
if (!person.faceAssetId) {
await this.createNewFeaturePhoto([person.id]);
}
}
async deleteFace(auth: AuthDto, id: string, dto: AssetFaceDeleteDto): Promise<void> {