-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(web): deduplication UI #9540
Changes from 44 commits
2649e48
a36e7fb
9645b52
3647ebb
6713115
ecc3469
aa92329
2bd8c94
e330b7f
aa62282
5341422
02b6275
74c4987
d683b63
20f8c66
d168b13
86425ef
7d7033f
659478d
f4e2e26
89e3282
7de0ab7
468a80a
dfdfff2
621c6b5
824aff3
3bfa0f4
310fd72
ecb08a3
bd6bc6c
83256c1
50265fd
f8df214
ac940d8
bc6b055
91e577e
2c161a0
aea6909
5bca35a
c723db6
b870524
d907a93
bb48d3a
1a9d21b
e607958
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -77,6 +77,40 @@ | |||||||||
</div> | ||||||||||
</SettingAccordion> | ||||||||||
|
||||||||||
<SettingAccordion | ||||||||||
key="duplicate-detection" | ||||||||||
title="Duplicate Detection" | ||||||||||
subtitle="Use CLIP embeddings to find likely duplicates" | ||||||||||
> | ||||||||||
<div class="ml-4 mt-4 flex flex-col gap-4"> | ||||||||||
<SettingSwitch | ||||||||||
id="enable-duplicate-detection" | ||||||||||
title="ENABLED" | ||||||||||
subtitle="If disabled, exactly identical assets will still be de-duplicated." | ||||||||||
bind:checked={config.machineLearning.duplicateDetection.enabled} | ||||||||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled} | ||||||||||
/> | ||||||||||
|
||||||||||
<hr /> | ||||||||||
|
||||||||||
<SettingInputField | ||||||||||
inputType={SettingInputFieldType.NUMBER} | ||||||||||
label="MAX DETECTION DISTANCE" | ||||||||||
bind:value={config.machineLearning.duplicateDetection.maxDistance} | ||||||||||
step="0.01" | ||||||||||
min={0.001} | ||||||||||
Comment on lines
+101
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the minimum value is .001, we probably want to allow more granular steps as well, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would agree, but @mertalev said not to when we lowered the minimum on the backend and UI. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I plan to test, fine-tune, and enable this feature by default in the subsequent PR. THis PR is dedicated mainly to UI work There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was originally against this because 0.001 increments are too subtle unless you're at the very bottom of the range and add more clicks. But it's fine since you can hold the arrow to move it quickly anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think this change will do it. (This is just the UI for raising and lowering the value, not min max or default.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I've been thinking about this as well, and there are also options in between .001 and .01. Maybe something like .005 could also be feasible? Ultimately it probably comes down to testing different options though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 0.001 will get rid of the annoying popup about the two nearest valid values though. So for now changing it to 0.001 and coming back to it later would prevent an annoying popup. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @alextran1502 you want to stick with 0.01 for now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @danieldietzler yes, I can modify it in subsequent PR after fine-tuning the value |
||||||||||
max={0.1} | ||||||||||
desc="Maximum distance between two images to consider them duplicates, ranging from 0.001-0.1. Higher values will detect more duplicates, but may result in false positives." | ||||||||||
disabled={disabled || | ||||||||||
!config.machineLearning.enabled || | ||||||||||
!config.machineLearning.clip.enabled || | ||||||||||
!config.machineLearning.duplicateDetection.enabled} | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be able to just use a feature flag here instead. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @NicholasFlamy What was the reason you didn't use the feature flag? I remember you have a PR put in for this, correct? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added the feature flag to the original backend PR. I didn't think to use the feature flag here (mainly I forgot about this part when I added that flag). So I think a feature flag could replace all of that.
Suggested change
I haven't tested this, either I'll make the change on my test instance or if it looks good you could commit it and I'll test it, but I won't get to testing either option until tomorrow. |
||||||||||
isEdited={config.machineLearning.duplicateDetection.maxDistance !== | ||||||||||
savedConfig.machineLearning.duplicateDetection.maxDistance} | ||||||||||
/> | ||||||||||
</div> | ||||||||||
</SettingAccordion> | ||||||||||
|
||||||||||
<SettingAccordion | ||||||||||
key="facial-recognition" | ||||||||||
title="Facial Recognition" | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
<script lang="ts"> | ||
import Button from '$lib/components/elements/buttons/button.svelte'; | ||
import Icon from '$lib/components/elements/icon.svelte'; | ||
import { getAssetThumbnailUrl } from '$lib/utils'; | ||
import { ThumbnailFormat, type AssetResponseDto, type DuplicateResponseDto, getAllAlbums } from '@immich/sdk'; | ||
import { mdiCheck, mdiTrashCanOutline } from '@mdi/js'; | ||
import { onMount } from 'svelte'; | ||
import { s } from '$lib/utils'; | ||
import { getAssetResolution, getFileSize } from '$lib/utils/asset-utils'; | ||
import { sortBy } from 'lodash-es'; | ||
|
||
export let duplicate: DuplicateResponseDto; | ||
export let onResolve: (duplicateAssetIds: string[], trashIds: string[]) => void; | ||
|
||
let selectedAssetIds = new Set<string>(); | ||
|
||
$: trashCount = duplicate.assets.length - selectedAssetIds.size; | ||
|
||
onMount(() => { | ||
const suggestedAsset = sortBy(duplicate.assets, (asset) => asset.exifInfo?.fileSizeInByte).pop(); | ||
|
||
if (!suggestedAsset) { | ||
selectedAssetIds = new Set(duplicate.assets[0].id); | ||
return; | ||
} | ||
|
||
selectedAssetIds.add(suggestedAsset.id); | ||
selectedAssetIds = selectedAssetIds; | ||
}); | ||
|
||
const onSelectAsset = (asset: AssetResponseDto) => { | ||
if (selectedAssetIds.has(asset.id)) { | ||
selectedAssetIds.delete(asset.id); | ||
} else { | ||
selectedAssetIds.add(asset.id); | ||
} | ||
|
||
selectedAssetIds = selectedAssetIds; | ||
}; | ||
|
||
const handleResolve = () => { | ||
const trashIds = duplicate.assets.map((asset) => asset.id).filter((id) => !selectedAssetIds.has(id)); | ||
const duplicateAssetIds = duplicate.assets.map((asset) => asset.id); | ||
onResolve(duplicateAssetIds, trashIds); | ||
}; | ||
</script> | ||
|
||
<div class="pt-4 rounded-3xl border dark:border-2 border-gray-300 dark:border-gray-700 max-w-[900px] m-auto mb-16"> | ||
<div class="flex flex-wrap gap-1 place-items-center place-content-center px-4 pt-4"> | ||
{#each duplicate.assets as asset, index (index)} | ||
{@const isSelected = selectedAssetIds.has(asset.id)} | ||
{@const isFromExternalLibrary = !!asset.libraryId} | ||
{@const assetData = JSON.stringify(asset, null, 2)} | ||
|
||
<div class="relative"> | ||
<button on:click={() => onSelectAsset(asset)} class="block relative"> | ||
<!-- THUMBNAIL--> | ||
<img | ||
src={getAssetThumbnailUrl(asset.id, ThumbnailFormat.Webp)} | ||
alt={asset.id} | ||
title={`${assetData}`} | ||
class={`w-[250px] h-[250px] object-cover rounded-t-xl border-t-[4px] border-l-[4px] border-r-[4px] border-gray-300 ${isSelected ? 'border-immich-primary dark:border-immich-dark-primary' : 'dark:border-gray-800'} transition-all`} | ||
draggable="false" | ||
/> | ||
|
||
<!-- OVERLAY CHIP --> | ||
<div | ||
class={`absolute bottom-2 right-3 ${isSelected ? 'bg-green-400/90' : 'bg-red-300/90'} px-4 py-1 rounded-xl text-xs font-semibold`} | ||
> | ||
{isSelected ? 'Keep' : 'Trash'} | ||
</div> | ||
|
||
<!-- EXTERNAL LIBRARY CHIP--> | ||
{#if isFromExternalLibrary} | ||
<div | ||
class="absolute top-2 right-3 bg-immich-primary/90 px-4 py-1 rounded-xl text-xs font-semibold text-white" | ||
> | ||
External | ||
</div> | ||
{/if} | ||
</button> | ||
|
||
<!-- ASSET INFO--> | ||
<table | ||
class={`text-xs w-full rounded-b-xl font-semibold ${isSelected ? 'bg-immich-primary text-white dark:bg-immich-dark-primary dark:text-black' : 'bg-gray-200 dark:bg-gray-800 dark:text-white'} mt-0 transition-all`} | ||
> | ||
<tr | ||
class={`h-[32px] ${isSelected ? 'border-immich-primary rounded-xl dark:border-immich-dark-primary' : 'border-gray-300'} text-center `} | ||
> | ||
<td>{asset.originalFileName}</td> | ||
</tr> | ||
|
||
<tr | ||
class={`h-[32px] ${isSelected ? 'border-immich-primary rounded-xl dark:border-immich-dark-primary' : 'border-gray-300'} text-center`} | ||
> | ||
<td>{getAssetResolution(asset)} - {getFileSize(asset)}</td> | ||
</tr> | ||
|
||
<tr | ||
class={`h-[32px] ${isSelected ? 'border-immich-primary rounded-xl dark:border-immich-dark-primary' : 'border-gray-300'} text-center `} | ||
> | ||
<td> | ||
{#await getAllAlbums({ assetId: asset.id })} | ||
Scanning for album... | ||
{:then albums} | ||
{#if albums.length === 0} | ||
Not in any album | ||
{:else} | ||
In {albums.length} album{s(albums.length)} | ||
{/if} | ||
{/await} | ||
</td> | ||
</tr> | ||
</table> | ||
</div> | ||
{/each} | ||
</div> | ||
|
||
<!-- CONFIRM BUTTONS --> | ||
<div class="flex gap-4 my-4 border-transparent w-full justify-end p-4 h-[85px]"> | ||
{#if trashCount === 0} | ||
<Button size="sm" color="primary" class="flex place-items-center gap-2" on:click={handleResolve} | ||
><Icon path={mdiCheck} size="20" />Keep All | ||
</Button> | ||
{:else} | ||
<Button size="sm" color="red" class="flex place-items-center gap-2" on:click={handleResolve} | ||
><Icon path={mdiTrashCanOutline} size="20" />{trashCount === duplicate.assets.length | ||
? 'Trash All' | ||
: `Trash ${trashCount}`} | ||
</Button> | ||
{/if} | ||
</div> | ||
</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<script lang="ts"> | ||
import { mdiContentDuplicate } from '@mdi/js'; | ||
import Icon from '$lib/components/elements/icon.svelte'; | ||
import { AppRoute } from '$lib/constants'; | ||
</script> | ||
|
||
<a href={AppRoute.DUPLICATES}> | ||
<div class="border border-gray-300 dark:border-immich-dark-gray rounded-3xl pt-1 pb-6 dark:text-white"> | ||
<p class="text-xs font-medium p-4">ORGANIZE YOUR LIBRARY</p> | ||
|
||
<button class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex gap-4 p-4"> | ||
<span | ||
><Icon path={mdiContentDuplicate} class="text-immich-primary dark:text-immich-dark-primary" size="24" /> | ||
</span> | ||
Review duplicates | ||
</button> | ||
</div> | ||
</a> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need
@ValidateUUID({ each: true })
here