mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +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) {
|
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 } }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
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) {
|
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 };
|
||||||
}
|
}
|
||||||
|
|
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 = {
|
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,
|
||||||
|
|
Loading…
Reference in a new issue