From 12e55f5bf00fe816b6a36bf617d5e7f48294c85b Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 16 Dec 2024 09:47:11 -0500 Subject: [PATCH] feat(server): Merge Faces sorted by Similarity (#14635) * Merge Faces sorted by Similarity * Adds face sorting to the side panel face merger * run make open-api * Make it one query * Only have the single order by when sorting by closest face --- mobile/openapi/lib/api/people_api.dart | Bin 16164 -> 16725 bytes open-api/immich-openapi-specs.json | 18 ++++ open-api/typescript-sdk/src/fetch-client.ts | 6 +- server/src/controllers/person.controller.ts | 4 +- server/src/dtos/person.dto.ts | 4 + server/src/interfaces/person.interface.ts | 1 + server/src/repositories/person.repository.ts | 20 +++- server/src/services/person.service.ts | 11 ++- .../faces-page/assign-face-side-panel.svelte | 89 ++++++++++++------ .../faces-page/merge-face-selector.svelte | 2 +- .../faces-page/person-side-panel.svelte | 5 - 11 files changed, 119 insertions(+), 41 deletions(-) diff --git a/mobile/openapi/lib/api/people_api.dart b/mobile/openapi/lib/api/people_api.dart index 7df0d66c79cfb9077e8af49e35848ca2a849768f..92bd0fdeeacbfe8df5f4973ed6e4e3f911272258 100644 GIT binary patch delta 416 zcmZ2dceROeLq5l32VoArmf!jk+@p@7t);`}_ISRBVmgW!^) z%)E4a1ytoa3TWamJvx)Ecr7-^ak4R@TX2BO+yT`PbsaQQH8rid6o8->OlGDjXrQ|Y zXowb?7{U~o8{o!lj^@&0BHpu0CQI-tljGq`M^uklq4^8$PnZkg3Sa^{n-}pm83F(# Cr<5cB delta 55 zcmccG#JHqxLq5mk20qrw9(;Y1edKv3$8kzb*5Wtce23 diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 3afda881cd..2f771b7eec 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -3846,6 +3846,24 @@ "get": { "operationId": "getAllPeople", "parameters": [ + { + "name": "closestAssetId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "closestPersonId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, { "name": "page", "required": false, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 7770f0c578..393a47a5df 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -2362,7 +2362,9 @@ export function updatePartner({ id, updatePartnerDto }: { body: updatePartnerDto }))); } -export function getAllPeople({ page, size, withHidden }: { +export function getAllPeople({ closestAssetId, closestPersonId, page, size, withHidden }: { + closestAssetId?: string; + closestPersonId?: string; page?: number; size?: number; withHidden?: boolean; @@ -2371,6 +2373,8 @@ export function getAllPeople({ page, size, withHidden }: { status: 200; data: PeopleResponseDto; }>(`/people${QS.query(QS.explode({ + closestAssetId, + closestPersonId, page, size, withHidden diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts index ba9a181c41..c8faf87e62 100644 --- a/server/src/controllers/person.controller.ts +++ b/server/src/controllers/person.controller.ts @@ -31,8 +31,8 @@ export class PersonController { @Get() @Authenticated({ permission: Permission.PERSON_READ }) - getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise { - return this.service.getAll(auth, withHidden); + getAllPeople(@Auth() auth: AuthDto, @Query() options: PersonSearchDto): Promise { + return this.service.getAll(auth, options); } @Post() diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index 94ee52d916..047ef600b8 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -67,6 +67,10 @@ export class MergePersonDto { export class PersonSearchDto { @ValidateBoolean({ optional: true }) withHidden?: boolean; + @ValidateUUID({ optional: true }) + closestPersonId?: string; + @ValidateUUID({ optional: true }) + closestAssetId?: string; /** Page number for pagination */ @ApiPropertyOptional() diff --git a/server/src/interfaces/person.interface.ts b/server/src/interfaces/person.interface.ts index b3e2c0990e..dc89f5c1b0 100644 --- a/server/src/interfaces/person.interface.ts +++ b/server/src/interfaces/person.interface.ts @@ -10,6 +10,7 @@ export const IPersonRepository = 'IPersonRepository'; export interface PersonSearchOptions { minimumFaceCount: number; withHidden: boolean; + closestFaceAssetId?: string; } export interface PersonNameSearchOptions { diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 81958d269d..4229286706 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -83,7 +83,11 @@ export class PersonRepository implements IPersonRepository { } @GenerateSql({ params: [{ take: 10, skip: 10 }, DummyValue.UUID] }) - getAllForUser(pagination: PaginationOptions, userId: string, options?: PersonSearchOptions): Paginated { + async getAllForUser( + pagination: PaginationOptions, + userId: string, + options?: PersonSearchOptions, + ): Paginated { const queryBuilder = this.personRepository .createQueryBuilder('person') .innerJoin('person.faces', 'face') @@ -97,10 +101,22 @@ export class PersonRepository implements IPersonRepository { .addOrderBy('person.createdAt') .having("person.name != '' OR COUNT(face.assetId) >= :faces", { faces: options?.minimumFaceCount || 1 }) .groupBy('person.id'); + if (options?.closestFaceAssetId) { + const innerQueryBuilder = this.faceSearchRepository + .createQueryBuilder('face_search') + .select('embedding', 'embedding') + .where('"face_search"."faceId" = "person"."faceAssetId"'); + const faceSelectQueryBuilder = this.faceSearchRepository + .createQueryBuilder('face_search') + .select('embedding', 'embedding') + .where('"face_search"."faceId" = :faceId', { faceId: options.closestFaceAssetId }); + queryBuilder + .orderBy('(' + innerQueryBuilder.getQuery() + ') <=> (' + faceSelectQueryBuilder.getQuery() + ')') + .setParameters(faceSelectQueryBuilder.getParameters()); + } if (!options?.withHidden) { queryBuilder.andWhere('person.isHidden = false'); } - return paginatedBuilder(queryBuilder, { mode: PaginationMode.LIMIT_OFFSET, ...pagination, diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 79e82bb742..bdec6f88e8 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -55,16 +55,25 @@ import { IsNull } from 'typeorm'; @Injectable() export class PersonService extends BaseService { async getAll(auth: AuthDto, dto: PersonSearchDto): Promise { - const { withHidden = false, page, size } = dto; + const { withHidden = false, closestAssetId, closestPersonId, page, size } = dto; + let closestFaceAssetId = closestAssetId; const pagination = { take: size, skip: (page - 1) * size, }; + if (closestPersonId) { + const person = await this.personRepository.getById(closestPersonId); + if (!person?.faceAssetId) { + throw new NotFoundException('Person not found'); + } + closestFaceAssetId = person.faceAssetId; + } const { machineLearning } = await this.getConfig({ withCache: false }); const { items, hasNextPage } = await this.personRepository.getAllForUser(pagination, auth.user.id, { minimumFaceCount: machineLearning.facialRecognition.minFaces, withHidden, + closestFaceAssetId, }); const { total, hidden } = await this.personRepository.getNumberOfPeople(auth.user.id); diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte index b6c9beb43a..fe6a454307 100644 --- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -1,8 +1,8 @@