mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
fix: people/face query
This commit is contained in:
parent
7042c63ed1
commit
42c791106a
7 changed files with 71 additions and 70 deletions
|
|
@ -215,6 +215,6 @@ export function mapFaces(
|
||||||
): AssetFaceResponseDto {
|
): AssetFaceResponseDto {
|
||||||
return {
|
return {
|
||||||
...mapFacesWithoutPerson(face, edits, assetDimensions),
|
...mapFacesWithoutPerson(face, edits, assetDimensions),
|
||||||
person: face.person?.ownerId === auth.user.id ? mapPerson(face.person) : null,
|
person: face.person ? mapPerson(face.person) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,16 +131,15 @@ select
|
||||||
"person"
|
"person"
|
||||||
where
|
where
|
||||||
"person"."personGroupId" = "asset_face"."personGroupId"
|
"person"."personGroupId" = "asset_face"."personGroupId"
|
||||||
and "person"."ownerId" = "asset"."ownerId"
|
and "person"."ownerId" = $1
|
||||||
) as obj
|
) as obj
|
||||||
) as "person"
|
) as "person"
|
||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
|
||||||
where
|
where
|
||||||
"asset_face"."assetId" = $1
|
"asset_face"."assetId" = $2
|
||||||
and "asset_face"."deletedAt" is null
|
and "asset_face"."deletedAt" is null
|
||||||
and "asset_face"."isVisible" = $2
|
and "asset_face"."isVisible" = $3
|
||||||
order by
|
order by
|
||||||
"asset_face"."boundingBoxX1" asc
|
"asset_face"."boundingBoxX1" asc
|
||||||
|
|
||||||
|
|
@ -158,14 +157,13 @@ select
|
||||||
"person"
|
"person"
|
||||||
where
|
where
|
||||||
"person"."personGroupId" = "asset_face"."personGroupId"
|
"person"."personGroupId" = "asset_face"."personGroupId"
|
||||||
and "person"."ownerId" = "asset"."ownerId"
|
and "person"."ownerId" = $1
|
||||||
) as obj
|
) as obj
|
||||||
) as "person"
|
) as "person"
|
||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
|
||||||
where
|
where
|
||||||
"asset_face"."id" = $1
|
"asset_face"."id" = $2
|
||||||
and "asset_face"."deletedAt" is null
|
and "asset_face"."deletedAt" is null
|
||||||
|
|
||||||
-- PersonRepository.getFaceForFacialRecognitionJob
|
-- PersonRepository.getFaceForFacialRecognitionJob
|
||||||
|
|
@ -521,15 +519,14 @@ select
|
||||||
"person"
|
"person"
|
||||||
where
|
where
|
||||||
"person"."personGroupId" = "asset_face"."personGroupId"
|
"person"."personGroupId" = "asset_face"."personGroupId"
|
||||||
and "person"."ownerId" = "asset"."ownerId"
|
and "person"."ownerId" = $1
|
||||||
) as obj
|
) as obj
|
||||||
) as "person"
|
) as "person"
|
||||||
from
|
from
|
||||||
"asset_face"
|
"asset_face"
|
||||||
inner join "asset" on "asset"."id" = "asset_face"."assetId"
|
|
||||||
where
|
where
|
||||||
"asset_face"."assetId" in ($1)
|
"asset_face"."assetId" in ($2)
|
||||||
and "asset_face"."personGroupId" in ($2)
|
and "asset_face"."personGroupId" in ($3)
|
||||||
and "asset_face"."deletedAt" is null
|
and "asset_face"."deletedAt" is null
|
||||||
|
|
||||||
-- PersonRepository.getRandomFace
|
-- PersonRepository.getRandomFace
|
||||||
|
|
|
||||||
|
|
@ -63,19 +63,27 @@ export type UnassignFacesOptions = DeleteFacesOptions;
|
||||||
|
|
||||||
export type SelectFaceOptions = (keyof Selectable<AssetFaceTable>)[];
|
export type SelectFaceOptions = (keyof Selectable<AssetFaceTable>)[];
|
||||||
|
|
||||||
|
export type GetFacesOptions = WithPersonOptions & { isVisible?: boolean };
|
||||||
|
|
||||||
/** a person is identified by its owner and the group it belongs to */
|
/** a person is identified by its owner and the group it belongs to */
|
||||||
export type PersonId = { ownerId: string; personGroupId: string };
|
export type PersonId = { ownerId: string; personGroupId: string };
|
||||||
|
|
||||||
export type ReassignCluster = { userId: string; newClusterId: string };
|
export type ReassignCluster = { userId: string; newClusterId: string };
|
||||||
|
|
||||||
const withPerson = (eb: ExpressionBuilder<DB, 'asset_face' | 'asset'>) => {
|
export type WithPersonOptions = {
|
||||||
return jsonObjectFrom(
|
/** whose version of the person to select */
|
||||||
eb
|
viewingUserId: string;
|
||||||
.selectFrom('person')
|
};
|
||||||
.selectAll('person')
|
|
||||||
.whereRef('person.personGroupId', '=', 'asset_face.personGroupId')
|
const withPerson = ({ viewingUserId }: WithPersonOptions) => {
|
||||||
.whereRef('person.ownerId', '=', 'asset.ownerId'),
|
return (eb: ExpressionBuilder<DB, 'asset_face'>) =>
|
||||||
).as('person');
|
jsonObjectFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom('person')
|
||||||
|
.selectAll('person')
|
||||||
|
.whereRef('person.personGroupId', '=', 'asset_face.personGroupId')
|
||||||
|
.where('person.ownerId', '=', viewingUserId),
|
||||||
|
).as('person');
|
||||||
};
|
};
|
||||||
|
|
||||||
const withFaceSearch = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
|
const withFaceSearch = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
|
||||||
|
|
@ -286,15 +294,14 @@ export class PersonRepository {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID, { viewingUserId: DummyValue.UUID, isVisible: true }] })
|
||||||
getFaces(assetId: string, options?: { isVisible?: boolean }) {
|
getFaces(assetId: string, options: GetFacesOptions) {
|
||||||
const isVisible = options === undefined ? true : options.isVisible;
|
const { viewingUserId, isVisible } = options;
|
||||||
|
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.innerJoin('asset', 'asset.id', 'asset_face.assetId')
|
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.select(withPerson)
|
.select(withPerson({ viewingUserId }))
|
||||||
.where('asset_face.assetId', '=', assetId)
|
.where('asset_face.assetId', '=', assetId)
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.$if(isVisible !== undefined, (qb) => qb.where('asset_face.isVisible', '=', isVisible!))
|
.$if(isVisible !== undefined, (qb) => qb.where('asset_face.isVisible', '=', isVisible!))
|
||||||
|
|
@ -302,14 +309,13 @@ export class PersonRepository {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID, { viewingUserId: DummyValue.UUID }] })
|
||||||
getFaceById(id: string) {
|
getFaceById(id: string, { viewingUserId }: WithPersonOptions) {
|
||||||
// TODO return null instead of find or fail
|
// TODO return null instead of find or fail
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.innerJoin('asset', 'asset.id', 'asset_face.assetId')
|
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.select(withPerson)
|
.select(withPerson({ viewingUserId }))
|
||||||
.where('asset_face.id', '=', id)
|
.where('asset_face.id', '=', id)
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
@ -644,9 +650,11 @@ export class PersonRepository {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, personGroupId: DummyValue.UUID }]] })
|
@GenerateSql({
|
||||||
|
params: [[{ assetId: DummyValue.UUID, personGroupId: DummyValue.UUID }], { viewingUserId: DummyValue.UUID }],
|
||||||
|
})
|
||||||
@ChunkedArray()
|
@ChunkedArray()
|
||||||
getFacesByIds(ids: AssetFaceId[]) {
|
getFacesByIds(ids: AssetFaceId[], { viewingUserId }: WithPersonOptions) {
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
@ -660,9 +668,8 @@ export class PersonRepository {
|
||||||
|
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset_face')
|
.selectFrom('asset_face')
|
||||||
.innerJoin('asset', 'asset.id', 'asset_face.assetId')
|
|
||||||
.selectAll('asset_face')
|
.selectAll('asset_face')
|
||||||
.select(withPerson)
|
.select(withPerson({ viewingUserId }))
|
||||||
.where('asset_face.assetId', 'in', assetIds)
|
.where('asset_face.assetId', 'in', assetIds)
|
||||||
.where('asset_face.personGroupId', 'in', personGroupIds)
|
.where('asset_face.personGroupId', 'in', personGroupIds)
|
||||||
.where('asset_face.deletedAt', 'is', null)
|
.where('asset_face.deletedAt', 'is', null)
|
||||||
|
|
|
||||||
|
|
@ -835,7 +835,7 @@ export class MediaService extends BaseService {
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const originalDimensions = getDimensions(asset.exifInfo!);
|
const originalDimensions = getDimensions(asset.exifInfo!);
|
||||||
const assetFaces = await this.personRepository.getFaces(asset.id, {});
|
const assetFaces = await this.personRepository.getFaces(asset.id, { viewingUserId: asset.ownerId });
|
||||||
const ocrData = await this.ocrRepository.getByAssetId(asset.id, {});
|
const ocrData = await this.ocrRepository.getByAssetId(asset.id, {});
|
||||||
|
|
||||||
const faceStatuses = checkFaceVisibility(assetFaces, originalDimensions, cropBox);
|
const faceStatuses = checkFaceVisibility(assetFaces, originalDimensions, cropBox);
|
||||||
|
|
|
||||||
|
|
@ -1446,11 +1446,5 @@ describe(PersonService.name, () => {
|
||||||
it('should not map person if person is null', () => {
|
it('should not map person if person is null', () => {
|
||||||
expect(mapFaces(getForAssetFace(AssetFaceFactory.create()), AuthFactory.create()).person).toBeNull();
|
expect(mapFaces(getForAssetFace(AssetFaceFactory.create()), AuthFactory.create()).person).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not map person if person does not match auth user id', () => {
|
|
||||||
expect(
|
|
||||||
mapFaces(getForAssetFace(AssetFaceFactory.from().person().build()), AuthFactory.create()).person,
|
|
||||||
).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,10 @@ export class PersonService extends BaseService {
|
||||||
const result: PersonResponseDto[] = [];
|
const result: PersonResponseDto[] = [];
|
||||||
const changeFeaturePhoto = new Map<string, PersonId>();
|
const changeFeaturePhoto = new Map<string, PersonId>();
|
||||||
for (const data of dto.data) {
|
for (const data of dto.data) {
|
||||||
const faces = await this.personRepository.getFacesByIds([
|
const faces = await this.personRepository.getFacesByIds(
|
||||||
{ personGroupId: data.personId, assetId: data.assetId },
|
[{ personGroupId: data.personId, assetId: data.assetId }],
|
||||||
]);
|
{ viewingUserId: auth.user.id },
|
||||||
|
);
|
||||||
|
|
||||||
for (const face of faces) {
|
for (const face of faces) {
|
||||||
await this.requireAccess({ auth, permission: Permission.PersonCreate, ids: [face.id] });
|
await this.requireAccess({ auth, permission: Permission.PersonCreate, ids: [face.id] });
|
||||||
|
|
@ -114,7 +115,7 @@ export class PersonService extends BaseService {
|
||||||
async reassignFacesById(auth: AuthDto, personGroupId: string, dto: FaceDto): Promise<PersonResponseDto> {
|
async reassignFacesById(auth: AuthDto, personGroupId: string, dto: FaceDto): Promise<PersonResponseDto> {
|
||||||
await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [personGroupId] });
|
await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [personGroupId] });
|
||||||
await this.requireAccess({ auth, permission: Permission.PersonCreate, ids: [dto.id] });
|
await this.requireAccess({ auth, permission: Permission.PersonCreate, ids: [dto.id] });
|
||||||
const face = await this.personRepository.getFaceById(dto.id);
|
const face = await this.personRepository.getFaceById(dto.id, { viewingUserId: auth.user.id });
|
||||||
const person = await this.findOrFail(auth, personGroupId);
|
const person = await this.findOrFail(auth, personGroupId);
|
||||||
|
|
||||||
await this.personRepository.reassignFace(face.id, person.personGroupId);
|
await this.personRepository.reassignFace(face.id, person.personGroupId);
|
||||||
|
|
@ -130,7 +131,7 @@ export class PersonService extends BaseService {
|
||||||
|
|
||||||
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||||
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.id] });
|
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.id] });
|
||||||
const faces = await this.personRepository.getFaces(dto.id);
|
const faces = await this.personRepository.getFaces(dto.id, { viewingUserId: auth.user.id, isVisible: true });
|
||||||
const asset = await this.assetRepository.getForFaces(dto.id);
|
const asset = await this.assetRepository.getForFaces(dto.id);
|
||||||
const assetDimensions = getDimensions(asset);
|
const assetDimensions = getDimensions(asset);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,42 +57,44 @@
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !authManager.isSharedLink && isOwner}
|
{#if !authManager.isSharedLink}
|
||||||
<section class="px-4 pt-4 text-sm">
|
<section class="px-4 pt-4 text-sm">
|
||||||
<div class="flex h-10 w-full items-center justify-between">
|
<div class="flex h-10 w-full items-center justify-between">
|
||||||
<Text size="small" color="muted">{$t('people')}</Text>
|
<Text size="small" color="muted">{$t('people')}</Text>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if people.some((person) => person.isHidden)}
|
{#if isOwner}
|
||||||
|
{#if people.some((person) => person.isHidden)}
|
||||||
|
<IconButton
|
||||||
|
aria-label={$t('show_hidden_people')}
|
||||||
|
icon={assetViewerManager.isShowingHiddenPeople ? mdiEyeOff : mdiEye}
|
||||||
|
size="medium"
|
||||||
|
shape="round"
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
onclick={() => assetViewerManager.toggleHiddenPeople()}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={$t('show_hidden_people')}
|
aria-label={$t('tag_people')}
|
||||||
icon={assetViewerManager.isShowingHiddenPeople ? mdiEyeOff : mdiEye}
|
icon={mdiPlus}
|
||||||
size="medium"
|
size="medium"
|
||||||
shape="round"
|
shape="round"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onclick={() => assetViewerManager.toggleHiddenPeople()}
|
onclick={() => assetViewerManager.toggleFaceEditMode()}
|
||||||
/>
|
/>
|
||||||
{/if}
|
|
||||||
<IconButton
|
|
||||||
aria-label={$t('tag_people')}
|
|
||||||
icon={mdiPlus}
|
|
||||||
size="medium"
|
|
||||||
shape="round"
|
|
||||||
color="secondary"
|
|
||||||
variant="ghost"
|
|
||||||
onclick={() => assetViewerManager.toggleFaceEditMode()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if faceManager.data.length > 0}
|
{#if faceManager.data.length > 0}
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={$t('edit_people')}
|
aria-label={$t('edit_people')}
|
||||||
icon={mdiPencil}
|
icon={mdiPencil}
|
||||||
size="medium"
|
size="medium"
|
||||||
shape="round"
|
shape="round"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onclick={() => assetViewerManager.openEditFacesPanel()}
|
onclick={() => assetViewerManager.openEditFacesPanel()}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue