feat(server): refresh face detection (#12335)

* refresh faces

handle non-ml faces

* fix metadata face handling

* updated tests

* added todo comment
This commit is contained in:
Mert
2024-10-03 21:58:28 -04:00
committed by GitHub
parent 9edc9d6151
commit 2c87683fd4
21 changed files with 409 additions and 152 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts" context="module">
export type Colors = 'light-gray' | 'gray';
export type Colors = 'light-gray' | 'gray' | 'dark-gray';
</script>
<script lang="ts">
@@ -7,8 +7,9 @@
export let disabled = false;
const colorClasses: Record<Colors, string> = {
'light-gray': 'bg-gray-300/90 dark:bg-gray-600/90',
gray: 'bg-gray-300 dark:bg-gray-600',
'light-gray': 'bg-gray-300/80 dark:bg-gray-700',
gray: 'bg-gray-300/90 dark:bg-gray-700/90',
'dark-gray': 'bg-gray-300 dark:bg-gray-700/80',
};
const hoverClasses = disabled

View File

@@ -9,6 +9,7 @@
mdiAllInclusive,
mdiClose,
mdiFastForward,
mdiImageRefreshOutline,
mdiPause,
mdiPlay,
mdiSelectionSearch,
@@ -23,16 +24,17 @@
export let description: ComponentType | undefined;
export let jobCounts: JobCountsDto;
export let queueStatus: QueueStatusDto;
export let allowForceCommand = true;
export let icon: string;
export let disabled = false;
export let allText: string;
export let allText: string | undefined;
export let refreshText: string | undefined;
export let missingText: string;
export let onCommand: (command: JobCommandDto) => void;
$: waitingCount = jobCounts.waiting + jobCounts.paused + jobCounts.delayed;
$: isIdle = !queueStatus.isActive && !queueStatus.isPaused;
$: multipleButtons = allText || refreshText;
const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pr-4 pl-6';
</script>
@@ -121,7 +123,9 @@
<Icon path={mdiAlertCircle} size="36" />
{$t('disabled').toUpperCase()}
</JobTileButton>
{:else if !isIdle}
{/if}
{#if !disabled && !isIdle}
{#if waitingCount > 0}
<JobTileButton color="gray" on:click={() => onCommand({ command: JobCommand.Empty, force: false })}>
<Icon path={mdiClose} size="24" />
@@ -141,16 +145,28 @@
{$t('pause').toUpperCase()}
</JobTileButton>
{/if}
{:else if allowForceCommand}
<JobTileButton color="gray" on:click={() => onCommand({ command: JobCommand.Start, force: true })}>
<Icon path={mdiAllInclusive} size="24" />
{allText}
</JobTileButton>
{/if}
{#if !disabled && multipleButtons && isIdle}
{#if allText}
<JobTileButton color="dark-gray" on:click={() => onCommand({ command: JobCommand.Start, force: true })}>
<Icon path={mdiAllInclusive} size="24" />
{allText}
</JobTileButton>
{/if}
{#if refreshText}
<JobTileButton color="gray" on:click={() => onCommand({ command: JobCommand.Start, force: undefined })}>
<Icon path={mdiImageRefreshOutline} size="24" />
{refreshText}
</JobTileButton>
{/if}
<JobTileButton color="light-gray" on:click={() => onCommand({ command: JobCommand.Start, force: false })}>
<Icon path={mdiSelectionSearch} size="24" />
{missingText}
</JobTileButton>
{:else}
{/if}
{#if !disabled && !multipleButtons && isIdle}
<JobTileButton color="light-gray" on:click={() => onCommand({ command: JobCommand.Start, force: false })}>
<Icon path={mdiPlay} size="48" />
{$t('start').toUpperCase()}

View File

@@ -32,10 +32,10 @@
subtitle?: string;
description?: ComponentType;
allText?: string;
missingText?: string;
refreshText?: string;
missingText: string;
disabled?: boolean;
icon: string;
allowForceCommand?: boolean;
handleCommand?: (jobId: JobName, jobCommand: JobCommandDto) => Promise<void>;
}
@@ -61,43 +61,54 @@
icon: mdiFileJpgBox,
title: $getJobName(JobName.ThumbnailGeneration),
subtitle: $t('admin.thumbnail_generation_job_description'),
allText: $t('all'),
missingText: $t('missing'),
},
[JobName.MetadataExtraction]: {
icon: mdiTable,
title: $getJobName(JobName.MetadataExtraction),
subtitle: $t('admin.metadata_extraction_job_description'),
allText: $t('all'),
missingText: $t('missing'),
},
[JobName.Library]: {
icon: mdiLibraryShelves,
title: $getJobName(JobName.Library),
subtitle: $t('admin.library_tasks_description'),
allText: $t('all').toUpperCase(),
missingText: $t('refresh').toUpperCase(),
allText: $t('all'),
missingText: $t('refresh'),
},
[JobName.Sidecar]: {
title: $getJobName(JobName.Sidecar),
icon: mdiFileXmlBox,
subtitle: $t('admin.sidecar_job_description'),
allText: $t('sync').toUpperCase(),
missingText: $t('discover').toUpperCase(),
allText: $t('sync'),
missingText: $t('discover'),
disabled: !$featureFlags.sidecar,
},
[JobName.SmartSearch]: {
icon: mdiImageSearch,
title: $getJobName(JobName.SmartSearch),
subtitle: $t('admin.smart_search_job_description'),
allText: $t('all'),
missingText: $t('missing'),
disabled: !$featureFlags.smartSearch,
},
[JobName.DuplicateDetection]: {
icon: mdiContentDuplicate,
title: $getJobName(JobName.DuplicateDetection),
subtitle: $t('admin.duplicate_detection_job_description'),
allText: $t('all'),
missingText: $t('missing'),
disabled: !$featureFlags.duplicateDetection,
},
[JobName.FaceDetection]: {
icon: mdiFaceRecognition,
title: $getJobName(JobName.FaceDetection),
subtitle: $t('admin.face_detection_description'),
allText: $t('reset'),
refreshText: $t('refresh'),
missingText: $t('missing'),
handleCommand: handleConfirmCommand,
disabled: !$featureFlags.facialRecognition,
},
@@ -105,6 +116,8 @@
icon: mdiTagFaces,
title: $getJobName(JobName.FacialRecognition),
subtitle: $t('admin.facial_recognition_job_description'),
allText: $t('reset'),
missingText: $t('missing'),
handleCommand: handleConfirmCommand,
disabled: !$featureFlags.facialRecognition,
},
@@ -112,18 +125,20 @@
icon: mdiVideo,
title: $getJobName(JobName.VideoConversion),
subtitle: $t('admin.video_conversion_job_description'),
allText: $t('all'),
missingText: $t('missing'),
},
[JobName.StorageTemplateMigration]: {
icon: mdiFolderMove,
title: $getJobName(JobName.StorageTemplateMigration),
allowForceCommand: false,
missingText: $t('missing'),
description: StorageMigrationDescription,
},
[JobName.Migration]: {
icon: mdiFolderMove,
title: $getJobName(JobName.Migration),
subtitle: $t('admin.migration_job_description'),
allowForceCommand: false,
missingText: $t('missing'),
},
};
$: jobList = Object.entries(jobDetails) as [JobName, JobDetails][];
@@ -150,7 +165,7 @@
</script>
<div class="flex flex-col gap-7">
{#each jobList as [jobName, { title, subtitle, description, disabled, allText, missingText, allowForceCommand, icon, handleCommand: handleCommandOverride }]}
{#each jobList as [jobName, { title, subtitle, description, disabled, allText, refreshText, missingText, icon, handleCommand: handleCommandOverride }]}
{@const { jobCounts, queueStatus } = jobs[jobName]}
<JobTile
{icon}
@@ -158,9 +173,9 @@
{disabled}
{subtitle}
{description}
allText={allText || $t('all').toUpperCase()}
missingText={missingText || $t('missing').toUpperCase()}
{allowForceCommand}
allText={allText?.toUpperCase()}
refreshText={refreshText?.toUpperCase()}
missingText={missingText.toUpperCase()}
{jobCounts}
{queueStatus}
onCommand={(command) => (handleCommandOverride || handleCommand)(jobName, command)}

View File

@@ -34,6 +34,7 @@
mdiContentCopy,
mdiDatabaseRefreshOutline,
mdiDotsVertical,
mdiHeadSyncOutline,
mdiImageRefreshOutline,
mdiImageSearch,
mdiMagnifyMinusOutline,
@@ -166,6 +167,11 @@
/>
{/if}
<hr />
<MenuOption
icon={mdiHeadSyncOutline}
onClick={() => onRunJob(AssetJobName.RefreshFaces)}
text={$getAssetJobName(AssetJobName.RefreshFaces)}
/>
<MenuOption
icon={mdiDatabaseRefreshOutline}
onClick={() => onRunJob(AssetJobName.RefreshMetadata)}

View File

@@ -49,8 +49,8 @@
"external_library_created_at": "External library (created on {date})",
"external_library_management": "External Library Management",
"face_detection": "Face detection",
"face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"All\" (re-)processes all assets. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.",
"facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"All\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.",
"face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.",
"facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.",
"failed_job_command": "Command {command} failed for job: {job}",
"force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.",
"forcing_refresh_library_files": "Forcing refresh of all library files",
@@ -1014,11 +1014,13 @@
"recent_searches": "Recent searches",
"refresh": "Refresh",
"refresh_encoded_videos": "Refresh encoded videos",
"refresh_faces": "Refresh faces",
"refresh_metadata": "Refresh metadata",
"refresh_thumbnails": "Refresh thumbnails",
"refreshed": "Refreshed",
"refreshes_every_file": "Re-reads all existing and new files",
"refreshing_encoded_video": "Refreshing encoded video",
"refreshing_faces": "Refreshing faces",
"refreshing_metadata": "Refreshing metadata",
"regenerating_thumbnails": "Regenerating thumbnails",
"remove": "Remove",

View File

@@ -21,7 +21,7 @@ import {
type SharedLinkResponseDto,
type UserResponseDto,
} from '@immich/sdk';
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js';
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiHeadSyncOutline, mdiImageRefreshOutline } from '@mdi/js';
import { sortBy } from 'lodash-es';
import { init, register, t } from 'svelte-i18n';
import { derived, get } from 'svelte/store';
@@ -214,6 +214,7 @@ export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: str
export const getAssetJobName = derived(t, ($t) => {
return (job: AssetJobName) => {
const names: Record<AssetJobName, string> = {
[AssetJobName.RefreshFaces]: $t('refresh_faces'),
[AssetJobName.RefreshMetadata]: $t('refresh_metadata'),
[AssetJobName.RegenerateThumbnail]: $t('refresh_thumbnails'),
[AssetJobName.TranscodeVideo]: $t('refresh_encoded_videos'),
@@ -226,6 +227,7 @@ export const getAssetJobName = derived(t, ($t) => {
export const getAssetJobMessage = derived(t, ($t) => {
return (job: AssetJobName) => {
const messages: Record<AssetJobName, string> = {
[AssetJobName.RefreshFaces]: $t('refreshing_faces'),
[AssetJobName.RefreshMetadata]: $t('refreshing_metadata'),
[AssetJobName.RegenerateThumbnail]: $t('regenerating_thumbnails'),
[AssetJobName.TranscodeVideo]: $t('refreshing_encoded_video'),
@@ -237,6 +239,7 @@ export const getAssetJobMessage = derived(t, ($t) => {
export const getAssetJobIcon = (job: AssetJobName) => {
const names: Record<AssetJobName, string> = {
[AssetJobName.RefreshFaces]: mdiHeadSyncOutline,
[AssetJobName.RefreshMetadata]: mdiDatabaseRefreshOutline,
[AssetJobName.RegenerateThumbnail]: mdiImageRefreshOutline,
[AssetJobName.TranscodeVideo]: mdiCogRefreshOutline,