diff --git a/Cargo.lock b/Cargo.lock index cd12b72..d7d447d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,20 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +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", +] + [[package]] name = "aho-corasick" version = "1.1.5" @@ -32,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 = "anyhow" version = "1.0.104" @@ -120,6 +140,27 @@ dependencies = [ "tracing", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.13.1" @@ -165,6 +206,26 @@ name = "bytemuck" version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder-lite" @@ -178,6 +239,73 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "candle-core" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c15b675b80d994b2eadb20a4bbe434eabeb454eac3ee5e2b4cf6f147ee9be091" +dependencies = [ + "byteorder", + "float8", + "gemm", + "half", + "libm", + "memmap2", + "num-traits", + "num_cpus", + "rand", + "rand_distr", + "rayon", + "safetensors", + "thiserror 2.0.20", + "yoke", + "zip", +] + +[[package]] +name = "candle-nn" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045fa9e7aef8567d209a27d56b692f60b96f4d0569f4c3011f8ca6715c65e03" +dependencies = [ + "candle-core", + "half", + "libc", + "num-traits", + "rayon", + "safetensors", + "serde", + "thiserror 2.0.20", +] + +[[package]] +name = "candle-transformers" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b538ec4aa807c416a2ddd3621044888f188827862e2a6fcacba4738e89795d01" +dependencies = [ + "byteorder", + "candle-core", + "candle-nn", + "fancy-regex 0.17.0", + "num-traits", + "rand", + "rayon", + "serde", + "serde_json", + "serde_plain", + "tracing", +] + +[[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.4.3" @@ -196,6 +324,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + [[package]] name = "compression-codecs" version = "0.4.38" @@ -238,6 +381,152 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[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.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dary_heap" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" +dependencies = [ + "serde", +] + +[[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", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.119", +] + +[[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.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -248,6 +537,34 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "esaxx-rs" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" + +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[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 = "fastrand" version = "2.5.0" @@ -279,6 +596,30 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float8" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719a903cc23e4a89e87962c2a80fdb45cdaad0983a89bd150bb57b4c8571a7d5" +dependencies = [ + "half", + "num-traits", + "rand", + "rand_distr", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -327,6 +668,137 @@ dependencies = [ "slab", ] +[[package]] +name = "gemm" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa0673db364b12263d103b68337a68fbecc541d6f6b61ba72fe438654709eacb" +dependencies = [ + "dyn-stack", + "gemm-c32", + "gemm-c64", + "gemm-common", + "gemm-f16", + "gemm-f32", + "gemm-f64", + "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", + "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", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[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", + "raw-cpuid", + "rayon", + "seq-macro", + "sysctl", +] + +[[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", + "gemm-f32", + "half", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "rayon", + "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", + "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", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -335,7 +807,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", ] [[package]] @@ -344,6 +816,52 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" +[[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", + "rand_distr", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "http" version = "1.5.0" @@ -430,6 +948,12 @@ dependencies = [ "tower-service", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "image" version = "0.25.10" @@ -456,6 +980,25 @@ dependencies = [ "quick-error", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[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" @@ -468,7 +1011,7 @@ version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom", + "getrandom 0.4.3", "libc", ] @@ -484,6 +1027,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libwebp-sys" version = "0.9.6" @@ -506,6 +1055,22 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "macro_rules_attribute" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ae8f6d608c795738406608304d30a2dfbdc8e58e44f7ba43236da5208ded3c" +dependencies = [ + "macro_rules_attribute-proc_macro", + "pastey", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c" + [[package]] name = "matchers" version = "0.2.0" @@ -527,6 +1092,16 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", + "stable_deref_trait", +] + [[package]] name = "mime" version = "0.3.17" @@ -543,6 +1118,12 @@ dependencies = [ "unicase", ] +[[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" @@ -564,6 +1145,28 @@ dependencies = [ "windows-sys", ] +[[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.119", +] + [[package]] name = "moxcms" version = "0.8.1" @@ -580,6 +1183,16 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" +[[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 = "nu-ansi-term" version = "0.50.3" @@ -589,6 +1202,16 @@ dependencies = [ "windows-sys", ] +[[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-traits" version = "0.2.19" @@ -596,6 +1219,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]] @@ -604,6 +1238,18 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -623,11 +1269,15 @@ dependencies = [ "anyhow", "axum", "blake3", + "candle-core", + "candle-nn", + "candle-transformers", "image", "natord", "serde", "serde_json", "tempfile", + "tokenizers", "tokio", "tower", "tower-http", @@ -650,6 +1300,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -659,6 +1318,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + [[package]] name = "pxfm" version = "0.1.30" @@ -680,12 +1362,115 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[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_distr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.18" @@ -716,12 +1501,29 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + [[package]] name = "ryu" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[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" @@ -731,6 +1533,12 @@ dependencies = [ "winapi-util", ] +[[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.229" @@ -785,6 +1593,15 @@ dependencies = [ "serde_core", ] +[[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_urlencoded" version = "0.7.1" @@ -850,6 +1667,36 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "spm_precompiled" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" +dependencies = [ + "base64", + "nom", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "stable_deref_trait" +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 = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.119" @@ -878,6 +1725,31 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sysctl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" +dependencies = [ + "bitflags", + "byteorder", + "enum-as-inner", + "libc", + "thiserror 1.0.69", + "walkdir", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -885,12 +1757,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "thread_local" version = "1.1.10" @@ -900,6 +1812,39 @@ dependencies = [ "cfg-if", ] +[[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", + "fancy-regex 0.14.0", + "getrandom 0.3.4", + "itertools", + "log", + "macro_rules_attribute", + "monostate", + "paste", + "rand", + "rayon", + "rayon-cond", + "regex", + "regex-syntax", + "serde", + "serde_json", + "spm_precompiled", + "thiserror 2.0.20", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +] + [[package]] name = "tokio" version = "1.53.1" @@ -1057,6 +2002,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "unicase" version = "2.9.0" @@ -1069,12 +2020,39 @@ 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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" @@ -1091,6 +2069,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "webp" version = "0.3.1" @@ -1125,6 +2112,88 @@ dependencies = [ "windows-link", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zip" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0" +dependencies = [ + "crc32fast", + "indexmap", + "memchr", + "typed-path", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 1c96995..60a61cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,9 @@ build = "build.rs" anyhow = "1.0.104" axum = { version = "0.8.9", features = ["json", "query", "tokio"] } blake3 = "1.8.7" +candle-core = { version = "0.9.2", default-features = false } +candle-nn = { version = "0.9.2", default-features = false } +candle-transformers = { version = "0.9.2", default-features = false } image = { version = "0.25.10", default-features = false, features = ["jpeg", "png", "webp"] } natord = "1.0.9" serde = { version = "1.0.229", features = ["derive"] } @@ -17,6 +20,7 @@ tower = { version = "0.5.3", features = ["util"] } tower-http = { version = "0.6.11", features = ["compression-br", "compression-gzip", "fs", "set-header", "trace"] } tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } +tokenizers = { version = "0.22.2", default-features = false, features = ["fancy-regex"] } walkdir = "2.5.0" webp = "0.3.1" diff --git a/README.md b/README.md index 1bc1fd2..579693b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Pixhelf -轻量的本地照片画廊,支持 JPEG、PNG、WebP、搜索和排序。 +轻量的本地照片画廊,支持 JPEG、PNG、WebP、自然语言文字搜图、随机探索和本地以图搜图。相似图片会直接显示在图片查看器下方的分页瀑布流中。 ## Docker Compose @@ -31,3 +31,88 @@ mkdir -p pic && docker run -d --name pixhelf --restart unless-stopped -p 3002:30 ``` 照片放进 `./pic`,访问 。 + +## 自然语言文字搜图 + +可选的 Chinese-CLIP ViT-B/16 模型支持直接输入中文描述,例如“海边日落”“草地上的狗”或“夜晚城市街道”。图片与文字都在本机编码,Pixhelf 不会在运行时联网或自动下载模型。索引尚未完成时搜索框仍按文件名搜索;完成后自动切换到语义搜索,并保留文件名精确匹配优先级。 + +下载并校验固定版本的官方模型与词表(模型约 753 MB): + +```bash +mkdir -p models/chinese-clip +curl --proto '=https' --tlsv1.2 --fail --location \ + https://huggingface.co/OFA-Sys/chinese-clip-vit-base-patch16/resolve/f4a64596bbcf9a2a94591b74b9dc39b2e4e77e3e/model.safetensors \ + --output models/chinese-clip/model.safetensors +curl --proto '=https' --tlsv1.2 --fail --location \ + https://huggingface.co/OFA-Sys/chinese-clip-vit-base-patch16/resolve/f4a64596bbcf9a2a94591b74b9dc39b2e4e77e3e/vocab.txt \ + --output models/chinese-clip/vocab.txt +printf '%s %s\n%s %s\n' \ + 29cc0b2bcf6ff777f2e15742be92b110e4acbdb2068356e862c4637a4b15fe4f \ + models/chinese-clip/model.safetensors \ + 45bbac6b341c319adc98a532532882e91a9cefc0329aa57bac9ae761c27b291c \ + models/chinese-clip/vocab.txt | sha256sum --check - +``` + +Compose 服务增加模型目录挂载与启动参数: + +```yaml +services: + pixhelf: + volumes: + - "./pic:/data/pic:ro" + - pixhelf-cache:/data/.pixhelf-cache + - "./models/chinese-clip:/models/chinese-clip:ro" + command: + - "--text-search-model" + - "/models/chinese-clip/model.safetensors" + - "--text-search-vocab" + - "/models/chinese-clip/vocab.txt" +``` + +本地运行: + +```bash +pixhelf \ + --text-search-model ./models/chinese-clip/model.safetensors \ + --text-search-vocab ./models/chinese-clip/vocab.txt +``` + +文字搜图使用一个 CPU 后台任务建立 512 维图像索引,向量按模型与词表指纹保存在缩略图缓存中,每张约 552 字节。侧边栏会显示“文字索引”进度;更换模型文件前请先停止 Pixhelf。 + +## 本地以图搜图 + +默认模式不需要模型:相似度完全在本机计算,综合比较图片的构图、亮度、色彩分布和边缘纹理,并过滤明显无关的结果。特征描述符会保存在缩略图缓存卷中;升级已有图库时会在后台自动补齐,之后重启无需重新计算。 + +如果希望识别“主体相近、构图和颜色不同”的图片,可以选择挂载 Meta 的 DINOv2 Small 官方权重。Pixhelf 不会联网或自动下载模型;开启后会用一个 CPU 后台任务建立 384 维语义索引,再与默认视觉特征混合排序。模型缺失或单张图片推理失败时会自动使用默认算法。 + +先在宿主机下载并校验固定版本的模型(88.2 MB,Apache-2.0): + +```bash +mkdir -p models +curl --proto '=https' --tlsv1.2 --fail --location \ + https://huggingface.co/facebook/dinov2-small/resolve/ed25f3a31f01632728cabb09d1542f84ab7b0056/model.safetensors \ + --output models/dinov2-small.safetensors +printf '%s %s\n' \ + ae1e99fcefd534ed978cdeb8326f08030c96e28b7a81ffcbc98a857c84d14be1 \ + models/dinov2-small.safetensors | sha256sum --check - +``` + +然后在 Compose 服务中增加只读挂载和启动参数: + +```yaml +services: + pixhelf: + volumes: + - "./pic:/data/pic:ro" + - pixhelf-cache:/data/.pixhelf-cache + - "./models/dinov2-small.safetensors:/models/dinov2-small.safetensors:ro" + command: ["--semantic-model", "/models/dinov2-small.safetensors"] +``` + +本地运行时使用同一个参数: + +```bash +pixhelf --semantic-model ./models/dinov2-small.safetensors +``` + +语义向量按模型指纹保存在缩略图缓存中,每张约 424 字节;侧边栏会显示“语义索引”进度。更换模型文件前请先停止 Pixhelf,替换后再启动。 diff --git a/frontend/scripts/visual-check.mjs b/frontend/scripts/visual-check.mjs index 4fa1b5b..701911b 100644 --- a/frontend/scripts/visual-check.mjs +++ b/frontend/scripts/visual-check.mjs @@ -328,16 +328,29 @@ try { })(), scrollCuePresentation: (() => { const cue = document.querySelector(".viewer-scroll-cue"); - const media = document.querySelector(".viewer-media"); - if (!cue || !media) return null; + if (!cue) return null; const style = getComputedStyle(cue); const cueBounds = cue.getBoundingClientRect(); - const mediaBounds = media.getBoundingClientRect(); return { - borderless: Number.parseFloat(style.borderTopWidth) === 0, - transparent: style.backgroundColor === "rgba(0, 0, 0, 0)", + surfaced: style.backgroundColor !== "rgba(0, 0, 0, 0)" + && Number.parseFloat(style.borderTopWidth) > 0, + circular: Number.parseFloat(style.borderTopLeftRadius) >= cueBounds.height / 2 - 1, width: cueBounds.width, - clearsMedia: cueBounds.top >= mediaBounds.bottom - 1, + }; + })(), + floatingToolbar: (() => { + const viewer = document.querySelector(".image-viewer"); + const header = document.querySelector(".viewer-header"); + const actions = document.querySelector(".viewer-header-actions"); + if (!viewer || !header || !actions) return null; + const headerStyle = getComputedStyle(header); + const actionsStyle = getComputedStyle(actions); + return { + mode: viewer.getAttribute("data-ui-layout"), + headerTransparent: headerStyle.backgroundImage === "none" + && headerStyle.backgroundColor === "rgba(0, 0, 0, 0)", + actionsSurfaced: actionsStyle.backgroundColor !== "rgba(0, 0, 0, 0)" + && Number.parseFloat(actionsStyle.borderTopWidth) > 0, }; })(), headingModules: document.querySelectorAll(".viewer-heading").length, @@ -1628,10 +1641,13 @@ try { viewer.switchPerformance.mediaAnimations !== 0 || viewer.switchPerformance.dispatchDuration > 80 || viewer.switchPerformance.firstFrameDuration > 100 || - !viewerChrome.scrollCuePresentation?.borderless || - !viewerChrome.scrollCuePresentation?.transparent || - viewerChrome.scrollCuePresentation.width < 88 || - !viewerChrome.scrollCuePresentation?.clearsMedia || + !viewerChrome.scrollCuePresentation?.surfaced || + !viewerChrome.scrollCuePresentation?.circular || + viewerChrome.scrollCuePresentation.width < 44 || + viewerChrome.scrollCuePresentation.width > 52 || + viewerChrome.floatingToolbar?.mode !== "floating" || + !viewerChrome.floatingToolbar?.headerTransparent || + !viewerChrome.floatingToolbar?.actionsSurfaced || (target.name === "desktop" && ( !viewer.detailsReachedByWheel || viewer.detailsName !== viewer.keyboardNavigation || diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9d19245..818d0a7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,7 +23,13 @@ import { } from "preact/hooks"; import { ToolbarPopover } from "./ToolbarPopover"; import { ImageViewer } from "./ImageViewer"; -import { getGallery, getImages, getStatus, takeInitialBootstrap } from "./api"; +import { + getGallery, + getImages, + getSimilarImages, + getStatus, + takeInitialBootstrap, +} from "./api"; import type { GalleryImage, GallerySummary, @@ -37,6 +43,7 @@ import { } from "./viewerAssets"; const PAGE_SIZE = 60; +const SIMILAR_PAGE_SIZE = 30; const CARD_PREFETCH_MARGIN = "1200px 0px"; const MOBILE_PAGE_PREFETCH_MARGIN = "1400px 0px"; const DESKTOP_PAGE_PREFETCH_MARGIN = "900px 0px"; @@ -57,6 +64,13 @@ type ImagePageState = { nextOffset: number | null; }; +type SimilarImagePageState = { + source: GalleryImage | null; + images: GalleryImage[]; + total: number; + nextOffset: number | null; +}; + type ViewerAnchor = { cardRatio: number; viewportRatio: number; @@ -85,6 +99,13 @@ const EMPTY_IMAGE_PAGE: ImagePageState = { nextOffset: null, }; +const EMPTY_SIMILAR_IMAGE_PAGE: SimilarImagePageState = { + source: null, + images: [], + total: 0, + nextOffset: null, +}; + function clampValue(value: number, minimum: number, maximum: number): number { return Math.min(maximum, Math.max(minimum, value)); } @@ -510,11 +531,34 @@ function App() { const [mobileNavOpen, setMobileNavOpen] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(initialSidebarCollapsed); const [viewerImageId, setViewerImageId] = useState(null); + const [viewerDiscoveredImages, setViewerDiscoveredImages] = useState([]); + const [similarPage, setSimilarPage] = useState( + EMPTY_SIMILAR_IMAGE_PAGE, + ); + const [similarLoading, setSimilarLoading] = useState(false); + const [similarLoadingMore, setSimilarLoadingMore] = useState(false); + const [similarError, setSimilarError] = useState(null); const [viewerReturnRequest, setViewerReturnRequest] = useState(null); const [reloadToken, setReloadToken] = useState(0); const compactLayout = useMediaQuery("(max-width: 720px)"); const mobileNavMounted = useDelayedUnmount(mobileNavOpen, MOBILE_NAV_EXIT_MS); - const debouncedSearch = useDebounced(search.trim(), 250); + const textSearchReady = Boolean( + status?.backgroundComplete + && status.textSearch.enabled + && status.textSearch.backgroundComplete + && status.textSearch.ready > 0, + ); + const textSearchMode: "filename" | "indexing" | "semantic" = status?.textSearch.enabled + ? textSearchReady + ? "semantic" + : status.backgroundComplete && status.textSearch.backgroundComplete + ? "filename" + : "indexing" + : "filename"; + const debouncedSearch = useDebounced( + search.trim(), + status?.textSearch.enabled ? 500 : 250, + ); const requestVersionRef = useRef(0); const summaryRevisionRef = useRef( INITIAL_BOOTSTRAP?.summary.revision ?? null, @@ -522,11 +566,25 @@ function App() { const skipInitialImagesRef = useRef(Boolean(INITIAL_BOOTSTRAP)); const loadMoreControllerRef = useRef(null); const loadMorePromiseRef = useRef | null>(null); + const similarRequestVersionRef = useRef(0); + const similarControllerRef = useRef(null); + const similarLoadMoreControllerRef = useRef(null); + const similarLoadMorePromiseRef = useRef | null>(null); + const backgroundCompletionRef = useRef( + Boolean( + INITIAL_BOOTSTRAP?.status.backgroundComplete + && INITIAL_BOOTSTRAP.status.semantic.backgroundComplete, + ), + ); const viewerImageIdRef = useRef(null); const viewerAnchorRef = useRef(null); const viewerReturnSequenceRef = useRef(0); const viewerReturnFlightRef = useRef(null); const { images, total, nextOffset } = imagePage; + const viewerImages = useMemo( + () => appendUniqueImages(images, viewerDiscoveredImages), + [images, viewerDiscoveredImages], + ); useLayoutEffect(() => { window.scrollTo({ top: 0, left: 0, behavior: "auto" }); @@ -539,6 +597,8 @@ function App() { useEffect(() => () => { disposeViewerReturnFlight(viewerReturnFlightRef.current); viewerReturnFlightRef.current = null; + similarControllerRef.current?.abort(); + similarLoadMoreControllerRef.current?.abort(); }, []); useEffect(() => { @@ -626,13 +686,22 @@ function App() { const next = await getStatus(controller.signal); if (controller.signal.aborted) return; setStatus(next); - timer = window.setTimeout(poll, next.backgroundComplete ? 10000 : 1500); + const complete = next.backgroundComplete + && next.semantic.backgroundComplete + && next.textSearch.backgroundComplete; + timer = window.setTimeout(poll, complete ? 10000 : 1500); } catch { if (!controller.signal.aborted) timer = window.setTimeout(poll, 5000); } }; const initialDelay = INITIAL_BOOTSTRAP - ? (INITIAL_BOOTSTRAP.status.backgroundComplete ? 10_000 : 1_500) + ? ( + INITIAL_BOOTSTRAP.status.backgroundComplete + && INITIAL_BOOTSTRAP.status.semantic.backgroundComplete + && INITIAL_BOOTSTRAP.status.textSearch.backgroundComplete + ? 10_000 + : 1_500 + ) : 0; timer = window.setTimeout(poll, initialDelay); return () => { @@ -691,7 +760,7 @@ function App() { loadMorePromiseRef.current = null; loadMoreController?.abort(); }; - }, [album, debouncedSearch, exploreSeed, reloadToken]); + }, [album, debouncedSearch, exploreSeed, reloadToken, textSearchReady]); const loadMore = useCallback((): Promise => { if (loading || nextOffset === null) return Promise.resolve([]); @@ -740,6 +809,124 @@ function App() { return promise; }, [album, debouncedSearch, exploreSeed, loading, nextOffset]); + const resetSimilarSearch = useCallback(() => { + similarRequestVersionRef.current += 1; + similarControllerRef.current?.abort(); + similarLoadMoreControllerRef.current?.abort(); + similarControllerRef.current = null; + similarLoadMoreControllerRef.current = null; + similarLoadMorePromiseRef.current = null; + setSimilarPage(EMPTY_SIMILAR_IMAGE_PAGE); + setSimilarLoading(false); + setSimilarLoadingMore(false); + setSimilarError(null); + }, []); + + const searchSimilar = useCallback((source: GalleryImage, force = false) => { + const sameSource = similarPage.source?.id === source.id; + if (!force && sameSource && (similarLoading || !similarError)) return; + + similarControllerRef.current?.abort(); + similarLoadMoreControllerRef.current?.abort(); + similarLoadMoreControllerRef.current = null; + similarLoadMorePromiseRef.current = null; + const controller = new AbortController(); + const requestVersion = ++similarRequestVersionRef.current; + similarControllerRef.current = controller; + setSimilarPage({ source, images: [], total: 0, nextOffset: null }); + setSimilarLoading(true); + setSimilarLoadingMore(false); + setSimilarError(null); + + void getSimilarImages( + source.id, + { offset: 0, limit: SIMILAR_PAGE_SIZE }, + controller.signal, + ) + .then((page) => { + if (requestVersion !== similarRequestVersionRef.current) return; + setSimilarPage({ + source, + images: page.items, + total: page.total, + nextOffset: page.nextOffset, + }); + setViewerDiscoveredImages((current) => appendUniqueImages(current, page.items)); + }) + .catch((reason: unknown) => { + if (!controller.signal.aborted && requestVersion === similarRequestVersionRef.current) { + setSimilarError(errorMessage(reason, "无法查找相似图片")); + } + }) + .finally(() => { + if (similarControllerRef.current === controller) { + similarControllerRef.current = null; + if (requestVersion === similarRequestVersionRef.current) setSimilarLoading(false); + } + }); + }, [similarError, similarLoading, similarPage.source?.id]); + + useEffect(() => { + const complete = Boolean( + status?.backgroundComplete && status.semantic.backgroundComplete, + ); + const justCompleted = complete && !backgroundCompletionRef.current; + backgroundCompletionRef.current = complete; + if (justCompleted && similarPage.source) { + searchSimilar(similarPage.source, true); + } + }, [searchSimilar, similarPage.source, status]); + + const loadMoreSimilar = useCallback((): Promise => { + const source = similarPage.source; + const offset = similarPage.nextOffset; + if (!source || similarLoading || offset === null) return Promise.resolve([]); + const pending = similarLoadMorePromiseRef.current; + if (pending) return pending; + + const controller = new AbortController(); + const requestVersion = similarRequestVersionRef.current; + similarLoadMoreControllerRef.current = controller; + setSimilarLoadingMore(true); + setSimilarError(null); + + const promise = (async () => { + try { + const page = await getSimilarImages( + source.id, + { offset, limit: SIMILAR_PAGE_SIZE }, + controller.signal, + ); + if (requestVersion !== similarRequestVersionRef.current) return []; + setSimilarPage((current) => current.source?.id === source.id + ? { + source, + images: appendUniqueImages(current.images, page.items), + total: page.total, + nextOffset: page.nextOffset, + } + : current); + setViewerDiscoveredImages((current) => appendUniqueImages(current, page.items)); + return page.items; + } catch (reason) { + if (!controller.signal.aborted && requestVersion === similarRequestVersionRef.current) { + setSimilarError(errorMessage(reason, "无法继续加载相似图片")); + } + return []; + } finally { + if (similarLoadMoreControllerRef.current === controller) { + similarLoadMoreControllerRef.current = null; + similarLoadMorePromiseRef.current = null; + if (requestVersion === similarRequestVersionRef.current) { + setSimilarLoadingMore(false); + } + } + } + })(); + similarLoadMorePromiseRef.current = promise; + return promise; + }, [similarLoading, similarPage.nextOffset, similarPage.source]); + const sentinelRef = useRef(null); useEffect(() => { const sentinel = sentinelRef.current; @@ -759,8 +946,22 @@ function App() { }, [compactLayout, loadMore, nextOffset]); const viewerIndex = viewerImageId + ? viewerImages.findIndex((image) => image.id === viewerImageId) + : -1; + const galleryViewerIndex = viewerImageId ? images.findIndex((image) => image.id === viewerImageId) : -1; + const viewerImage = viewerIndex >= 0 ? viewerImages[viewerIndex] : null; + const similarActive = Boolean( + viewerImage && similarPage.source?.id === viewerImage.id, + ); + const viewerCanLoadMore = Boolean( + viewerImage && ( + images.some((candidate) => candidate.id === viewerImage.id) + ? nextOffset !== null + : similarActive && similarPage.nextOffset !== null + ), + ); const openViewer = useCallback(( imageId: string, @@ -771,10 +972,12 @@ function App() { viewerReturnFlightRef.current = null; viewerAnchorRef.current = captureViewerAnchor(card, pointerY); prepareViewerImages(images, images.findIndex((image) => image.id === imageId)); + resetSimilarSearch(); + setViewerDiscoveredImages([]); setViewerReturnRequest(null); viewerImageIdRef.current = imageId; setViewerImageId(imageId); - }, [images]); + }, [images, resetSimilarSearch]); const closeViewer = useCallback(() => { const currentImageId = viewerImageIdRef.current; @@ -792,9 +995,11 @@ function App() { imageId: currentImageId, sequence: viewerReturnSequenceRef.current, }); + resetSimilarSearch(); + setViewerDiscoveredImages([]); viewerImageIdRef.current = null; setViewerImageId(null); - }, []); + }, [resetSimilarSearch]); useLayoutEffect(() => { if (viewerImageId || !viewerReturnRequest) return; @@ -1016,31 +1221,45 @@ function App() { }, [viewerImageId, viewerReturnRequest]); useEffect(() => { - if (!viewerImageId || viewerIndex < 0 || nextOffset === null) return; - if (viewerIndex >= images.length - 5) void loadMore(); - }, [images.length, loadMore, nextOffset, viewerImageId, viewerIndex]); + if (!viewerImageId || galleryViewerIndex < 0 || nextOffset === null) return; + if (galleryViewerIndex >= images.length - 5) void loadMore(); + }, [galleryViewerIndex, images.length, loadMore, nextOffset, viewerImageId]); + + const openSimilarImage = useCallback((candidate: GalleryImage) => { + setViewerDiscoveredImages((current) => appendUniqueImages(current, [candidate])); + prepareViewerImages([candidate], 0); + viewerImageIdRef.current = candidate.id; + setViewerImageId(candidate.id); + searchSimilar(candidate); + }, [searchSimilar]); const navigateViewer = useCallback((direction: -1 | 1) => { const currentImageId = viewerImageIdRef.current; if (!currentImageId) return; - const currentIndex = images.findIndex((image) => image.id === currentImageId); + const currentIndex = viewerImages.findIndex((image) => image.id === currentImageId); if (currentIndex < 0) return; - const target = images[currentIndex + direction]; + const target = viewerImages[currentIndex + direction]; if (target) { viewerImageIdRef.current = target.id; setViewerImageId(target.id); return; } - if (direction < 0 || nextOffset === null) return; + if (direction < 0) return; const startingId = currentImageId; - void loadMore().then((incoming) => { + const pending = similarPage.source?.id === currentImageId + && similarPage.nextOffset !== null + ? loadMoreSimilar() + : nextOffset !== null && images.some((candidate) => candidate.id === currentImageId) + ? loadMore() + : Promise.resolve([]); + void pending.then((incoming) => { const next = incoming[0]; if (!next || viewerImageIdRef.current !== startingId) return; viewerImageIdRef.current = next.id; setViewerImageId(next.id); }); - }, [images, loadMore, nextOffset]); + }, [images, loadMore, loadMoreSimilar, nextOffset, similarPage, viewerImages]); const galleryPath = `/${album.replace(/^\/+/, "")}`; const chooseAlbum = (path: string) => { @@ -1077,6 +1296,7 @@ function App() { >
{summary?.total === 0 ? "图库暂无图片" : "没有找到图片"} - {summary?.total === 0 ? "添加图片后将自动显示" : "请调整相册或搜索条件"} + {summary?.total === 0 + ? "添加图片后将自动显示" + : textSearchReady && debouncedSearch + ? "换一种自然语言描述,或减少限定词" + : "请调整相册或搜索条件"} )} @@ -1162,13 +1386,23 @@ function App() { {viewerIndex >= 0 && ( void loadMoreSimilar()} + onOpenSimilar={openSimilarImage} /> )} @@ -1177,6 +1411,7 @@ function App() { function Header({ search, + searchMode, onSearchChange, onExplore, exploreActive, @@ -1186,6 +1421,7 @@ function Header({ navigationOpen, }: { search: string; + searchMode: "filename" | "indexing" | "semantic"; onSearchChange: (value: string) => void; onExplore: () => void; exploreActive: boolean; @@ -1197,6 +1433,13 @@ function Header({ const [searchOpen, setSearchOpen] = useState(false); const [exploreMotionKey, setExploreMotionKey] = useState(0); const searchInputRef = useRef(null); + const semanticSearch = searchMode === "semantic"; + const searchIndexing = searchMode === "indexing"; + const searchPlaceholder = semanticSearch + ? "描述想找的图片" + : searchIndexing + ? "文字索引中 · 暂搜文件名" + : "搜索文件名"; useEffect(() => { if (!searchOpen) return; @@ -1280,7 +1523,7 @@ function Header({