fix(ai-tags): refresh tagger readiness for lightbox

Load the selected tagger model and model status during app startup so the lightbox AI tags action does not stay disabled until Settings refreshes the state.

Refresh readiness after tagger downloads complete and replace stale WD-specific unavailable copy with generic AI tagger wording.
This commit is contained in:
2026-07-02 20:13:45 +01:00
parent 749b23723a
commit b7e82dbf91
6 changed files with 26 additions and 5 deletions
+2 -1
View File
@@ -466,7 +466,8 @@ pub fn probe_tagger_runtime(app_data_dir: &Path) -> Result<TaggerRuntimeProbe> {
let status = tagger_model_status(app_data_dir); let status = tagger_model_status(app_data_dir);
if !status.ready { if !status.ready {
anyhow::bail!( anyhow::bail!(
"WD Tagger model is missing {} required file(s): {}", "{} is missing {} required file(s): {}",
status.model_name,
status.missing_files.len(), status.missing_files.len(),
status.missing_files.join(", ") status.missing_files.join(", ")
); );
+4
View File
@@ -23,6 +23,8 @@ export default function App() {
const loadBackgroundJobProgress = useGalleryStore((state) => state.loadBackgroundJobProgress); const loadBackgroundJobProgress = useGalleryStore((state) => state.loadBackgroundJobProgress);
const loadImages = useGalleryStore((state) => state.loadImages); const loadImages = useGalleryStore((state) => state.loadImages);
const loadCaptionModelStatus = useGalleryStore((state) => state.loadCaptionModelStatus); const loadCaptionModelStatus = useGalleryStore((state) => state.loadCaptionModelStatus);
const loadTaggerModelStatus = useGalleryStore((state) => state.loadTaggerModelStatus);
const loadTaggerModel = useGalleryStore((state) => state.loadTaggerModel);
const loadDuplicateScanCache = useGalleryStore((state) => state.loadDuplicateScanCache); const loadDuplicateScanCache = useGalleryStore((state) => state.loadDuplicateScanCache);
const loadAlbums = useGalleryStore((state) => state.loadAlbums); const loadAlbums = useGalleryStore((state) => state.loadAlbums);
const loadMutedFolderIds = useGalleryStore((state) => state.loadMutedFolderIds); const loadMutedFolderIds = useGalleryStore((state) => state.loadMutedFolderIds);
@@ -53,6 +55,8 @@ export default function App() {
loadFolders().then(async () => { loadFolders().then(async () => {
void loadBackgroundJobProgress(); void loadBackgroundJobProgress();
void loadCaptionModelStatus(); void loadCaptionModelStatus();
void loadTaggerModel();
void loadTaggerModelStatus();
void loadDuplicateScanCache(); void loadDuplicateScanCache();
await loadAlbums(); await loadAlbums();
await loadImages(true); await loadImages(true);
+15 -2
View File
@@ -156,6 +156,7 @@ export function Lightbox() {
const addUserTag = useGalleryStore((state) => state.addUserTag); const addUserTag = useGalleryStore((state) => state.addUserTag);
const removeTag = useGalleryStore((state) => state.removeTag); const removeTag = useGalleryStore((state) => state.removeTag);
const taggerModelStatus = useGalleryStore((state) => state.taggerModelStatus); const taggerModelStatus = useGalleryStore((state) => state.taggerModelStatus);
const loadTaggerModelStatus = useGalleryStore((state) => state.loadTaggerModelStatus);
const queueTaggingForImage = useGalleryStore((state) => state.queueTaggingForImage); const queueTaggingForImage = useGalleryStore((state) => state.queueTaggingForImage);
const albums = useGalleryStore((state) => state.albums); const albums = useGalleryStore((state) => state.albums);
const addToAlbum = useGalleryStore((state) => state.addToAlbum); const addToAlbum = useGalleryStore((state) => state.addToAlbum);
@@ -254,6 +255,13 @@ export function Lightbox() {
const canStartSlideshow = slideshowImages.length > 0; const canStartSlideshow = slideshowImages.length > 0;
const canFindSimilar = selectedImage?.embedding_status === "ready"; const canFindSimilar = selectedImage?.embedding_status === "ready";
const canSearchRegion = canFindSimilar && selectedImage?.media_kind === "image"; const canSearchRegion = canFindSimilar && selectedImage?.media_kind === "image";
const taggerReady = taggerModelStatus?.ready ?? false;
const taggerStatusKnown = taggerModelStatus !== null;
const taggerButtonTooltip = !taggerStatusKnown
? "Checking AI tagger model..."
: taggerReady
? "Queue AI tagging for this image"
: "AI tagger model not installed";
const goPrev = useCallback(() => { const goPrev = useCallback(() => {
if (currentIndex > 0) openImage(images[currentIndex - 1]); if (currentIndex > 0) openImage(images[currentIndex - 1]);
@@ -421,6 +429,11 @@ export function Lightbox() {
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [selectedImage?.id, selectedImage?.media_kind, getImageExif]); }, [selectedImage?.id, selectedImage?.media_kind, getImageExif]);
useEffect(() => {
if (selectedImage?.media_kind !== "image" || taggerStatusKnown) return;
void loadTaggerModelStatus();
}, [loadTaggerModelStatus, selectedImage?.media_kind, taggerStatusKnown]);
// Reset the queued state once the worker finishes so the button is usable again // Reset the queued state once the worker finishes so the button is usable again
useEffect(() => { useEffect(() => {
if (selectedImage?.ai_tagged_at) setTaggingQueued(false); if (selectedImage?.ai_tagged_at) setTaggingQueued(false);
@@ -1156,10 +1169,10 @@ export function Lightbox() {
</span> </span>
) : null} ) : null}
{selectedImage.media_kind === "image" ? ( {selectedImage.media_kind === "image" ? (
<Tooltip label={!(taggerModelStatus?.ready ?? false) ? "WD Tagger model not downloaded" : "Queue AI tagging for this image"} followCursor> <Tooltip label={taggerButtonTooltip} followCursor>
<button <button
className="rounded-md border border-white/10 bg-white/5 px-2 py-0.5 text-[10px] text-gray-400 transition-colors hover:bg-white/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-40" className="rounded-md border border-white/10 bg-white/5 px-2 py-0.5 text-[10px] text-gray-400 transition-colors hover:bg-white/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-40"
disabled={!(taggerModelStatus?.ready ?? false) || taggingQueued} disabled={!taggerReady || taggingQueued}
onClick={() => { onClick={() => {
setTaggingQueued(true); setTaggingQueued(true);
void queueTaggingForImage(selectedImage.id).catch(() => undefined); void queueTaggingForImage(selectedImage.id).catch(() => undefined);
+1 -1
View File
@@ -322,7 +322,7 @@ export function SettingsModal() {
: taggerModelProgress : taggerModelProgress
? `Downloading ${taggerModelProgress.completed_files}/${taggerModelProgress.total_files}` ? `Downloading ${taggerModelProgress.completed_files}/${taggerModelProgress.total_files}`
: taggerModelPreparing : taggerModelPreparing
? "Preparing WD Tagger..." ? "Preparing AI tagger..."
: taggerReady : taggerReady
? "Installed" ? "Installed"
: "Install model"; : "Install model";
+1 -1
View File
@@ -43,7 +43,7 @@ export function StepAiFeatures() {
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm text-white">Automatic tags for every image</p> <p className="text-sm text-white">Automatic tags for every image</p>
<p className="mt-1 text-xs leading-relaxed text-gray-500"> <p className="mt-1 text-xs leading-relaxed text-gray-500">
The WD tagger model (~1.3 GB download) labels images so you can search with{" "} The AI tagger model labels images so you can search with{" "}
<code className="rounded bg-gray-900 px-1 py-0.5 text-[11px] text-gray-200 light-theme:bg-gray-800 light-theme:text-gray-100">/t</code> — tags look like: <code className="rounded bg-gray-900 px-1 py-0.5 text-[11px] text-gray-200 light-theme:bg-gray-800 light-theme:text-gray-100">/t</code> — tags look like:
</p> </p>
<span className="mt-2 flex flex-wrap gap-1.5"> <span className="mt-2 flex flex-wrap gap-1.5">
+3
View File
@@ -3010,6 +3010,9 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
taggerModelProgress: event.payload.done ? null : event.payload, taggerModelProgress: event.payload.done ? null : event.payload,
taggerModelPreparing: !event.payload.done, taggerModelPreparing: !event.payload.done,
}); });
if (event.payload.done) {
void get().loadTaggerModelStatus();
}
}); });
const unlistenImages = await listen<IndexedImagesBatch>("indexed-images", (event) => { const unlistenImages = await listen<IndexedImagesBatch>("indexed-images", (event) => {