mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat: facial recognition (#2180)
This commit is contained in:
@@ -36,6 +36,10 @@
|
||||
title: 'Encode Clip',
|
||||
subtitle: 'Run machine learning to generate clip embeddings'
|
||||
},
|
||||
[JobName.RecognizeFacesQueue]: {
|
||||
title: 'Recognize Faces',
|
||||
subtitle: 'Run machine learning to recognize faces'
|
||||
},
|
||||
[JobName.VideoConversionQueue]: {
|
||||
title: 'Transcode Videos',
|
||||
subtitle: 'Transcode videos not in the desired format'
|
||||
|
||||
@@ -120,10 +120,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
const clearMultiSelectAssetAssetHandler = () => {
|
||||
multiSelectAsset = new Set();
|
||||
};
|
||||
|
||||
// Update Album Name
|
||||
$: {
|
||||
if (!isEditingTitle && currentAlbumName != album.albumName && isOwned) {
|
||||
@@ -340,7 +336,7 @@
|
||||
{#if isMultiSelectionMode}
|
||||
<AssetSelectControlBar
|
||||
assets={multiSelectAsset}
|
||||
clearSelect={clearMultiSelectAssetAssetHandler}
|
||||
clearSelect={() => (multiSelectAsset = new Set())}
|
||||
>
|
||||
<DownloadFiles filename={album.albumName} sharedLinkKey={sharedLink?.key} />
|
||||
{#if isOwned}
|
||||
|
||||
@@ -91,6 +91,11 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseViewer = () => {
|
||||
isShowDetail = false;
|
||||
closeViewer();
|
||||
};
|
||||
|
||||
const closeViewer = () => {
|
||||
dispatch('close');
|
||||
};
|
||||
@@ -398,6 +403,7 @@
|
||||
{asset}
|
||||
albums={appearsInAlbums}
|
||||
on:close={() => (isShowDetail = false)}
|
||||
on:close-viewer={handleCloseViewer}
|
||||
on:description-focus-in={disableKeyDownEvent}
|
||||
on:description-focus-out={enableKeyDownEvent}
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script lang="ts">
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { LatLngTuple } from 'leaflet';
|
||||
import { DateTime } from 'luxon';
|
||||
import Calendar from 'svelte-material-icons/Calendar.svelte';
|
||||
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
|
||||
import CameraIris from 'svelte-material-icons/CameraIris.svelte';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
|
||||
import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { AssetResponseDto, AlbumResponseDto, api, ThumbnailFormat } from '@api';
|
||||
import { asByteUnitString } from '../../utils/byte-units';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { LatLngTuple } from 'leaflet';
|
||||
import { page } from '$app/stores';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
|
||||
export let asset: AssetResponseDto;
|
||||
export let albums: AlbumResponseDto[] = [];
|
||||
@@ -20,9 +21,10 @@
|
||||
$: {
|
||||
// Get latest description from server
|
||||
if (asset.id) {
|
||||
api.assetApi
|
||||
.getAssetById(asset.id)
|
||||
.then((res) => (textarea.value = res.data?.exifInfo?.description || ''));
|
||||
api.assetApi.getAssetById(asset.id).then((res) => {
|
||||
people = res.data?.people || [];
|
||||
textarea.value = res.data?.exifInfo?.description || '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +37,8 @@
|
||||
}
|
||||
})();
|
||||
|
||||
$: people = asset.people || [];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const getMegapixel = (width: number, height: number): number | undefined => {
|
||||
@@ -81,7 +85,7 @@
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg text-lg">Info</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-4 mt-10">
|
||||
<section class="mx-4 mt-10">
|
||||
<textarea
|
||||
bind:this={textarea}
|
||||
class="max-h-[500px]
|
||||
@@ -96,13 +100,35 @@
|
||||
bind:value={description}
|
||||
disabled={$page?.data?.user?.id !== asset.ownerId}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if people.length > 0}
|
||||
<section class="px-4 py-4 text-sm">
|
||||
<h2>PEOPLE</h2>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mt-4">
|
||||
{#each people as person (person.id)}
|
||||
<a href="/people/{person.id}" class="w-[90px]" on:click={() => dispatch('close-viewer')}>
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(person.id)}
|
||||
altText={person.name}
|
||||
widthStyle="90px"
|
||||
heightStyle="90px"
|
||||
/>
|
||||
<p class="font-medium mt-1 truncate">{person.name}</p>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<div class="px-4 py-4">
|
||||
{#if !asset.exifInfo}
|
||||
<p class="text-sm pb-4">NO EXIF INFO AVAILABLE</p>
|
||||
<p class="text-sm">NO EXIF INFO AVAILABLE</p>
|
||||
{:else}
|
||||
<p class="text-sm pb-4">DETAILS</p>
|
||||
<p class="text-sm">DETAILS</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo?.dateTimeOriginal}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { imageLoad } from '$lib/utils/image-load';
|
||||
|
||||
export let url: string;
|
||||
export let altText: string;
|
||||
export let heightStyle: string;
|
||||
export let heightStyle: string | undefined = undefined;
|
||||
export let widthStyle: string;
|
||||
|
||||
export let curve = false;
|
||||
export let shadow = false;
|
||||
export let circle = false;
|
||||
let loading = true;
|
||||
</script>
|
||||
|
||||
@@ -13,7 +17,11 @@
|
||||
src={url}
|
||||
alt={altText}
|
||||
class="object-cover transition-opacity duration-300"
|
||||
class:rounded-lg={curve}
|
||||
class:shadow-lg={shadow}
|
||||
class:rounded-full={circle}
|
||||
class:opacity-0={loading}
|
||||
draggable="false"
|
||||
on:load|once={() => (loading = false)}
|
||||
use:imageLoad
|
||||
on:image-load|once={() => (loading = false)}
|
||||
/>
|
||||
|
||||
42
web/src/lib/components/faces-page/edit-name-input.svelte
Normal file
42
web/src/lib/components/faces-page/edit-name-input.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { PersonResponseDto, api } from '@api';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
|
||||
export let person: PersonResponseDto;
|
||||
let name = person.name;
|
||||
|
||||
const dispatch = createEventDispatcher<{ change: string }>();
|
||||
const handleNameChange = () => dispatch('change', name);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex place-items-center max-w-lg rounded-lg border dark:border-transparent p-2 bg-gray-100 dark:bg-gray-700"
|
||||
>
|
||||
<ImageThumbnail
|
||||
circle
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(person.id)}
|
||||
altText={person.name}
|
||||
widthStyle="2rem"
|
||||
heightStyle="2rem"
|
||||
/>
|
||||
<form
|
||||
class="ml-4 flex justify-between w-full gap-16"
|
||||
autocomplete="off"
|
||||
on:submit|preventDefault={handleNameChange}
|
||||
>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
class="gap-2 w-full bg-gray-100 dark:bg-gray-700 dark:text-white"
|
||||
type="text"
|
||||
placeholder="New name or nickname"
|
||||
required
|
||||
bind:value={name}
|
||||
on:blur
|
||||
/>
|
||||
<Button size="sm" type="submit">Done</Button>
|
||||
</form>
|
||||
</div>
|
||||
Reference in New Issue
Block a user