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
This commit was merged in pull request #8.
This commit is contained in:
+60
-3
@@ -1,14 +1,17 @@
|
||||
mod captioner;
|
||||
mod commands;
|
||||
mod db;
|
||||
mod embedder;
|
||||
mod hnsw_index;
|
||||
mod indexer;
|
||||
mod media;
|
||||
mod storage;
|
||||
mod tagger;
|
||||
mod thumbnail;
|
||||
mod vector;
|
||||
|
||||
use tauri::Manager;
|
||||
use crate::storage::StorageProfile;
|
||||
use tauri::Manager;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
@@ -16,6 +19,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.setup(|app| {
|
||||
let app_dir = app
|
||||
.path()
|
||||
@@ -34,11 +38,19 @@ pub fn run() {
|
||||
let conn = pool.get().expect("Failed to get connection for migration");
|
||||
db::migrate(&conn).expect("Failed to run migrations");
|
||||
db::reset_inflight_jobs(&conn).expect("Failed to reset inflight jobs");
|
||||
let backfilled = db::backfill_embedding_jobs(&conn)
|
||||
.expect("Failed to backfill embedding jobs");
|
||||
let backfilled =
|
||||
db::backfill_embedding_jobs(&conn).expect("Failed to backfill embedding jobs");
|
||||
if backfilled > 0 {
|
||||
println!("Backfilled {} embedding jobs.", backfilled);
|
||||
}
|
||||
let (orphaned_vectors, missing_vectors) = db::repair_embedding_consistency(&conn)
|
||||
.expect("Failed to repair embedding consistency");
|
||||
if orphaned_vectors > 0 || missing_vectors > 0 {
|
||||
println!(
|
||||
"Repaired embedding consistency: removed {} orphaned vectors, requeued {} missing vectors.",
|
||||
orphaned_vectors, missing_vectors
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let thumb_dir = app_dir.join("thumbnails");
|
||||
@@ -58,6 +70,9 @@ pub fn run() {
|
||||
}
|
||||
indexer::start_metadata_worker(app.handle().clone(), pool.clone(), media_tools.clone());
|
||||
indexer::start_embedding_worker(app.handle().clone(), pool.clone());
|
||||
// Caption worker disabled — UI removed; keeping backend code intact for future use.
|
||||
// indexer::start_caption_worker(app.handle().clone(), pool.clone(), app_dir.clone());
|
||||
indexer::start_tagging_worker(app.handle().clone(), pool.clone(), app_dir.clone());
|
||||
|
||||
app.manage(pool);
|
||||
app.manage(media_tools);
|
||||
@@ -73,12 +88,54 @@ pub fn run() {
|
||||
commands::reindex_folder,
|
||||
commands::update_image_details,
|
||||
commands::find_similar_images,
|
||||
commands::find_similar_by_region,
|
||||
commands::debug_similar_images,
|
||||
commands::retry_failed_embeddings,
|
||||
commands::semantic_search_images,
|
||||
commands::search_images_by_tag,
|
||||
commands::get_caption_model_status,
|
||||
commands::get_caption_acceleration,
|
||||
commands::set_caption_acceleration,
|
||||
commands::get_caption_detail,
|
||||
commands::set_caption_detail,
|
||||
commands::prepare_caption_model,
|
||||
commands::delete_caption_model,
|
||||
commands::probe_caption_runtime,
|
||||
commands::probe_caption_image,
|
||||
commands::generate_caption_for_image,
|
||||
commands::queue_caption_jobs,
|
||||
commands::clear_caption_jobs,
|
||||
commands::reset_generated_captions,
|
||||
commands::set_generated_caption,
|
||||
commands::suggest_image_tags,
|
||||
commands::set_worker_paused,
|
||||
commands::get_worker_states,
|
||||
commands::get_tag_cloud,
|
||||
commands::get_explore_tags,
|
||||
commands::get_images_by_ids,
|
||||
commands::get_failed_embedding_images,
|
||||
commands::get_tagger_model_status,
|
||||
commands::get_tagger_acceleration,
|
||||
commands::set_tagger_acceleration,
|
||||
commands::probe_tagger_runtime,
|
||||
commands::get_tagger_threshold,
|
||||
commands::set_tagger_threshold,
|
||||
commands::get_tagger_batch_size,
|
||||
commands::set_tagger_batch_size,
|
||||
commands::prepare_tagger_model,
|
||||
commands::delete_tagger_model,
|
||||
commands::queue_tagging_jobs,
|
||||
commands::clear_tagging_jobs,
|
||||
commands::get_image_tags,
|
||||
commands::add_user_tag,
|
||||
commands::remove_tag,
|
||||
commands::search_tags_autocomplete,
|
||||
commands::find_duplicates,
|
||||
commands::load_duplicate_scan_cache,
|
||||
commands::invalidate_duplicate_scan_cache,
|
||||
commands::delete_images_from_disk,
|
||||
commands::rename_folder,
|
||||
commands::update_folder_path,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
Reference in New Issue
Block a user