From 51e4c2c1f714d81abaa4cc29e3333c4a0dab7780 Mon Sep 17 00:00:00 2001 From: LyAhn Date: Mon, 6 Apr 2026 00:54:14 +0100 Subject: [PATCH] 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 --- src-tauri/Cargo.lock | 1583 +++++++++++++++++++++++++++- src-tauri/Cargo.toml | 4 + src-tauri/src/commands.rs | 43 +- src-tauri/src/db.rs | 181 +++- src-tauri/src/embedder.rs | 118 +++ src-tauri/src/indexer.rs | 117 ++ src-tauri/src/lib.rs | 11 + src-tauri/src/vector.rs | 28 + src/App.tsx | 6 +- src/components/BackgroundTasks.tsx | 50 +- src/components/Gallery.tsx | 68 +- src/components/Lightbox.tsx | 25 +- src/components/Toolbar.tsx | 3 +- src/store.ts | 55 +- 14 files changed, 2219 insertions(+), 73 deletions(-) create mode 100644 src-tauri/src/embedder.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5fe666c..655fbd4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -15,7 +15,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -44,6 +46,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -234,6 +242,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -246,6 +260,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bit-set" version = "0.8.0" @@ -339,6 +359,20 @@ name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "byteorder" @@ -395,6 +429,87 @@ dependencies = [ "serde_core", ] +[[package]] +name = "candle-core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd9895436c1ba5dc1037a19935d084b838db066ff4e15ef7dded020b7c12a4a" +dependencies = [ + "byteorder", + "candle-kernels", + "candle-ug", + "cudarc 0.19.4", + "float8", + "gemm 0.19.0", + "half", + "libm", + "memmap2", + "num-traits", + "num_cpus", + "rand 0.9.2", + "rand_distr", + "rayon", + "safetensors 0.7.0", + "thiserror 2.0.18", + "tokenizers", + "yoke 0.8.2", + "zip 7.2.0", +] + +[[package]] +name = "candle-kernels" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "742e2ac226b777134436e9e692f44e77c278b8a7abb1554dc10e44dc911b349f" +dependencies = [ + "cudaforge", +] + +[[package]] +name = "candle-nn" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9317a09d6530b758990ed7f625ac69ff43653bc9ee28b0464644ad1169ada87" +dependencies = [ + "candle-core", + "half", + "libc", + "num-traits", + "rayon", + "safetensors 0.7.0", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "candle-transformers" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f59d08c89e9f4af9c464e2f3a8e16199e7cc601e6f34538c2cfbb42b623b1783" +dependencies = [ + "byteorder", + "candle-core", + "candle-nn", + "fancy-regex", + "num-traits", + "rand 0.9.2", + "rayon", + "serde", + "serde_json", + "serde_plain", + "tracing", +] + +[[package]] +name = "candle-ug" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0fc3167cbc99c8ec1be618cb620aa21dca95038f118c3579a79370e3dc5f77" +dependencies = [ + "ug", + "ug-cuda", +] + [[package]] name = "cargo-platform" version = "0.1.9" @@ -428,6 +543,15 @@ dependencies = [ "toml 0.9.12+spec-1.1.0", ] +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.59" @@ -512,6 +636,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -521,6 +660,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -533,10 +684,39 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ + "percent-encoding", "time", "version_check", ] +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap 2.13.1", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -560,9 +740,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -573,7 +753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -704,14 +884,78 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "cudaforge" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f7a0d45b139b5beeeb1c34188717e12241c44a0120afb498815ce7f5373c691" +dependencies = [ + "anyhow", + "fs2", + "glob", + "num_cpus", + "rayon", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "walkdir", + "which", +] + +[[package]] +name = "cudarc" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf99ab37ee7072d64d906aa2dada9a3422f1d975cdf8c8055a573bc84897ed8" +dependencies = [ + "half", + "libloading 0.8.9", +] + +[[package]] +name = "cudarc" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f071cd6a7b5d51607df76aa2d426aaabc7a74bc6bdb885b8afa63a880572ad9b" +dependencies = [ + "float8", + "half", + "libloading 0.9.0", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", ] [[package]] @@ -727,17 +971,47 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", "quote", "syn 2.0.117", ] +[[package]] +name = "dary_heap" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" +dependencies = [ + "serde", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -759,6 +1033,37 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -930,6 +1235,22 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "dyn-stack" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8" +dependencies = [ + "bytemuck", + "dyn-stack-macros", +] + +[[package]] +name = "dyn-stack-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9" + [[package]] name = "either" version = "1.15.0" @@ -956,12 +1277,39 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "enumflags2" version = "0.7.12" @@ -983,6 +1331,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "equivalent" version = "1.0.2" @@ -1010,6 +1364,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "esaxx-rs" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" + [[package]] name = "event-listener" version = "5.4.1" @@ -1043,6 +1403,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fancy-regex" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fast_image_resize" version = "6.0.0" @@ -1102,7 +1473,7 @@ dependencies = [ "tar", "ureq", "xz2", - "zip", + "zip 4.6.1", ] [[package]] @@ -1143,6 +1514,18 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "float8" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d1f04709a8ac06e8e8042875a3c466cc4832d3c1a18dbcb9dba3c6e83046bc" +dependencies = [ + "half", + "num-traits", + "rand 0.9.2", + "rand_distr", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1161,6 +1544,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1168,7 +1560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1182,6 +1574,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1197,6 +1595,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "futf" version = "0.1.5" @@ -1399,6 +1807,244 @@ dependencies = [ "x11", ] +[[package]] +name = "gemm" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451" +dependencies = [ + "dyn-stack", + "gemm-c32 0.18.2", + "gemm-c64 0.18.2", + "gemm-common 0.18.2", + "gemm-f16 0.18.2", + "gemm-f32 0.18.2", + "gemm-f64 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa0673db364b12263d103b68337a68fbecc541d6f6b61ba72fe438654709eacb" +dependencies = [ + "dyn-stack", + "gemm-c32 0.19.0", + "gemm-c64 0.19.0", + "gemm-common 0.19.0", + "gemm-f16 0.19.0", + "gemm-f32 0.19.0", + "gemm-f64 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-c32" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-c32" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "086936dbdcb99e37aad81d320f98f670e53c1e55a98bee70573e83f95beb128c" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-c64" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-c64" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c8aeeeec425959bda4d9827664029ba1501a90a0d1e6228e48bef741db3a3f" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-common" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3" +dependencies = [ + "bytemuck", + "dyn-stack", + "half", + "libm", + "num-complex", + "num-traits", + "once_cell", + "paste", + "pulp 0.21.5", + "raw-cpuid", + "rayon", + "seq-macro", + "sysctl", +] + +[[package]] +name = "gemm-common" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88027625910cc9b1085aaaa1c4bc46bb3a36aad323452b33c25b5e4e7c8e2a3e" +dependencies = [ + "bytemuck", + "dyn-stack", + "half", + "libm", + "num-complex", + "num-traits", + "once_cell", + "paste", + "pulp 0.22.2", + "raw-cpuid", + "rayon", + "seq-macro", + "sysctl", +] + +[[package]] +name = "gemm-f16" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "gemm-f32 0.18.2", + "half", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "rayon", + "seq-macro", +] + +[[package]] +name = "gemm-f16" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3df7a55202e6cd6739d82ae3399c8e0c7e1402859b30e4cb780e61525d9486e" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "gemm-f32 0.19.0", + "half", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "rayon", + "seq-macro", +] + +[[package]] +name = "gemm-f32" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-f32" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0b8c9da1fbec6e3e3ab2ce6bc259ef18eb5f6f0d3e4edf54b75f9fd41a81c" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-f64" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-f64" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056131e8f2a521bfab322f804ccd652520c79700d81209e9d9275bbdecaadc6a" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1615,14 +2261,37 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ + "bytemuck", "cfg-if", "crunchy", + "num-traits", + "rand 0.9.2", + "rand_distr", "zerocopy", ] @@ -1655,6 +2324,13 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", +] [[package]] name = "hashlink" @@ -1689,6 +2365,27 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hf-hub" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef3982638978efa195ff11b305f51f1f22f4f0a6cabee7af79b383ebee6a213" +dependencies = [ + "dirs", + "http", + "indicatif", + "libc", + "log", + "native-tls", + "rand 0.9.2", + "reqwest 0.12.28", + "serde", + "serde_json", + "thiserror 2.0.18", + "ureq", + "windows-sys 0.61.2", +] + [[package]] name = "html5ever" version = "0.29.1" @@ -1766,6 +2463,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1776,6 +2474,38 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1794,9 +2524,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1842,7 +2574,7 @@ dependencies = [ "displaydoc", "potential_utf", "utf8_iter", - "yoke", + "yoke 0.8.2", "zerofrom", "zerovec", ] @@ -1909,7 +2641,7 @@ dependencies = [ "displaydoc", "icu_locale_core", "writeable", - "yoke", + "yoke 0.8.2", "zerofrom", "zerotrie", "zerovec", @@ -2000,6 +2732,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + [[package]] name = "infer" version = "0.19.0" @@ -2044,6 +2789,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -2200,7 +2954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] @@ -2220,6 +2974,32 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.15" @@ -2293,6 +3073,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "macro_rules_attribute" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" + [[package]] name = "markup5ever" version = "0.14.1" @@ -2341,6 +3137,16 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", + "stable_deref_trait", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -2356,6 +3162,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2377,6 +3189,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "monostate" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67" +dependencies = [ + "monostate-impl", + "serde", + "serde_core", +] + +[[package]] +name = "monostate-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "moxcms" version = "0.8.1" @@ -2408,6 +3242,23 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2450,6 +3301,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -2459,12 +3320,77 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2472,6 +3398,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", ] [[package]] @@ -2636,6 +3573,28 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.11.0", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "open" version = "5.3.3" @@ -2648,6 +3607,50 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2718,12 +3721,27 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2922,9 +3940,13 @@ name = "phokus" version = "0.1.0" dependencies = [ "anyhow", + "candle-core", + "candle-nn", + "candle-transformers", "chrono", "fast_image_resize", "ffmpeg-sidecar", + "hf-hub", "image", "log", "r2d2", @@ -3028,6 +4050,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.5" @@ -3136,6 +4164,43 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulp" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e205bb30d5b916c55e584c22201771bcf2bad9aabd5d4127f38387140c38632" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e24eee682d89fb193496edf918a7f407d30175b2e785fe057e4392dfd182e0" + [[package]] name = "pxfm" version = "0.1.28" @@ -3225,6 +4290,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.0" @@ -3256,6 +4331,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -3274,12 +4359,31 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +[[package]] +name = "rand_distr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" +dependencies = [ + "num-traits", + "rand 0.9.2", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -3298,6 +4402,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3314,6 +4427,17 @@ dependencies = [ "rayon-core", ] +[[package]] +name = "rayon-cond" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" +dependencies = [ + "either", + "itertools", + "rayon", +] + [[package]] name = "rayon-core" version = "1.13.0" @@ -3324,6 +4448,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3402,6 +4532,49 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", +] + [[package]] name = "reqwest" version = "0.13.2" @@ -3432,7 +4605,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] @@ -3557,6 +4730,33 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "safetensors" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44560c11236a6130a46ce36c836a62936dc81ebf8c36a37947423571be0e55b6" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675656c1eabb620b921efea4f9199f97fc86e36dd6ffd1fbbe48d0f59a4987f5" +dependencies = [ + "hashbrown 0.16.1", + "serde", + "serde_json", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3566,6 +4766,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scheduled-thread-pool" version = "0.2.7" @@ -3632,6 +4841,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -3679,6 +4911,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + [[package]] name = "serde" version = "1.0.228" @@ -3745,6 +4983,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -3774,6 +5021,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.18.0" @@ -3799,7 +5058,7 @@ version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.117", @@ -3913,6 +5172,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + [[package]] name = "softbuffer" version = "0.4.8" @@ -3961,6 +5231,18 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spm_precompiled" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" +dependencies = [ + "base64 0.13.1", + "nom", + "serde", + "unicode-segmentation", +] + [[package]] name = "sqlite-vec" version = "0.1.9" @@ -3976,6 +5258,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.9" @@ -4090,6 +5378,20 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "sysctl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" +dependencies = [ + "bitflags 2.11.0", + "byteorder", + "enum-as-inner", + "libc", + "thiserror 1.0.69", + "walkdir", +] + [[package]] name = "sysinfo" version = "0.38.4" @@ -4104,6 +5406,27 @@ dependencies = [ "windows 0.62.2", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -4125,7 +5448,7 @@ checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" dependencies = [ "bitflags 2.11.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch2", @@ -4214,7 +5537,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -4608,6 +5931,39 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokenizers" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223" +dependencies = [ + "ahash", + "aho-corasick", + "compact_str", + "dary_heap", + "derive_builder", + "esaxx-rs", + "getrandom 0.3.4", + "itertools", + "log", + "macro_rules_attribute", + "monostate", + "onig", + "paste", + "rand 0.9.2", + "rayon", + "rayon-cond", + "regex", + "regex-syntax", + "serde", + "serde_json", + "spm_precompiled", + "thiserror 2.0.18", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +] + [[package]] name = "tokio" version = "1.51.0" @@ -4636,6 +5992,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4858,6 +6234,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typeid" version = "1.0.3" @@ -4881,6 +6263,40 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ug" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b761acf8af3494640d826a8609e2265e19778fb43306c7f15379c78c9b05b0" +dependencies = [ + "gemm 0.18.2", + "half", + "libloading 0.8.9", + "memmap2", + "num", + "num-traits", + "num_cpus", + "rayon", + "safetensors 0.4.5", + "serde", + "thiserror 1.0.69", + "tracing", + "yoke 0.7.5", +] + +[[package]] +name = "ug-cuda" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f0a1fa748f26166778c33b8498255ebb7c6bffb472bcc0a72839e07ebb1d9b5" +dependencies = [ + "cudarc 0.17.8", + "half", + "serde", + "thiserror 1.0.69", + "ug", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -4928,18 +6344,45 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization-alignments" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de" +dependencies = [ + "smallvec", +] + [[package]] name = "unicode-segmentation" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "untrusted" version = "0.9.0" @@ -4953,13 +6396,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ "base64 0.22.1", + "cookie_store", + "der", "flate2", "log", + "native-tls", "percent-encoding", "rustls", "rustls-pki-types", + "serde", + "serde_json", + "socks", "ureq-proto", "utf8-zero", + "webpki-root-certs", "webpki-roots", ] @@ -5195,6 +6645,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -5230,6 +6693,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.2.3" @@ -5286,6 +6759,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.6" @@ -5337,6 +6819,18 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix", + "winsafe", +] + [[package]] name = "winapi" version = "0.3.9" @@ -5528,6 +7022,17 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -5859,6 +7364,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -6043,6 +7554,18 @@ dependencies = [ "lzma-sys", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive 0.7.5", + "zerofrom", +] + [[package]] name = "yoke" version = "0.8.2" @@ -6050,10 +7573,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", - "yoke-derive", + "yoke-derive 0.8.2", "zerofrom", ] +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "yoke-derive" version = "0.8.2" @@ -6181,7 +7716,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", - "yoke", + "yoke 0.8.2", "zerofrom", ] @@ -6191,7 +7726,7 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ - "yoke", + "yoke 0.8.2", "zerofrom", "zerovec-derive", ] @@ -6221,6 +7756,18 @@ dependencies = [ "zopfli", ] +[[package]] +name = "zip" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0" +dependencies = [ + "crc32fast", + "indexmap 2.13.1", + "memchr", + "typed-path", +] + [[package]] name = "zlib-rs" version = "0.6.3" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9cf529b..c16a3ce 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,3 +35,7 @@ log = "0.4" ffmpeg-sidecar = "2.5.0" xxhash-rust = { version = "0.8", features = ["xxh3"] } sysinfo = "0.38.4" +candle-core = { version = "0.10.2", features = ["cuda"] } +candle-nn = { version = "0.10.2", features = ["cuda"] } +candle-transformers = { version = "0.10.2", features = ["cuda"] } +hf-hub = { version = "0.5.0", default-features = false, features = ["ureq", "native-tls"] } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 2ece03c..eb5af3e 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -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, } +#[derive(Deserialize)] +pub struct FindSimilarImagesParams { + pub image_id: i64, + pub limit: Option, +} + +#[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, String> db::get_folders(&conn).map_err(|e| e.to_string()) } +#[tauri::command] +pub async fn get_background_job_progress( + db: State<'_, DbState>, +) -> Result, 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, 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 { + 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()) +} diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index a971305..fd38fcd 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -63,6 +63,10 @@ pub struct ImageRecord { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmbeddingJob { pub image_id: i64, + pub folder_id: i64, + pub path: String, + pub thumbnail_path: Option, + pub media_kind: String, pub status: String, pub attempts: i64, pub last_error: Option, @@ -99,6 +103,9 @@ pub struct FolderJobProgress { pub folder_id: i64, pub thumbnail_pending: i64, pub metadata_pending: i64, + pub embedding_pending: i64, + pub embedding_ready: i64, + pub embedding_failed: i64, } pub fn create_pool(db_path: &Path) -> Result { @@ -280,6 +287,56 @@ pub fn enqueue_embedding_job(conn: &Connection, image_id: i64) -> Result<()> { Ok(()) } +pub fn backfill_embedding_jobs(conn: &Connection) -> Result { + let inserted = conn.execute( + "INSERT INTO embedding_jobs (image_id, status, attempts, last_error, created_at, updated_at) + SELECT i.id, 'pending', 0, NULL, datetime('now'), datetime('now') + FROM images i + LEFT JOIN embedding_jobs j ON j.image_id = i.id + WHERE i.embedding_status != 'ready' + AND j.image_id IS NULL", + [], + )?; + Ok(inserted) +} + +pub fn retry_failed_embedding_jobs(conn: &Connection, folder_id: i64) -> Result { + let updated = conn.execute( + "INSERT INTO embedding_jobs (image_id, status, attempts, last_error, created_at, updated_at) + SELECT id, 'pending', 0, NULL, datetime('now'), datetime('now') + FROM images + WHERE folder_id = ?1 AND embedding_status = 'failed' + ON CONFLICT(image_id) DO UPDATE SET + status = 'pending', + last_error = NULL, + updated_at = datetime('now')", + [folder_id], + )?; + conn.execute( + "UPDATE images + SET embedding_status = 'pending', embedding_error = NULL + WHERE folder_id = ?1 AND embedding_status = 'failed'", + [folder_id], + )?; + Ok(updated) +} + +pub fn reset_inflight_jobs(conn: &Connection) -> Result<()> { + conn.execute( + "UPDATE thumbnail_jobs SET status = 'pending' WHERE status = 'processing'", + [], + )?; + conn.execute( + "UPDATE metadata_jobs SET status = 'pending' WHERE status = 'processing'", + [], + )?; + conn.execute( + "UPDATE embedding_jobs SET status = 'pending' WHERE status = 'processing'", + [], + )?; + Ok(()) +} + pub fn enqueue_thumbnail_job(conn: &Connection, image_id: i64) -> Result<()> { conn.execute( "INSERT INTO thumbnail_jobs (image_id, status, attempts, last_error, created_at, updated_at) @@ -306,40 +363,57 @@ pub fn enqueue_metadata_job(conn: &Connection, image_id: i64) -> Result<()> { Ok(()) } -#[allow(dead_code)] -pub fn get_next_embedding_job(conn: &Connection) -> Result> { +pub fn get_pending_embedding_jobs(conn: &Connection, limit: usize) -> Result> { let mut stmt = conn.prepare( - "SELECT image_id, status, attempts, last_error, created_at, updated_at - FROM embedding_jobs + "SELECT j.image_id, i.folder_id, i.path, i.thumbnail_path, i.media_kind, + j.status, j.attempts, j.last_error, j.created_at, j.updated_at + FROM embedding_jobs j + JOIN images i ON i.id = j.image_id WHERE status = 'pending' - ORDER BY updated_at, image_id - LIMIT 1", + ORDER BY j.updated_at, j.image_id + LIMIT ?1", )?; - - let mut rows = stmt.query([])?; - let Some(row) = rows.next()? else { - return Ok(None); - }; - - Ok(Some(EmbeddingJob { - image_id: row.get(0)?, - status: row.get(1)?, - attempts: row.get(2)?, - last_error: row.get(3)?, - created_at: row.get(4)?, - updated_at: row.get(5)?, - })) + let rows = stmt.query_map([limit as i64], |row| { + Ok(EmbeddingJob { + image_id: row.get(0)?, + folder_id: row.get(1)?, + path: row.get(2)?, + thumbnail_path: row.get(3)?, + media_kind: row.get(4)?, + status: row.get(5)?, + attempts: row.get(6)?, + last_error: row.get(7)?, + created_at: row.get(8)?, + updated_at: row.get(9)?, + }) + })?; + Ok(rows.collect::>>()?) } -#[allow(dead_code)] -pub fn mark_embedding_job_processing(conn: &Connection, image_id: i64) -> Result<()> { - conn.execute( - "UPDATE embedding_jobs - SET status = 'processing', attempts = attempts + 1, updated_at = datetime('now') - WHERE image_id = ?1", - [image_id], - )?; - Ok(()) +pub fn claim_embedding_jobs(conn: &mut Connection, limit: usize) -> Result> { + let tx = conn.transaction()?; + let candidates = get_pending_embedding_jobs(&tx, limit * 2)?; + let mut claimed = Vec::with_capacity(limit); + + for job in candidates { + let updated = tx.execute( + "UPDATE embedding_jobs + SET status = 'processing', attempts = attempts + 1, updated_at = datetime('now') + WHERE image_id = ?1 AND status = 'pending'", + [job.image_id], + )?; + + if updated == 1 { + claimed.push(job); + } + + if claimed.len() >= limit { + break; + } + } + + tx.commit()?; + Ok(claimed) } #[allow(dead_code)] @@ -426,13 +500,54 @@ pub fn get_folder_job_progress(conn: &Connection, folder_id: i64) -> Result Result> { + let folder_ids = get_folders(conn)? + .into_iter() + .map(|folder| folder.id) + .collect::>(); + + let mut progress = Vec::with_capacity(folder_ids.len()); + for folder_id in folder_ids { + progress.push(get_folder_job_progress(conn, folder_id)?); + } + Ok(progress) +} + pub fn get_pending_thumbnail_jobs(conn: &Connection, limit: usize) -> Result> { let mut stmt = conn.prepare( "SELECT j.image_id, i.folder_id, i.path, i.media_kind @@ -658,6 +773,14 @@ pub fn get_image_by_id(conn: &Connection, image_id: i64) -> Result .map_err(Into::into) } +pub fn get_images_by_ids(conn: &Connection, image_ids: &[i64]) -> Result> { + let mut images = Vec::with_capacity(image_ids.len()); + for image_id in image_ids { + images.push(get_image_by_id(conn, *image_id)?); + } + Ok(images) +} + pub fn get_folders(conn: &Connection) -> Result> { let mut stmt = conn.prepare("SELECT id, path, name, image_count, indexed_at FROM folders ORDER BY name")?; diff --git a/src-tauri/src/embedder.rs b/src-tauri/src/embedder.rs new file mode 100644 index 0000000..148632a --- /dev/null +++ b/src-tauri/src/embedder.rs @@ -0,0 +1,118 @@ +use anyhow::Result; +use candle_core::{DType, Device, Tensor}; +use candle_nn::VarBuilder; +use candle_transformers::models::clip::{self, ClipModel}; +use hf_hub::{api::sync::Api, Repo, RepoType}; +use std::path::{Path, PathBuf}; + +pub struct ClipImageEmbedder { + model: ClipModel, + device: Device, + image_size: usize, +} + +impl ClipImageEmbedder { + pub fn new() -> Result { + println!("Initializing CLIP image embedder..."); + let api = Api::new()?; + let repo = api.repo(Repo::new( + "laion/CLIP-ViT-B-32-laion2B-s34B-b79K".to_string(), + RepoType::Model, + )); + println!("Resolving CLIP model weights from Hugging Face cache..."); + let model_path = repo.get("model.safetensors")?; + + let config = clip::ClipConfig::vit_base_patch32(); + let device = resolve_device()?; + let vb = unsafe { + VarBuilder::from_mmaped_safetensors( + std::slice::from_ref(&model_path), + DType::F32, + &device, + )? + }; + let model = ClipModel::new(vb, &config)?; + println!("CLIP image embedder ready."); + + Ok(Self { + model, + device, + image_size: config.image_size, + }) + } + + pub fn embed_image(&self, path: &Path) -> Result> { + Ok(self.embed_images(&[path.to_path_buf()])?.remove(0)) + } + + pub fn embed_images(&self, paths: &[PathBuf]) -> Result>> { + let images = load_images(paths, self.image_size)?.to_device(&self.device)?; + let features = self.model.get_image_features(&images)?; + let normalized = clip::div_l2_norm(&features)?; + + let mut embeddings = Vec::with_capacity(paths.len()); + for index in 0..paths.len() { + embeddings.push(normalized.get(index)?.flatten_all()?.to_vec1::()?); + } + Ok(embeddings) + } +} + +fn resolve_device() -> Result { + let cuda_device = Device::cuda_if_available(0)?; + if cuda_device.is_cuda() { + println!("CLIP embedder using CUDA device."); + return Ok(cuda_device); + } + + #[cfg(target_os = "macos")] + { + let metal_device = Device::metal_if_available(0)?; + if metal_device.is_metal() { + println!("CLIP embedder using Metal device."); + return Ok(metal_device); + } + } + + println!("CLIP embedder using CPU device."); + Ok(Device::Cpu) +} + +fn load_image(path: &Path, image_size: usize) -> Result { + let image = image::ImageReader::open(path)? + .with_guessed_format()? + .decode()?; + let image = image.resize_to_fill( + image_size as u32, + image_size as u32, + image::imageops::FilterType::Triangle, + ); + let image = image.to_rgb8().into_raw(); + let tensor = Tensor::from_vec(image, (image_size, image_size, 3), &Device::Cpu)? + .permute((2, 0, 1))? + .to_dtype(DType::F32)? + .affine(2.0 / 255.0, -1.0)?; + Ok(tensor) +} + +fn load_images(paths: &[PathBuf], image_size: usize) -> Result { + let mut images = Vec::with_capacity(paths.len()); + for path in paths { + images.push(load_image(path, image_size)?); + } + Ok(Tensor::stack(&images, 0)?) +} + +pub fn embedding_source_path( + path: &str, + thumbnail_path: Option<&str>, + media_kind: &str, +) -> PathBuf { + if media_kind == "video" { + thumbnail_path + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(path)) + } else { + PathBuf::from(path) + } +} diff --git a/src-tauri/src/indexer.rs b/src-tauri/src/indexer.rs index 30cf848..581f3d6 100644 --- a/src-tauri/src/indexer.rs +++ b/src-tauri/src/indexer.rs @@ -1,4 +1,5 @@ use crate::db::{self, DbPool, FolderJobProgress, ImageRecord, IndexedMediaEntry}; +use crate::embedder::{embedding_source_path, ClipImageEmbedder}; use crate::media::{probe_video_metadata, MediaTools}; use crate::storage::{detect_storage_profile, RuntimeAdaptiveProfile, StorageProfile}; use crate::thumbnail; @@ -26,6 +27,7 @@ static ACTIVE_INDEXING_FOLDERS: OnceLock>> = OnceLock::new(); static FOLDER_STORAGE_PROFILES: OnceLock>> = OnceLock::new(); static DB_WRITE_LOCK: OnceLock> = OnceLock::new(); +const EMBEDDING_BATCH_SIZE: usize = 8; #[derive(Clone, Serialize)] pub struct IndexProgress { @@ -89,6 +91,20 @@ pub fn start_metadata_worker(app: AppHandle, pool: DbPool, media_tools: MediaToo }); } +pub fn start_embedding_worker(app: AppHandle, pool: DbPool) { + std::thread::spawn(move || { + let mut embedder: Option = None; + println!("Embedding worker started."); + loop { + if let Err(error) = process_embedding_batch(&app, &pool, &mut embedder) { + eprintln!("Embedding worker error: {}", error); + } + + std::thread::sleep(std::time::Duration::from_millis(500)); + } + }); +} + fn do_index(app: AppHandle, pool: DbPool, folder_id: i64, folder_path: PathBuf) -> Result<()> { let existing_entries = { let conn = pool.get()?; @@ -303,6 +319,8 @@ fn process_thumbnail_batch( return Ok(()); } + println!("Embedding batch claimed: {} items", jobs.len()); + let (image_jobs, video_jobs): (Vec<_>, Vec<_>) = jobs.into_iter().partition(|job| job.media_kind == "image"); @@ -463,6 +481,105 @@ fn process_metadata_batch(app: &AppHandle, pool: &DbPool, media_tools: &MediaToo Ok(()) } +fn process_embedding_batch( + app: &AppHandle, + pool: &DbPool, + embedder: &mut Option, +) -> Result<()> { + let batch_started_at = Instant::now(); + if embedder.is_none() { + *embedder = Some(ClipImageEmbedder::new()?); + } + + let claim_started_at = Instant::now(); + let jobs = with_db_write_lock(|| { + let mut conn = pool.get()?; + db::claim_embedding_jobs(&mut conn, EMBEDDING_BATCH_SIZE) + })?; + let claim_elapsed = claim_started_at.elapsed(); + + if jobs.is_empty() { + return Ok(()); + } + + println!("Embedding batch claimed: {} items", jobs.len()); + let folder_ids = jobs.iter().map(|job| job.folder_id).collect::>(); + emit_folder_job_progress(app, pool, &folder_ids.iter().copied().collect::>()); + let embedder = embedder.as_ref().expect("embedder should be initialized"); + + let infer_started_at = Instant::now(); + let source_paths = jobs + .iter() + .map(|job| embedding_source_path(&job.path, job.thumbnail_path.as_deref(), &job.media_kind)) + .collect::>(); + + let results = match embedder.embed_images(&source_paths) { + Ok(embeddings) => jobs + .into_iter() + .zip(embeddings.into_iter().map(Ok)) + .collect::>(), + Err(batch_error) => { + eprintln!( + "Embedding batch fallback to per-image mode: {}", + batch_error + ); + jobs.into_iter() + .zip(source_paths.into_iter()) + .map(|(job, source_path)| (job, embedder.embed_image(&source_path))) + .collect::>() + } + }; + let infer_elapsed = infer_started_at.elapsed(); + + let write_started_at = Instant::now(); + let updated_images = with_db_write_lock(|| { + let mut conn = pool.get()?; + let tx = conn.transaction()?; + let mut updated_images = Vec::new(); + + for (job, embedding_result) in results { + match embedding_result { + Ok(embedding) => { + vector::upsert_embedding(&tx, job.image_id, &embedding)?; + db::mark_embedding_ready(&tx, job.image_id, vector::CLIP_MODEL_NAME)?; + } + Err(error) => { + db::mark_embedding_failed(&tx, job.image_id, &error.to_string())?; + } + } + + updated_images.push(db::get_image_by_id(&tx, job.image_id)?); + } + + tx.commit()?; + Ok(updated_images) + })?; + + if !updated_images.is_empty() { + println!("Embedding batch completed: {} items", updated_images.len()); + let folder_ids = updated_images + .iter() + .map(|image| image.folder_id) + .collect::>(); + emit_media_updates( + app, + &MediaUpdateBatch { + images: updated_images, + }, + ); + emit_folder_job_progress(app, pool, &folder_ids.into_iter().collect::>()); + } + + let write_elapsed = write_started_at.elapsed(); + let batch_elapsed = batch_started_at.elapsed(); + println!( + "Embedding batch timing: claimed {} in {:?}, infer {:?}, write {:?}, total {:?}", + EMBEDDING_BATCH_SIZE, claim_elapsed, infer_elapsed, write_elapsed, batch_elapsed + ); + + Ok(()) +} + fn active_indexing_folders() -> HashSet { ACTIVE_INDEXING_FOLDERS .get_or_init(|| Mutex::new(HashSet::new())) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f3adae6..d5513a8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ mod commands; mod db; +mod embedder; mod indexer; mod media; mod storage; @@ -32,6 +33,12 @@ 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"); + if backfilled > 0 { + println!("Backfilled {} embedding jobs.", backfilled); + } } let thumb_dir = app_dir.join("thumbnails"); @@ -50,6 +57,7 @@ pub fn run() { ); } indexer::start_metadata_worker(app.handle().clone(), pool.clone(), media_tools.clone()); + indexer::start_embedding_worker(app.handle().clone(), pool.clone()); app.manage(pool); app.manage(media_tools); @@ -59,10 +67,13 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ commands::add_folder, commands::get_folders, + commands::get_background_job_progress, commands::remove_folder, commands::get_images, commands::reindex_folder, commands::update_image_details, + commands::find_similar_images, + commands::retry_failed_embeddings, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/vector.rs b/src-tauri/src/vector.rs index 4dc8ceb..ed88b7e 100644 --- a/src-tauri/src/vector.rs +++ b/src-tauri/src/vector.rs @@ -50,6 +50,34 @@ pub fn upsert_embedding(conn: &Connection, image_id: i64, embedding: &[f32]) -> Ok(()) } +pub fn find_similar_image_ids(conn: &Connection, image_id: i64, limit: usize) -> Result> { + let embedding: Vec = conn.query_row( + "SELECT embedding FROM image_vec WHERE image_id = ?1", + [image_id], + |row| row.get(0), + )?; + + let mut stmt = conn.prepare( + "SELECT image_id + FROM image_vec + WHERE embedding MATCH vec_f32(?1) + AND k = ?2", + )?; + let rows = stmt.query_map((&embedding, (limit as i64) + 1), |row| row.get::<_, i64>(0))?; + + let mut ids = Vec::new(); + for row in rows { + let candidate_id = row?; + if candidate_id != image_id { + ids.push(candidate_id); + } + if ids.len() >= limit { + break; + } + } + Ok(ids) +} + #[allow(dead_code)] fn pack_f32(values: &[f32]) -> Vec { let mut out = Vec::with_capacity(values.len() * std::mem::size_of::()); diff --git a/src/App.tsx b/src/App.tsx index a71173a..f5bd3af 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,11 +9,15 @@ import { Lightbox } from "./components/Lightbox"; export default function App() { const loadFolders = useGalleryStore((state) => state.loadFolders); + const loadBackgroundJobProgress = useGalleryStore((state) => state.loadBackgroundJobProgress); const loadImages = useGalleryStore((state) => state.loadImages); const subscribeToProgress = useGalleryStore((state) => state.subscribeToProgress); useEffect(() => { - loadFolders().then(() => loadImages(true)); + loadFolders().then(() => { + void loadBackgroundJobProgress(); + return loadImages(true); + }); let unlisten: (() => void) | undefined; subscribeToProgress().then((fn) => { unlisten = fn; diff --git a/src/components/BackgroundTasks.tsx b/src/components/BackgroundTasks.tsx index 4535730..c8d8c66 100644 --- a/src/components/BackgroundTasks.tsx +++ b/src/components/BackgroundTasks.tsx @@ -16,19 +16,27 @@ export function BackgroundTasks() { const folders = useGalleryStore((state) => state.folders); const indexingProgress = useGalleryStore((state) => state.indexingProgress); const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress); + const retryFailedEmbeddings = useGalleryStore((state) => state.retryFailedEmbeddings); const tasks = useMemo(() => { return folders .map((folder) => { const index = indexingProgress[folder.id]; const jobs = mediaJobProgress[folder.id]; - const pendingMediaWork = (jobs?.thumbnail_pending ?? 0) + (jobs?.metadata_pending ?? 0); + const pendingMediaWork = + (jobs?.thumbnail_pending ?? 0) + + (jobs?.metadata_pending ?? 0) + + (jobs?.embedding_pending ?? 0); + const embeddingProcessed = (jobs?.embedding_ready ?? 0) + (jobs?.embedding_failed ?? 0); + const embeddingTotal = embeddingProcessed + (jobs?.embedding_pending ?? 0); + const hasFailedEmbeddings = (jobs?.embedding_failed ?? 0) > 0; - if (!index && pendingMediaWork === 0) { + if (!index && pendingMediaWork === 0 && !hasFailedEmbeddings) { return null; } const indexPercent = index && index.total > 0 ? (index.indexed / index.total) * 100 : 0; + const embeddingPercent = embeddingTotal > 0 ? (embeddingProcessed / embeddingTotal) * 100 : 0; return { id: folder.id, name: folder.name, @@ -36,6 +44,10 @@ export function BackgroundTasks() { jobs, pendingMediaWork, indexPercent, + embeddingProcessed, + embeddingTotal, + embeddingPercent, + hasFailedEmbeddings, }; }) .filter((task) => task !== null); @@ -60,12 +72,20 @@ export function BackgroundTasks() {

