1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02: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:
martin 2023-10-24 15:12:42 +02:00 committed by GitHub
parent d4c23c8df8
commit 99c6f8fb13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 71 additions and 22 deletions

View File

@ -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 } }),
),
);
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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;
}

View 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")`);
}
}

View File

@ -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 };
}

View File

@ -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,