import { useEffect } from "react"; import { TaggerModel, TaggerModelProgress, useGalleryStore } from "../../store"; import { TAGGER_MODELS } from "../../taggerModels"; import { FakeProgressBar, formatBytes } from "./fakes"; const FAKE_TAGS = ["landscape", "sunset", "outdoors", "no_humans", "ocean", "cloudy_sky"]; // Prefer the current file's byte fraction (the 1.3 GB model dominates); fall // back to the coarse step count; null renders an indeterminate bar. function taggerDownloadFraction(progress: TaggerModelProgress | null): number | null { if (!progress) return null; if (progress.downloaded_bytes != null && progress.total_bytes != null && progress.total_bytes > 0) { return progress.downloaded_bytes / progress.total_bytes; } if (progress.total_files > 0) return progress.completed_files / progress.total_files; return null; } function TaggerModelChoice({ model, current, disabled, onSelect }: { model: TaggerModel; current: TaggerModel; disabled: boolean; onSelect: (model: TaggerModel) => void; }) { const active = model === current; return ( ); } export function StepAiFeatures() { const taggerModelStatus = useGalleryStore((s) => s.taggerModelStatus); const taggerModelPreparing = useGalleryStore((s) => s.taggerModelPreparing); const taggerModelProgress = useGalleryStore((s) => s.taggerModelProgress); const taggerModelError = useGalleryStore((s) => s.taggerModelError); const taggerModel = useGalleryStore((s) => s.taggerModel); const prepareTaggerModel = useGalleryStore((s) => s.prepareTaggerModel); const loadTaggerModel = useGalleryStore((s) => s.loadTaggerModel); const loadTaggerModelStatus = useGalleryStore((s) => s.loadTaggerModelStatus); const setTaggerModel = useGalleryStore((s) => s.setTaggerModel); useEffect(() => { void loadTaggerModel(); void loadTaggerModelStatus(); }, [loadTaggerModel, loadTaggerModelStatus]); const taggerReady = taggerModelStatus?.ready ?? false; const taggerStatusLabel = taggerReady ? "Installed" : taggerModelPreparing ? "Preparing" : "Not installed"; return (

Phokus's AI runs entirely on this machine — nothing is sent anywhere. Semantic search sets itself up automatically; AI tagging is optional and only downloads if you want it.

AI tagging — optional

Automatic tags for every image

The AI tagger model labels images so you can search with{" "} /t — tags look like:

{(["wd", "joytag"] as const).map((model) => ( { if (nextModel === taggerModel) return; void setTaggerModel(nextModel); }} /> ))}

Current: {TAGGER_MODELS[taggerModel].name} · {taggerStatusLabel}

{TAGGER_MODELS[taggerModel].description}

{FAKE_TAGS.map((tag) => ( {tag} ))}
{taggerReady ? ( Installed ) : ( )}
{taggerModelPreparing ? (
{taggerModelProgress?.current_file ?? "Preparing..."} {taggerModelProgress?.downloaded_bytes != null && taggerModelProgress.total_bytes != null ? ( {formatBytes(taggerModelProgress.downloaded_bytes)} / {formatBytes(taggerModelProgress.total_bytes)} ) : null}
) : null} {!taggerModelPreparing && taggerModelError ? (

Download failed: {taggerModelError}

) : null}

Semantic search & similarity — built in

Search by meaning, find look-alikes

Powers /s search, "find similar", and the Explore view, so it's part of the standard pipeline: the CLIP model (~580 MB) downloads automatically the first time embeddings run. Nothing to do — you'll see it in the background-tasks bar.

Semantic search and similarity are part of the standard pipeline; AI tagging stays optional and can be downloaded any time from Settings.

); }