{task.index && !task.index.done ? `${task.index.indexed.toLocaleString()} of ${task.index.total.toLocaleString()} scanned` - : `${task.pendingMediaWork.toLocaleString()} media jobs remaining`} + : task.hasFailedEmbeddings && task.pendingMediaWork === 0 + ? `Embedding failures require attention` + : `${task.pendingMediaWork.toLocaleString()} media jobs remaining`}

{task.jobs?.thumbnail_pending ?
{task.jobs.thumbnail_pending.toLocaleString()} thumbnails
: null} {task.jobs?.metadata_pending ?
{task.jobs.metadata_pending.toLocaleString()} metadata
: null} + {task.embeddingTotal > 0 ? ( +
+ {task.embeddingProcessed.toLocaleString()} / {task.embeddingTotal.toLocaleString()} embeddings +
+ ) : null} + {task.jobs?.embedding_failed ?
{task.jobs.embedding_failed.toLocaleString()} failed
: null}
@@ -74,10 +94,32 @@ export function BackgroundTasks() {

{task.index.current_file || "Scanning..."}

+ ) : task.embeddingTotal > 0 && (task.jobs?.embedding_pending ?? 0) > 0 ? ( +
+ +

+ {task.embeddingProcessed.toLocaleString()} completed, {task.jobs?.embedding_pending?.toLocaleString() ?? 0} remaining +

+
+ ) : task.hasFailedEmbeddings ? ( +
+ +
+

+ {task.jobs?.embedding_failed?.toLocaleString() ?? 0} embedding failures need attention +

+ +
+
) : task.pendingMediaWork > 0 ? (
-

Processing thumbnails and metadata

+

Processing thumbnails, metadata, and embeddings

) : null} diff --git a/src/components/Gallery.tsx b/src/components/Gallery.tsx index a7a8ed9..d4d6336 100644 --- a/src/components/Gallery.tsx +++ b/src/components/Gallery.tsx @@ -38,6 +38,19 @@ function formatDuration(durationMs: number | null): string | null { return `${minutes}:${seconds.toString().padStart(2, "0")}`; } +function embeddingLabel(image: ImageRecord): string { + if (image.embedding_status === "ready") { + return image.embedding_model ? `Embeddings ready` : "Embeddings ready"; + } + if (image.embedding_status === "failed") { + return "Embeddings failed"; + } + if (image.embedding_status === "processing") { + return "Embedding..."; + } + return "Embedding queued"; +} + function ContextMenu({ x, y, @@ -49,10 +62,13 @@ function ContextMenu({ image: ImageRecord; onClose: () => void; }) { - const { openImage, updateImageDetails } = useGalleryStore(); + const openImage = useGalleryStore((state) => state.openImage); + const updateImageDetails = useGalleryStore((state) => state.updateImageDetails); + const loadSimilarImages = useGalleryStore((state) => state.loadSimilarImages); return (
event.stopPropagation()} @@ -75,21 +91,37 @@ function ContextMenu({ > {image.favorite ? "Remove Favorite" : "Add to Favorites"} +
Rating
-
+
{Array.from({ length: 5 }, (_, index) => { const rating = index + 1; return ( ); })} @@ -123,6 +155,7 @@ function ImageTile({ }) { const [loaded, setLoaded] = useState(false); const [errored, setErrored] = useState(false); + const loadSimilarImages = useGalleryStore((state) => state.loadSimilarImages); const src = image.thumbnail_path ? convertFileSrc(image.thumbnail_path) @@ -207,7 +240,19 @@ function ImageTile({

{image.filename}

- {image.embedding_status === "ready" ? "indexed" : image.embedding_status} + {embeddingLabel(image)} +
+
+ + Right-click for more
@@ -242,18 +287,21 @@ export function Gallery() { }, [handleScroll]); useEffect(() => { - const close = () => setContextMenu(null); + const close = (event: PointerEvent) => { + if ((event.target as HTMLElement | null)?.closest("[data-gallery-context-menu]")) { + return; + } + setContextMenu(null); + }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { setContextMenu(null); } }; - window.addEventListener("click", close); - window.addEventListener("contextmenu", close); + window.addEventListener("pointerdown", close); window.addEventListener("keydown", handleKeyDown); return () => { - window.removeEventListener("click", close); - window.removeEventListener("contextmenu", close); + window.removeEventListener("pointerdown", close); window.removeEventListener("keydown", handleKeyDown); }; }, []); diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index adcc721..3ee3b26 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -32,11 +32,25 @@ function formatDuration(durationMs: number | null): string { return `${minutes}:${seconds.toString().padStart(2, "0")}`; } +function embeddingLabel(status: string, model: string | null): string { + if (status === "ready") { + return model ? `Ready (${model})` : "Ready"; + } + if (status === "failed") { + return "Failed"; + } + if (status === "processing") { + return "Processing"; + } + return "Queued"; +} + export function Lightbox() { const selectedImage = useGalleryStore((state) => state.selectedImage); const closeImage = useGalleryStore((state) => state.closeImage); const images = useGalleryStore((state) => state.images); const openImage = useGalleryStore((state) => state.openImage); + const loadSimilarImages = useGalleryStore((state) => state.loadSimilarImages); const updateImageDetails = useGalleryStore((state) => state.updateImageDetails); const [zoom, setZoom] = useState(1); const imageViewportRef = useRef(null); @@ -184,6 +198,12 @@ export function Lightbox() { +