Commit Graph

79 Commits

Author SHA1 Message Date
LyAhn 69e53ed62a chore(release): add MIT license, bundle metadata, and single NSIS target
Phase 1 of the 0.1.0 release prep:
- LICENSE (MIT) plus license fields in Cargo.toml, package.json, and bundle config
- tauri.conf.json: publisher/copyright/descriptions/homepage so the NSIS
  installer no longer derives 'jezz' from the identifier as manufacturer
- version: null in tauri.conf.json — Cargo.toml is now the version source
- bundle.targets narrowed from 'all' to nsis (updater-friendly, faster builds)
- track pnpm-workspace.yaml (esbuild build-script approval, needed by CI)
2026-06-12 18:20:27 +01:00
LyAhn d30fe47876 perf(workers): strict priority pipeline across background workers
Workers now form a priority pipeline (thumbnails -> metadata ->
embeddings -> tagging): a worker only claims jobs when no
higher-priority tier has claimable work, so stages drain one at a time
at full speed instead of all workers contending for CPU (shared rayon
pool), disk, and the DB writer simultaneously.

Each tier gate is a single read-only EXISTS query per idle tick using
the same exclusion set the corresponding claim would (that worker''s
paused folders plus actively-indexing folders), so paused or mid-scan
work never blocks lower tiers, and failed jobs stop gating once they
leave pending. Deferred workers wake within one backoff interval
(250-750ms) of the higher tier draining.

A watcher-driven trickle of new files briefly pauses lower tiers by
design. A user-initiated tag run during a large embedding backlog
waits for it; pausing embeddings per folder overrides if tags are
wanted first.
2026-06-12 10:45:12 +01:00
LyAhn cbfcbea96a perf(metadata): idle-only backoff and per-item commits
The metadata worker now sleeps its 250ms backoff only when the queue
is empty or a batch errored, claiming the next batch immediately while
work is pending. Each ffprobe result is committed and emitted
individually (in its own transaction) instead of after the whole
batch, so video metadata progress moves steadily rather than stalling
for up to 16 sequential probes.
2026-06-12 10:36:05 +01:00
LyAhn 948a489a8a fix(indexer): keep embedding and tagging off folders mid-scan
Embedding and tagging claims now exclude actively-indexing folders,
matching the thumbnail and metadata workers. Previously the embedding
worker started processing a folder while it was still being scanned,
competing with the scanner for CPU (shared rayon pool), disk, and the
DB writer — slowing scans and skewing the adaptive storage profile
toward Conservative. Video embedding jobs claimed mid-scan also
fail-fasted pointlessly since their thumbnails are deferred until
indexing completes; the existing backfill at scan end covers requeue.
2026-06-12 10:36:05 +01:00
LyAhn 334ac54e00 perf(workers): scaled embedding preprocessing and idle-only backoff
- Embedding and tagging workers now sleep their backoff interval
  (500ms/750ms) only when the queue is empty, the model is not ready,
  or a batch errored — previously every batch paid the sleep even with
  work pending.
- CLIP preprocessing no longer decodes originals at full resolution:
  decode_for_thumbnail is generalized into decode_image_scaled with a
  cover mode (shortest side >= target) and the embedder decodes JPEGs
  at the smallest DCT scale covering its 224px fill-crop, in parallel
  via rayon. The forward pass, not image decode, is now the dominant
  embedding cost.
- EXIF orientation is now applied before embedding, so rotated photos
  embed the way they are displayed (previously embedded sideways).
  Existing stored embeddings are unaffected.
2026-06-12 09:00:25 +01:00
LyAhn a4486547e8 perf(thumbnails): scaled JPEG decode and continuous worker batching
Major speedups for large-folder thumbnail generation:

- JPEGs decode through mozjpeg at the smallest DCT scale covering
  320px instead of full resolution, with catch_unwind and a fallback
  to the image crate for anything mozjpeg rejects. EXIF orientation
  preserved in both paths.
- RGB8 pipeline end to end (no RGBA round-trip) and CatmullRom resize
  in place of Lanczos3.
- Video posters grab the seeked frame directly instead of running the
  ffmpeg thumbnail filter (which decoded ~100 frames), with decode
  threads capped at 2 per process.
- Workers only sleep when the queue is empty rather than 250ms after
  every batch. Image batches commit before videos start, and videos
  run sequentially off the rayon pool (blocking ffmpeg waits were
  starving image decode) with per-item commits so progress keeps
  moving through slow stretches.
