Skip to content

Commit

Permalink
fix(server): "all" button for facial recognition deleting faces inste…
Browse files Browse the repository at this point in the history
…ad of unassigning them (#13042)

* unassign faces instead of deleting them

* formatting
  • Loading branch information
mertalev authored Sep 30, 2024
1 parent 9b309e8 commit 2f13db5
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 29 deletions.
10 changes: 7 additions & 3 deletions server/src/interfaces/person.interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { SourceType } from 'src/enum';
import { Paginated, PaginationOptions } from 'src/utils/pagination';
import { FindManyOptions, FindOptionsRelations, FindOptionsSelect } from 'typeorm';

Expand Down Expand Up @@ -40,10 +41,12 @@ export interface PeopleStatistics {
hidden: number;
}

export interface DeleteAllFacesOptions {
sourceType?: string;
export interface DeleteFacesOptions {
sourceType: SourceType;
}

export type UnassignFacesOptions = DeleteFacesOptions;

export interface IPersonRepository {
getAll(pagination: PaginationOptions, options?: FindManyOptions<PersonEntity>): Paginated<PersonEntity>;
getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated<PersonEntity>;
Expand All @@ -59,7 +62,7 @@ export interface IPersonRepository {
createFaces(entities: Partial<AssetFaceEntity>[]): Promise<string[]>;
delete(entities: PersonEntity[]): Promise<void>;
deleteAll(): Promise<void>;
deleteAllFaces(options: DeleteAllFacesOptions): Promise<void>;
deleteFaces(options: DeleteFacesOptions): Promise<void>;
replaceFaces(assetId: string, entities: Partial<AssetFaceEntity>[], sourceType?: string): Promise<string[]>;
getAllFaces(pagination: PaginationOptions, options?: FindManyOptions<AssetFaceEntity>): Paginated<AssetFaceEntity>;
getFaceById(id: string): Promise<AssetFaceEntity>;
Expand All @@ -75,6 +78,7 @@ export interface IPersonRepository {
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
reassignFaces(data: UpdateFacesData): Promise<number>;
unassignFaces(options: UnassignFacesOptions): Promise<void>;
update(person: Partial<PersonEntity>): Promise<PersonEntity>;
updateAll(people: Partial<PersonEntity>[]): Promise<void>;
getLatestFaceDate(): Promise<string | undefined>;
Expand Down
36 changes: 25 additions & 11 deletions server/src/repositories/person.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import { PersonEntity } from 'src/entities/person.entity';
import { PaginationMode, SourceType } from 'src/enum';
import {
AssetFaceId,
DeleteAllFacesOptions,
DeleteFacesOptions,
IPersonRepository,
PeopleStatistics,
PersonNameResponse,
PersonNameSearchOptions,
PersonSearchOptions,
PersonStatistics,
UnassignFacesOptions,
UpdateFacesData,
} from 'src/interfaces/person.interface';
import { Instrumentation } from 'src/utils/instrumentation';
Expand All @@ -39,12 +40,23 @@ export class PersonRepository implements IPersonRepository {
.createQueryBuilder()
.update()
.set({ personId: newPersonId })
.where(_.omitBy({ personId: oldPersonId ?? undefined, id: faceIds ? In(faceIds) : undefined }, _.isUndefined))
.where(_.omitBy({ personId: oldPersonId, id: faceIds ? In(faceIds) : undefined }, _.isUndefined))
.execute();

return result.affected ?? 0;
}

async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
await this.assetFaceRepository
.createQueryBuilder()
.update()
.set({ personId: null })
.where({ sourceType })
.execute();

await this.vacuum({ reindexVectors: false });
}

async delete(entities: PersonEntity[]): Promise<void> {
await this.personRepository.remove(entities);
}
Expand All @@ -53,21 +65,14 @@ export class PersonRepository implements IPersonRepository {
await this.personRepository.clear();
}

async deleteAllFaces({ sourceType }: DeleteAllFacesOptions): Promise<void> {
if (!sourceType) {
return this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE');
}

async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
await this.assetFaceRepository
.createQueryBuilder('asset_faces')
.delete()
.andWhere('sourceType = :sourceType', { sourceType })
.execute();

await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search');
if (sourceType === SourceType.MACHINE_LEARNING) {
await this.assetFaceRepository.query('REINDEX INDEX face_index');
}
await this.vacuum({ reindexVectors: sourceType === SourceType.MACHINE_LEARNING });
}

getAllFaces(
Expand Down Expand Up @@ -331,4 +336,13 @@ export class PersonRepository implements IPersonRepository {
const { id } = await this.personRepository.save(person);
return this.personRepository.findOneByOrFail({ id });
}

private async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise<void> {
await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search, person');
await this.assetFaceRepository.query('REINDEX TABLE asset_faces');
await this.assetFaceRepository.query('REINDEX TABLE person');
if (reindexVectors) {
await this.assetFaceRepository.query('REINDEX TABLE face_search');
}
}
}
5 changes: 3 additions & 2 deletions server/src/services/person.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ describe(PersonService.name, () => {
expect(systemMock.set).not.toHaveBeenCalled();
});

it('should delete existing people and faces if forced', async () => {
it('should delete existing people if forced', async () => {
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
personMock.getAll.mockResolvedValue({
items: [faceStub.face1.person, personStub.randomPerson],
Expand All @@ -675,7 +675,8 @@ describe(PersonService.name, () => {

await sut.handleQueueRecognizeFaces({ force: true });

expect(personMock.deleteAllFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING });
expect(personMock.deleteFaces).not.toHaveBeenCalled();
expect(personMock.unassignFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING });
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.FACIAL_RECOGNITION,
Expand Down
14 changes: 2 additions & 12 deletions server/src/services/person.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,16 +276,6 @@ export class PersonService {
this.logger.debug(`Deleted ${people.length} people`);
}

private async deleteAllPeople() {
const personPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
this.repository.getAll({ ...pagination, skip: 0 }),
);

for await (const people of personPagination) {
await this.delete(people); // deletes thumbnails too
}
}

async handlePersonCleanup(): Promise<JobStatus> {
const people = await this.repository.getAllWithoutFaces();
await this.delete(people);
Expand All @@ -299,7 +289,7 @@ export class PersonService {
}

if (force) {
await this.repository.deleteAllFaces({ sourceType: SourceType.MACHINE_LEARNING });
await this.repository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING });
await this.handlePersonCleanup();
}

Expand Down Expand Up @@ -407,7 +397,7 @@ export class PersonService {
const { waiting } = await this.jobRepository.getJobCounts(QueueName.FACIAL_RECOGNITION);

if (force) {
await this.repository.deleteAllFaces({ sourceType: SourceType.MACHINE_LEARNING });
await this.repository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING });
await this.handlePersonCleanup();
} else if (waiting) {
this.logger.debug(
Expand Down
3 changes: 2 additions & 1 deletion server/test/repositories/person.repository.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ export const newPersonRepositoryMock = (): Mocked<IPersonRepository> => {
updateAll: vitest.fn(),
delete: vitest.fn(),
deleteAll: vitest.fn(),
deleteAllFaces: vitest.fn(),
deleteFaces: vitest.fn(),

getStatistics: vitest.fn(),
getAllFaces: vitest.fn(),
getFacesByIds: vitest.fn(),
getRandomFace: vitest.fn(),

reassignFaces: vitest.fn(),
unassignFaces: vitest.fn(),
createFaces: vitest.fn(),
replaceFaces: vitest.fn(),
getFaces: vitest.fn(),
Expand Down

0 comments on commit 2f13db5

Please sign in to comment.