mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
fix(server): "all" button for facial recognition deleting faces instead of unassigning them (#13042)
* unassign faces instead of deleting them * formatting
This commit is contained in:
parent
9b309e84c9
commit
2f13db51df
5 changed files with 39 additions and 29 deletions
|
@ -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';
|
||||
|
||||
|
@ -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>;
|
||||
|
@ -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>;
|
||||
|
@ -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>;
|
||||
|
|
|
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
|
|
@ -18,7 +18,7 @@ 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(),
|
||||
|
@ -26,6 +26,7 @@ export const newPersonRepositoryMock = (): Mocked<IPersonRepository> => {
|
|||
getRandomFace: vitest.fn(),
|
||||
|
||||
reassignFaces: vitest.fn(),
|
||||
unassignFaces: vitest.fn(),
|
||||
createFaces: vitest.fn(),
|
||||
replaceFaces: vitest.fn(),
|
||||
getFaces: vitest.fn(),
|
||||
|
|
Loading…
Reference in a new issue