2026-06-12 08:13:12 +01:00
LyAhn 665c315f56 feat(duplicates): phased scan progress with skipped-file reporting
Replace the [scanned, total] tuple progress event with a structured
{phase, processed, total, skipped} payload covering all three scan
phases (checking, hashing, confirming). find_duplicates now returns a
DuplicateScanResult with scanned/candidate/skipped counts, and the UI
surfaces phase labels plus a warning when unreadable files were
skipped. Also adds a clean:app script for cargo clean.
2026-06-12 08:12:49 +01:00
LyAhn a34d38d9d3 feat: notification batching, per-folder mute, and global pause
Debounce embedding/tagging completion notifications per folder (6s window)
so rapid file additions from downloaders fire one notification instead of
one per file. Add per-folder mute via the sidebar context menu and a global
pause toggle in Settings > General, both persisted across restarts.
2026-06-11 06:28:16 +01:00
LyAhn ceb51f8fad feat: smart thumbnail cleanup and rename-aware file tracking
Delete thumbnails alongside DB rows in all three removal paths (watcher
detect, delete-from-disk command, remove-folder command) so orphans no
longer accumulate passively.

Intercept RenameMode::Both watcher events and handle them as in-place
path updates: renames the thumbnail file to the new hash and updates the
DB row without touching the embedding, avoiding a full rebuild on move.
Falls back to thumbnail regeneration if the file rename fails.
2026-06-09 01:16:12 +01:00
LyAhn 3707a35cc4 feat(settings): add orphaned thumbnail cleanup to General section
Scans the thumbnails directory and cross-references against thumbnail_path
values in the images table, deleting any files not linked to an indexed image.
Surfaces count and reclaimable MB in the General section alongside the
existing Compact database card.
2026-06-09 01:09:29 +01:00
LyAhn d1eb75a4f5 feat(settings): add compact database card to General section
Adds get_database_info and vacuum_database Tauri commands. The General
section now shows current DB size and reclaimable space on load, with a
Compact now button that runs PRAGMA wal_checkpoint(FULL) + VACUUM and
reports how many MB were freed. Button disables automatically when the
database is already compact (< 0.5 MB reclaimable).
2026-06-09 00:40:05 +01:00
LyAhn 33fb3c6c77 feat(settings): overhaul Settings modal with improvements and General section
- Remove Workers section (read-only status, belongs in background tasks panel)
- Remove Captioning coming-soon placeholder
- Add General section with Open data folder button (tauri-plugin-opener)
- Persist queue scope and folder selection across sessions via settings files
- Show inline validation errors on threshold/batch size inputs instead of silent revert
- Fix acceleration save errors appearing in wrong card
- Disable folder selection controls when target scope is All media
2026-06-09 00:14:49 +01:00
LyAhn 0ab156d2d9 fix: startup crash, watcher count, timeline date fallback
db.rs:
- Move idx_images_taken_at creation to after ensure_column so it never
  runs against a schema where taken_at doesn't exist yet; this was causing
  a startup panic on any DB that predates Phase 1
- COALESCE(taken_at, created_at) → COALESCE(taken_at, modified_at) since
  created_at is never populated (modified_at is always set)
- Remove dead is_tagging_job_cancelled function (superseded by
  is_tagging_job_processing) and unused thumbnail_path from ImagePathRecord

indexer.rs:
- Watcher create path now calls update_folder_count after commit_batch and
  emits folder-counts-changed so the sidebar count stays in sync; the
  notification is only emitted when the DB write actually succeeds

store.ts / Timeline.tsx:
- compareImages taken_asc/taken_desc: fall back to modified_at not
  created_at (created_at is always null)
- Timeline groupImages: same fallback fix so images group by month
  correctly for libraries not yet re-indexed for EXIF
- subscribeToProgress: listen for folder-counts-changed and call
  loadFolders() to keep the sidebar image count live
2026-06-08 22:09:50 +01:00
LyAhn ae9e806e61 feat(watcher): adaptive filesystem watchdog with zero CPU when idle
Monitors all registered folders using OS-native events (ReadDirectoryChangesW
on Windows) so new, modified, and deleted files are reflected in the gallery
automatically without manual reindexing.

Key design points:
- Adaptive blocking: recv() when no events pending (zero CPU), switches to
  recv_timeout(earliest_deadline) only when debounce timers are running — wakes
  exactly when the soonest event is ready, no busy-polling
- 500 ms per-path debounce coalesces rapid OS event bursts into one action
- Change detection preserved: build_record skips upsert if file_size + mtime
  unchanged, preventing spurious thumbnail/embedding re-queues and avoiding
  clobbering of existing metadata like thumbnail_path
- Access events (reads) filtered out; only Create/Modify/Remove trigger work
- Deletion path: emits watcher-deleted event with image IDs; frontend removes
  those images from state and clears selectedImage if it was deleted

WatcherHandle stored in app state; add_folder / remove_folder / update_folder_path
commands keep the watched path set in sync with the DB.
2026-06-08 06:50:01 +01:00
LyAhn 9ee5b08c93 feat(exif): extract capture date and surface as sortable taken_at field
Adds EXIF date extraction during indexing so photos can be sorted by when
they were actually taken, not just when the file was modified on disk.

