refactor!: remove asset faces from AssetResponseDto (#27779)

* refactor!: remove faces from `people` in AssetResposnseDto

* chore: tests

* chore: e2e generator

* chore: code review readonly

* chore: code review changes

* chore: cleanup

* fix: openapi

* chore: format

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
Brandon Wees
2026-05-11 16:05:40 -05:00
committed by GitHub
parent a278c10c75
commit 38438c8d9a
21 changed files with 134 additions and 942 deletions
@@ -15,6 +15,7 @@
import { editManager, EditToolType } from '$lib/managers/edit/edit-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { getAssetActions } from '$lib/services/asset.service';
import { faceManager } from '$lib/stores/face.svelte';
import { ocrManager } from '$lib/stores/ocr.svelte';
import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
@@ -333,6 +334,7 @@
case AssetAction.SET_PERSON_FEATURED_PHOTO: {
const assetInfo = await getAssetInfo({ id: asset.id });
cursor.current = { ...asset, people: assetInfo.people };
eventManager.emit('AssetUpdate', cursor.current);
break;
}
case AssetAction.RATING: {
@@ -376,11 +378,14 @@
const refresh = async () => {
await refreshStack();
ocrManager.clear();
faceManager.clear();
if (!sharedLink) {
if (previewStackedAsset) {
await ocrManager.getAssetOcr(previewStackedAsset.id);
await faceManager.getAssetFaces(previewStackedAsset.id);
}
await ocrManager.getAssetOcr(asset.id);
await faceManager.getAssetFaces(asset.id);
}
};
@@ -3,6 +3,7 @@
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { Route } from '$lib/route';
import { faceManager } from '$lib/stores/face.svelte';
import { locale } from '$lib/stores/preferences.store';
import { getPeopleThumbnailUrl } from '$lib/utils';
import { type AssetResponseDto } from '@immich/sdk';
@@ -19,8 +20,7 @@
const { asset, isOwner, previousRoute }: Props = $props();
const unassignedFaces = $derived(asset.unassignedFaces || []);
const people = $derived(asset.people || []);
const people = $derived(Array.from(faceManager.people));
const visiblePeople = $derived(
people
.filter((p) => assetViewerManager.isShowingHiddenPeople || !p.isHidden)
@@ -82,7 +82,7 @@
onclick={() => assetViewerManager.toggleFaceEditMode()}
/>
{#if people.length > 0 || unassignedFaces.length > 0}
{#if faceManager.data.length > 0}
<IconButton
aria-label={$t('edit_people')}
icon={mdiPencil}
@@ -98,15 +98,14 @@
<div class="mt-2 grid {visiblePeople.length <= 6 ? 'grid-cols-3 gap-3' : 'grid-cols-4 gap-2'}">
{#each visiblePeople as person (person.id)}
{@const isHighlighted = person.faces.some((f) =>
assetViewerManager.highlightedFaces.some((b) => b.id === f.id),
)}
{@const personFaces = faceManager.facesByPersonId.get(person.id) ?? []}
{@const isHighlighted = personFaces.some((f) => assetViewerManager.highlightedFaces.some((b) => b.id === f.id))}
<a
class="group outline-none"
href={Route.viewPerson(person, { previousRoute })}
onfocus={() => assetViewerManager.setHighlightedFaces(person.faces)}
onfocus={() => assetViewerManager.setHighlightedFaces(personFaces)}
onblur={() => assetViewerManager.clearHighlightedFaces()}
onpointerenter={() => assetViewerManager.setHighlightedFaces(person.faces)}
onpointerenter={() => assetViewerManager.setHighlightedFaces(personFaces)}
onpointerleave={() => assetViewerManager.clearHighlightedFaces()}
>
<ImageThumbnail
@@ -8,6 +8,7 @@
import AssetViewerEvents from '$lib/components/AssetViewerEvents.svelte';
import { assetViewerManager, type Faces } from '$lib/managers/asset-viewer-manager.svelte';
import { castManager } from '$lib/managers/cast-manager.svelte';
import { faceManager } from '$lib/stores/face.svelte';
import { ocrManager } from '$lib/stores/ocr.svelte';
import { SlideshowLook, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { handlePromiseError } from '$lib/utils';
@@ -157,13 +158,14 @@
const faceToNameMap = $derived.by(() => {
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const map = new Map<Faces, string>();
for (const person of asset.people ?? []) {
if (person.isHidden && !assetViewerManager.isShowingHiddenPeople) {
for (const face of faceManager.data) {
if (!face.person) {
continue;
}
for (const face of person.faces ?? []) {
map.set(face, person.name);
if (face.person.isHidden && !assetViewerManager.isShowingHiddenPeople) {
continue;
}
map.set(face, face.person.name);
}
return map;
});
@@ -1,4 +1,5 @@
<script lang="ts">
import type { OnAction } from '$lib/components/asset-viewer/actions/action';
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
import { AssetAction } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error';
@@ -6,7 +7,6 @@
import { toastManager } from '@immich/ui';
import { mdiFaceManProfile } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { OnAction } from './action';
interface Props {
asset: AssetResponseDto;
@@ -1,4 +1,4 @@
import { getAssetInfo, getAssetOcr } from '@immich/sdk';
import { getAssetInfo, getAssetOcr, getFaces } from '@immich/sdk';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
@@ -38,6 +38,7 @@ class AsyncCache<K, V> {
class AssetCacheManager {
#assetCache = new AsyncCache(getAssetInfo);
#ocrCache = new AsyncCache(getAssetOcr);
#faceCache = new AsyncCache(getFaces);
constructor() {
eventManager.on({
@@ -58,10 +59,15 @@ class AssetCacheManager {
return this.#ocrCache.getOrFetch({ id }, true);
}
async getAssetFaces(id: string) {
return this.#faceCache.getOrFetch({ id }, true);
}
invalidateAsset(id: string) {
const { key, slug } = authManager.params;
this.#assetCache.clearKey({ id, key, slug });
this.#ocrCache.clearKey({ id });
this.#faceCache.clearKey({ id });
}
clearAssetCache() {
@@ -72,9 +78,14 @@ class AssetCacheManager {
this.#ocrCache.clear();
}
clearFaceCache() {
this.#faceCache.clear();
}
invalidate() {
this.clearAssetCache();
this.clearOcrCache();
this.clearFaceCache();
}
}
+74
View File
@@ -0,0 +1,74 @@
import type { AssetFaceResponseDto, PersonResponseDto } from '@immich/sdk';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { assetCacheManager } from '$lib/managers/AssetCacheManager.svelte';
import type { Faces } from '$lib/managers/asset-viewer-manager.svelte';
import { CancellableTask } from '$lib/utils/cancellable-task';
class FaceManager {
#data = $state<AssetFaceResponseDto[]>([]);
#faceLoader = new CancellableTask();
#cleared = false;
readonly faceNames = $derived.by(() => {
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const map = new Map<Faces, string>();
for (const face of this.data) {
if (!face.person) {
continue;
}
map.set(face, face.person.name);
}
return map;
});
readonly people = $derived.by(() => {
const people = new SvelteSet<PersonResponseDto>();
for (const face of this.data) {
if (face.person) {
people.add(face.person);
}
}
return people;
});
readonly facesByPersonId = $derived.by(() => {
const map = new SvelteMap<string, AssetFaceResponseDto[]>();
for (const face of faceManager.data) {
if (!face.person) {
continue;
}
const existing = map.get(face.person.id);
if (existing) {
existing.push(face);
} else {
map.set(face.person.id, [face]);
}
}
return map;
});
get data() {
return this.#data;
}
async getAssetFaces(id: string) {
if (this.#cleared) {
await this.#faceLoader.reset();
this.#cleared = false;
}
await this.#faceLoader.execute(async () => {
this.#data = await assetCacheManager.getAssetFaces(id);
}, false);
}
clear() {
this.#cleared = true;
this.#data = [];
}
}
export const faceManager = new FaceManager();
+1
View File
@@ -7,6 +7,7 @@ import { ocrManager, type OcrBoundingBox } from '$lib/stores/ocr.svelte';
vi.mock('@immich/sdk', () => ({
getAssetInfo: vi.fn(),
getAssetOcr: vi.fn(),
getFaces: vi.fn(),
}));
const createMockOcrData = (overrides?: Partial<OcrBoundingBox>): OcrBoundingBox[] => [