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 { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
|
import { SourceType } from 'src/enum';
|
||||||
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
||||||
import { FindManyOptions, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
|
import { FindManyOptions, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
|
||||||
|
|
||||||
|
@ -40,10 +41,12 @@ export interface PeopleStatistics {
|
||||||
hidden: number;
|
hidden: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteAllFacesOptions {
|
export interface DeleteFacesOptions {
|
||||||
sourceType?: string;
|
sourceType: SourceType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UnassignFacesOptions = DeleteFacesOptions;
|
||||||
|
|
||||||
export interface IPersonRepository {
|
export interface IPersonRepository {
|
||||||
getAll(pagination: PaginationOptions, options?: FindManyOptions<PersonEntity>): Paginated<PersonEntity>;
|
getAll(pagination: PaginationOptions, options?: FindManyOptions<PersonEntity>): Paginated<PersonEntity>;
|
||||||
getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated<PersonEntity>;
|
getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated<PersonEntity>;
|
||||||
|
@ -59,7 +62,7 @@ export interface IPersonRepository {
|
||||||
createFaces(entities: Partial<AssetFaceEntity>[]): Promise<string[]>;
|
createFaces(entities: Partial<AssetFaceEntity>[]): Promise<string[]>;
|
||||||
delete(entities: PersonEntity[]): Promise<void>;
|
delete(entities: PersonEntity[]): Promise<void>;
|
||||||
deleteAll(): 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[]>;
|
replaceFaces(assetId: string, entities: Partial<AssetFaceEntity>[], sourceType?: string): Promise<string[]>;
|
||||||
getAllFaces(pagination: PaginationOptions, options?: FindManyOptions<AssetFaceEntity>): Paginated<AssetFaceEntity>;
|
getAllFaces(pagination: PaginationOptions, options?: FindManyOptions<AssetFaceEntity>): Paginated<AssetFaceEntity>;
|
||||||
getFaceById(id: string): Promise<AssetFaceEntity>;
|
getFaceById(id: string): Promise<AssetFaceEntity>;
|
||||||
|
@ -75,6 +78,7 @@ export interface IPersonRepository {
|
||||||
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
|
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
|
||||||
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
|
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
|
||||||
reassignFaces(data: UpdateFacesData): Promise<number>;
|
reassignFaces(data: UpdateFacesData): Promise<number>;
|
||||||
|
unassignFaces(options: UnassignFacesOptions): Promise<void>;
|
||||||
update(person: Partial<PersonEntity>): Promise<PersonEntity>;
|
update(person: Partial<PersonEntity>): Promise<PersonEntity>;
|
||||||
updateAll(people: Partial<PersonEntity>[]): Promise<void>;
|
updateAll(people: Partial<PersonEntity>[]): Promise<void>;
|
||||||
getLatestFaceDate(): Promise<string | undefined>;
|
getLatestFaceDate(): Promise<string | undefined>;
|
||||||
|
|
|
@ -9,13 +9,14 @@ import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { PaginationMode, SourceType } from 'src/enum';
|
import { PaginationMode, SourceType } from 'src/enum';
|
||||||
import {
|
import {
|
||||||
AssetFaceId,
|
AssetFaceId,
|
||||||
DeleteAllFacesOptions,
|
DeleteFacesOptions,
|
||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
PeopleStatistics,
|
PeopleStatistics,
|
||||||
PersonNameResponse,
|
PersonNameResponse,
|
||||||
PersonNameSearchOptions,
|
PersonNameSearchOptions,
|
||||||
PersonSearchOptions,
|
PersonSearchOptions,
|
||||||
PersonStatistics,
|
PersonStatistics,
|
||||||
|
UnassignFacesOptions,
|
||||||
UpdateFacesData,
|
UpdateFacesData,
|
||||||
} from 'src/interfaces/person.interface';
|
} from 'src/interfaces/person.interface';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
|
@ -39,12 +40,23 @@ export class PersonRepository implements IPersonRepository {
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.update()
|
.update()
|
||||||
.set({ personId: newPersonId })
|
.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();
|
.execute();
|
||||||
|
|
||||||
return result.affected ?? 0;
|
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> {
|
async delete(entities: PersonEntity[]): Promise<void> {
|
||||||
await this.personRepository.remove(entities);
|
await this.personRepository.remove(entities);
|
||||||
}
|
}
|
||||||
|
@ -53,21 +65,14 @@ export class PersonRepository implements IPersonRepository {
|
||||||
await this.personRepository.clear();
|
await this.personRepository.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllFaces({ sourceType }: DeleteAllFacesOptions): Promise<void> {
|
async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
|
||||||
if (!sourceType) {
|
|
||||||
return this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.assetFaceRepository
|
await this.assetFaceRepository
|
||||||
.createQueryBuilder('asset_faces')
|
.createQueryBuilder('asset_faces')
|
||||||
.delete()
|
.delete()
|
||||||
.andWhere('sourceType = :sourceType', { sourceType })
|
.andWhere('sourceType = :sourceType', { sourceType })
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search');
|
await this.vacuum({ reindexVectors: sourceType === SourceType.MACHINE_LEARNING });
|
||||||
if (sourceType === SourceType.MACHINE_LEARNING) {
|
|
||||||
await this.assetFaceRepository.query('REINDEX INDEX face_index');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllFaces(
|
getAllFaces(
|
||||||
|
@ -331,4 +336,13 @@ export class PersonRepository implements IPersonRepository {
|
||||||
const { id } = await this.personRepository.save(person);
|
const { id } = await this.personRepository.save(person);
|
||||||
return this.personRepository.findOneByOrFail({ id });
|
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();
|
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 });
|
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
|
||||||
personMock.getAll.mockResolvedValue({
|
personMock.getAll.mockResolvedValue({
|
||||||
items: [faceStub.face1.person, personStub.randomPerson],
|
items: [faceStub.face1.person, personStub.randomPerson],
|
||||||
|
@ -675,7 +675,8 @@ describe(PersonService.name, () => {
|
||||||
|
|
||||||
await sut.handleQueueRecognizeFaces({ force: true });
|
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([
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
name: JobName.FACIAL_RECOGNITION,
|
name: JobName.FACIAL_RECOGNITION,
|
||||||
|
|
|
@ -276,16 +276,6 @@ export class PersonService {
|
||||||
this.logger.debug(`Deleted ${people.length} people`);
|
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> {
|
async handlePersonCleanup(): Promise<JobStatus> {
|
||||||
const people = await this.repository.getAllWithoutFaces();
|
const people = await this.repository.getAllWithoutFaces();
|
||||||
await this.delete(people);
|
await this.delete(people);
|
||||||
|
@ -299,7 +289,7 @@ export class PersonService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
await this.repository.deleteAllFaces({ sourceType: SourceType.MACHINE_LEARNING });
|
await this.repository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING });
|
||||||
await this.handlePersonCleanup();
|
await this.handlePersonCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,7 +397,7 @@ export class PersonService {
|
||||||
const { waiting } = await this.jobRepository.getJobCounts(QueueName.FACIAL_RECOGNITION);
|
const { waiting } = await this.jobRepository.getJobCounts(QueueName.FACIAL_RECOGNITION);
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
await this.repository.deleteAllFaces({ sourceType: SourceType.MACHINE_LEARNING });
|
await this.repository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING });
|
||||||
await this.handlePersonCleanup();
|
await this.handlePersonCleanup();
|
||||||
} else if (waiting) {
|
} else if (waiting) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
|
|
|
@ -18,7 +18,7 @@ export const newPersonRepositoryMock = (): Mocked<IPersonRepository> => {
|
||||||
updateAll: vitest.fn(),
|
updateAll: vitest.fn(),
|
||||||
delete: vitest.fn(),
|
delete: vitest.fn(),
|
||||||
deleteAll: vitest.fn(),
|
deleteAll: vitest.fn(),
|
||||||
deleteAllFaces: vitest.fn(),
|
deleteFaces: vitest.fn(),
|
||||||
|
|
||||||
getStatistics: vitest.fn(),
|
getStatistics: vitest.fn(),
|
||||||
getAllFaces: vitest.fn(),
|
getAllFaces: vitest.fn(),
|
||||||
|
@ -26,6 +26,7 @@ export const newPersonRepositoryMock = (): Mocked<IPersonRepository> => {
|
||||||
getRandomFace: vitest.fn(),
|
getRandomFace: vitest.fn(),
|
||||||
|
|
||||||
reassignFaces: vitest.fn(),
|
reassignFaces: vitest.fn(),
|
||||||
|
unassignFaces: vitest.fn(),
|
||||||
createFaces: vitest.fn(),
|
createFaces: vitest.fn(),
|
||||||
replaceFaces: vitest.fn(),
|
replaceFaces: vitest.fn(),
|
||||||
getFaces: vitest.fn(),
|
getFaces: vitest.fn(),
|
||||||
|
|
Loading…
Reference in a new issue