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) { if (asset.faces) {
await Promise.all( await Promise.all(
asset.faces.map(({ assetId, personId }) => asset.faces.map(
this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_FACE, data: { assetId, personId } }), ({ 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, smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
livePhotoVideoId: entity.livePhotoVideoId, livePhotoVideoId: entity.livePhotoVideoId,
tags: entity.tags?.map(mapTag), 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'), checksum: entity.checksum.toString('base64'),
stackParentId: entity.stackParentId, stackParentId: entity.stackParentId,
stack: withStack ? entity.stack?.map((a) => mapAsset(a, { stripMetadata })) ?? undefined : undefined, 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 { export function mapFace(face: AssetFaceEntity): PersonResponseDto | null {
return mapPerson(face.person); if (face.person) {
return mapPerson(face.person);
}
return null;
} }

View File

@ -345,7 +345,7 @@ export class PersonService {
} as const; } as const;
await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions); await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions);
await this.repository.update({ id: personId, thumbnailPath }); await this.repository.update({ id: person.id, thumbnailPath });
return true; return true;
} }

View File

@ -360,13 +360,20 @@ export class SearchService {
} }
private patchFaces(faces: AssetFaceEntity[]): OwnedFaceEntity[] { private patchFaces(faces: AssetFaceEntity[]): OwnedFaceEntity[] {
return faces.map((face) => ({ const results: OwnedFaceEntity[] = [];
id: this.asKey(face), for (const face of faces) {
ownerId: face.asset.ownerId, if (face.personId) {
assetId: face.assetId, results.push({
personId: face.personId, id: this.asKey(face as AssetFaceId),
embedding: face.embedding, ownerId: face.asset.ownerId,
})); assetId: face.assetId,
personId: face.personId,
embedding: face.embedding,
});
}
}
return results;
} }
private asKey(face: AssetFaceId): string { 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 { AssetEntity } from './asset.entity';
import { PersonEntity } from './person.entity'; import { PersonEntity } from './person.entity';
@Entity('asset_faces') @Entity('asset_faces')
export class AssetFaceEntity { export class AssetFaceEntity {
@PrimaryColumn() @PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
assetId!: string; assetId!: string;
@PrimaryColumn() @Column({ nullable: true, type: 'uuid' })
personId!: string; personId!: string | null;
@Column({ @Column({
type: 'float4', type: 'float4',
@ -38,6 +41,6 @@ export class AssetFaceEntity {
@ManyToOne(() => AssetEntity, (asset) => asset.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) @ManyToOne(() => AssetEntity, (asset) => asset.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
asset!: AssetEntity; asset!: AssetEntity;
@ManyToOne(() => PersonEntity, (person) => person.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) @ManyToOne(() => PersonEntity, (person) => person.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
person!: PersonEntity; 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) { if (lat && lng && lat !== 0 && lng !== 0) {
custom = { ...custom, geo: [lat, lng] }; custom = { ...custom, geo: [lat, lng] };
} }
const people = asset.faces
const people = ?.filter((face) => !face.person?.isHidden && face.person?.name)
asset.faces?.filter((face) => !face.person.isHidden && face.person.name).map((face) => face.person.name) || []; .map((face) => face.person?.name)
.filter((name) => name !== undefined) as string[];
if (people.length) { if (people.length) {
custom = { ...custom, people }; custom = { ...custom, people };
} }

View File

@ -4,6 +4,7 @@ import { personStub } from './person.stub';
export const faceStub = { export const faceStub = {
face1: Object.freeze<AssetFaceEntity>({ face1: Object.freeze<AssetFaceEntity>({
id: 'assetFaceId',
assetId: assetStub.image.id, assetId: assetStub.image.id,
asset: assetStub.image, asset: assetStub.image,
personId: personStub.withName.id, personId: personStub.withName.id,
@ -17,6 +18,7 @@ export const faceStub = {
imageWidth: 1024, imageWidth: 1024,
}), }),
primaryFace1: Object.freeze<AssetFaceEntity>({ primaryFace1: Object.freeze<AssetFaceEntity>({
id: 'assetFaceId',
assetId: assetStub.image.id, assetId: assetStub.image.id,
asset: assetStub.image, asset: assetStub.image,
personId: personStub.primaryPerson.id, personId: personStub.primaryPerson.id,
@ -30,6 +32,7 @@ export const faceStub = {
imageWidth: 1024, imageWidth: 1024,
}), }),
mergeFace1: Object.freeze<AssetFaceEntity>({ mergeFace1: Object.freeze<AssetFaceEntity>({
id: 'assetFaceId',
assetId: assetStub.image.id, assetId: assetStub.image.id,
asset: assetStub.image, asset: assetStub.image,
personId: personStub.mergePerson.id, personId: personStub.mergePerson.id,
@ -43,6 +46,7 @@ export const faceStub = {
imageWidth: 1024, imageWidth: 1024,
}), }),
mergeFace2: Object.freeze<AssetFaceEntity>({ mergeFace2: Object.freeze<AssetFaceEntity>({
id: 'assetFaceId',
assetId: assetStub.image1.id, assetId: assetStub.image1.id,
asset: assetStub.image1, asset: assetStub.image1,
personId: personStub.mergePerson.id, personId: personStub.mergePerson.id,
@ -56,6 +60,7 @@ export const faceStub = {
imageWidth: 1024, imageWidth: 1024,
}), }),
start: Object.freeze<AssetFaceEntity>({ start: Object.freeze<AssetFaceEntity>({
id: 'assetFaceId',
assetId: assetStub.image.id, assetId: assetStub.image.id,
asset: assetStub.image, asset: assetStub.image,
personId: personStub.newThumbnail.id, personId: personStub.newThumbnail.id,
@ -69,6 +74,7 @@ export const faceStub = {
imageWidth: 1000, imageWidth: 1000,
}), }),
middle: Object.freeze<AssetFaceEntity>({ middle: Object.freeze<AssetFaceEntity>({
id: 'assetFaceId',
assetId: assetStub.image.id, assetId: assetStub.image.id,
asset: assetStub.image, asset: assetStub.image,
personId: personStub.newThumbnail.id, personId: personStub.newThumbnail.id,
@ -82,6 +88,7 @@ export const faceStub = {
imageWidth: 400, imageWidth: 400,
}), }),
end: Object.freeze<AssetFaceEntity>({ end: Object.freeze<AssetFaceEntity>({
id: 'assetFaceId',
assetId: assetStub.image.id, assetId: assetStub.image.id,
asset: assetStub.image, asset: assetStub.image,
personId: personStub.newThumbnail.id, personId: personStub.newThumbnail.id,