mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00: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
9 changed files with 71 additions and 22 deletions
|
@ -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…
Reference in a new issue