- Add `taken_at` (nullable TEXT, ISO 8601) column to the images table via
  ensure_column migration and a new idx_images_taken_at index
- Populate taken_at in build_record via extract_exif_date, which tries
  DateTimeOriginal → DateTimeDigitized → DateTime tags and rejects all-zero
  sentinel dates written by uninitialised cameras
- upsert_image uses taken_at = excluded.taken_at so a re-indexed file gets
  fresh EXIF; stale dates from replaced files are not preserved
- Add taken_asc / taken_desc sort using COALESCE(taken_at, created_at) so
  images without EXIF gracefully fall back to their creation date
- Surface as "Taken: newest / oldest" in the Toolbar sort dropdown
- Shift map_image_row column indices after the new taken_at (index 10)
2026-06-08 00:26:36 +01:00
LyAhn 0ca4d142d8 feat(discovery): AI tagging, semantic search, similarity, duplicate finder, folder management
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
2026-06-07 22:43:16 +00:00
LyAhn 8905baf4a5 Merge pull request #7 from JezzWTF/feat/custom-titlebar
feat: custom TitleBar with window controls (no native decorations)
2026-04-06 21:02:45 +01:00
LyAhn d0b41119c6 Surface failed embeddings and add filter for affected files
- Fix root cause: embedding_source_path() returns Result<PathBuf>, returning
  Err for videos without a thumbnail instead of silently falling back to the
  raw .mp4 path that CLIP cannot decode
- indexer: split embedding batch into pre-failed (no source) and embeddable
  jobs; pre-failed are marked immediately without hitting the CLIP model
- db: retry_failed_embedding_jobs skips videos still without a thumbnail so
  they no longer re-fail immediately on retry
- Add get_failed_embedding_images command listing failed files + error per folder
- Gallery: amber warning badge on tiles with embedding_status = 'failed'
- BackgroundTasks: fetch and show failed filenames/errors in expanded panel
- Toolbar: conditional 'Failed Embeddings' amber filter pill shown when any
  folder has embedding_failed > 0; filters at DB level via new
  embedding_failed_only param on get_images / count_images
- TagCloud: replaced vocabulary/dictionary label system with representative
  image thumbnails per cluster; results cached in SQLite by image-id hash
2026-04-06 16:54:03 +01:00
LyAhn ab8fba2e3f fix: remove unused window permissions (allow-maximize, allow-unmaximize, allow-start-dragging) 2026-04-06 15:00:52 +01:00
LyAhn aed8fcfd67 feat: add window management permissions for custom titlebar 2026-04-06 14:05:19 +01:00
LyAhn 53f21580a2 feat: disable native decorations for custom titlebar 2026-04-06 14:05:07 +01:00
LyAhn 6c3fd449ce Add tag cloud feature with k-means clustering and CUDA support
Introduces an Explore view with a tag cloud that clusters image embeddings
using cosine k-means and labels clusters via vocabulary-nearest-neighbour
CLIP matching. Vocabulary embeddings are disk-cached (FNV hash-keyed) to
avoid redundant inference. Enables CUDA for candle dependencies and adds a
build.rs check that surfaces a clear error when the toolkit is missing.
2026-04-06 12:43:44 +01:00
LyAhn da9a20f8f7 Refine semantic search and CPU fallback
- make Candle CUDA support opt-in so default builds work on CPU-only machines
- improve semantic search loading and empty states in the gallery
- keep semantic search UX clearer when no matches are found

Refs: #4
2026-04-06 02:26:43 +01:00
LyAhn c6a66d1ba9 Polish search and embedding UX
- add semantic text search with toolbar mode switching and sqlite-vec query support
- improve embedding progress visibility, failure recovery, and similar-image affordances
- add search clearing and keyboard controls for filename vs semantic search modes
- refine background task interactions and gallery/lightbox embedding states

Refs: #4
2026-04-06 01:45:25 +01:00
LyAhn 51e4c2c1f7 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
2026-04-06 00:54:14 +01:00
LyAhn 76ec424167 Optimize thumbnail workers and adaptive indexing 2026-04-05 20:36:11 +01:00
LyAhn c4036140e6 Improve media indexing and processing flow 2026-04-05 18:59:17 +01:00
LyAhn c299c7d452 Improve indexing updates and thumbnail streaming 2026-04-05 16:52:31 +01:00
LyAhn c5e9c83ac9 Initial commit — Tauri image gallery with sqlite-vec
Sets up Tauri v2 + React frontend with SQLite metadata store,
sqlite-vec for CLIP embedding infrastructure, and r2d2 connection
pool replacing the single Arc<Mutex<Connection>>. Indexer now uses
rayon for parallel file metadata collection.
2026-04-05 15:59:48 +01:00