mirror of
https://github.com/immich-app/immich.git
synced 2026-05-22 07:32:32 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c66d7e97cb |
@@ -8409,7 +8409,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/MergePersonDto"
|
"$ref": "#/components/schemas/MergeFaceClusterDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -18878,10 +18878,10 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"MergePersonDto": {
|
"MergeFaceClusterDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"ids": {
|
"ids": {
|
||||||
"description": "Person IDs to merge",
|
"description": "Face cluster IDs to merge",
|
||||||
"items": {
|
"items": {
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -23055,6 +23055,70 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SyncAssetFaceV3": {
|
||||||
|
"properties": {
|
||||||
|
"assetId": {
|
||||||
|
"description": "Asset ID",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"boundingBoxX1": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"boundingBoxX2": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"boundingBoxY1": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"boundingBoxY2": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"deletedAt": {
|
||||||
|
"description": "Face deleted at",
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"faceClusterId": {
|
||||||
|
"description": "Face cluster ID",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "Asset face ID",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"imageHeight": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"imageWidth": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"isVisible": {
|
||||||
|
"description": "Is the face visible in the asset",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"sourceType": {
|
||||||
|
"description": "Source type",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"assetId",
|
||||||
|
"boundingBoxX1",
|
||||||
|
"boundingBoxX2",
|
||||||
|
"boundingBoxY1",
|
||||||
|
"boundingBoxY2",
|
||||||
|
"deletedAt",
|
||||||
|
"faceClusterId",
|
||||||
|
"id",
|
||||||
|
"imageHeight",
|
||||||
|
"imageWidth",
|
||||||
|
"isVisible",
|
||||||
|
"sourceType"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SyncAssetMetadataDeleteV1": {
|
"SyncAssetMetadataDeleteV1": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"assetId": {
|
"assetId": {
|
||||||
@@ -23348,10 +23412,14 @@
|
|||||||
"StackV1",
|
"StackV1",
|
||||||
"StackDeleteV1",
|
"StackDeleteV1",
|
||||||
"PersonV1",
|
"PersonV1",
|
||||||
|
"PersonV2",
|
||||||
"PersonDeleteV1",
|
"PersonDeleteV1",
|
||||||
"AssetFaceV1",
|
"AssetFaceV1",
|
||||||
"AssetFaceV2",
|
"AssetFaceV2",
|
||||||
|
"AssetFaceV3",
|
||||||
"AssetFaceDeleteV1",
|
"AssetFaceDeleteV1",
|
||||||
|
"FaceClusterV1",
|
||||||
|
"FaceClusterDeleteV1",
|
||||||
"UserMetadataV1",
|
"UserMetadataV1",
|
||||||
"UserMetadataDeleteV1",
|
"UserMetadataDeleteV1",
|
||||||
"SyncAckV1",
|
"SyncAckV1",
|
||||||
@@ -23360,6 +23428,47 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"SyncFaceClusterDeleteV1": {
|
||||||
|
"properties": {
|
||||||
|
"faceClusterId": {
|
||||||
|
"description": "Face cluster ID",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"faceClusterId"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"SyncFaceClusterV1": {
|
||||||
|
"properties": {
|
||||||
|
"createdAt": {
|
||||||
|
"description": "Created at",
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "Face cluster ID",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"description": "Owner ID",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"description": "Updated at",
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"createdAt",
|
||||||
|
"id",
|
||||||
|
"ownerId",
|
||||||
|
"updatedAt"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SyncMemoryAssetDeleteV1": {
|
"SyncMemoryAssetDeleteV1": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"assetId": {
|
"assetId": {
|
||||||
@@ -23602,6 +23711,75 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SyncPersonV2": {
|
||||||
|
"properties": {
|
||||||
|
"birthDate": {
|
||||||
|
"description": "Birth date",
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"description": "Color",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"description": "Created at",
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"faceAssetId": {
|
||||||
|
"description": "Face asset ID",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"faceClusterId": {
|
||||||
|
"description": "Face cluster ID",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "Person ID",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isFavorite": {
|
||||||
|
"description": "Is favorite",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isHidden": {
|
||||||
|
"description": "Is hidden",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "Person name",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"description": "Owner ID",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"description": "Updated at",
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"birthDate",
|
||||||
|
"color",
|
||||||
|
"createdAt",
|
||||||
|
"faceAssetId",
|
||||||
|
"faceClusterId",
|
||||||
|
"id",
|
||||||
|
"isFavorite",
|
||||||
|
"isHidden",
|
||||||
|
"name",
|
||||||
|
"ownerId",
|
||||||
|
"updatedAt"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SyncRequestType": {
|
"SyncRequestType": {
|
||||||
"description": "Sync request types",
|
"description": "Sync request types",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -23624,8 +23802,11 @@
|
|||||||
"StacksV1",
|
"StacksV1",
|
||||||
"UsersV1",
|
"UsersV1",
|
||||||
"PeopleV1",
|
"PeopleV1",
|
||||||
|
"PeopleV2",
|
||||||
"AssetFacesV1",
|
"AssetFacesV1",
|
||||||
"AssetFacesV2",
|
"AssetFacesV2",
|
||||||
|
"AssetFacesV3",
|
||||||
|
"FaceClusterV1",
|
||||||
"UserMetadataV1"
|
"UserMetadataV1"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
|||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
AssetFaceUpdateDto,
|
AssetFaceUpdateDto,
|
||||||
MergePersonDto,
|
MergeFaceClusterDto,
|
||||||
PeopleResponseDto,
|
PeopleResponseDto,
|
||||||
PeopleUpdateDto,
|
PeopleUpdateDto,
|
||||||
PersonCreateDto,
|
PersonCreateDto,
|
||||||
@@ -182,7 +182,7 @@ export class PersonController {
|
|||||||
mergePerson(
|
mergePerson(
|
||||||
@Auth() auth: AuthDto,
|
@Auth() auth: AuthDto,
|
||||||
@Param() { id }: UUIDParamDto,
|
@Param() { id }: UUIDParamDto,
|
||||||
@Body() dto: MergePersonDto,
|
@Body() dto: MergeFaceClusterDto,
|
||||||
): Promise<BulkIdResponseDto[]> {
|
): Promise<BulkIdResponseDto[]> {
|
||||||
return this.service.mergePerson(auth, id, dto);
|
return this.service.mergePerson(auth, id, dto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export type AssetFace = {
|
|||||||
boundingBoxY2: number;
|
boundingBoxY2: number;
|
||||||
imageHeight: number;
|
imageHeight: number;
|
||||||
imageWidth: number;
|
imageWidth: number;
|
||||||
personId: string | null;
|
faceClusterId: string | null;
|
||||||
sourceType: SourceType;
|
sourceType: SourceType;
|
||||||
person?: ShallowDehydrateObject<Person> | null;
|
person?: ShallowDehydrateObject<Person> | null;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ export class PeopleUpdateItem extends PersonUpdateDto {
|
|||||||
id!: string;
|
id!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MergePersonDto {
|
export class MergeFaceClusterDto {
|
||||||
@ValidateUUID({ each: true, description: 'Person IDs to merge' })
|
@ValidateUUID({ each: true, description: 'Face cluster IDs to merge' })
|
||||||
ids!: string[];
|
ids!: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -411,6 +411,18 @@ export class SyncPersonV1 {
|
|||||||
faceAssetId!: string | null;
|
faceAssetId!: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExtraModel()
|
||||||
|
export class SyncPersonV2 extends SyncPersonV1 {
|
||||||
|
@ApiProperty({ description: 'Face cluster ID' })
|
||||||
|
faceClusterId!: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncPersonV2ToV1(personV2: SyncPersonV2): SyncPersonV1 {
|
||||||
|
const { faceClusterId: _, ...personV1 } = personV2;
|
||||||
|
|
||||||
|
return personV1;
|
||||||
|
}
|
||||||
|
|
||||||
@ExtraModel()
|
@ExtraModel()
|
||||||
export class SyncPersonDeleteV1 {
|
export class SyncPersonDeleteV1 {
|
||||||
@ApiProperty({ description: 'Person ID' })
|
@ApiProperty({ description: 'Person ID' })
|
||||||
@@ -449,6 +461,40 @@ export class SyncAssetFaceV2 extends SyncAssetFaceV1 {
|
|||||||
isVisible!: boolean;
|
isVisible!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExtraModel()
|
||||||
|
export class SyncAssetFaceV3 {
|
||||||
|
@ApiProperty({ description: 'Asset face ID' })
|
||||||
|
id!: string;
|
||||||
|
@ApiProperty({ description: 'Asset ID' })
|
||||||
|
assetId!: string;
|
||||||
|
@ApiProperty({ description: 'Face cluster ID' })
|
||||||
|
faceClusterId!: string | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
imageWidth!: number;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
imageHeight!: number;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
boundingBoxX1!: number;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
boundingBoxY1!: number;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
boundingBoxX2!: number;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
boundingBoxY2!: number;
|
||||||
|
@ApiProperty({ description: 'Source type' })
|
||||||
|
sourceType!: string;
|
||||||
|
@ApiProperty({ description: 'Face deleted at' })
|
||||||
|
deletedAt!: Date | null;
|
||||||
|
@ApiProperty({ description: 'Is the face visible in the asset' })
|
||||||
|
isVisible!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncAssetFaceV3ToV2(faceV3: SyncAssetFaceV3, personId: string | null): SyncAssetFaceV2 {
|
||||||
|
const { faceClusterId: _, ...face } = faceV3;
|
||||||
|
|
||||||
|
return { ...face, personId };
|
||||||
|
}
|
||||||
|
|
||||||
export function syncAssetFaceV2ToV1(faceV2: SyncAssetFaceV2): SyncAssetFaceV1 {
|
export function syncAssetFaceV2ToV1(faceV2: SyncAssetFaceV2): SyncAssetFaceV1 {
|
||||||
const { deletedAt: _, isVisible: __, ...faceV1 } = faceV2;
|
const { deletedAt: _, isVisible: __, ...faceV1 } = faceV2;
|
||||||
|
|
||||||
@@ -461,6 +507,24 @@ export class SyncAssetFaceDeleteV1 {
|
|||||||
assetFaceId!: string;
|
assetFaceId!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExtraModel()
|
||||||
|
export class SyncFaceClusterV1 {
|
||||||
|
@ApiProperty({ description: 'Face cluster ID' })
|
||||||
|
id!: string;
|
||||||
|
@ApiProperty({ description: 'Created at' })
|
||||||
|
createdAt!: Date;
|
||||||
|
@ApiProperty({ description: 'Updated at' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
@ApiProperty({ description: 'Owner ID' })
|
||||||
|
ownerId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExtraModel()
|
||||||
|
export class SyncFaceClusterDeleteV1 {
|
||||||
|
@ApiProperty({ description: 'Face cluster ID' })
|
||||||
|
faceClusterId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
@ExtraModel()
|
@ExtraModel()
|
||||||
export class SyncUserMetadataV1 {
|
export class SyncUserMetadataV1 {
|
||||||
@ApiProperty({ description: 'User ID' })
|
@ApiProperty({ description: 'User ID' })
|
||||||
@@ -530,10 +594,14 @@ export type SyncItem = {
|
|||||||
[SyncEntityType.PartnerStackDeleteV1]: SyncStackDeleteV1;
|
[SyncEntityType.PartnerStackDeleteV1]: SyncStackDeleteV1;
|
||||||
[SyncEntityType.PartnerStackV1]: SyncStackV1;
|
[SyncEntityType.PartnerStackV1]: SyncStackV1;
|
||||||
[SyncEntityType.PersonV1]: SyncPersonV1;
|
[SyncEntityType.PersonV1]: SyncPersonV1;
|
||||||
|
[SyncEntityType.PersonV2]: SyncPersonV2;
|
||||||
[SyncEntityType.PersonDeleteV1]: SyncPersonDeleteV1;
|
[SyncEntityType.PersonDeleteV1]: SyncPersonDeleteV1;
|
||||||
[SyncEntityType.AssetFaceV1]: SyncAssetFaceV1;
|
[SyncEntityType.AssetFaceV1]: SyncAssetFaceV1;
|
||||||
[SyncEntityType.AssetFaceV2]: SyncAssetFaceV2;
|
[SyncEntityType.AssetFaceV2]: SyncAssetFaceV2;
|
||||||
|
[SyncEntityType.AssetFaceV3]: SyncAssetFaceV3;
|
||||||
[SyncEntityType.AssetFaceDeleteV1]: SyncAssetFaceDeleteV1;
|
[SyncEntityType.AssetFaceDeleteV1]: SyncAssetFaceDeleteV1;
|
||||||
|
[SyncEntityType.FaceClusterV1]: SyncFaceClusterV1;
|
||||||
|
[SyncEntityType.FaceClusterDeleteV1]: SyncFaceClusterDeleteV1;
|
||||||
[SyncEntityType.UserMetadataV1]: SyncUserMetadataV1;
|
[SyncEntityType.UserMetadataV1]: SyncUserMetadataV1;
|
||||||
[SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1;
|
[SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1;
|
||||||
[SyncEntityType.SyncAckV1]: SyncAckV1;
|
[SyncEntityType.SyncAckV1]: SyncAckV1;
|
||||||
|
|||||||
@@ -735,8 +735,11 @@ export enum SyncRequestType {
|
|||||||
StacksV1 = 'StacksV1',
|
StacksV1 = 'StacksV1',
|
||||||
UsersV1 = 'UsersV1',
|
UsersV1 = 'UsersV1',
|
||||||
PeopleV1 = 'PeopleV1',
|
PeopleV1 = 'PeopleV1',
|
||||||
|
PeopleV2 = 'PeopleV2',
|
||||||
AssetFacesV1 = 'AssetFacesV1',
|
AssetFacesV1 = 'AssetFacesV1',
|
||||||
AssetFacesV2 = 'AssetFacesV2',
|
AssetFacesV2 = 'AssetFacesV2',
|
||||||
|
AssetFacesV3 = 'AssetFacesV3',
|
||||||
|
FaceClusterV1 = 'FaceClusterV1',
|
||||||
UserMetadataV1 = 'UserMetadataV1',
|
UserMetadataV1 = 'UserMetadataV1',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,12 +797,17 @@ export enum SyncEntityType {
|
|||||||
StackDeleteV1 = 'StackDeleteV1',
|
StackDeleteV1 = 'StackDeleteV1',
|
||||||
|
|
||||||
PersonV1 = 'PersonV1',
|
PersonV1 = 'PersonV1',
|
||||||
|
PersonV2 = 'PersonV2',
|
||||||
PersonDeleteV1 = 'PersonDeleteV1',
|
PersonDeleteV1 = 'PersonDeleteV1',
|
||||||
|
|
||||||
AssetFaceV1 = 'AssetFaceV1',
|
AssetFaceV1 = 'AssetFaceV1',
|
||||||
AssetFaceV2 = 'AssetFaceV2',
|
AssetFaceV2 = 'AssetFaceV2',
|
||||||
|
AssetFaceV3 = 'AssetFaceV3',
|
||||||
AssetFaceDeleteV1 = 'AssetFaceDeleteV1',
|
AssetFaceDeleteV1 = 'AssetFaceDeleteV1',
|
||||||
|
|
||||||
|
FaceClusterV1 = 'FaceClusterV1',
|
||||||
|
FaceClusterDeleteV1 = 'FaceClusterDeleteV1',
|
||||||
|
|
||||||
UserMetadataV1 = 'UserMetadataV1',
|
UserMetadataV1 = 'UserMetadataV1',
|
||||||
UserMetadataDeleteV1 = 'UserMetadataDeleteV1',
|
UserMetadataDeleteV1 = 'UserMetadataDeleteV1',
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ export interface AssetFaceId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateFacesData {
|
export interface UpdateFacesData {
|
||||||
oldPersonId?: string;
|
oldFaceClusterId?: string;
|
||||||
faceIds?: string[];
|
faceIds?: string[];
|
||||||
newPersonId: string;
|
newFaceClusterId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PersonStatistics {
|
export interface PersonStatistics {
|
||||||
@@ -54,7 +54,7 @@ export interface GetAllPeopleOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GetAllFacesOptions {
|
export interface GetAllFacesOptions {
|
||||||
personId?: string | null;
|
faceClusterId?: string | null;
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
sourceType?: SourceType;
|
sourceType?: SourceType;
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ export type SelectFaceOptions = (keyof Selectable<AssetFaceTable>)[];
|
|||||||
|
|
||||||
const withPerson = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
|
const withPerson = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
|
||||||
return jsonObjectFrom(
|
return jsonObjectFrom(
|
||||||
eb.selectFrom('person').selectAll('person').whereRef('person.id', '=', 'asset_face.personId'),
|
eb.selectFrom('person').selectAll('person').whereRef('person.faceClusterId', '=', 'asset_face.faceClusterId'),
|
||||||
).as('person');
|
).as('person');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,11 +80,11 @@ export class PersonRepository {
|
|||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
|
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
|
||||||
async reassignFaces({ oldPersonId, faceIds, newPersonId }: UpdateFacesData): Promise<number> {
|
async reassignFaces({ oldFaceClusterId, faceIds, newFaceClusterId }: UpdateFacesData): Promise<number> {
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.updateTable('asset_face')
|
.updateTable('asset_face')
|
||||||
.set({ personId: newPersonId })
|
.set({ faceClusterId: newFaceClusterId })
|
||||||
.$if(!!oldPersonId, (qb) => qb.where('asset_face.personId', '=', oldPersonId!))
|
.$if(!!oldFaceClusterId, (qb) => qb.where('asset_face.faceClusterId', '=', oldFaceClusterId!))
|
||||||
.$if(!!faceIds, (qb) => qb.where('asset_face.id', 'in', faceIds!))
|
.$if(!!faceIds, (qb) => qb.where('asset_face.id', 'in', faceIds!))
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ export class PersonRepository {
|
|||||||
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
|
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.updateTable('asset_face')
|
.updateTable('asset_face')
|
||||||
.set({ personId: null })
|
.set({ faceClusterId: null })
|
||||||
.where('asset_face.sourceType', '=', sourceType)
|
.where('asset_face.sourceType', '=', sourceType)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
@@ -117,8 +117,8 @@ export class PersonRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.$if(options.personId === null, (qb) => qb.where('asset_face.personId', 'is', null))
|
.$if(options.faceClusterId === null, (qb) => qb.where('asset_face.faceClusterId', 'is', null))
|
||||||
.$if(!!options.personId, (qb) => qb.where('asset_face.personId', '=', options.personId!))
|
.$if(!!options.faceClusterId, (qb) => qb.where('asset_face.faceClusterId', '=', options.faceClusterId!))
|
||||||
.$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!))
|
.$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!))
|
||||||
.$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!))
|
.$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!))
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
@@ -153,7 +153,7 @@ export class PersonRepository {
|
|||||||
const items = await this.db
|
const items = await this.db
|
||||||
.selectFrom('person')
|
.selectFrom('person')
|
||||||
.selectAll('person')
|
.selectAll('person')
|
||||||
.innerJoin('asset_face', 'asset_face.personId', 'person.id')
|
.innerJoin('asset_face', 'asset_face.faceClusterId', 'person.faceClusterId')
|
||||||
.innerJoin('asset', (join) =>
|
.innerJoin('asset', (join) =>
|
||||||
join
|
join
|
||||||
.onRef('asset_face.assetId', '=', 'asset.id')
|
.onRef('asset_face.assetId', '=', 'asset.id')
|
||||||
@@ -209,7 +209,7 @@ export class PersonRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('person')
|
.selectFrom('person')
|
||||||
.selectAll('person')
|
.selectAll('person')
|
||||||
.leftJoin('asset_face', 'asset_face.personId', 'person.id')
|
.leftJoin('asset_face', 'asset_face.faceClusterId', 'person.faceClusterId')
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.where('asset_face.isVisible', 'is', true)
|
.where('asset_face.isVisible', 'is', true)
|
||||||
.having((eb) => eb.fn.count('asset_face.assetId'), '=', 0)
|
.having((eb) => eb.fn.count('asset_face.assetId'), '=', 0)
|
||||||
@@ -248,7 +248,7 @@ export class PersonRepository {
|
|||||||
getFaceForFacialRecognitionJob(id: string) {
|
getFaceForFacialRecognitionJob(id: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select(['asset_face.id', 'asset_face.personId', 'asset_face.sourceType'])
|
.select(['asset_face.id', 'asset_face.faceClusterId', 'asset_face.sourceType'])
|
||||||
.select((eb) =>
|
.select((eb) =>
|
||||||
jsonObjectFrom(
|
jsonObjectFrom(
|
||||||
eb
|
eb
|
||||||
@@ -297,10 +297,10 @@ export class PersonRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||||
async reassignFace(assetFaceId: string, newPersonId: string): Promise<number> {
|
async reassignFace(assetFaceId: string, newFaceClusterId: string): Promise<number> {
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.updateTable('asset_face')
|
.updateTable('asset_face')
|
||||||
.set({ personId: newPersonId })
|
.set({ faceClusterId: newFaceClusterId })
|
||||||
.where('asset_face.id', '=', assetFaceId)
|
.where('asset_face.id', '=', assetFaceId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
@@ -346,13 +346,13 @@ export class PersonRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
async getStatistics(personId: string): Promise<PersonStatistics> {
|
async getStatistics(faceClusterId: string): Promise<PersonStatistics> {
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.leftJoin('asset', (join) =>
|
.leftJoin('asset', (join) =>
|
||||||
join
|
join
|
||||||
.onRef('asset.id', '=', 'asset_face.assetId')
|
.onRef('asset.id', '=', 'asset_face.assetId')
|
||||||
.on('asset_face.personId', '=', personId)
|
.on('asset_face.faceClusterId', '=', faceClusterId)
|
||||||
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
|
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
|
||||||
.on('asset.deletedAt', 'is', null),
|
.on('asset.deletedAt', 'is', null),
|
||||||
)
|
)
|
||||||
@@ -375,7 +375,7 @@ export class PersonRepository {
|
|||||||
eb.exists((eb) =>
|
eb.exists((eb) =>
|
||||||
eb
|
eb
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.whereRef('asset_face.personId', '=', 'person.id')
|
.whereRef('asset_face.faceClusterId', '=', 'person.faceClusterId')
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.where('asset_face.isVisible', '=', true)
|
.where('asset_face.isVisible', '=', true)
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
@@ -395,7 +395,16 @@ export class PersonRepository {
|
|||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
create(person: Insertable<PersonTable>) {
|
async create(person: Insertable<PersonTable>) {
|
||||||
|
if (!person.faceClusterId) {
|
||||||
|
const { id } = await this.db
|
||||||
|
.insertInto('face_cluster')
|
||||||
|
.values({ ownerId: person.ownerId })
|
||||||
|
.returning('id')
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
person.faceClusterId = id;
|
||||||
|
}
|
||||||
|
|
||||||
return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow();
|
return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,18 +495,19 @@ export class PersonRepository {
|
|||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.select(withPerson)
|
.select(withPerson)
|
||||||
|
.innerJoin('person', (join) => join.onRef('person.faceClusterId', '=', 'asset_face.faceClusterId'))
|
||||||
|
.where('person.id', 'in', personIds)
|
||||||
.where('asset_face.assetId', 'in', assetIds)
|
.where('asset_face.assetId', 'in', assetIds)
|
||||||
.where('asset_face.personId', 'in', personIds)
|
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
getRandomFace(personId: string) {
|
getRandomFace(faceClusterId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.where('asset_face.personId', '=', personId)
|
.where('asset_face.faceClusterId', '=', faceClusterId)
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.where('asset_face.isVisible', 'is', true)
|
.where('asset_face.isVisible', 'is', true)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
@@ -584,7 +594,9 @@ export class PersonRepository {
|
|||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select('asset_face.id')
|
.select('asset_face.id')
|
||||||
.where('asset_face.assetId', '=', assetId)
|
.where('asset_face.assetId', '=', assetId)
|
||||||
.where('asset_face.personId', '=', personId)
|
.innerJoin('person', (join) =>
|
||||||
|
join.onRef('person.faceClusterId', '=', 'asset_face.faceClusterId').on('person.id', '=', personId),
|
||||||
|
)
|
||||||
.innerJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId').on('asset.isOffline', '=', false))
|
.innerJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId').on('asset.isOffline', '=', false))
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,15 +338,15 @@ export class SearchRepository {
|
|||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select([
|
.select([
|
||||||
'asset_face.id',
|
'asset_face.id',
|
||||||
'asset_face.personId',
|
'asset_face.faceClusterId',
|
||||||
sql<number>`face_search.embedding <=> ${embedding}`.as('distance'),
|
sql<number>`face_search.embedding <=> ${embedding}`.as('distance'),
|
||||||
])
|
])
|
||||||
.innerJoin('asset', 'asset.id', 'asset_face.assetId')
|
.innerJoin('asset', 'asset.id', 'asset_face.assetId')
|
||||||
.innerJoin('face_search', 'face_search.faceId', 'asset_face.id')
|
.innerJoin('face_search', 'face_search.faceId', 'asset_face.id')
|
||||||
.leftJoin('person', 'person.id', 'asset_face.personId')
|
.leftJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
|
||||||
.where('asset.ownerId', '=', anyUuid(userIds))
|
.where('asset.ownerId', '=', anyUuid(userIds))
|
||||||
.where('asset.deletedAt', 'is', null)
|
.where('asset.deletedAt', 'is', null)
|
||||||
.$if(!!hasPerson, (qb) => qb.where('asset_face.personId', 'is not', null))
|
.$if(!!hasPerson, (qb) => qb.where('asset_face.faceClusterId', 'is not', null))
|
||||||
.$if(!!minBirthDate, (qb) =>
|
.$if(!!minBirthDate, (qb) =>
|
||||||
qb.where((eb) =>
|
qb.where((eb) =>
|
||||||
eb.or([eb('person.birthDate', 'is', null), eb('person.birthDate', '<=', minBirthDate!)]),
|
eb.or([eb('person.birthDate', 'is', null), eb('person.birthDate', '<=', minBirthDate!)]),
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export class SyncRepository {
|
|||||||
assetFace: AssetFaceSync;
|
assetFace: AssetFaceSync;
|
||||||
assetMetadata: AssetMetadataSync;
|
assetMetadata: AssetMetadataSync;
|
||||||
authUser: AuthUserSync;
|
authUser: AuthUserSync;
|
||||||
|
faceCluster: FaceClusterSync;
|
||||||
memory: MemorySync;
|
memory: MemorySync;
|
||||||
memoryToAsset: MemoryToAssetSync;
|
memoryToAsset: MemoryToAssetSync;
|
||||||
partner: PartnerSync;
|
partner: PartnerSync;
|
||||||
@@ -80,6 +81,7 @@ export class SyncRepository {
|
|||||||
this.assetFace = new AssetFaceSync(this.db);
|
this.assetFace = new AssetFaceSync(this.db);
|
||||||
this.assetMetadata = new AssetMetadataSync(this.db);
|
this.assetMetadata = new AssetMetadataSync(this.db);
|
||||||
this.authUser = new AuthUserSync(this.db);
|
this.authUser = new AuthUserSync(this.db);
|
||||||
|
this.faceCluster = new FaceClusterSync(this.db);
|
||||||
this.memory = new MemorySync(this.db);
|
this.memory = new MemorySync(this.db);
|
||||||
this.memoryToAsset = new MemoryToAssetSync(this.db);
|
this.memoryToAsset = new MemoryToAssetSync(this.db);
|
||||||
this.partner = new PartnerSync(this.db);
|
this.partner = new PartnerSync(this.db);
|
||||||
@@ -447,6 +449,7 @@ class PersonSync extends BaseSync {
|
|||||||
'color',
|
'color',
|
||||||
'updateId',
|
'updateId',
|
||||||
'faceAssetId',
|
'faceAssetId',
|
||||||
|
'faceClusterId',
|
||||||
])
|
])
|
||||||
.where('ownerId', '=', options.userId)
|
.where('ownerId', '=', options.userId)
|
||||||
.stream();
|
.stream();
|
||||||
@@ -473,7 +476,7 @@ class AssetFaceSync extends BaseSync {
|
|||||||
.select([
|
.select([
|
||||||
'asset_face.id',
|
'asset_face.id',
|
||||||
'assetId',
|
'assetId',
|
||||||
'personId',
|
'faceClusterId',
|
||||||
'imageWidth',
|
'imageWidth',
|
||||||
'imageHeight',
|
'imageHeight',
|
||||||
'boundingBoxX1',
|
'boundingBoxX1',
|
||||||
@@ -485,6 +488,8 @@ class AssetFaceSync extends BaseSync {
|
|||||||
'asset_face.deletedAt',
|
'asset_face.deletedAt',
|
||||||
'asset_face.updateId',
|
'asset_face.updateId',
|
||||||
])
|
])
|
||||||
|
.leftJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
|
||||||
|
.select('person.id as personId')
|
||||||
.leftJoin('asset', 'asset.id', 'asset_face.assetId')
|
.leftJoin('asset', 'asset.id', 'asset_face.assetId')
|
||||||
.where('asset.ownerId', '=', options.userId)
|
.where('asset.ownerId', '=', options.userId)
|
||||||
.where('asset_face.isVisible', '=', true)
|
.where('asset_face.isVisible', '=', true)
|
||||||
@@ -492,6 +497,35 @@ class AssetFaceSync extends BaseSync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FaceClusterSync extends BaseSync {
|
||||||
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
|
getDeletes(options: SyncQueryOptions) {
|
||||||
|
return this.auditQuery('face_cluster_audit', options)
|
||||||
|
.select(['face_cluster_audit.id', 'face_cluster_audit.faceClusterId'])
|
||||||
|
.leftJoin('face_cluster', 'face_cluster.id', 'face_cluster_audit.id')
|
||||||
|
.where('face_cluster.ownerId', '=', options.userId)
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupAuditTable(daysAgo: number) {
|
||||||
|
return this.auditCleanup('face_cluster_audit', daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
|
getUpserts(options: SyncQueryOptions) {
|
||||||
|
return this.upsertQuery('face_cluster', options)
|
||||||
|
.select([
|
||||||
|
'face_cluster.id',
|
||||||
|
'face_cluster.createdAt',
|
||||||
|
'face_cluster.updatedAt',
|
||||||
|
'face_cluster.ownerId',
|
||||||
|
'face_cluster.updateId',
|
||||||
|
])
|
||||||
|
.where('face_cluster.ownerId', '=', options.userId)
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AssetExifSync extends BaseSync {
|
class AssetExifSync extends BaseSync {
|
||||||
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
@GenerateSql({ params: [dummyQueryOptions], stream: true })
|
||||||
getUpserts(options: SyncQueryOptions) {
|
getUpserts(options: SyncQueryOptions) {
|
||||||
|
|||||||
@@ -299,3 +299,16 @@ export const asset_edit_audit = registerFunction({
|
|||||||
RETURN NULL;
|
RETURN NULL;
|
||||||
END`,
|
END`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const face_cluster_delete_audit = registerFunction({
|
||||||
|
name: 'face_cluster_delete_audit',
|
||||||
|
returnType: 'TRIGGER',
|
||||||
|
language: 'PLPGSQL',
|
||||||
|
body: `
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO face_cluster_audit ("faceClusterId", "ownerId")
|
||||||
|
SELECT "id", "ownerId"
|
||||||
|
FROM OLD;
|
||||||
|
RETURN NULL;
|
||||||
|
END`,
|
||||||
|
});
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table';
|
|||||||
import { AssetOcrTable } from 'src/schema/tables/asset-ocr.table';
|
import { AssetOcrTable } from 'src/schema/tables/asset-ocr.table';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { AuditTable } from 'src/schema/tables/audit.table';
|
import { AuditTable } from 'src/schema/tables/audit.table';
|
||||||
|
import { FaceClusterAuditTable } from 'src/schema/tables/face-cluster-audit.table';
|
||||||
|
import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
||||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||||
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
||||||
import { LibraryTable } from 'src/schema/tables/library.table';
|
import { LibraryTable } from 'src/schema/tables/library.table';
|
||||||
@@ -199,6 +201,8 @@ export interface DB {
|
|||||||
|
|
||||||
audit: AuditTable;
|
audit: AuditTable;
|
||||||
|
|
||||||
|
face_cluster: FaceClusterTable;
|
||||||
|
face_cluster_audit: FaceClusterAuditTable;
|
||||||
face_search: FaceSearchTable;
|
face_search: FaceSearchTable;
|
||||||
|
|
||||||
geodata_places: GeodataPlacesTable;
|
geodata_places: GeodataPlacesTable;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { SourceType } from 'src/enum';
|
|||||||
import { asset_face_source_type } from 'src/schema/enums';
|
import { asset_face_source_type } from 'src/schema/enums';
|
||||||
import { asset_face_audit } from 'src/schema/functions';
|
import { asset_face_audit } from 'src/schema/functions';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { PersonTable } from 'src/schema/tables/person.table';
|
import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
||||||
|
|
||||||
@Table({ name: 'asset_face' })
|
@Table({ name: 'asset_face' })
|
||||||
@UpdatedAtTrigger('asset_face_updatedAt')
|
@UpdatedAtTrigger('asset_face_updatedAt')
|
||||||
@@ -26,13 +26,13 @@ import { PersonTable } from 'src/schema/tables/person.table';
|
|||||||
when: 'pg_trigger_depth() = 0',
|
when: 'pg_trigger_depth() = 0',
|
||||||
})
|
})
|
||||||
// schemaFromDatabase does not preserve column order
|
// schemaFromDatabase does not preserve column order
|
||||||
@Index({ name: 'asset_face_assetId_personId_idx', columns: ['assetId', 'personId'] })
|
@Index({ name: 'asset_face_assetId_faceClusterId_idx', columns: ['assetId', 'faceClusterId'] })
|
||||||
@Index({
|
@Index({
|
||||||
name: 'asset_face_personId_assetId_notDeleted_isVisible_idx',
|
name: 'asset_face_faceClusterId_assetId_notDeleted_isVisible_idx',
|
||||||
columns: ['personId', 'assetId'],
|
columns: ['faceClusterId', 'assetId'],
|
||||||
where: '"deletedAt" IS NULL AND "isVisible" IS TRUE',
|
where: '"deletedAt" IS NULL AND "isVisible" IS TRUE',
|
||||||
})
|
})
|
||||||
@Index({ columns: ['personId', 'assetId'] })
|
@Index({ columns: ['faceClusterId', 'assetId'] })
|
||||||
export class AssetFaceTable {
|
export class AssetFaceTable {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: Generated<string>;
|
id!: Generated<string>;
|
||||||
@@ -45,14 +45,14 @@ export class AssetFaceTable {
|
|||||||
})
|
})
|
||||||
assetId!: string;
|
assetId!: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => PersonTable, {
|
@ForeignKeyColumn(() => FaceClusterTable, {
|
||||||
onDelete: 'SET NULL',
|
onDelete: 'SET NULL',
|
||||||
onUpdate: 'CASCADE',
|
onUpdate: 'CASCADE',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
// [personId, assetId] makes this redundant
|
// [faceClusterId, assetId] makes this redundant
|
||||||
index: false,
|
index: false,
|
||||||
})
|
})
|
||||||
personId!: string | null;
|
faceClusterId!: string | null;
|
||||||
|
|
||||||
@Column({ default: 0, type: 'integer' })
|
@Column({ default: 0, type: 'integer' })
|
||||||
imageWidth!: Generated<number>;
|
imageWidth!: Generated<number>;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools';
|
||||||
|
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||||
|
|
||||||
|
@Table('face_cluster_audit')
|
||||||
|
export class FaceClusterAuditTable {
|
||||||
|
@PrimaryGeneratedUuidV7Column()
|
||||||
|
id!: Generated<string>;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', index: true })
|
||||||
|
faceClusterId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', index: true })
|
||||||
|
ownerId!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
|
||||||
|
deletedAt!: Generated<Timestamp>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
AfterDeleteTrigger,
|
||||||
|
CreateDateColumn,
|
||||||
|
ForeignKeyColumn,
|
||||||
|
Generated,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Table,
|
||||||
|
Timestamp,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from '@immich/sql-tools';
|
||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
|
import { face_cluster_delete_audit } from 'src/schema/functions';
|
||||||
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
|
|
||||||
|
@Table('face_cluster')
|
||||||
|
@UpdatedAtTrigger('face_cluster_updatedAt')
|
||||||
|
@AfterDeleteTrigger({
|
||||||
|
scope: 'statement',
|
||||||
|
function: face_cluster_delete_audit,
|
||||||
|
referencingOldTableAs: 'old',
|
||||||
|
when: 'pg_trigger_depth() = 0',
|
||||||
|
})
|
||||||
|
export class FaceClusterTable {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: Generated<string>;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt!: Generated<Timestamp>;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt!: Generated<Timestamp>;
|
||||||
|
|
||||||
|
@UpdateIdColumn({ index: true })
|
||||||
|
updateId!: Generated<string>;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||||
|
ownerId!: string;
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { person_delete_audit } from 'src/schema/functions';
|
import { person_delete_audit } from 'src/schema/functions';
|
||||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||||
|
import { FaceClusterTable } from 'src/schema/tables/face-cluster.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
|
|
||||||
@Table('person')
|
@Table('person')
|
||||||
@@ -60,4 +61,7 @@ export class PersonTable {
|
|||||||
|
|
||||||
@UpdateIdColumn({ index: true })
|
@UpdateIdColumn({ index: true })
|
||||||
updateId!: Generated<string>;
|
updateId!: Generated<string>;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => FaceClusterTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true, index: true })
|
||||||
|
faceClusterId!: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -876,7 +876,7 @@ export class MetadataService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
this.logger.debugFn(() => `Creating missing persons: ${missing.map((p) => `${p.name}/${p.id}`)}`);
|
this.logger.debug(`Creating missing people: ${missing.map((p) => `${p.name}/${p.id}`)}`);
|
||||||
const newPersonIds = await this.personRepository.createAll(missing);
|
const newPersonIds = await this.personRepository.createAll(missing);
|
||||||
const jobs = newPersonIds.map((id) => ({ name: JobName.PersonGenerateThumbnail, data: { id } }) as const);
|
const jobs = newPersonIds.map((id) => ({ name: JobName.PersonGenerateThumbnail, data: { id } }) as const);
|
||||||
await this.jobRepository.queueAll(jobs);
|
await this.jobRepository.queueAll(jobs);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
FaceDto,
|
FaceDto,
|
||||||
mapFaces,
|
mapFaces,
|
||||||
mapPerson,
|
mapPerson,
|
||||||
MergePersonDto,
|
MergeFaceClusterDto,
|
||||||
PeopleResponseDto,
|
PeopleResponseDto,
|
||||||
PeopleUpdateDto,
|
PeopleUpdateDto,
|
||||||
PersonCreateDto,
|
PersonCreateDto,
|
||||||
@@ -438,7 +438,7 @@ export class PersonService extends BaseService {
|
|||||||
|
|
||||||
const lastRun = new Date().toISOString();
|
const lastRun = new Date().toISOString();
|
||||||
const facePagination = this.personRepository.getAllFaces(
|
const facePagination = this.personRepository.getAllFaces(
|
||||||
force ? undefined : { personId: null, sourceType: SourceType.MachineLearning },
|
force ? undefined : { faceClusterId: null, sourceType: SourceType.MachineLearning },
|
||||||
);
|
);
|
||||||
|
|
||||||
let jobs: { name: JobName.FacialRecognition; data: { id: string; deferred: false } }[] = [];
|
let jobs: { name: JobName.FacialRecognition; data: { id: string; deferred: false } }[] = [];
|
||||||
@@ -481,8 +481,8 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.Failed;
|
return JobStatus.Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (face.personId) {
|
if (face.faceClusterId) {
|
||||||
this.logger.debug(`Face ${id} already has a person assigned`);
|
this.logger.debug(`Face ${id} already belongs to a face cluster`);
|
||||||
return JobStatus.Skipped;
|
return JobStatus.Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,8 +511,8 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.Skipped;
|
return JobStatus.Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
let personId = matches.find((match) => match.personId)?.personId;
|
let faceClusterId = matches.find((match) => match.faceClusterId)?.faceClusterId;
|
||||||
if (!personId) {
|
if (!faceClusterId) {
|
||||||
const matchWithPerson = await this.searchRepository.searchFaces({
|
const matchWithPerson = await this.searchRepository.searchFaces({
|
||||||
userIds: [face.asset.ownerId],
|
userIds: [face.asset.ownerId],
|
||||||
embedding: face.faceSearch.embedding,
|
embedding: face.faceSearch.embedding,
|
||||||
@@ -523,20 +523,20 @@ export class PersonService extends BaseService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (matchWithPerson.length > 0) {
|
if (matchWithPerson.length > 0) {
|
||||||
personId = matchWithPerson[0].personId;
|
faceClusterId = matchWithPerson[0].faceClusterId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCore && !personId) {
|
if (isCore && !faceClusterId) {
|
||||||
this.logger.log(`Creating new person for face ${id}`);
|
this.logger.log(`Creating new person for face ${id}`);
|
||||||
const newPerson = await this.personRepository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id });
|
const newPerson = await this.personRepository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id });
|
||||||
await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id: newPerson.id } });
|
await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id: newPerson.id } });
|
||||||
personId = newPerson.id;
|
faceClusterId = newPerson.faceClusterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (personId) {
|
if (faceClusterId) {
|
||||||
this.logger.debug(`Assigning face ${id} to person ${personId}`);
|
this.logger.debug(`Assigning face ${id} to face cluster ${faceClusterId}`);
|
||||||
await this.personRepository.reassignFaces({ faceIds: [id], newPersonId: personId });
|
await this.personRepository.reassignFaces({ faceIds: [id], newFaceClusterId: faceClusterId });
|
||||||
}
|
}
|
||||||
|
|
||||||
return JobStatus.Success;
|
return JobStatus.Success;
|
||||||
@@ -554,7 +554,7 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.Success;
|
return JobStatus.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
|
async mergePerson(auth: AuthDto, id: string, dto: MergeFaceClusterDto): Promise<BulkIdResponseDto[]> {
|
||||||
const mergeIds = dto.ids;
|
const mergeIds = dto.ids;
|
||||||
if (mergeIds.includes(id)) {
|
if (mergeIds.includes(id)) {
|
||||||
throw new BadRequestException('Cannot merge a person into themselves');
|
throw new BadRequestException('Cannot merge a person into themselves');
|
||||||
@@ -600,7 +600,7 @@ export class PersonService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mergeName = mergePerson.name || mergePerson.id;
|
const mergeName = mergePerson.name || mergePerson.id;
|
||||||
const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id };
|
const mergeData: UpdateFacesData = { oldFaceClusterId: mergeId, newFaceClusterId: id };
|
||||||
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
||||||
|
|
||||||
await this.personRepository.reassignFaces(mergeData);
|
await this.personRepository.reassignFaces(mergeData);
|
||||||
@@ -678,8 +678,14 @@ export class PersonService extends BaseService {
|
|||||||
dto.imageHeight = originalDimensions.height;
|
dto.imageHeight = originalDimensions.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const person = await this.personRepository.getById(dto.personId);
|
||||||
|
|
||||||
|
if (!person?.faceClusterId) {
|
||||||
|
throw new Error('Person must already have some recognized faces and belong to a face cluster');
|
||||||
|
}
|
||||||
|
|
||||||
await this.personRepository.createAssetFace({
|
await this.personRepository.createAssetFace({
|
||||||
personId: dto.personId,
|
faceClusterId: person.faceClusterId,
|
||||||
assetId: dto.assetId,
|
assetId: dto.assetId,
|
||||||
imageHeight: dto.imageHeight,
|
imageHeight: dto.imageHeight,
|
||||||
imageWidth: dto.imageWidth,
|
imageWidth: dto.imageWidth,
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import {
|
|||||||
SyncAckDeleteDto,
|
SyncAckDeleteDto,
|
||||||
SyncAckSetDto,
|
SyncAckSetDto,
|
||||||
syncAssetFaceV2ToV1,
|
syncAssetFaceV2ToV1,
|
||||||
|
syncAssetFaceV3ToV2,
|
||||||
SyncAssetV1,
|
SyncAssetV1,
|
||||||
SyncItem,
|
SyncItem,
|
||||||
|
syncPersonV2ToV1,
|
||||||
SyncStreamDto,
|
SyncStreamDto,
|
||||||
} from 'src/dtos/sync.dto';
|
} from 'src/dtos/sync.dto';
|
||||||
import {
|
import {
|
||||||
@@ -192,8 +194,11 @@ export class SyncService extends BaseService {
|
|||||||
[SyncRequestType.StacksV1]: () => this.syncStackV1(options, response, checkpointMap),
|
[SyncRequestType.StacksV1]: () => this.syncStackV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(options, response, checkpointMap, session.id),
|
[SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(options, response, checkpointMap, session.id),
|
||||||
[SyncRequestType.PeopleV1]: () => this.syncPeopleV1(options, response, checkpointMap),
|
[SyncRequestType.PeopleV1]: () => this.syncPeopleV1(options, response, checkpointMap),
|
||||||
|
[SyncRequestType.PeopleV2]: () => this.syncPeopleV2(options, response, checkpointMap),
|
||||||
[SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(options, response, checkpointMap),
|
[SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.AssetFacesV2]: async () => this.syncAssetFacesV2(options, response, checkpointMap),
|
[SyncRequestType.AssetFacesV2]: async () => this.syncAssetFacesV2(options, response, checkpointMap),
|
||||||
|
[SyncRequestType.AssetFacesV3]: async () => this.syncAssetFacesV3(options, response, checkpointMap),
|
||||||
|
[SyncRequestType.FaceClusterV1]: async () => this.syncFaceClusterV1(options, response, checkpointMap),
|
||||||
[SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(options, response, checkpointMap),
|
[SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(options, response, checkpointMap),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -796,6 +801,20 @@ export class SyncService extends BaseService {
|
|||||||
|
|
||||||
const upsertType = SyncEntityType.PersonV1;
|
const upsertType = SyncEntityType.PersonV1;
|
||||||
const upserts = this.syncRepository.person.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
const upserts = this.syncRepository.person.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||||
|
for await (const { updateId, ...data } of upserts) {
|
||||||
|
send(response, { type: upsertType, ids: [updateId], data: syncPersonV2ToV1(data) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncPeopleV2(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
|
const deleteType = SyncEntityType.PersonDeleteV1;
|
||||||
|
const deletes = this.syncRepository.person.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
||||||
|
for await (const { id, ...data } of deletes) {
|
||||||
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertType = SyncEntityType.PersonV2;
|
||||||
|
const upserts = this.syncRepository.person.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
@@ -810,8 +829,8 @@ export class SyncService extends BaseService {
|
|||||||
|
|
||||||
const upsertType = SyncEntityType.AssetFaceV1;
|
const upsertType = SyncEntityType.AssetFaceV1;
|
||||||
const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, personId, ...data } of upserts) {
|
||||||
const v1 = syncAssetFaceV2ToV1(data);
|
const v1 = syncAssetFaceV2ToV1(syncAssetFaceV3ToV2(data, personId));
|
||||||
send(response, { type: upsertType, ids: [updateId], data: v1 });
|
send(response, { type: upsertType, ids: [updateId], data: v1 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -825,6 +844,34 @@ export class SyncService extends BaseService {
|
|||||||
|
|
||||||
const upsertType = SyncEntityType.AssetFaceV2;
|
const upsertType = SyncEntityType.AssetFaceV2;
|
||||||
const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||||
|
for await (const { updateId, personId, ...data } of upserts) {
|
||||||
|
send(response, { type: upsertType, ids: [updateId], data: syncAssetFaceV3ToV2(data, personId) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncAssetFacesV3(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
|
const deleteType = SyncEntityType.AssetFaceDeleteV1;
|
||||||
|
const deletes = this.syncRepository.assetFace.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
||||||
|
for await (const { id, ...data } of deletes) {
|
||||||
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertType = SyncEntityType.AssetFaceV3;
|
||||||
|
const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||||
|
for await (const { updateId, personId: _, ...data } of upserts) {
|
||||||
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncFaceClusterV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||||
|
const deleteType = SyncEntityType.FaceClusterDeleteV1;
|
||||||
|
const deletes = this.syncRepository.faceCluster.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
||||||
|
for await (const { id, ...data } of deletes) {
|
||||||
|
send(response, { type: deleteType, ids: [id], data });
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertType = SyncEntityType.FaceClusterV1;
|
||||||
|
const upserts = this.syncRepository.faceCluster.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||||
for await (const { updateId, ...data } of upserts) {
|
for await (const { updateId, ...data } of upserts) {
|
||||||
send(response, { type: upsertType, ids: [updateId], data });
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,12 @@ export function withFacesAndPeople(
|
|||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.leftJoinLateral(
|
.leftJoinLateral(
|
||||||
(eb) =>
|
(eb) =>
|
||||||
eb.selectFrom('person').selectAll('person').whereRef('asset_face.personId', '=', 'person.id').as('person'),
|
eb
|
||||||
|
.selectFrom('face_cluster')
|
||||||
|
.where('face_cluster.id', '=', 'asset_face.faceClusterId')
|
||||||
|
.innerJoin('person', 'person.faceClusterId', 'face_cluster.id')
|
||||||
|
.selectAll('person')
|
||||||
|
.as('person'),
|
||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
)
|
)
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
@@ -161,11 +166,12 @@ export function hasPeople<O>(qb: SelectQueryBuilder<DB, 'asset', O>, personIds:
|
|||||||
eb
|
eb
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.select('assetId')
|
.select('assetId')
|
||||||
.where('personId', '=', anyUuid(personIds!))
|
.innerJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
|
||||||
|
.where('person.id', '=', anyUuid(personIds!))
|
||||||
.where('deletedAt', 'is', null)
|
.where('deletedAt', 'is', null)
|
||||||
.where('isVisible', 'is', true)
|
.where('isVisible', 'is', true)
|
||||||
.groupBy('assetId')
|
.groupBy('assetId')
|
||||||
.having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
.having((eb) => eb.fn.count('person.id').distinct(), '=', personIds.length)
|
||||||
.as('has_people'),
|
.as('has_people'),
|
||||||
(join) => join.onRef('has_people.assetId', '=', 'asset.id'),
|
(join) => join.onRef('has_people.assetId', '=', 'asset.id'),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user