Adds a full discovery and organisation layer to the gallery.
## AI Tagger
- WD tagger (ONNX via ort) with DirectML/CPU acceleration
- Per-image and per-folder tag queuing; configurable batch size and
confidence threshold; model downloaded on first use via ureq/zip
- Tags stored with source ('ai' | 'user'); user tags are never
overwritten by AI; AI tags protected from accidental promotion
- Tagger pause/resume per folder; in-flight batches discarded cleanly
on pause or cancellation without leaving jobs stuck in processing
## Semantic & Tag Search
- CLIP text-query embedding via candle (HuggingFace hub model)
- Progressive candidate doubling with filter post-processing so
folder/rating/media-kind filters do not silently under-return results
- Tag search with DB-backed pagination (offset + COUNT total)
- Filename search unchanged; prefix syntax: s:, t:, f: in search bar
## Similarity Search
- HNSW in-memory index (hnsw_rs) with monotonic embedding_revision
counter for cache invalidation; build_index retries if a concurrent
write advances the revision during construction
- Folder-scoped similar search uses brute-force cosine scan (no k limit)
- Region-based similarity: crop an arbitrary rectangle in the lightbox,
embed it with CLIP, find the nearest images globally or within folder
- Pagination for both similar and region results; scope toggle
(all media / current folder) re-runs the query without reopening image
## Duplicate Finder
- Three-phase detection: stat (size) → sample hash (4×16 KB windows)
→ full-file hash (xxh3, large files only) to eliminate false positives
- Filesystem-first deletion: only removes DB rows for files successfully
deleted from disk; failed files remain visible for retry
- Persisted scan cache (SQLite) with invalidation on reindex and deletion;
both global and per-folder scopes invalidated after any deletion
## Tag Cloud & Explore
- Visual cluster explore: k-means over CLIP embeddings, configurable k,
representative thumbnail per cluster; cache keyed on xxh3 of embedding set
- Tag explore: ranked tag list with image counts and representative
thumbnail per tag; invalidated when AI tagging completes or folder removed
- Tag autocomplete for search bar
## Folder Management
- Inline rename (display name only, not OS rename)
- Relocate: file-picker to update folder path; all image paths rewritten
using SUBSTR prefix replacement to avoid corrupting paths that contain
the folder name as a substring
- Missing-folder recovery banner: Locate or Remove, never silent deletion
- Right-click context menu on sidebar items: Reindex, Rename, Locate,
Remove; hover buttons preserved alongside context menu
## Background Tasks
- Per-folder progress panel with embedding, tagging, caption, and
thumbnail stage breakdown
- Retry button per task for failed embeddings and tagging jobs
- Completed-task notifications (system tray via tauri-plugin-notification)
- Scan errors shown inline; WalkDir permission errors protect existing
records from deletion rather than cascading data loss
## Backend correctness
- sqlite-vec DML performed outside transactions throughout (virtual-table
limitation); embedding revision incremented on delete as well as insert
- Tagging job discard checks status = 'processing' so paused jobs reset
to 'pending' are also skipped, not just cancelled ones
- Stale async responses in Lightbox guarded with cancellation flags and
currentImageIdRef for both tag-load and tag-add callbacks
- Duplicate cache cleared on reindex; folder-removal clears vector rows
outside transaction before deleting the folder row
4.5 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project overview
Phokus — a Tauri v2 desktop image gallery app with a React/TypeScript frontend and a Rust backend. The app indexes local media folders, generates thumbnails via FFmpeg, computes visual embeddings for semantic search and similarity, and AI-tags images using the WD tagger (ONNX via ort). AI captioning code exists in the backend but the UI surface has been removed; the worker is commented out in lib.rs.
Commands
# Development (Vite hot-reload + Rust auto-rebuild)
pnpm dev:app
# Frontend only (no Tauri window)
pnpm dev:vite
# Production build
pnpm build:app
# Type-check frontend
pnpm build:vite
Use pnpm — never npm.
There are no test suites configured.
Architecture
Frontend (src/)
store.ts— single Zustand store (useGalleryStore) that owns all app state and allinvoke()calls to the Tauri backend. Every feature (folders, images, search, similar images, tags, captions, tagger, duplicates) is implemented as store actions here. React components are thin consumers.App.tsx— sets up Tauri event listeners (subscribeToProgress) and renders the top-level layout (sidebar + active view).src/components/— UI components:Gallery,Lightbox,Sidebar,Toolbar,TagCloud,DuplicateFinder,BackgroundTasks,SettingsModal,MenuBar,TitleBar.- State management: Zustand v5, no selectors library — components call
useGalleryStore(s => s.field)directly. - Styling: Tailwind CSS v4 (Vite plugin, no config file).
- Virtualized gallery grid:
@tanstack/react-virtual. - Animation:
framer-motion.
Search modes
The search bar supports prefix syntax parsed by parseSearchValue in store.ts:
- No prefix /
f:— filename search (paginated, DB-backed) /s <query>ors: <query>— semantic (embedding) search/t <tag>ort: <tag>— tag search
Backend (src-tauri/src/)
Workers are started in lib.rs and run as background threads throughout the app lifetime:
- thumbnail worker (multiple threads, count from
StorageProfile::Balanced) - metadata worker — FFmpeg probe for video files
- embedding worker — generates CLIP-style visual embeddings (candle, HuggingFace hub)
- tagging worker — WD tagger via ONNX Runtime (
ort), DirectML/CPU acceleration
Key modules:
| File | Purpose |
|---|---|
db.rs |
SQLite pool (r2d2 + rusqlite), schema migrations, all query functions |
commands.rs |
All #[tauri::command] handlers — one-to-one with frontend invoke() calls |
indexer.rs |
Worker thread launchers and job dispatch |
embedder.rs |
Visual embedding generation (candle + HF hub models) |
vector.rs |
sqlite-vec integration + HNSW index for ANN search |
hnsw_index.rs |
In-memory HNSW index wrapper (hnsw_rs) |
tagger.rs |
WD tagger: ONNX model download, inference, CSV tag loading |
captioner.rs |
AI captioning (ONNX, disabled in workers but code intact) |
thumbnail.rs |
Thumbnail generation (image crate + fast_image_resize, FFmpeg for video) |
media.rs |
FFmpeg sidecar provisioning and probing |
storage.rs |
StorageProfile for tuning worker counts |
Database: SQLite with WAL mode, stored in the Tauri app data directory as gallery.db. Thumbnails stored alongside as thumbnails/.
Tauri events (backend → frontend)
| Event | Payload |
|---|---|
index-progress |
IndexProgress |
media-job-progress |
MediaJobProgressEvent |
indexed-images |
IndexedImagesBatch |
media-updated |
ThumbnailBatch |
caption-model-progress |
CaptionModelProgress |
tagger-model-progress |
TaggerModelProgress |
duplicate_scan_progress |
[scanned, total] |
Key types
ImageRecord (mirrored in store.ts and db.rs) is the central data type. It carries embedding status, tagging status, caption data, and media metadata. The frontend type must stay in sync with the Rust struct serialization.
Development notes
- Hot Reload is active during
dev:app— do not restart the server for frontend changes. Restart only when adding Rust crates or changing Vite config. - ML inference crates (
candle-*,ort,image,rayon,tokenizers,xxhash-rust,rusqlite) useopt-level = 3in dev profile to keep inference performance acceptable. - The caption worker is intentionally disabled (
lib.rs:73) — the backend code is intact for future re-enabling. - Never use
anytype in TypeScript — look up correct types.