Add CLIP embeddings and similar-image search
- add Candle + HF Hub CLIP image embedding pipeline with background embedding worker - write image embeddings into sqlite-vec and expose similar-image lookup through a new backend command - surface embedding progress and recovery in the UI, including retries for failed embeddings - improve gallery/lightbox embedding UX and make similar-image actions directly accessible Refs: #3, #4
This commit is contained in:
@@ -16,19 +16,27 @@ export function BackgroundTasks() {
|
||||
const folders = useGalleryStore((state) => state.folders);
|
||||
const indexingProgress = useGalleryStore((state) => state.indexingProgress);
|
||||
const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress);
|
||||
const retryFailedEmbeddings = useGalleryStore((state) => state.retryFailedEmbeddings);
|
||||
|
||||
const tasks = useMemo(() => {
|
||||
return folders
|
||||
.map((folder) => {
|
||||
const index = indexingProgress[folder.id];
|
||||
const jobs = mediaJobProgress[folder.id];
|
||||
const pendingMediaWork = (jobs?.thumbnail_pending ?? 0) + (jobs?.metadata_pending ?? 0);
|
||||
const pendingMediaWork =
|
||||
(jobs?.thumbnail_pending ?? 0) +
|
||||
(jobs?.metadata_pending ?? 0) +
|
||||
(jobs?.embedding_pending ?? 0);
|
||||
const embeddingProcessed = (jobs?.embedding_ready ?? 0) + (jobs?.embedding_failed ?? 0);
|
||||
const embeddingTotal = embeddingProcessed + (jobs?.embedding_pending ?? 0);
|
||||
const hasFailedEmbeddings = (jobs?.embedding_failed ?? 0) > 0;
|
||||
|
||||
if (!index && pendingMediaWork === 0) {
|
||||
if (!index && pendingMediaWork === 0 && !hasFailedEmbeddings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const indexPercent = index && index.total > 0 ? (index.indexed / index.total) * 100 : 0;
|
||||
const embeddingPercent = embeddingTotal > 0 ? (embeddingProcessed / embeddingTotal) * 100 : 0;
|
||||
return {
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
@@ -36,6 +44,10 @@ export function BackgroundTasks() {
|
||||
jobs,
|
||||
pendingMediaWork,
|
||||
indexPercent,
|
||||
embeddingProcessed,
|
||||
embeddingTotal,
|
||||
embeddingPercent,
|
||||
hasFailedEmbeddings,
|
||||
};
|
||||
})
|
||||
.filter((task) => task !== null);
|
||||
@@ -60,12 +72,20 @@ export function BackgroundTasks() {
|
||||
<p className="text-[11px] text-gray-500">
|
||||
{task.index && !task.index.done
|
||||
? `${task.index.indexed.toLocaleString()} of ${task.index.total.toLocaleString()} scanned`
|
||||
: `${task.pendingMediaWork.toLocaleString()} media jobs remaining`}
|
||||
: task.hasFailedEmbeddings && task.pendingMediaWork === 0
|
||||
? `Embedding failures require attention`
|
||||
: `${task.pendingMediaWork.toLocaleString()} media jobs remaining`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right text-[11px] text-gray-400">
|
||||
{task.jobs?.thumbnail_pending ? <div>{task.jobs.thumbnail_pending.toLocaleString()} thumbnails</div> : null}
|
||||
{task.jobs?.metadata_pending ? <div>{task.jobs.metadata_pending.toLocaleString()} metadata</div> : null}
|
||||
{task.embeddingTotal > 0 ? (
|
||||
<div>
|
||||
{task.embeddingProcessed.toLocaleString()} / {task.embeddingTotal.toLocaleString()} embeddings
|
||||
</div>
|
||||
) : null}
|
||||
{task.jobs?.embedding_failed ? <div>{task.jobs.embedding_failed.toLocaleString()} failed</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -74,10 +94,32 @@ export function BackgroundTasks() {
|
||||
<ProgressBar value={task.indexPercent} />
|
||||
<p className="truncate text-[11px] text-gray-500">{task.index.current_file || "Scanning..."}</p>
|
||||
</div>
|
||||
) : task.embeddingTotal > 0 && (task.jobs?.embedding_pending ?? 0) > 0 ? (
|
||||
<div className="mt-2 space-y-1">
|
||||
<ProgressBar value={task.embeddingPercent} />
|
||||
<p className="text-[11px] text-gray-500">
|
||||
{task.embeddingProcessed.toLocaleString()} completed, {task.jobs?.embedding_pending?.toLocaleString() ?? 0} remaining
|
||||
</p>
|
||||
</div>
|
||||
) : task.hasFailedEmbeddings ? (
|
||||
<div className="mt-2 space-y-1">
|
||||
<ProgressBar value={100} />
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-[11px] text-amber-300">
|
||||
{task.jobs?.embedding_failed?.toLocaleString() ?? 0} embedding failures need attention
|
||||
</p>
|
||||
<button
|
||||
className="rounded-full border border-amber-400/20 bg-amber-500/10 px-2.5 py-1 text-[11px] text-amber-200 hover:bg-amber-500/20"
|
||||
onClick={() => void retryFailedEmbeddings(task.id)}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : task.pendingMediaWork > 0 ? (
|
||||
<div className="mt-2 space-y-1">
|
||||
<ProgressBar value={0} />
|
||||
<p className="text-[11px] text-gray-500">Processing thumbnails and metadata</p>
|
||||
<p className="text-[11px] text-gray-500">Processing thumbnails, metadata, and embeddings</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user