mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	feat(server): allow unassigned asset-faces (#4474)
* feat: un-assign people * regenerate api * edit migration script * fix: tests * fix: typeorm * fix: typo * fix: type * fix: migration * fix: update * fix: contraints * fix: remove set * feat: add assetId * remove assetId * remove unassignedFaces * fix: migration * regenerate api * fix: tests * remove changes to the api * fix: migration * fix migration * pr feedback * fix: revert change * fix: tests --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
		
							parent
							
								
									d4c23c8df8
								
							
						
					
					
						commit
						99c6f8fb13
					
				@ -392,8 +392,10 @@ export class AssetService {
 | 
			
		||||
 | 
			
		||||
    if (asset.faces) {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        asset.faces.map(({ assetId, personId }) =>
 | 
			
		||||
          this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_FACE, data: { assetId, personId } }),
 | 
			
		||||
        asset.faces.map(
 | 
			
		||||
          ({ assetId, personId }) =>
 | 
			
		||||
            personId != null &&
 | 
			
		||||
            this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_FACE, data: { assetId, personId } }),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -96,7 +96,9 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
 | 
			
		||||
    smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
 | 
			
		||||
    livePhotoVideoId: entity.livePhotoVideoId,
 | 
			
		||||
    tags: entity.tags?.map(mapTag),
 | 
			
		||||
    people: entity.faces?.map(mapFace).filter((person) => !person.isHidden),
 | 
			
		||||
    people: entity.faces
 | 
			
		||||
      ?.map(mapFace)
 | 
			
		||||
      .filter((person): person is PersonResponseDto => person !== null && !person.isHidden),
 | 
			
		||||
    checksum: entity.checksum.toString('base64'),
 | 
			
		||||
    stackParentId: entity.stackParentId,
 | 
			
		||||
    stack: withStack ? entity.stack?.map((a) => mapAsset(a, { stripMetadata })) ?? undefined : undefined,
 | 
			
		||||
 | 
			
		||||
@ -93,6 +93,10 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function mapFace(face: AssetFaceEntity): PersonResponseDto {
 | 
			
		||||
  return mapPerson(face.person);
 | 
			
		||||
export function mapFace(face: AssetFaceEntity): PersonResponseDto | null {
 | 
			
		||||
  if (face.person) {
 | 
			
		||||
    return mapPerson(face.person);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -345,7 +345,7 @@ export class PersonService {
 | 
			
		||||
    } as const;
 | 
			
		||||
 | 
			
		||||
    await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions);
 | 
			
		||||
    await this.repository.update({ id: personId, thumbnailPath });
 | 
			
		||||
    await this.repository.update({ id: person.id, thumbnailPath });
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -360,13 +360,20 @@ export class SearchService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private patchFaces(faces: AssetFaceEntity[]): OwnedFaceEntity[] {
 | 
			
		||||
    return faces.map((face) => ({
 | 
			
		||||
      id: this.asKey(face),
 | 
			
		||||
      ownerId: face.asset.ownerId,
 | 
			
		||||
      assetId: face.assetId,
 | 
			
		||||
      personId: face.personId,
 | 
			
		||||
      embedding: face.embedding,
 | 
			
		||||
    }));
 | 
			
		||||
    const results: OwnedFaceEntity[] = [];
 | 
			
		||||
    for (const face of faces) {
 | 
			
		||||
      if (face.personId) {
 | 
			
		||||
        results.push({
 | 
			
		||||
          id: this.asKey(face as AssetFaceId),
 | 
			
		||||
          ownerId: face.asset.ownerId,
 | 
			
		||||
          assetId: face.assetId,
 | 
			
		||||
          personId: face.personId,
 | 
			
		||||
          embedding: face.embedding,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return results;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private asKey(face: AssetFaceId): string {
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,17 @@
 | 
			
		||||
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
 | 
			
		||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
 | 
			
		||||
import { AssetEntity } from './asset.entity';
 | 
			
		||||
import { PersonEntity } from './person.entity';
 | 
			
		||||
 | 
			
		||||
@Entity('asset_faces')
 | 
			
		||||
export class AssetFaceEntity {
 | 
			
		||||
  @PrimaryColumn()
 | 
			
		||||
  @PrimaryGeneratedColumn('uuid')
 | 
			
		||||
  id!: string;
 | 
			
		||||
 | 
			
		||||
  @Column()
 | 
			
		||||
  assetId!: string;
 | 
			
		||||
 | 
			
		||||
  @PrimaryColumn()
 | 
			
		||||
  personId!: string;
 | 
			
		||||
  @Column({ nullable: true, type: 'uuid' })
 | 
			
		||||
  personId!: string | null;
 | 
			
		||||
 | 
			
		||||
  @Column({
 | 
			
		||||
    type: 'float4',
 | 
			
		||||
@ -38,6 +41,6 @@ export class AssetFaceEntity {
 | 
			
		||||
  @ManyToOne(() => AssetEntity, (asset) => asset.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
 | 
			
		||||
  asset!: AssetEntity;
 | 
			
		||||
 | 
			
		||||
  @ManyToOne(() => PersonEntity, (person) => person.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
 | 
			
		||||
  person!: PersonEntity;
 | 
			
		||||
  @ManyToOne(() => PersonEntity, (person) => person.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
 | 
			
		||||
  person!: PersonEntity | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								server/src/infra/migrations/1697272818851-UnassignFace.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								server/src/infra/migrations/1697272818851-UnassignFace.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
import { MigrationInterface, QueryRunner } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
export class UnassignFace1697272818851 implements MigrationInterface {
 | 
			
		||||
  name = 'UnassignFace1697272818851';
 | 
			
		||||
 | 
			
		||||
  public async up(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_faces" DROP CONSTRAINT "PK_bf339a24070dac7e71304ec530a"`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_faces" ADD COLUMN "id" UUID DEFAULT uuid_generate_v4() NOT NULL`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_faces" ADD CONSTRAINT "PK_6df76ab2eb6f5b57b7c2f1fc684" PRIMARY KEY ("id")`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_faces" DROP CONSTRAINT "FK_95ad7106dd7b484275443f580f9"`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_faces" ALTER COLUMN "personId" DROP NOT NULL`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_95ad7106dd7b484275443f580f9" FOREIGN KEY ("personId") REFERENCES "person"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async down(queryRunner: QueryRunner): Promise<void> {
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_faces" DROP CONSTRAINT "PK_6df76ab2eb6f5b57b7c2f1fc684"`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_faces" DROP COLUMN "id"`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_faces" DROP CONSTRAINT "FK_95ad7106dd7b484275443f580f9"`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_faces" ALTER COLUMN "personId" SET NOT NULL`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_95ad7106dd7b484275443f580f9" FOREIGN KEY ("personId") REFERENCES "person"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
 | 
			
		||||
    await queryRunner.query(`ALTER TABLE "asset_faces" ADD  CONSTRAINT "PK_bf339a24070dac7e71304ec530a" PRIMARY KEY ("assetId", "personId")`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -420,9 +420,10 @@ export class TypesenseRepository implements ISearchRepository {
 | 
			
		||||
    if (lat && lng && lat !== 0 && lng !== 0) {
 | 
			
		||||
      custom = { ...custom, geo: [lat, lng] };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const people =
 | 
			
		||||
      asset.faces?.filter((face) => !face.person.isHidden && face.person.name).map((face) => face.person.name) || [];
 | 
			
		||||
    const people = asset.faces
 | 
			
		||||
      ?.filter((face) => !face.person?.isHidden && face.person?.name)
 | 
			
		||||
      .map((face) => face.person?.name)
 | 
			
		||||
      .filter((name) => name !== undefined) as string[];
 | 
			
		||||
    if (people.length) {
 | 
			
		||||
      custom = { ...custom, people };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								server/test/fixtures/face.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								server/test/fixtures/face.stub.ts
									
									
									
									
										vendored
									
									
								
							@ -4,6 +4,7 @@ import { personStub } from './person.stub';
 | 
			
		||||
 | 
			
		||||
export const faceStub = {
 | 
			
		||||
  face1: Object.freeze<AssetFaceEntity>({
 | 
			
		||||
    id: 'assetFaceId',
 | 
			
		||||
    assetId: assetStub.image.id,
 | 
			
		||||
    asset: assetStub.image,
 | 
			
		||||
    personId: personStub.withName.id,
 | 
			
		||||
@ -17,6 +18,7 @@ export const faceStub = {
 | 
			
		||||
    imageWidth: 1024,
 | 
			
		||||
  }),
 | 
			
		||||
  primaryFace1: Object.freeze<AssetFaceEntity>({
 | 
			
		||||
    id: 'assetFaceId',
 | 
			
		||||
    assetId: assetStub.image.id,
 | 
			
		||||
    asset: assetStub.image,
 | 
			
		||||
    personId: personStub.primaryPerson.id,
 | 
			
		||||
@ -30,6 +32,7 @@ export const faceStub = {
 | 
			
		||||
    imageWidth: 1024,
 | 
			
		||||
  }),
 | 
			
		||||
  mergeFace1: Object.freeze<AssetFaceEntity>({
 | 
			
		||||
    id: 'assetFaceId',
 | 
			
		||||
    assetId: assetStub.image.id,
 | 
			
		||||
    asset: assetStub.image,
 | 
			
		||||
    personId: personStub.mergePerson.id,
 | 
			
		||||
@ -43,6 +46,7 @@ export const faceStub = {
 | 
			
		||||
    imageWidth: 1024,
 | 
			
		||||
  }),
 | 
			
		||||
  mergeFace2: Object.freeze<AssetFaceEntity>({
 | 
			
		||||
    id: 'assetFaceId',
 | 
			
		||||
    assetId: assetStub.image1.id,
 | 
			
		||||
    asset: assetStub.image1,
 | 
			
		||||
    personId: personStub.mergePerson.id,
 | 
			
		||||
@ -56,6 +60,7 @@ export const faceStub = {
 | 
			
		||||
    imageWidth: 1024,
 | 
			
		||||
  }),
 | 
			
		||||
  start: Object.freeze<AssetFaceEntity>({
 | 
			
		||||
    id: 'assetFaceId',
 | 
			
		||||
    assetId: assetStub.image.id,
 | 
			
		||||
    asset: assetStub.image,
 | 
			
		||||
    personId: personStub.newThumbnail.id,
 | 
			
		||||
@ -69,6 +74,7 @@ export const faceStub = {
 | 
			
		||||
    imageWidth: 1000,
 | 
			
		||||
  }),
 | 
			
		||||
  middle: Object.freeze<AssetFaceEntity>({
 | 
			
		||||
    id: 'assetFaceId',
 | 
			
		||||
    assetId: assetStub.image.id,
 | 
			
		||||
    asset: assetStub.image,
 | 
			
		||||
    personId: personStub.newThumbnail.id,
 | 
			
		||||
@ -82,6 +88,7 @@ export const faceStub = {
 | 
			
		||||
    imageWidth: 400,
 | 
			
		||||
  }),
 | 
			
		||||
  end: Object.freeze<AssetFaceEntity>({
 | 
			
		||||
    id: 'assetFaceId',
 | 
			
		||||
    assetId: assetStub.image.id,
 | 
			
		||||
    asset: assetStub.image,
 | 
			
		||||
    personId: personStub.newThumbnail.id,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user