From 4b55888d16a16fdc0fcdc3de143b59e7a3b0580d Mon Sep 17 00:00:00 2001 From: David Bourgault Date: Wed, 26 Feb 2025 21:53:21 -0500 Subject: [PATCH] fix: ensure manually tagged faces have proper source type (#16364) immich-app/immich#16062 added manual face tagging and deletion, but did not add a new 'SourceType'. The create faces would default to 'machine-learning' which is incorrect, and has the annoying downside that they will be wiped when the 'Refresh Faces' job is run. Handling of non-machine-learning faces was previously added in immich-app/immich#6455. This PR simply extends it to the new manually tagged faces. --- .../lib/model/asset_face_create_dto.dart | Bin 4407 -> 4690 bytes mobile/openapi/lib/model/source_type.dart | Bin 2590 -> 2701 bytes open-api/immich-openapi-specs.json | 12 +++++++- open-api/typescript-sdk/src/fetch-client.ts | 4 ++- server/src/db.d.ts | 2 +- server/src/dtos/person.dto.ts | 6 +++- server/src/enum.ts | 1 + .../1740619600996-AddManualSourceType.ts | 27 ++++++++++++++++++ server/src/services/person.service.ts | 1 + .../face-editor/face-editor.svelte | 3 +- 10 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 server/src/migrations/1740619600996-AddManualSourceType.ts diff --git a/mobile/openapi/lib/model/asset_face_create_dto.dart b/mobile/openapi/lib/model/asset_face_create_dto.dart index 29e8244a9623da9378af98f016d6507d11a5e241..d25a5d8b828ad218737ebebee46c86cf41e6dcba 100644 GIT binary patch delta 259 zcmdn4bV+4{1e0<}MrN^Iaeir0a%xCrL8^kSLNJ`8mz$VZnwT@$fk|Yt8q;)9gp2}0 z$L1YOZj2HLzJjeSvg`+DNk%S&?Bwq(FU1gCTNMO*^F3BqMmYpuJr?K~H0Ny=Vb@_c nMyRnuc5PZwey&$>ex61akcci)M<`Q|-E6}3kPX=#yn@R81PcL^js@4VeFj4Tv+V~R1G7mB)drI=5+X_+ B4ov_6 diff --git a/mobile/openapi/lib/model/source_type.dart b/mobile/openapi/lib/model/source_type.dart index 13c450b010d5cb48594133caf11f085f17383f52..4da5aba495d0e2d53a5e155281c4467b1210aa50 100644 GIT binary patch delta 99 zcmbOy(kr@Q0+X(SLUBoANoKM_a(-TMi9&8-UTI>Ef~`VuerZv1YDi^4s$RTCkvc?t kat~7%hXPd5W+7&FHs0jK;#38={9X=m7LWm(`8oL+0juvI@c;k- delta 21 dcmeAbohPzk0@LJ!Ol_NknVH!(cW|;Y0svO(2G0Nh diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 6a5700108..aeafc27ee 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8301,6 +8301,14 @@ "format": "uuid", "type": "string" }, + "sourceType": { + "allOf": [ + { + "$ref": "#/components/schemas/SourceType" + } + ], + "default": "manual" + }, "width": { "type": "integer" }, @@ -8317,6 +8325,7 @@ "imageHeight", "imageWidth", "personId", + "sourceType", "width", "x", "y" @@ -11952,7 +11961,8 @@ "SourceType": { "enum": [ "machine-learning", - "exif" + "exif", + "manual" ], "type": "string" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 7786c09d9..7237e0aac 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -529,6 +529,7 @@ export type AssetFaceCreateDto = { imageHeight: number; imageWidth: number; personId: string; + sourceType: SourceType; width: number; x: number; y: number; @@ -3453,7 +3454,8 @@ export enum AlbumUserRole { } export enum SourceType { MachineLearning = "machine-learning", - Exif = "exif" + Exif = "exif", + Manual = "manual" } export enum AssetTypeEnum { Image = "IMAGE", diff --git a/server/src/db.d.ts b/server/src/db.d.ts index 4a2adc917..bc88d7de3 100644 --- a/server/src/db.d.ts +++ b/server/src/db.d.ts @@ -29,7 +29,7 @@ export type JsonPrimitive = boolean | number | string | null; export type JsonValue = JsonArray | JsonObject | JsonPrimitive; -export type Sourcetype = 'exif' | 'machine-learning'; +export type Sourcetype = 'exif' | 'machine-learning' | 'manual'; export type Timestamp = ColumnType; diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index 0778c35b8..c4d3018be 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsArray, IsInt, IsNotEmpty, IsNumber, IsString, Max, Min, ValidateNested } from 'class-validator'; +import { IsArray, IsEnum, IsInt, IsNotEmpty, IsNumber, IsString, Max, Min, ValidateNested } from 'class-validator'; import { DateTime } from 'luxon'; import { PropertyLifecycle } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -194,6 +194,10 @@ export class AssetFaceCreateDto extends AssetFaceUpdateItem { @IsNotEmpty() @IsNumber() height!: number; + + @ApiProperty({ type: 'string', enum: SourceType, enumName: 'SourceType' }) + @IsEnum(SourceType) + sourceType: SourceType = SourceType.MANUAL; } export class AssetFaceDeleteDto { diff --git a/server/src/enum.ts b/server/src/enum.ts index 7bf4ca3dc..676e1d27d 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -228,6 +228,7 @@ export enum AssetStatus { export enum SourceType { MACHINE_LEARNING = 'machine-learning', EXIF = 'exif', + MANUAL = 'manual', } export enum ManualJobName { diff --git a/server/src/migrations/1740619600996-AddManualSourceType.ts b/server/src/migrations/1740619600996-AddManualSourceType.ts new file mode 100644 index 000000000..dd53312ad --- /dev/null +++ b/server/src/migrations/1740619600996-AddManualSourceType.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddManualSourceType1740619600996 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TYPE sourceType ADD VALUE 'manual'`); + } + + public async down(queryRunner: QueryRunner): Promise { + // Prior to this migration, manually tagged pictures had the 'machine-learning' type + await queryRunner.query( + `UPDATE "asset_faces" SET "sourceType" = 'machine-learning' WHERE "sourceType" = 'manual';`, + ); + + // Postgres doesn't allow removing values from enums, we have to recreate the type + await queryRunner.query(`ALTER TYPE sourceType RENAME TO oldSourceType`); + await queryRunner.query(`CREATE TYPE sourceType AS ENUM ('machine-learning', 'exif');`); + + await queryRunner.query(`ALTER TABLE "asset_faces" ALTER COLUMN "sourceType" DROP DEFAULT;`); + await queryRunner.query( + `ALTER TABLE "asset_faces" ALTER COLUMN "sourceType" TYPE sourceType USING "sourceType"::text::sourceType;`, + ); + await queryRunner.query( + `ALTER TABLE "asset_faces" ALTER COLUMN "sourceType" SET DEFAULT 'machine-learning'::sourceType;`, + ); + await queryRunner.query(`DROP TYPE oldSourceType;`); + } +} diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index dd998cc0f..62bf55a78 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -736,6 +736,7 @@ export class PersonService extends BaseService { boundingBoxX2: dto.x + dto.width, boundingBoxY1: dto.y, boundingBoxY2: dto.y + dto.height, + sourceType: dto.sourceType, }); } diff --git a/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte index afe45331e..bcc9ee687 100644 --- a/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte +++ b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte @@ -4,7 +4,7 @@ import { notificationController } from '$lib/components/shared-components/notification/notification'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { getPeopleThumbnailUrl } from '$lib/utils'; - import { getAllPeople, createFace, type PersonResponseDto } from '@immich/sdk'; + import { getAllPeople, createFace, type PersonResponseDto, SourceType } from '@immich/sdk'; import { Button } from '@immich/ui'; import { Canvas, InteractiveFabricObject, Rect } from 'fabric'; import { onMount } from 'svelte'; @@ -288,6 +288,7 @@ assetFaceCreateDto: { assetId, personId: person.id, + sourceType: SourceType.Manual, ...data, }, });