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
This commit is contained in:
2026-04-06 00:54:14 +01:00
parent 76ec424167
commit 51e4c2c1f7
14 changed files with 2219 additions and 73 deletions
+42 -1
View File
@@ -1,5 +1,6 @@
use crate::db::{self, DbPool, Folder, ImageRecord};
use crate::db::{self, DbPool, Folder, FolderJobProgress, ImageRecord};
use crate::indexer;
use crate::vector;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tauri::{AppHandle, State};
@@ -32,6 +33,17 @@ pub struct UpdateImageDetailsParams {
pub rating: Option<i64>,
}
#[derive(Deserialize)]
pub struct FindSimilarImagesParams {
pub image_id: i64,
pub limit: Option<usize>,
}
#[derive(Deserialize)]
pub struct RetryFailedEmbeddingsParams {
pub folder_id: i64,
}
#[tauri::command]
pub async fn add_folder(
app: AppHandle,
@@ -75,6 +87,14 @@ pub async fn get_folders(db: State<'_, DbState>) -> Result<Vec<Folder>, String>
db::get_folders(&conn).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn get_background_job_progress(
db: State<'_, DbState>,
) -> Result<Vec<FolderJobProgress>, String> {
let conn = db.get().map_err(|e| e.to_string())?;
db::get_all_folder_job_progress(&conn).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn remove_folder(db: State<'_, DbState>, folder_id: i64) -> Result<(), String> {
let conn = db.get().map_err(|e| e.to_string())?;
@@ -147,3 +167,24 @@ pub async fn reindex_folder(
indexer::index_folder(app, db.inner().clone(), folder_id, folder_path);
Ok(())
}
#[tauri::command]
pub async fn find_similar_images(
db: State<'_, DbState>,
params: FindSimilarImagesParams,
) -> Result<Vec<ImageRecord>, String> {
let conn = db.get().map_err(|e| e.to_string())?;
let limit = params.limit.unwrap_or(32);
let image_ids = vector::find_similar_image_ids(&conn, params.image_id, limit)
.map_err(|e| e.to_string())?;
db::get_images_by_ids(&conn, &image_ids).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn retry_failed_embeddings(
db: State<'_, DbState>,
params: RetryFailedEmbeddingsParams,
) -> Result<usize, String> {
let conn = db.get().map_err(|e| e.to_string())?;
db::retry_failed_embedding_jobs(&conn, params.folder_id).map_err(|e| e.to_string())
}