77e6a6d78b
* feat: faces-from-metadata - Import face regions from metadata Implements immich-app#1692. - OpenAPI spec changes to accomodate metadata face import configs. New settings to enable the feature. - Updates admin UI compoments - ML faces detection/recognition & Exif/Metadata faces compatibility Signed-off-by: BugFest <bugfest.dev@pm.me> * chore(web): remove unused file confirm-enable-import-faces * chore(web): format metadata-settings * fix(server): faces-from-metadata tests and format * fix(server): code refinements, nullable face asset sourceType * fix(server): Add RegionInfo to ImmichTags interface * fix(server): deleteAllFaces sourceType param can be undefined * fix(server): exiftool-vendored 27.0.0 moves readArgs into ExifToolOptions * fix(server): rename isImportFacesFromMetadataEnabled to isFaceImportEnabled * fix(server): simplify sourceType conditional * fix(server): small fixes * fix(server): handling sourceType * fix(server): sourceType enum * fix(server): refactor metadata applyTaggedFaces * fix(server): create/update signature changes * fix(server): reduce computational cost of Person.getManyByName * fix(server): use faceList instead of faceSet * fix(server): Skip regions without Name defined * fix(mobile): Update open-api (face assets feature changes) * fix(server): Face-Person reconciliation with map/index * fix(server): tags.RegionInfo.AppliedToDimensions must be defined to process face-region * fix(server): fix shared-link.service.ts format * fix(mobile): Update open-api after branch update * simplify * fix(server): minor fixes * fix(server): person create/update methods type enforcement * fix(server): style fixes * fix(server): remove unused metadata code * fix(server): metadata faces unit tests * fix(server): top level config metadata category * fix(server): rename upsertFaces to replaceFaces * fix(server): remove sourceType when unnecessary * fix(server): sourceType as ENUM * fix(server): format fixes * fix(server): fix tests after sourceType ENUM change * fix(server): remove unnecessary JobItem cast * fix(server): fix asset enum imports * fix(open-api): add metadata config * fix(mobile): update open-api after metadata open-api spec changes * fix(web): update web/api metadata config * fix(server): remove duplicated sourceType def * fix(server): update generated sql queries * fix(e2e): tests for metadata face import feature * fix(web): Fix check:typescript * fix(e2e): update subproject ref * fix(server): revert format changes to pass format checks after ci * fix(mobile): update open-api * fix(server,movile,open-api,mobile): sourceType as DB data type * fix(e2e): upload face asset after enabling metadata face import * fix(web): simplify metadata admin settings and i18n keys * Update person.repository.ts Co-authored-by: Jason Rasmussen <jason@rasm.me> * fix(server): asset_faces.sourceType column not nullable * fix(server): simplified syntax * fix(e2e): use SDK for everything except the endpoint being tested * fix(e2e): fix test format * chore: clean up * chore: clean up * chore: update e2e/test-assets --------- Signed-off-by: BugFest <bugfest.dev@pm.me> Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com> Co-authored-by: Jason Rasmussen <jason@rasm.me>
192 lines
4.6 KiB
TypeScript
192 lines
4.6 KiB
TypeScript
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
import { Type } from 'class-transformer';
|
|
import { IsArray, IsInt, IsNotEmpty, IsString, Max, Min, ValidateNested } from 'class-validator';
|
|
import { DateTime } from 'luxon';
|
|
import { PropertyLifecycle } from 'src/decorators';
|
|
import { AuthDto } from 'src/dtos/auth.dto';
|
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
|
import { PersonEntity } from 'src/entities/person.entity';
|
|
import { SourceType } from 'src/enum';
|
|
import { IsDateStringFormat, MaxDateString, Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
|
|
|
export class PersonCreateDto {
|
|
/**
|
|
* Person name.
|
|
*/
|
|
@Optional()
|
|
@IsString()
|
|
name?: string;
|
|
|
|
/**
|
|
* Person date of birth.
|
|
* Note: the mobile app cannot currently set the birth date to null.
|
|
*/
|
|
@ApiProperty({ format: 'date' })
|
|
@MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' })
|
|
@IsDateStringFormat('yyyy-MM-dd')
|
|
@Optional({ nullable: true })
|
|
birthDate?: string | null;
|
|
|
|
/**
|
|
* Person visibility
|
|
*/
|
|
@ValidateBoolean({ optional: true })
|
|
isHidden?: boolean;
|
|
}
|
|
|
|
export class PersonUpdateDto extends PersonCreateDto {
|
|
/**
|
|
* Asset is used to get the feature face thumbnail.
|
|
*/
|
|
@Optional()
|
|
@IsString()
|
|
featureFaceAssetId?: string;
|
|
}
|
|
|
|
export class PeopleUpdateDto {
|
|
@IsArray()
|
|
@ValidateNested({ each: true })
|
|
@Type(() => PeopleUpdateItem)
|
|
people!: PeopleUpdateItem[];
|
|
}
|
|
|
|
export class PeopleUpdateItem extends PersonUpdateDto {
|
|
/**
|
|
* Person id.
|
|
*/
|
|
@IsString()
|
|
@IsNotEmpty()
|
|
id!: string;
|
|
}
|
|
|
|
export class MergePersonDto {
|
|
@ValidateUUID({ each: true })
|
|
ids!: string[];
|
|
}
|
|
|
|
export class PersonSearchDto {
|
|
@ValidateBoolean({ optional: true })
|
|
withHidden?: boolean;
|
|
|
|
/** Page number for pagination */
|
|
@ApiPropertyOptional()
|
|
@IsInt()
|
|
@Min(1)
|
|
@Type(() => Number)
|
|
page: number = 1;
|
|
|
|
/** Number of items per page */
|
|
@ApiPropertyOptional()
|
|
@IsInt()
|
|
@Min(1)
|
|
@Max(1000)
|
|
@Type(() => Number)
|
|
size: number = 500;
|
|
}
|
|
|
|
export class PersonResponseDto {
|
|
id!: string;
|
|
name!: string;
|
|
@ApiProperty({ format: 'date' })
|
|
birthDate!: string | null;
|
|
thumbnailPath!: string;
|
|
isHidden!: boolean;
|
|
@PropertyLifecycle({ addedAt: 'v1.107.0' })
|
|
updatedAt?: Date;
|
|
}
|
|
|
|
export class PersonWithFacesResponseDto extends PersonResponseDto {
|
|
faces!: AssetFaceWithoutPersonResponseDto[];
|
|
}
|
|
|
|
export class AssetFaceWithoutPersonResponseDto {
|
|
@ValidateUUID()
|
|
id!: string;
|
|
@ApiProperty({ type: 'integer' })
|
|
imageHeight!: number;
|
|
@ApiProperty({ type: 'integer' })
|
|
imageWidth!: number;
|
|
@ApiProperty({ type: 'integer' })
|
|
boundingBoxX1!: number;
|
|
@ApiProperty({ type: 'integer' })
|
|
boundingBoxX2!: number;
|
|
@ApiProperty({ type: 'integer' })
|
|
boundingBoxY1!: number;
|
|
@ApiProperty({ type: 'integer' })
|
|
boundingBoxY2!: number;
|
|
@ApiProperty({ enum: SourceType, enumName: 'SourceType' })
|
|
sourceType?: SourceType;
|
|
}
|
|
|
|
export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto {
|
|
person!: PersonResponseDto | null;
|
|
}
|
|
|
|
export class AssetFaceUpdateDto {
|
|
@IsArray()
|
|
@ValidateNested({ each: true })
|
|
@Type(() => AssetFaceUpdateItem)
|
|
data!: AssetFaceUpdateItem[];
|
|
}
|
|
|
|
export class FaceDto {
|
|
@ValidateUUID()
|
|
id!: string;
|
|
}
|
|
|
|
export class AssetFaceUpdateItem {
|
|
@ValidateUUID()
|
|
personId!: string;
|
|
|
|
@ValidateUUID()
|
|
assetId!: string;
|
|
}
|
|
|
|
export class PersonStatisticsResponseDto {
|
|
@ApiProperty({ type: 'integer' })
|
|
assets!: number;
|
|
}
|
|
|
|
export class PeopleResponseDto {
|
|
@ApiProperty({ type: 'integer' })
|
|
total!: number;
|
|
@ApiProperty({ type: 'integer' })
|
|
hidden!: number;
|
|
people!: PersonResponseDto[];
|
|
|
|
// TODO: make required after a few versions
|
|
@PropertyLifecycle({ addedAt: 'v1.110.0' })
|
|
hasNextPage?: boolean;
|
|
}
|
|
|
|
export function mapPerson(person: PersonEntity): PersonResponseDto {
|
|
return {
|
|
id: person.id,
|
|
name: person.name,
|
|
birthDate: person.birthDate,
|
|
thumbnailPath: person.thumbnailPath,
|
|
isHidden: person.isHidden,
|
|
updatedAt: person.updatedAt,
|
|
};
|
|
}
|
|
|
|
export function mapFacesWithoutPerson(face: AssetFaceEntity): AssetFaceWithoutPersonResponseDto {
|
|
return {
|
|
id: face.id,
|
|
imageHeight: face.imageHeight,
|
|
imageWidth: face.imageWidth,
|
|
boundingBoxX1: face.boundingBoxX1,
|
|
boundingBoxX2: face.boundingBoxX2,
|
|
boundingBoxY1: face.boundingBoxY1,
|
|
boundingBoxY2: face.boundingBoxY2,
|
|
sourceType: face.sourceType,
|
|
};
|
|
}
|
|
|
|
export function mapFaces(face: AssetFaceEntity, auth: AuthDto): AssetFaceResponseDto {
|
|
return {
|
|
...mapFacesWithoutPerson(face),
|
|
person: face.person?.ownerId === auth.user.id ? mapPerson(face.person) : null,
|
|
};
|
|
}
|