From 2778f145743392f19673615d03e3278567998839 Mon Sep 17 00:00:00 2001 From: Breadway Date: Thu, 2 Jul 2026 20:59:18 +0800 Subject: [PATCH] Initial commit --- .gitignore | 31 + Cargo.lock | 3722 +++++++++++++++++ Cargo.toml | 3 + DESIGN.md | 82 + LICENSE | 21 + README.md | 134 + bakery.toml | 18 + benchmark_embed.py | 96 + breadmill/Cargo.toml | 55 + breadmill/src/chunk.rs | 142 + breadmill/src/embed.rs | 258 ++ breadmill/src/extract.rs | 84 + breadmill/src/indexer.rs | 414 ++ breadmill/src/main.rs | 242 ++ breadmill/src/power.rs | 31 + breadmill/src/serve.rs | 127 + breadmill/src/store.rs | 235 ++ breadsearch-shared/Cargo.toml | 14 + breadsearch-shared/src/lib.rs | 268 ++ breadsearch/Cargo.toml | 16 + breadsearch/src/main.rs | 429 ++ config.example.toml | 41 + .../2026-06-24-oom-minified-json-chunking.md | 238 ++ original-info-signature.txt | 1 + original-model-signature.txt | 1 + packaging/breadmill.service | 15 + quantize_nomic.py | 144 + quantized_info.csv | 151 + test_npu_quantized.py | 97 + 29 files changed, 7110 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 DESIGN.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bakery.toml create mode 100644 benchmark_embed.py create mode 100644 breadmill/Cargo.toml create mode 100644 breadmill/src/chunk.rs create mode 100644 breadmill/src/embed.rs create mode 100644 breadmill/src/extract.rs create mode 100644 breadmill/src/indexer.rs create mode 100644 breadmill/src/main.rs create mode 100644 breadmill/src/power.rs create mode 100644 breadmill/src/serve.rs create mode 100644 breadmill/src/store.rs create mode 100644 breadsearch-shared/Cargo.toml create mode 100644 breadsearch-shared/src/lib.rs create mode 100644 breadsearch/Cargo.toml create mode 100644 breadsearch/src/main.rs create mode 100644 config.example.toml create mode 100644 docs/error-reports/2026-06-24-oom-minified-json-chunking.md create mode 100644 original-info-signature.txt create mode 100644 original-model-signature.txt create mode 100644 packaging/breadmill.service create mode 100644 quantize_nomic.py create mode 100644 quantized_info.csv create mode 100644 test_npu_quantized.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36f7f5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Rust build artifacts +target/ + +# Editor and IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS artifacts +.DS_Store +Thumbs.db +desktop.ini + +# Environment and secrets +.env +.env.* +*.env +*.pem +*.key +*.p12 +secrets/ + +# Log files +*.log +logs/ + +# Runtime files +*.sock +*.pid diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fc122a0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3722 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "adobe-cmap-parser" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3" +dependencies = [ + "pom", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +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 = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bread-theme" +version = "0.2.3" +source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.8#5e58558dd36031433d4a8d8e70c71206c3f1f8f4" +dependencies = [ + "dirs", + "gtk4", + "serde", + "serde_json", +] + +[[package]] +name = "breadmill" +version = "0.1.0" +dependencies = [ + "breadsearch-shared", + "hex", + "ignore", + "notify", + "ort", + "pdf-extract", + "quick-xml", + "rusqlite", + "serde", + "serde_json", + "sha2 0.11.0", + "tokenizers", + "ureq 2.12.1", + "usearch", + "zip", +] + +[[package]] +name = "breadsearch" +version = "0.1.0" +dependencies = [ + "bread-theme", + "breadsearch-shared", + "gtk4", + "gtk4-layer-shell", + "serde_json", +] + +[[package]] +name = "breadsearch-shared" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "toml 0.8.23", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "cairo-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cff-parser" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f5b6e9141c036f3ff4ce7b2f7e432b0f00dee416ddcd4f17741d189ddc2e9d" + +[[package]] +name = "cfg-expr" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[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 = "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 = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "cxx" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf" +dependencies = [ + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "daachorse" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f55d7153ba3b507595872a3874803f07a8a81d1e888abed8e5db7da0597d6e2" + +[[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", +] + +[[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", +] + +[[package]] +name = "dary_heap" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" +dependencies = [ + "serde", +] + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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", +] + +[[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", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ecb" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7" +dependencies = [ + "cipher", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[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 = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "esaxx-rs" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" +dependencies = [ + "cc", +] + +[[package]] +name = "euclid" +version = "0.20.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb7ef65b3777a325d1eeefefab5b6d4959da54747e33bd6258e789640f307ad" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[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 = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "gl", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "gio" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3848bcba3a35cc0a71df8ba8ecfd799d6bfb862342a53a4a915fb62213aa4e6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.61.2", +] + +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glib" +version = "0.22.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "gobject-sys" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-layer-shell" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a" +dependencies = [ + "bitflags 2.13.0", + "gdk4", + "glib", + "glib-sys", + "gtk4", + "gtk4-layer-shell-sys", + "libc", +] + +[[package]] +name = "gtk4-layer-shell-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f566a5ec5bcc454e7fcf2ab76930887ced5365afce12c1e5201bb296b95f1b9" +dependencies = [ + "gdk4-sys", + "glib-sys", + "gtk4-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk4-macros" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gtk4-sys" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5081f264ed7adee96ea4b4778b6bb9da0a7228b084587aa3bd3ff05da7c5a3b" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha256" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[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 = "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 = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.0", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lopdf" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7184fdea2bc3cd272a1acec4030c321a8f9875e877b3f92a53f2f6033fdc289" +dependencies = [ + "aes", + "bitflags 2.13.0", + "cbc", + "ecb", + "encoding_rs", + "flate2", + "getrandom 0.3.4", + "indexmap", + "itoa", + "log", + "md-5", + "nom 8.0.0", + "nom_locate", + "rand", + "rangemap", + "sha2 0.10.9", + "stringprep", + "thiserror 2.0.18", + "ttf-parser", + "weezl", +] + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-rust2" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f57f9918e5bd7bc58c22cdd70a6afc7375d4dd9683af5f2b34bd3d2bba619" + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[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 = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[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", +] + +[[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 = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[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 = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom_locate" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" +dependencies = [ + "bytecount", + "memchr", + "nom 8.0.0", +] + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.13.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[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-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "numkong" +version = "7.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58d4bb97df102ebdde66a352a20c0bc65c7c407ee9bef12ebe317edc2458a55" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "onig" +version = "6.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" +dependencies = [ + "bitflags 2.13.0", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types", + "libc", + "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", +] + +[[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.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ort" +version = "2.0.0-rc.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133" +dependencies = [ + "libloading", + "ndarray", + "ort-sys", + "smallvec", + "tracing", + "ureq 3.3.0", +] + +[[package]] +name = "ort-sys" +version = "2.0.0-rc.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90" +dependencies = [ + "hmac-sha256", + "lzma-rust2", + "ureq 3.3.0", +] + +[[package]] +name = "pango" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438" +dependencies = [ + "gio", + "glib", + "libc", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + +[[package]] +name = "pdf-extract" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28ba1758a3d3f361459645780e09570b573fc3c82637449e9963174c813a98" +dependencies = [ + "adobe-cmap-parser", + "cff-parser", + "encoding_rs", + "euclid", + "log", + "lopdf", + "postscript", + "type1-encoding-parser", + "unicode-normalization", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "pom" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postscript" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78451badbdaebaf17f053fd9152b3ffb33b516104eacb45e7864aaa9c712f306" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[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-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +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 = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +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 = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[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 = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + +[[package]] +name = "rusqlite" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" +dependencies = [ + "bitflags 2.13.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +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 = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +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 = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "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 = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[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 = "spm_precompiled" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" +dependencies = [ + "base64 0.13.1", + "nom 7.1.3", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[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 = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 1.1.2+spec-1.1.0", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[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", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokenizers" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44e5bea67576e04b6ff8564c5d9e09c2ef0cf476502245f2f120e497769d3112" +dependencies = [ + "ahash", + "compact_str", + "daachorse", + "dary_heap", + "derive_builder", + "esaxx-rs", + "getrandom 0.3.4", + "indicatif", + "itertools", + "log", + "macro_rules_attribute", + "monostate", + "onig", + "paste", + "rand", + "rayon", + "rayon-cond", + "regex", + "regex-syntax", + "serde", + "serde_json", + "spm_precompiled", + "thiserror 2.0.18", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "type1-encoding-parser" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa10c302f5a53b7ad27fd42a3996e23d096ba39b5b8dd6d9e683a05b01bee749" +dependencies = [ + "pom", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[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-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64 0.22.1", + "der", + "log", + "native-tls", + "percent-encoding", + "rustls-pki-types", + "socks", + "ureq-proto", + "utf8-zero", + "webpki-root-certs", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "usearch" +version = "2.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c08f764417012cf6aea6d1380ef9ea8712c5795a938b726fc67b9bf7ea8824b" +dependencies = [ + "cxx", + "cxx-build", + "numkong", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +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 = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[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 = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + +[[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", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror 2.0.18", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a0b3ed4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["breadsearch-shared", "breadmill", "breadsearch"] +resolver = "2" diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..ce706f3 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,82 @@ +# breadsearch + breadmill — semantic system-wide search for BOS + +## Context +BOS/bread has no content search — only breadbox's app launcher (exact/fuzzy over `.desktop` files). The goal is a **semantic** "find anything by meaning" engine: a flagship, differentiating BOS feature that's also the *right* workload for the AMD XDNA NPU (small encoder model, compute-bound single forward pass, always-on background embedding — none of the bandwidth-bound problems that make LLMs a bad NPU fit). + +Two components, mirroring the breadpad/breadman split and the breadbox/breadbox-sync precedent (GUI + background helper + shared lib in one repo): +- **breadmill** — always-on daemon: walks files → extracts text → chunks → embeds → vector index; answers queries over a Unix socket. ("mill grain into flour.") +- **breadsearch** — standalone GTK4 GUI, *forked from breadbox's UI*, that queries breadmill and shows ranked hits. ("sift the flour.") + +breadbox stays a pure app launcher, unchanged. + +## Decisions (confirmed with user) +- **Index scope (v1):** curated roots — `~/Documents`, `~/Projects` (notes/docs, not code yet), `~/.config/breadpad`. Extract `md, txt, org, pdf, odt, docx`. Skip binaries/images/build dirs/`.git`. +- **Repo layout:** one cargo workspace at `~/Projects/breadsearch/`. +- **Embedding model:** `nomic-embed-text-v1.5` (768-dim ONNX, ~550MB). Requires task prefixes: `search_document: ` for indexed chunks, `search_query: ` for queries; mean-pool + L2-normalize. +- **Compute:** CPU-first; NPU (XDNA via ONNX Runtime VitisAI EP) is a later backend swap, not a v1 dependency. + +## Workspace layout +``` +~/Projects/breadsearch/ + Cargo.toml # [workspace] members = breadsearch-shared, breadmill, breadsearch + bakery.toml # bread package manifest (binaries: breadsearch, breadmill) + config.example.toml # ~/.config/breadsearch/config.toml template + README.md + breadsearch-shared/ # lib: XDG paths, config, IPC types + socket client + breadmill/ # daemon bin + breadsearch/ # GUI bin (forked breadbox UI) +``` + +## Component: breadsearch-shared (lib) +Model on `breadbox/breadbox-shared/src/lib.rs` (XDG helpers + serde/toml config). +- **Paths:** `config_dir()` → `~/.config/breadsearch`; `state_dir()` → `~/.local/state/breadsearch` (index); `cache_dir()` → `~/.cache/breadsearch` (models); `socket_path()` → `$XDG_RUNTIME_DIR/breadmill.sock`. +- **Config** (serde + `toml`): `[index] roots, extensions, max_file_mb`; `[search] limit, snippet_len`; `[model] name, dim`. +- **IPC types** (serde_json, newline-delimited JSON over the Unix socket): + - Request: `Query { query: String, limit: usize }`, `Status`, `Reindex`. + - Response: `Hits(Vec)` where `Hit { title, path, snippet, score }`; `StatusInfo { indexed, pending, model_ready }`. +- **Socket client** helper used by the GUI (connect, send, read one response). + +## Component: breadmill (daemon) +Pipeline, isolated behind small traits so each stage is swappable: +1. **Walk** — `ignore` crate (parallel, respects `.gitignore`) over configured roots; filter by extension + size. +2. **Extract** — `md/txt/org`: read directly; `pdf`: `pdf-extract`; `docx/odt`: unzip + strip XML (`zip` + `quick-xml`), best-effort. +3. **Chunk** — ~512-token windows with overlap; keep byte offsets for snippets. +4. **Embed** — `Embedder` trait. v1 impl: `ort` (ONNX Runtime 2.x, CPU EP) + `tokenizers` (HF) running nomic-embed-text-v1.5. Apply `search_document:`/`search_query:` prefixes, mean-pool, normalize. +5. **Store** — `rusqlite` for metadata (path, mtime, content-hash, chunk text/offsets) keyed by rowid + `usearch` (HNSW, 768-dim, cosine) for vectors keyed by the same id. Both persisted under `state_dir()`. +6. **Incremental** — on start, diff roots against sqlite (mtime+hash): embed new/changed, drop deleted. Then live-watch with `notify` (debounced) to re-embed on change. +7. **Serve** — `tokio` (or std threads) Unix-socket listener: `Query` → embed query → usearch top-k → join sqlite metadata → `Hits`. Also `Status`/`Reindex`. +- **Model fetch:** first run downloads `model.onnx` + `tokenizer.json` from HF into `cache_dir()/models/` (needs network once); `breadmill --fetch-model` to pre-fetch. Log clearly if absent. +- **Lifecycle:** systemd **user** service `breadmill.service` (pattern from `breadbox-sync.service` / breadd), `WantedBy=default.target`. + +## Component: breadsearch (GUI) — fork of breadbox +Start from `breadbox/breadbox/src/main.rs`. **Reuse verbatim:** the gtk4-layer-shell overlay window (rename namespace/app-id to `breadsearch` / `com.breadway.breadsearch`), `SearchEntry` + `ScrolledWindow` + `ListBox`, ↑/↓/Enter/Esc handling, click-outside-to-close, PID-toggle (`breadsearch.pid`), and the theming path: `bread_theme::gtk::apply_shared()` + `apply_app_css(|| build_css(&load_palette()))` + user `style.css`. Pin `bread-theme` git tag `v0.2.8`, feature `gtk` (same as breadbox). +**Swap:** +- **Result source:** delete `load_sorted_entries`/`fuzzy_*`/`DesktopEntry`. On `search.connect_changed`, **debounce ~150ms** (`glib::timeout_add_local`) then query breadmill **off the UI thread** (`std::thread` + `glib::MainContext::channel`), clear the `ListBox`, append a row per `Hit`. +- **Row content:** title (filename/heading) + muted path + snippet line; filetype icon via `gio::content_type_guess` → `Image::from_gicon`. Extend `build_css` with a `.hit-snippet` class. +- **Action:** replace `do_launch` with open-file — `Enter`/row-activated → `xdg-open `; `Ctrl+Enter` → open containing folder. Then close. + +## Key crates +`ort` (ONNX Runtime), `tokenizers`, `usearch`, `rusqlite`, `ignore`, `notify`, `pdf-extract`, `zip`+`quick-xml`, `serde`/`serde_json`/`toml`, `gtk4` 0.11 + `gtk4-layer-shell` 0.8, `bread-theme` (git tag v0.2.8). + +## Packaging & BOS integration (last phase — post-1.0, per earlier decision) +- `bakery.toml` (model on `breadbox/bakery.toml`): `binaries = ["breadsearch","breadmill"]`, system_deps for onnxruntime/gtk; `[[service]] unit="breadmill.service" enable=true`; `[config] dir="~/.config/breadsearch"`. +- BOS: add `breadsearch`+`breadmill` to `build-local.sh` `BREAD_BINS`; autostart `breadmill.service`; Hyprland keybind (e.g. `SUPER+slash`) → `breadsearch` in the skel `hyprland.lua`. +- Release: dual remotes (origin GitHub + forgejo), bakery index regen — per the bread release train. + +## Phasing (de-risked: ship CPU, NPU later) +1. Scaffold workspace + `breadsearch-shared` (paths, config, IPC types, socket client). +2. `breadmill` CPU pipeline end-to-end (walk→extract→chunk→embed→store→serve) + `--reindex`/`--fetch-model` + systemd unit. +3. `breadsearch` GUI fork (socket query + xdg-open + theme). +4. Packaging (bakery, config.example, README) + BOS wiring. +5. **Later:** NPU `Embedder` impl (ort VitisAI/XDNA EP) — the go/no-go POC; pure backend swap. + +## Verification +- `breadmill --fetch-model` then `--reindex` over a small test corpus; log embedded-chunk count; confirm `state_dir` index persists across restart. +- Query the socket directly (a `breadmill query "..."` subcommand or `socat`) and confirm semantically-relevant hits with sane scores for a concept query (not keyword). +- Launch `breadsearch`, type a *concept* (e.g. "tax stuff", "that suspend bug fix"), see relevant files ranked, `Enter` opens via xdg-open, `Ctrl+Enter` reveals folder, `Esc` closes; theme matches breadbox; hot-reloads on `bread-theme reload`. +- Edit/add/delete a file in a root → `notify` re-index → new content findable within seconds. + +## Notes / risks +- nomic prefixes + mean-pool + normalize must match between index and query or recall collapses. +- `ort` linking: prefer the crate's downloaded/bundled ONNX Runtime to avoid version skew with Arch's `onnxruntime`. +- Office formats (docx/odt) are best-effort in v1; md/txt/org/pdf are the reliable path. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..373e4ee --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Breadway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..19d1fee --- /dev/null +++ b/README.md @@ -0,0 +1,134 @@ +# breadsearch + +Semantic document search for Bread OS. Type a concept — not a keyword — and get ranked hits from your documents. + +Two binaries in one Cargo workspace: + +- **breadmill** — background daemon. Walks configured directories, extracts text, chunks and embeds documents with [nomic-embed-text-v1.5](https://huggingface.co/nomic-ai/nomic-embed-text-v1.5) (768-dim ONNX), stores vectors in an HNSW index (usearch) backed by SQLite metadata, and serves queries over a Unix socket. Watches for filesystem changes and re-indexes incrementally. +- **breadsearch** — GTK4 overlay GUI. Queries breadmill via the Unix socket and shows ranked results. Press Enter to open a file, Ctrl+Enter to reveal its folder, Esc to close. Bind it to a hotkey (e.g. Super+/) and invoke it as a toggle. + +## Build + +System dependencies: `gtk4`, `gtk4-layer-shell`, `librsvg`. + +``` +cargo build --release +``` + +The `ort` crate downloads a bundled ONNX Runtime at build time (no system `onnxruntime` needed for the default CPU build). + +Optional features: + +| Feature | What it adds | +|---------|-------------| +| `npu` | AMD XDNA NPU via VitisAI ONNX Runtime EP (requires Ryzen AI SDK) | +| `rocm` | AMD iGPU via ROCm ONNX Runtime EP | + +``` +# NPU build +cargo build --release -p breadmill --features npu +``` + +## Setup + +**1. Fetch the embedding model** (~550 MB, downloaded once from Hugging Face): + +``` +breadmill fetch-model +``` + +Model files are stored in `~/.cache/breadsearch/models/`. + +**2. Enable the systemd user service:** + +``` +cp packaging/breadmill.service ~/.config/systemd/user/ +systemctl --user daemon-reload +systemctl --user enable --now breadmill +``` + +Or run it directly: `breadmill serve` (or just `breadmill`). + +**3. Copy the example config** (optional — built-in defaults are used otherwise): + +``` +mkdir -p ~/.config/breadsearch +cp config.example.toml ~/.config/breadsearch/config.toml +``` + +## breadsearch (GUI) + +``` +breadsearch +``` + +Invoking again while running closes the window (PID-toggle). Bind to a hotkey in your compositor config. + +Results show: filename/title, full path, and a snippet from the matching chunk. Score is cosine similarity as a percentage. + +User CSS overrides go in `~/.config/breadsearch/style.css`. + +## breadmill (daemon / CLI) + +``` +# Start the daemon (also the default when called with no arguments) +breadmill serve + +# Force a full re-index from scratch +breadmill reindex + +# Download model files +breadmill fetch-model + +# Query from the terminal +breadmill query "tax stuff" + +# Show daemon status (chunks indexed, pending, model ready) +breadmill status + +# Backend flags (requires the matching Cargo feature) +breadmill --npu +breadmill --rocm +``` + +## Config + +`~/.config/breadsearch/config.toml` — all keys are optional; built-in defaults are shown. + +```toml +[index] +roots = ["~/Documents", "~/Projects", "~/.config/breadpad"] +extensions = ["md", "txt", "org", "pdf", "odt", "docx"] +excludes = [] # paths to skip (prefix match) +max_file_mb = 10.0 + +[search] +limit = 10 # max results per query +snippet_len = 200 # max characters in result snippet + +[model] +name = "nomic-embed-text-v1.5" +dim = 768 +backend = "cpu" # "cpu", "npu", or "rocm" +``` + +`roots` and `excludes` support `~/` expansion. The index respects `.gitignore` files found during the walk. + +### NPU backend + +Set `backend = "npu"` in config (or pass `--npu`) when running a build compiled with `--features npu`. breadmill looks for the VitisAI EP config file in this order: + +1. `$VAIP_CONFIG` +2. `~/.config/breadsearch/vaip_config.json` +3. `~/.local/share/ryzen-ai-1.7.1/voe-4.0-linux_x86_64/vaip_config.json` +4. `/etc/vaip_config.json` +5. `/opt/xilinx/vaip_config.json` + +## Runtime paths + +| Purpose | Path | +|---------|------| +| Config | `~/.config/breadsearch/` | +| Index (SQLite + HNSW) | `~/.local/state/breadsearch/` | +| Model cache | `~/.cache/breadsearch/models/` | +| Unix socket | `$XDG_RUNTIME_DIR/breadmill.sock` | diff --git a/bakery.toml b/bakery.toml new file mode 100644 index 0000000..8b6e1ed --- /dev/null +++ b/bakery.toml @@ -0,0 +1,18 @@ +name = "breadsearch" +description = "Semantic system-wide search for BOS" +binaries = ["breadsearch", "breadmill"] +system_deps = ["gtk4", "gtk4-layer-shell", "librsvg"] +bread_deps = [] + +[[service]] +unit = "breadmill.service" +enable = true + +[config] +dir = "~/.config/breadsearch" +example = "config.example.toml" + +[install] +post_install = [ + "systemctl --user start breadmill.service 2>/dev/null || breadmill &", +] diff --git a/benchmark_embed.py b/benchmark_embed.py new file mode 100644 index 0000000..dbd6f34 --- /dev/null +++ b/benchmark_embed.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Benchmark CPU (float32) vs CPU (int8 quantized) vs VitisAI (int8 quantized static). +""" + +import os +import time +import numpy as np +from pathlib import Path + +FLOAT32_MODEL = Path.home() / ".cache/breadsearch/models/model.onnx" +QUANT_MODEL = Path.home() / ".cache/breadsearch/models/model_quantized.onnx" +STATIC_MODEL = Path.home() / ".cache/breadsearch/models/model_quantized_static.onnx" +CACHE_DIR = Path.home() / ".cache/breadsearch/npu/nomic-quantized-static" +VAIP_CONFIG = Path.home() / ".config/breadsearch/vaip_config.json" +RYZEN_AI_LIB = Path.home() / ".local/share/ryzen-ai-1.7.1/lib" + +os.environ["RYZEN_AI_INSTALLATION_PATH"] = str(RYZEN_AI_LIB) +os.environ["LD_LIBRARY_PATH"] = str(RYZEN_AI_LIB) + ":" + os.environ.get("LD_LIBRARY_PATH", "") + +import onnxruntime as ort + +WARMUP = 2 +RUNS = 10 +SEQ_FLOAT = 512 # dynamic model accepts any seq len +SEQ_STATIC = 512 # static model locked to this + +def make_input(seq_len: int): + ids = np.ones((1, seq_len), dtype=np.int64) + mask = np.ones((1, seq_len), dtype=np.int64) + types = np.zeros((1, seq_len), dtype=np.int64) + return {"input_ids": ids, "token_type_ids": types, "attention_mask": mask} + +def time_session(sess: ort.InferenceSession, feed: dict, n: int) -> list[float]: + times = [] + for _ in range(n): + t0 = time.perf_counter() + sess.run(None, feed) + times.append(time.perf_counter() - t0) + return times + +def stats(times): + arr = np.array(times) + return arr.mean(), arr.min(), arr.max() + +print("=" * 60) +print("BENCHMARK: nomic-embed-text-v1.5 embedding speed") +print("=" * 60) + +# ── 1. float32 CPU ──────────────────────────────────────────── +print("\n[1] float32 model — CPU EP") +sess = ort.InferenceSession(str(FLOAT32_MODEL), providers=["CPUExecutionProvider"]) +feed = make_input(SEQ_FLOAT) +for _ in range(WARMUP): sess.run(None, feed) +times = time_session(sess, feed, RUNS) +mean, lo, hi = stats(times) +print(f" seq={SEQ_FLOAT} mean={mean*1000:.0f}ms min={lo*1000:.0f}ms max={hi*1000:.0f}ms ({RUNS} runs)") + +# ── 2. int8 quantized CPU (dynamic) ─────────────────────────── +print("\n[2] int8 quantized model (dynamic shapes) — CPU EP") +sess = ort.InferenceSession(str(QUANT_MODEL), providers=["CPUExecutionProvider"]) +feed = make_input(SEQ_FLOAT) +for _ in range(WARMUP): sess.run(None, feed) +times = time_session(sess, feed, RUNS) +mean2, lo2, hi2 = stats(times) +print(f" seq={SEQ_FLOAT} mean={mean2*1000:.0f}ms min={lo2*1000:.0f}ms max={hi2*1000:.0f}ms ({RUNS} runs)") +print(f" Speedup vs float32: {mean/mean2:.2f}x") + +# ── 3. int8 quantized static — VitisAI EP ───────────────────── +print("\n[3] int8 quantized model (static shapes) — VitisAI EP (NPU+CPU)") +providers = [ + ("VitisAIExecutionProvider", { + "config_file": str(VAIP_CONFIG), + "cacheDir": str(CACHE_DIR), + "cacheKey": "nomic-quantized-static", + }), + "CPUExecutionProvider", +] +print(" Loading session (should be fast — already compiled)...") +t_load = time.perf_counter() +sess_npu = ort.InferenceSession(str(STATIC_MODEL), providers=providers) +print(f" Load time: {time.perf_counter()-t_load:.1f}s") +feed_static = make_input(SEQ_STATIC) +for _ in range(WARMUP): sess_npu.run(None, feed_static) +times_npu = time_session(sess_npu, feed_static, RUNS) +mean3, lo3, hi3 = stats(times_npu) +print(f" seq={SEQ_STATIC} mean={mean3*1000:.0f}ms min={lo3*1000:.0f}ms max={hi3*1000:.0f}ms ({RUNS} runs)") +print(f" Speedup vs float32: {mean/mean3:.2f}x") +print(f" Speedup vs int8 CPU: {mean2/mean3:.2f}x") + +print("\n" + "=" * 60) +print("SUMMARY") +print(f" float32 CPU : {mean*1000:.0f}ms/inference") +print(f" int8 CPU : {mean2*1000:.0f}ms/inference ({mean/mean2:.2f}x)") +print(f" int8 VitisAI : {mean3*1000:.0f}ms/inference ({mean/mean3:.2f}x)") +print("=" * 60) diff --git a/breadmill/Cargo.toml b/breadmill/Cargo.toml new file mode 100644 index 0000000..91b4096 --- /dev/null +++ b/breadmill/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "breadmill" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[features] +default = [] +npu = ["ort/vitis", "ort/load-dynamic"] +rocm = ["ort/rocm", "ort/load-dynamic"] + +[[bin]] +name = "breadmill" +path = "src/main.rs" + +[dependencies] +breadsearch-shared = { path = "../breadsearch-shared" } + +# Embedding: ONNX Runtime + HF tokenizers +# download-binaries: fetches the MLAS-optimized ORT 1.24.x at build time (CPU default). +# api-23: compatible with both the downloaded ORT 1.24.x and the Ryzen AI SDK ORT 1.23.3; +# ORT 1.24 is backwards-compatible and honours GetApi(23) requests. +# npu feature adds load-dynamic + vitis: dlopen at runtime lets ORT_DYLIB_PATH redirect +# to the Ryzen AI SDK ORT; rpath from download-binaries means no ORT_DYLIB_PATH +# needed for the plain CPU path even in the npu build. +ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "tracing", "download-binaries", "tls-native", "copy-dylibs", "api-23"] } +tokenizers = "0" + +# Vector index +usearch = "2" + +# Metadata store +rusqlite = { version = "0", features = ["bundled"] } + +# File walking (respects .gitignore) +ignore = "0" + +# Live filesystem watching +notify = "6" + +# Text extraction +pdf-extract = "0" +zip = "2" +quick-xml = { version = "0", features = ["serialize"] } + +# Hashing +sha2 = "0" +hex = "0" + +# HTTP for model download +ureq = "2" + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/breadmill/src/chunk.rs b/breadmill/src/chunk.rs new file mode 100644 index 0000000..dcddca8 --- /dev/null +++ b/breadmill/src/chunk.rs @@ -0,0 +1,142 @@ +pub struct Chunk { + pub text: String, + pub start: usize, + pub end: usize, +} + +/// Split `text` into overlapping word-based windows, then enforce `max_chunk_chars`. +/// +/// Any word-window that exceeds `max_chunk_chars` characters is split further at +/// character boundaries so that no chunk passed to the embedder is pathologically +/// large (e.g. minified JSON where a single "word" is hundreds of KB). +/// +/// Set `max_chunk_chars = 0` to skip the character cap. +pub fn chunk_text(text: &str, words_per_chunk: usize, overlap_words: usize, max_chunk_chars: usize) -> Vec { + let word_chunks = chunk_by_words(text, words_per_chunk, overlap_words); + + if max_chunk_chars == 0 { + return word_chunks; + } + + let mut result = Vec::new(); + for chunk in word_chunks { + if chunk.text.len() <= max_chunk_chars { + result.push(chunk); + } else { + result.extend(split_by_chars(chunk, max_chunk_chars)); + } + } + result +} + +fn chunk_by_words(text: &str, words_per_chunk: usize, overlap_words: usize) -> Vec { + let mut positions: Vec<(usize, usize)> = Vec::new(); + let mut in_word = false; + let mut word_start = 0; + + for (i, c) in text.char_indices() { + if c.is_whitespace() { + if in_word { + positions.push((word_start, i)); + in_word = false; + } + } else if !in_word { + word_start = i; + in_word = true; + } + } + if in_word { + positions.push((word_start, text.len())); + } + + if positions.is_empty() { + return Vec::new(); + } + + let step = words_per_chunk.saturating_sub(overlap_words).max(1); + let mut chunks = Vec::new(); + let mut i = 0; + + while i < positions.len() { + let last = (i + words_per_chunk - 1).min(positions.len() - 1); + let start = positions[i].0; + let end = positions[last].1; + + chunks.push(Chunk { + text: text[start..end].to_string(), + start, + end, + }); + + if last == positions.len() - 1 { + break; + } + i += step; + } + + chunks +} + +fn split_by_chars(chunk: Chunk, max_chars: usize) -> Vec { + let text = &chunk.text; + let mut result = Vec::new(); + let mut seg_start = 0usize; + let mut count = 0usize; + + for (byte_idx, _) in text.char_indices() { + if count > 0 && count % max_chars == 0 { + result.push(Chunk { + text: text[seg_start..byte_idx].to_string(), + start: chunk.start + seg_start, + end: chunk.start + byte_idx, + }); + seg_start = byte_idx; + } + count += 1; + } + if seg_start < text.len() { + result.push(Chunk { + text: text[seg_start..].to_string(), + start: chunk.start + seg_start, + end: chunk.end, + }); + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic_chunk() { + let text = "one two three four five six seven eight nine ten"; + let chunks = chunk_text(text, 4, 1, 0); + assert!(!chunks.is_empty()); + for c in &chunks { + assert!(!c.text.is_empty()); + assert!(c.start <= c.end); + assert_eq!(&text[c.start..c.end], c.text); + } + } + + #[test] + fn char_cap_splits_large_chunks() { + // Simulate a "word" that is 200 chars long — exceeds cap of 50. + let text = "a".repeat(200); + let chunks = chunk_text(&text, 1, 0, 50); + assert_eq!(chunks.len(), 4); + for c in &chunks { + assert!(c.text.len() <= 50); + assert_eq!(&text[c.start..c.end], c.text); + } + } + + #[test] + fn char_cap_disabled() { + let text = "a".repeat(200); + let chunks = chunk_text(&text, 1, 0, 0); + assert_eq!(chunks.len(), 1); + assert_eq!(chunks[0].text.len(), 200); + } +} diff --git a/breadmill/src/embed.rs b/breadmill/src/embed.rs new file mode 100644 index 0000000..9287518 --- /dev/null +++ b/breadmill/src/embed.rs @@ -0,0 +1,258 @@ +use std::path::{Path, PathBuf}; + +use ort::{ + session::{Session, builder::{GraphOptimizationLevel, SessionBuilder}}, + value::Tensor, +}; +use tokenizers::Tokenizer; + +const DOCUMENT_PREFIX: &str = "search_document: "; +const QUERY_PREFIX: &str = "search_query: "; + +/// Hard token cap for nomic-embed-text-v1.5 (8192-token context window). +/// Sequences longer than this are truncated before ONNX inference to prevent +/// quadratic attention memory blowup. +pub const MAX_SEQ_LEN: usize = 8192; + +pub enum Backend { + Cpu, + /// AMD XDNA NPU via the VitisAI ONNX Runtime execution provider. + /// `cache_dir` is used to store the compiled NPU model between runs. + Npu { cache_dir: PathBuf }, + /// AMD iGPU via the ROCm ONNX Runtime execution provider. + Rocm, +} + +pub struct OrtEmbedder { + session: Session, + tokenizer: Tokenizer, + dim: usize, +} + +impl OrtEmbedder { + pub fn load(model_path: &Path, tokenizer_path: &Path, dim: usize, backend: Backend) -> Result { + let builder = Session::builder() + .map_err(|e| e.to_string())? + .with_optimization_level(GraphOptimizationLevel::All) + .map_err(|e| e.to_string())?; + + let mut builder = configure_eps(builder, &backend)?; + let session = builder.commit_from_file(model_path).map_err(|e| e.to_string())?; + let tokenizer = Tokenizer::from_file(tokenizer_path).map_err(|e| e.to_string())?; + + Ok(Self { session, tokenizer, dim }) + } + + pub fn embed_document(&mut self, text: &str) -> Result, String> { + self.embed_with_prefix(text, DOCUMENT_PREFIX) + } + + pub fn embed_query(&mut self, text: &str) -> Result, String> { + self.embed_with_prefix(text, QUERY_PREFIX) + } + + fn embed_with_prefix(&mut self, text: &str, prefix: &str) -> Result, String> { + let input = format!("{}{}", prefix, text); + + let encoding = self + .tokenizer + .encode(input, true) + .map_err(|e| e.to_string())?; + + let mut ids: Vec = encoding.get_ids().iter().map(|&x| x as i64).collect(); + let mut mask: Vec = encoding + .get_attention_mask() + .iter() + .map(|&x| x as i64) + .collect(); + let mut type_ids: Vec = encoding + .get_type_ids() + .iter() + .map(|&x| x as i64) + .collect(); + + if ids.len() > MAX_SEQ_LEN { + eprintln!( + "breadmill: truncating {} tokens to {} (chunk too large)", + ids.len(), + MAX_SEQ_LEN + ); + ids.truncate(MAX_SEQ_LEN); + mask.truncate(MAX_SEQ_LEN); + type_ids.truncate(MAX_SEQ_LEN); + } + + let seq_len = ids.len() as i64; + + let id_tensor = + Tensor::::from_array((vec![1i64, seq_len], ids.clone())).map_err(|e| e.to_string())?; + let mask_tensor = + Tensor::::from_array((vec![1i64, seq_len], mask.clone())).map_err(|e| e.to_string())?; + let type_tensor = + Tensor::::from_array((vec![1i64, seq_len], type_ids)).map_err(|e| e.to_string())?; + + let outputs = self + .session + .run(ort::inputs! { + "input_ids" => id_tensor, + "attention_mask" => mask_tensor, + "token_type_ids" => type_tensor, + }) + .map_err(|e| e.to_string())?; + + // last_hidden_state: shape [1, seq_len, dim] + let (shape, data) = outputs["last_hidden_state"] + .try_extract_tensor::() + .map_err(|e| e.to_string())?; + + let actual_seq = shape[1] as usize; + let actual_dim = shape[2] as usize; + + // Mean-pool over non-padding positions + let mut result = vec![0.0f32; actual_dim]; + let mut count = 0usize; + + for t in 0..actual_seq { + if mask[t] > 0 { + for d in 0..actual_dim { + result[d] += data[t * actual_dim + d]; + } + count += 1; + } + } + + if count > 0 { + for x in &mut result { + *x /= count as f32; + } + } + + l2_normalize(&mut result); + + // Clamp/pad to configured dim + result.truncate(self.dim); + while result.len() < self.dim { + result.push(0.0); + } + + Ok(result) + } +} + +fn l2_normalize(v: &mut Vec) { + let norm: f32 = v.iter().map(|x| x * x).sum::().sqrt(); + if norm > 1e-10 { + for x in v.iter_mut() { + *x /= norm; + } + } +} + +// ---- Execution provider selection ------------------------------------------- + +fn configure_eps(builder: SessionBuilder, backend: &Backend) -> Result { + match backend { + Backend::Cpu => Ok(builder), + Backend::Npu { cache_dir } => npu_session(builder, cache_dir), + Backend::Rocm => rocm_session(builder), + } +} + +#[cfg(feature = "npu")] +fn npu_session(builder: SessionBuilder, cache_dir: &Path) -> Result { + let vitis_ep = build_vitis_ep(cache_dir)?; + eprintln!("breadmill: using NPU (VitisAI) execution provider"); + if std::env::var("ORT_DYLIB_PATH").is_err() { + eprintln!( + "breadmill: hint — set ORT_DYLIB_PATH to the Ryzen AI SDK ORT, e.g.:\n \ + ORT_DYLIB_PATH=~/.local/share/ryzen-ai-1.7.1/lib/libonnxruntime.so" + ); + } + builder + .with_execution_providers([vitis_ep, ort::ep::CPU::default().build()]) + .map_err(|e| e.to_string()) +} + +#[cfg(not(feature = "npu"))] +fn npu_session(builder: SessionBuilder, _cache_dir: &Path) -> Result { + eprintln!("breadmill: NPU backend requested but not compiled in (rebuild with --features npu); using CPU"); + Ok(builder) +} + +// ---- VitisAI EP (NPU) ------------------------------------------------------- + +#[cfg(feature = "npu")] +fn build_vitis_ep(cache_dir: &Path) -> Result { + let vaip_config = find_vaip_config()?; + let npu_cache = cache_dir.join("npu"); + std::fs::create_dir_all(&npu_cache).map_err(|e| e.to_string())?; + eprintln!("breadmill: vaip_config: {}", vaip_config.display()); + eprintln!("breadmill: NPU model cache: {}", npu_cache.display()); + Ok(ort::ep::Vitis::default() + .with_config_file(vaip_config.to_string_lossy()) + .with_cache_dir(npu_cache.to_string_lossy()) + .with_cache_key("nomic-embed-text-v1.5") + .build()) +} + +// ---- ROCm EP (AMD iGPU) ----------------------------------------------------- + +#[cfg(feature = "rocm")] +fn rocm_session(builder: SessionBuilder) -> Result { + eprintln!("breadmill: using ROCm execution provider (device 0)"); + builder + .with_execution_providers([ + ort::execution_providers::ROCmExecutionProvider::default().build(), + ort::ep::CPU::default().build(), + ]) + .map_err(|e| e.to_string()) +} + +#[cfg(not(feature = "rocm"))] +fn rocm_session(builder: SessionBuilder) -> Result { + eprintln!("breadmill: ROCm backend requested but not compiled in (rebuild with --features rocm); using CPU"); + Ok(builder) +} + +/// Locate the VitisAI EP config file required by the AMD Ryzen AI SDK. +/// +/// Search order: +/// 1. `VAIP_CONFIG` environment variable +/// 2. `~/.config/breadsearch/vaip_config.json` +/// 3. `/etc/vaip_config.json` +/// 4. `/opt/xilinx/vaip_config.json` +#[cfg(feature = "npu")] +fn find_vaip_config() -> Result { + if let Ok(p) = std::env::var("VAIP_CONFIG") { + let path = PathBuf::from(&p); + if path.exists() { + return Ok(path); + } + return Err(format!("VAIP_CONFIG={p} does not exist")); + } + + let user_path = breadsearch_shared::config_dir().join("vaip_config.json"); + if user_path.exists() { + return Ok(user_path); + } + + // Standard system / SDK paths (checked in priority order) + let home = std::env::var("HOME").unwrap_or_default(); + let sdk_paths = [ + format!("{home}/.local/share/ryzen-ai-1.7.1/voe-4.0-linux_x86_64/vaip_config.json"), + "/etc/vaip_config.json".into(), + "/opt/xilinx/vaip_config.json".into(), + ]; + for p in &sdk_paths { + let path = Path::new(p.as_str()); + if path.exists() { + return Ok(path.to_path_buf()); + } + } + + Err( + "vaip_config.json not found; set VAIP_CONFIG=/path/to/vaip_config.json, \ + copy to ~/.config/breadsearch/vaip_config.json, or install the AMD Ryzen AI SDK" + .into(), + ) +} diff --git a/breadmill/src/extract.rs b/breadmill/src/extract.rs new file mode 100644 index 0000000..c0d7811 --- /dev/null +++ b/breadmill/src/extract.rs @@ -0,0 +1,84 @@ +use std::{ + fs, io::Read, path::Path, +}; + +pub type ExtractResult = Result; + +/// Extract plain text from a file based on its extension. +/// Returns Err on hard failures; Err with message if format unsupported. +pub fn extract(path: &Path) -> ExtractResult { + let ext = path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_lowercase(); + + match ext.as_str() { + "md" | "txt" | "org" => read_text(path), + "pdf" => extract_pdf(path), + "docx" => extract_docx(path), + "odt" => extract_odt(path), + other => Err(format!("unsupported extension: {}", other)), + } +} + +fn read_text(path: &Path) -> ExtractResult { + fs::read_to_string(path).map_err(|e| e.to_string()) +} + +fn extract_pdf(path: &Path) -> ExtractResult { + // pdf-extract panics on some malformed PDFs; catch_unwind prevents indexer thread death. + let path = path.to_path_buf(); + match std::panic::catch_unwind(|| pdf_extract::extract_text(&path)) { + Ok(result) => result.map_err(|e| e.to_string()), + Err(_) => Err("pdf-extract panicked on malformed content stream".into()), + } +} + +fn extract_docx(path: &Path) -> ExtractResult { + extract_office_xml(path, "word/document.xml", "w:t") +} + +fn extract_odt(path: &Path) -> ExtractResult { + extract_office_xml(path, "content.xml", "text:p") +} + +/// Open a zip-based office format and concatenate text from the named XML entry. +/// We grab all Text events as a best-effort extraction. +fn extract_office_xml(path: &Path, xml_entry: &str, _tag_hint: &str) -> ExtractResult { + let file = fs::File::open(path).map_err(|e| e.to_string())?; + let mut archive = zip::ZipArchive::new(file).map_err(|e| e.to_string())?; + + let mut xml_bytes = Vec::new(); + archive + .by_name(xml_entry) + .map_err(|e| format!("entry '{}' not found: {}", xml_entry, e))? + .read_to_end(&mut xml_bytes) + .map_err(|e| e.to_string())?; + + let xml_str = String::from_utf8_lossy(&xml_bytes); + let mut reader = quick_xml::Reader::from_str(&xml_str); + reader.config_mut().trim_text(true); + + let mut text = String::new(); + let mut buf = Vec::new(); + + loop { + match reader.read_event_into(&mut buf) { + Ok(quick_xml::events::Event::Text(e)) => { + if let Ok(s) = e.decode() { + if !text.is_empty() { + text.push(' '); + } + text.push_str(&s); + } + } + Ok(quick_xml::events::Event::Eof) => break, + Err(e) => return Err(e.to_string()), + _ => {} + } + buf.clear(); + } + + Ok(text) +} diff --git a/breadmill/src/indexer.rs b/breadmill/src/indexer.rs new file mode 100644 index 0000000..3ad20b5 --- /dev/null +++ b/breadmill/src/indexer.rs @@ -0,0 +1,414 @@ +use std::{ + collections::{HashMap, HashSet}, + fs, + path::{Path, PathBuf}, + sync::{Arc, Mutex, atomic::{AtomicBool, AtomicUsize, Ordering}}, + time::{Duration, Instant, UNIX_EPOCH}, +}; + +use ignore::WalkBuilder; +use notify::{RecommendedWatcher, RecursiveMode, Watcher, EventKind}; +use sha2::{Digest, Sha256}; + +use crate::{embed::OrtEmbedder, extract, chunk, power, store::Store}; + +pub struct SharedState { + pub store: Mutex, + pub embedder: Mutex>, + pub model_ready: AtomicBool, + pub indexed: AtomicUsize, + pub pending: AtomicUsize, + pub reindex_signal: AtomicBool, +} + +impl SharedState { + pub fn new(store: Store) -> Self { + SharedState { + store: Mutex::new(store), + embedder: Mutex::new(None), + model_ready: AtomicBool::new(false), + indexed: AtomicUsize::new(0), + pending: AtomicUsize::new(0), + reindex_signal: AtomicBool::new(false), + } + } +} + +pub struct Indexer { + state: Arc, + config: breadsearch_shared::Config, + state_dir: PathBuf, +} + +impl Indexer { + pub fn new(state: Arc, config: breadsearch_shared::Config, state_dir: PathBuf) -> Self { + Indexer { state, config, state_dir } + } + + pub fn run(self) { + self.initial_scan(); + self.watch_loop(); + } + + /// True when embedding should be skipped right now: the user turned + /// indexing off entirely, or the machine is on battery and + /// `power.run_on_battery` is not set. Cheap sysfs reads — safe to call + /// per-file and on every watch_loop tick. + fn indexing_paused(&self) -> bool { + if !self.config.power.enabled { + return true; + } + !self.config.power.run_on_battery && !power::on_ac_power() + } + + pub fn full_reindex(&self) { + eprintln!("breadmill: full reindex triggered"); + { + let mut store = self.state.store.lock().unwrap(); + // Clear all state + let _ = store.conn.execute_batch("DELETE FROM chunks; DELETE FROM files;"); + let _ = store.index.reserve(4096); + } + self.initial_scan(); + } + + fn initial_scan(&self) { + eprintln!("breadmill: scanning roots..."); + + let roots: Vec = self.config.index.roots + .iter() + .map(|r| expand_home(r)) + .collect(); + + let excludes: Vec = self.config.index.excludes + .iter() + .map(|r| expand_home(r)) + .collect(); + + // Snapshot existing indexed files + let known: HashMap = { + let store = self.state.store.lock().unwrap(); + store.all_files() + .unwrap_or_default() + .into_iter() + .map(|f| (f.path, (f.mtime, f.hash))) + .collect() + }; + + let mut seen: HashSet = HashSet::new(); + let max_bytes = (self.config.index.max_file_mb * 1024.0 * 1024.0) as u64; + + for root in &roots { + if !root.exists() { + continue; + } + + for entry in WalkBuilder::new(root) + .hidden(false) + .ignore(true) + .git_ignore(true) + .build() + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_file()) + { + let path = entry.path(); + + if excludes.iter().any(|excl| path.starts_with(excl)) { + continue; + } + + if !self.is_indexed_extension(path) { + continue; + } + + let meta = match fs::metadata(path) { + Ok(m) => m, + Err(_) => continue, + }; + + if meta.len() > max_bytes { + continue; + } + + let path_str = path.to_string_lossy().into_owned(); + seen.insert(path_str.clone()); + + let mtime = mtime_secs(&meta); + + if let Some((known_mtime, _)) = known.get(&path_str) { + if *known_mtime == mtime { + continue; // unchanged + } + } + + self.state.pending.fetch_add(1, Ordering::Relaxed); + self.index_file(path, &path_str, mtime); + self.state.pending.fetch_sub(1, Ordering::Relaxed); + } + } + + // Drop files that were deleted + let to_delete: Vec = known + .keys() + .filter(|p| !seen.contains(*p)) + .cloned() + .collect(); + + if !to_delete.is_empty() { + let mut store = self.state.store.lock().unwrap(); + for path in to_delete { + eprintln!("breadmill: removing deleted file: {}", path); + let _ = store.delete_file(&path); + } + } + + let count = { + let store = self.state.store.lock().unwrap(); + let n = store.chunk_count(); + let _ = store.save_index(&self.state_dir); + n + }; + self.state.indexed.store(count, Ordering::Relaxed); + eprintln!("breadmill: initial scan done — {} chunks indexed", count); + } + + fn watch_loop(self) { + let (tx, rx) = std::sync::mpsc::channel(); + + let mut watcher: RecommendedWatcher = match notify::recommended_watcher(tx) { + Ok(w) => w, + Err(e) => { + eprintln!("breadmill: watcher init failed: {}", e); + return; + } + }; + + for root in self.config.index.roots.iter().map(|r| expand_home(r)) { + if root.exists() { + let _ = watcher.watch(&root, RecursiveMode::Recursive); + } + } + + let mut pending_paths: HashSet = HashSet::new(); + let mut last_event = Instant::now(); + let quiet = Duration::from_secs(2); + let mut was_paused = self.indexing_paused(); + let mut last_power_check = Instant::now(); + + eprintln!("breadmill: watching for changes"); + + loop { + // Drain the reindex signal + if self.state.reindex_signal.swap(false, Ordering::Relaxed) { + self.full_reindex(); + } + + // Re-check the power gate periodically (sysfs reads are cheap but + // no need to do it every 500ms tick). On a paused->active + // transition, re-run the incremental scan to catch up anything + // skipped while gated. + if last_power_check.elapsed() >= Duration::from_secs(30) { + last_power_check = Instant::now(); + let now_paused = self.indexing_paused(); + if was_paused && !now_paused { + eprintln!("breadmill: power gate opened — resuming indexing"); + self.initial_scan(); + } + was_paused = now_paused; + } + + match rx.recv_timeout(Duration::from_millis(500)) { + Ok(Ok(event)) => { + match event.kind { + EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => { + for p in event.paths { + pending_paths.insert(p); + } + last_event = Instant::now(); + } + _ => {} + } + } + Ok(Err(e)) => eprintln!("breadmill: watch error: {}", e), + Err(_) => {} // timeout — check quiet period + } + + if !pending_paths.is_empty() && last_event.elapsed() >= quiet { + for path in pending_paths.drain() { + self.handle_fs_event(&path); + } + let count = { + let store = self.state.store.lock().unwrap(); + let n = store.chunk_count(); + let _ = store.save_index(&self.state_dir); + n + }; + self.state.indexed.store(count, Ordering::Relaxed); + } + } + } + + fn handle_fs_event(&self, path: &Path) { + let excludes: Vec = self.config.index.excludes + .iter() + .map(|r| expand_home(r)) + .collect(); + + if excludes.iter().any(|excl| path.starts_with(excl)) { + return; + } + + if !path.is_file() { + let path_str = path.to_string_lossy().into_owned(); + // File deleted — remove from index + let mut store = self.state.store.lock().unwrap(); + let _ = store.delete_file(&path_str); + return; + } + + if !self.is_indexed_extension(path) { + return; + } + + let meta = match fs::metadata(path) { + Ok(m) => m, + Err(_) => return, + }; + + let max_bytes = (self.config.index.max_file_mb * 1024.0 * 1024.0) as u64; + if meta.len() > max_bytes { + return; + } + + let path_str = path.to_string_lossy().into_owned(); + let mtime = mtime_secs(&meta); + + self.state.pending.fetch_add(1, Ordering::Relaxed); + self.index_file(path, &path_str, mtime); + self.state.pending.fetch_sub(1, Ordering::Relaxed); + } + + fn index_file(&self, path: &Path, path_str: &str, mtime: i64) { + if self.indexing_paused() { + // Leave the file unrecorded so it's picked up again once indexing + // resumes (initial_scan/watch_loop treat it as not-yet-indexed). + return; + } + + eprintln!("breadmill: extracting {}", path_str); + let text = match extract::extract(path) { + Ok(t) if !t.trim().is_empty() => t, + Ok(_) => return, + Err(e) => { + eprintln!("breadmill: extract {}: {}", path_str, e); + return; + } + }; + + let hash = sha256_str(text.as_bytes()); + + // Check if hash changed (catches content changes without mtime change) + { + let store = self.state.store.lock().unwrap(); + if let Ok(files) = store.all_files() { + if files.iter().any(|f| f.path == path_str && f.hash == hash) { + return; + } + } + } + + // 2000 char cap keeps even minified single-line files to ~500–2000 tokens, + // avoiding quadratic attention blowup while still splitting at word boundaries + // for natural-language files. + let chunks = chunk::chunk_text(&text, 400, 80, 2_000); + eprintln!("breadmill: embedding {} ({} chars, {} chunks)", path_str, text.len(), chunks.len()); + let mut embedder_guard = self.state.embedder.lock().unwrap(); + + if !self.state.model_ready.load(Ordering::Relaxed) { + eprintln!("breadmill: model not ready, skipping embed for {}", path_str); + return; + } + + let embedder = match embedder_guard.as_mut() { + Some(e) => e, + None => return, + }; + + { + let mut store = self.state.store.lock().unwrap(); + let _ = store.delete_file(path_str); // remove old chunks/vectors first + } + + let mut any_ok = false; + let mut chunks_added = 0usize; + + for (i, chunk) in chunks.iter().enumerate() { + eprintln!("breadmill: embed chunk {}/{} ({} chars) for {}", i + 1, chunks.len(), chunk.text.len(), path_str); + match embedder.embed_document(&chunk.text) { + Ok(embedding) => { + let mut store = self.state.store.lock().unwrap(); + // Ensure file row exists before inserting chunks (FK constraint) + let _ = store.upsert_file(path_str, mtime, &hash); + let _ = store.insert_chunk( + path_str, + &chunk.text, + chunk.start, + chunk.end, + &embedding, + ); + any_ok = true; + chunks_added += 1; + } + Err(e) => eprintln!("breadmill: embed error for {}: {}", path_str, e), + } + } + + if !any_ok { + eprintln!("breadmill: no chunks embedded for {}", path_str); + // Record the file so the mtime+hash check skips it on the next startup + // rather than re-entering the same embed-fail loop. + let store = self.state.store.lock().unwrap(); + let _ = store.upsert_file(path_str, mtime, &hash); + } else { + // Increment live so `status` reflects progress before the full scan ends. + self.state.indexed.fetch_add(chunks_added, Ordering::Relaxed); + eprintln!("breadmill: done {} ({} chunks indexed)", path_str, chunks_added); + } + } + + fn is_indexed_extension(&self, path: &Path) -> bool { + path.extension() + .and_then(|e| e.to_str()) + .map(|ext| { + self.config + .index + .extensions + .iter() + .any(|e| e.eq_ignore_ascii_case(ext)) + }) + .unwrap_or(false) + } +} + +fn mtime_secs(meta: &fs::Metadata) -> i64 { + meta.modified() + .ok() + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as i64) + .unwrap_or(0) +} + +fn sha256_str(bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(bytes); + hex::encode(hasher.finalize()) +} + +pub fn expand_home(path: &str) -> PathBuf { + if path.starts_with("~/") { + let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into()); + PathBuf::from(home).join(&path[2..]) + } else { + PathBuf::from(path) + } +} diff --git a/breadmill/src/main.rs b/breadmill/src/main.rs new file mode 100644 index 0000000..eb6c9b4 --- /dev/null +++ b/breadmill/src/main.rs @@ -0,0 +1,242 @@ +use std::{ + io::Read, + path::{Path, PathBuf}, + sync::{Arc, atomic::Ordering}, +}; + +use breadsearch_shared::{Request, Response}; + +mod chunk; +mod embed; +mod extract; +mod indexer; +mod power; +mod serve; +mod store; + +use embed::{Backend, OrtEmbedder}; +use indexer::{Indexer, SharedState}; +use store::Store; + +const MODEL_URL: &str = + "https://huggingface.co/nomic-ai/nomic-embed-text-v1.5/resolve/main/onnx/model.onnx"; +const TOKENIZER_URL: &str = + "https://huggingface.co/nomic-ai/nomic-embed-text-v1.5/resolve/main/tokenizer.json"; + +fn main() { + let raw_args: Vec = std::env::args().collect(); + + // Extract global flags before command dispatch. + let use_npu = raw_args.iter().any(|a| a == "--npu"); + let use_rocm = raw_args.iter().any(|a| a == "--rocm"); + + // Build a view of argv without backend flags for command matching. + let args: Vec<&str> = raw_args + .iter() + .skip(1) + .filter(|a| a.as_str() != "--npu" && a.as_str() != "--rocm") + .map(|s| s.as_str()) + .collect(); + + match args.first().copied() { + Some("--version") | Some("-V") => { + println!("breadmill {}", env!("CARGO_PKG_VERSION")); + } + Some("--fetch-model") | Some("fetch-model") => { + if let Err(e) = fetch_model() { + eprintln!("breadmill: {}", e); + std::process::exit(1); + } + } + Some("--reindex") | Some("reindex") => { + if let Err(e) = run_daemon(true, use_npu, use_rocm) { + eprintln!("breadmill: {}", e); + std::process::exit(1); + } + } + Some("query") => { + let q = args.get(1).copied().unwrap_or(""); + if q.is_empty() { + eprintln!("usage: breadmill query "); + std::process::exit(1); + } + cli_query(q); + } + Some("status") => { + cli_status(); + } + None | Some("serve") | Some("--serve") => { + if let Err(e) = run_daemon(false, use_npu, use_rocm) { + eprintln!("breadmill: {}", e); + std::process::exit(1); + } + } + Some(cmd) => { + eprintln!("breadmill: unknown command: {}", cmd); + eprintln!( + "usage: breadmill [serve|reindex|fetch-model|query |status] [--npu|--rocm] [--version]" + ); + std::process::exit(1); + } + } +} + +// ---- Daemon ----------------------------------------------------------------- + +fn run_daemon(force_reindex: bool, use_npu: bool, use_rocm: bool) -> Result<(), String> { + let config = breadsearch_shared::Config::load(); + let state_dir = breadsearch_shared::state_dir(); + let cache_dir = breadsearch_shared::cache_dir(); + let socket_path = breadsearch_shared::socket_path(); + let dim = config.model.dim; + let snippet_len = config.search.snippet_len; + let search_limit = config.search.limit; + + std::fs::create_dir_all(&state_dir).map_err(|e| e.to_string())?; + std::fs::create_dir_all(&cache_dir).map_err(|e| e.to_string())?; + + let backend = if use_npu || config.model.backend == "npu" { + eprintln!("breadmill: NPU backend selected"); + Backend::Npu { cache_dir: cache_dir.clone() } + } else if use_rocm || config.model.backend == "rocm" { + eprintln!("breadmill: ROCm backend selected"); + Backend::Rocm + } else { + Backend::Cpu + }; + + let store = Store::open(&state_dir, dim)?; + let state = Arc::new(SharedState::new(store)); + + // Load embedder if model files present + let model_dir = model_dir(&cache_dir); + let model_path = model_dir.join("model.onnx"); + let tokenizer_path = model_dir.join("tokenizer.json"); + + if model_path.exists() && tokenizer_path.exists() { + eprintln!("breadmill: loading model..."); + match OrtEmbedder::load(&model_path, &tokenizer_path, dim, backend) { + Ok(embedder) => { + *state.embedder.lock().unwrap() = Some(embedder); + state.model_ready.store(true, Ordering::Relaxed); + eprintln!("breadmill: model loaded"); + } + Err(e) => eprintln!("breadmill: model load failed: {} — run --fetch-model", e), + } + } else { + eprintln!( + "breadmill: model files not found in {} — run: breadmill --fetch-model", + model_dir.display() + ); + } + + // Indexer runs in a background thread + { + let state_clone = Arc::clone(&state); + let config_clone = config.clone(); + let state_dir_clone = state_dir.clone(); + + std::thread::spawn(move || { + let indexer = Indexer::new(state_clone, config_clone, state_dir_clone); + if force_reindex { + indexer.full_reindex(); + } + indexer.run(); + }); + } + + // Server runs on the main thread (blocking) + serve::run(&socket_path, Arc::clone(&state), snippet_len, search_limit); + + Ok(()) +} + +// ---- Model fetch ------------------------------------------------------------ + +fn fetch_model() -> Result<(), String> { + let cache_dir = breadsearch_shared::cache_dir(); + let model_dir = model_dir(&cache_dir); + std::fs::create_dir_all(&model_dir).map_err(|e| e.to_string())?; + + download_if_missing(MODEL_URL, &model_dir.join("model.onnx"))?; + download_if_missing(TOKENIZER_URL, &model_dir.join("tokenizer.json"))?; + + eprintln!("breadmill: model files ready in {}", model_dir.display()); + Ok(()) +} + +fn download_if_missing(url: &str, dest: &Path) -> Result<(), String> { + if dest.exists() { + eprintln!(" already present: {}", dest.display()); + return Ok(()); + } + + eprintln!(" downloading {} ...", url); + let agent = ureq::AgentBuilder::new() + .timeout(std::time::Duration::from_secs(300)) + .build(); + + let response = agent.get(url).call().map_err(|e| e.to_string())?; + let mut bytes = Vec::new(); + response + .into_reader() + .read_to_end(&mut bytes) + .map_err(|e| e.to_string())?; + + if bytes.is_empty() { + return Err(format!("empty download from {}", url)); + } + + // Write atomically via temp file + let tmp = dest.with_extension("tmp"); + std::fs::write(&tmp, &bytes).map_err(|e| e.to_string())?; + std::fs::rename(&tmp, dest).map_err(|e| e.to_string())?; + + eprintln!(" saved {} ({:.1} MB)", dest.display(), bytes.len() as f64 / 1_048_576.0); + Ok(()) +} + +fn model_dir(cache_dir: &Path) -> PathBuf { + cache_dir.join("models") +} + +// ---- CLI helpers ------------------------------------------------------------ + +fn cli_query(query: &str) { + let req = Request::Query { + query: query.to_string(), + limit: 10, + }; + match breadsearch_shared::send_request(&req) { + Ok(Response::Hits { hits }) => { + if hits.is_empty() { + println!("no results"); + } + for (i, h) in hits.iter().enumerate() { + println!( + "{:2}. {} ({:.3})\n {}\n {}\n", + i + 1, + h.title, + h.score, + h.path, + h.snippet.lines().next().unwrap_or(""), + ); + } + } + Ok(Response::Error { message }) => eprintln!("error: {}", message), + Ok(_) => eprintln!("unexpected response"), + Err(e) => eprintln!("could not reach breadmill: {}", e), + } +} + +fn cli_status() { + match breadsearch_shared::send_request(&Request::Status) { + Ok(Response::StatusInfo(s)) => { + println!("indexed: {}", s.indexed); + println!("pending: {}", s.pending); + println!("model ready: {}", s.model_ready); + } + Ok(_) => eprintln!("unexpected response"), + Err(e) => eprintln!("could not reach breadmill: {}", e), + } +} diff --git a/breadmill/src/power.rs b/breadmill/src/power.rs new file mode 100644 index 0000000..49ad02c --- /dev/null +++ b/breadmill/src/power.rs @@ -0,0 +1,31 @@ +use std::fs; +use std::path::Path; + +/// Best-effort check for whether the system is currently on AC/mains power. +/// Scans `/sys/class/power_supply` for a Mains or USB supply with `online=1`. +/// Systems with no such supply at all (desktops, no battery) are treated as +/// always on power, so this never blocks indexing on hardware without a +/// battery to protect. +pub fn on_ac_power() -> bool { + let dir = Path::new("/sys/class/power_supply"); + let Ok(entries) = fs::read_dir(dir) else { + return true; + }; + + let mut found_mains = false; + for entry in entries.flatten() { + let path = entry.path(); + let supply_type = fs::read_to_string(path.join("type")).unwrap_or_default(); + let supply_type = supply_type.trim(); + if supply_type != "Mains" && supply_type != "USB" { + continue; + } + found_mains = true; + let online = fs::read_to_string(path.join("online")).unwrap_or_default(); + if online.trim() == "1" { + return true; + } + } + + !found_mains +} diff --git a/breadmill/src/serve.rs b/breadmill/src/serve.rs new file mode 100644 index 0000000..cd67e67 --- /dev/null +++ b/breadmill/src/serve.rs @@ -0,0 +1,127 @@ +use std::{ + io::{BufRead, BufReader, Write}, + os::unix::net::{UnixListener, UnixStream}, + path::Path, + sync::Arc, +}; + +use breadsearch_shared::{Request, Response, StatusInfo}; + +use crate::indexer::SharedState; + +pub fn run(socket_path: &Path, state: Arc, snippet_len: usize, search_limit: usize) { + let _ = std::fs::remove_file(socket_path); + + let listener = match UnixListener::bind(socket_path) { + Ok(l) => l, + Err(e) => { + eprintln!("breadmill: bind {}: {}", socket_path.display(), e); + return; + } + }; + + eprintln!("breadmill: listening on {}", socket_path.display()); + + for stream in listener.incoming() { + match stream { + Ok(s) => { + let state = Arc::clone(&state); + std::thread::spawn(move || { + handle(s, state, snippet_len, search_limit); + }); + } + Err(e) => eprintln!("breadmill: accept error: {}", e), + } + } +} + +fn handle(stream: UnixStream, state: Arc, snippet_len: usize, search_limit: usize) { + let stream_write = match stream.try_clone() { + Ok(s) => s, + Err(_) => return, + }; + + let mut reader = BufReader::new(&stream); + let mut writer = std::io::BufWriter::new(stream_write); + + let mut line = String::new(); + if reader.read_line(&mut line).is_err() { + return; + } + + let response = match serde_json::from_str::(line.trim()) { + Ok(req) => { + let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + dispatch(req, &state, snippet_len, search_limit) + })); + match r { + Ok(resp) => resp, + Err(_) => Response::Error { message: "internal error".into() }, + } + } + Err(e) => Response::Error { message: e.to_string() }, + }; + + if let Ok(mut json) = serde_json::to_string(&response) { + json.push('\n'); + let _ = writer.write_all(json.as_bytes()); + let _ = writer.flush(); + } +} + +fn dispatch( + req: Request, + state: &SharedState, + snippet_len: usize, + search_limit: usize, +) -> Response { + use std::sync::atomic::Ordering; + + match req { + Request::Query { query, limit } => { + if !state.model_ready.load(Ordering::Relaxed) { + return Response::Error { + message: "model not ready — run breadmill --fetch-model".into(), + }; + } + + let embedding = { + let mut embedder = state.embedder.lock().unwrap(); + match embedder.as_mut() { + Some(e) => match e.embed_query(&query) { + Ok(v) => v, + Err(e) => return Response::Error { message: e }, + }, + None => { + return Response::Error { + message: "embedder unavailable".into(), + } + } + } + }; + + let limit = limit.min(search_limit).max(1); + let store = state.store.lock().unwrap(); + + match store.search(&embedding, limit, snippet_len) { + Ok(hits) => Response::Hits { hits }, + Err(e) => Response::Error { message: e }, + } + } + + Request::Status => { + use std::sync::atomic::Ordering; + Response::StatusInfo(StatusInfo { + indexed: state.indexed.load(Ordering::Relaxed), + pending: state.pending.load(Ordering::Relaxed), + model_ready: state.model_ready.load(Ordering::Relaxed), + }) + } + + Request::Reindex => { + use std::sync::atomic::Ordering; + state.reindex_signal.store(true, Ordering::Relaxed); + Response::Ok + } + } +} diff --git a/breadmill/src/store.rs b/breadmill/src/store.rs new file mode 100644 index 0000000..077736d --- /dev/null +++ b/breadmill/src/store.rs @@ -0,0 +1,235 @@ +use std::path::Path; + +use rusqlite::{Connection, params}; +use usearch::{Index, IndexOptions, MetricKind, ScalarKind, new_index}; + +pub struct Store { + pub conn: Connection, + pub index: Index, + pub dim: usize, +} + +// usearch::Index wraps a raw C++ pointer; access is serialized by the Mutex. +unsafe impl Send for Store {} + +#[derive(Debug)] +pub struct FileMeta { + pub path: String, + pub mtime: i64, + pub hash: String, +} + +impl Store { + pub fn open(state_dir: &Path, dim: usize) -> Result { + std::fs::create_dir_all(state_dir).map_err(|e| e.to_string())?; + + let db_path = state_dir.join("meta.db"); + let conn = Connection::open(&db_path).map_err(|e| e.to_string())?; + + conn.execute_batch( + "PRAGMA journal_mode=WAL; + PRAGMA foreign_keys=ON; + CREATE TABLE IF NOT EXISTS files ( + path TEXT PRIMARY KEY, + mtime INTEGER NOT NULL, + hash TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS chunks ( + id INTEGER PRIMARY KEY, + path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE, + chunk_text TEXT NOT NULL, + chunk_start INTEGER NOT NULL, + chunk_end INTEGER NOT NULL + );", + ) + .map_err(|e| e.to_string())?; + + let idx_path = state_dir.join("vectors.usearch"); + let options = IndexOptions { + dimensions: dim, + metric: MetricKind::Cos, + quantization: ScalarKind::F32, + connectivity: 16, + expansion_add: 128, + expansion_search: 64, + multi: false, + }; + let index = new_index(&options).map_err(|e| e.to_string())?; + + if idx_path.exists() { + index + .load(idx_path.to_str().unwrap()) + .map_err(|e| e.to_string())?; + } else { + index.reserve(4096).map_err(|e| e.to_string())?; + } + + Ok(Self { conn, index, dim }) + } + + // ---- file state --------------------------------------------------------- + + pub fn all_files(&self) -> Result, String> { + let mut stmt = self + .conn + .prepare("SELECT path, mtime, hash FROM files") + .map_err(|e| e.to_string())?; + let rows = stmt + .query_map([], |row| { + Ok(FileMeta { + path: row.get(0)?, + mtime: row.get(1)?, + hash: row.get(2)?, + }) + }) + .map_err(|e| e.to_string())? + .filter_map(|r| r.ok()) + .collect(); + Ok(rows) + } + + pub fn upsert_file(&self, path: &str, mtime: i64, hash: &str) -> Result<(), String> { + self.conn + .execute( + "INSERT OR REPLACE INTO files (path, mtime, hash) VALUES (?1, ?2, ?3)", + params![path, mtime, hash], + ) + .map_err(|e| e.to_string())?; + Ok(()) + } + + /// Delete a file and all its chunks from SQLite; also remove chunk vectors. + pub fn delete_file(&mut self, path: &str) -> Result<(), String> { + // Collect chunk IDs before deletion for usearch removal + let ids = self.chunk_ids_for(path)?; + + self.conn + .execute("DELETE FROM files WHERE path = ?1", params![path]) + .map_err(|e| e.to_string())?; + + for id in ids { + let _ = self.index.remove(id); // best-effort; stale entries are harmless + } + + Ok(()) + } + + fn chunk_ids_for(&self, path: &str) -> Result, String> { + let mut stmt = self + .conn + .prepare("SELECT id FROM chunks WHERE path = ?1") + .map_err(|e| e.to_string())?; + let ids = stmt + .query_map(params![path], |row| row.get::<_, i64>(0)) + .map_err(|e| e.to_string())? + .filter_map(|r| r.ok()) + .map(|id| id as u64) + .collect(); + Ok(ids) + } + + // ---- chunk operations --------------------------------------------------- + + pub fn insert_chunk( + &mut self, + path: &str, + text: &str, + start: usize, + end: usize, + embedding: &[f32], + ) -> Result { + self.conn + .execute( + "INSERT INTO chunks (path, chunk_text, chunk_start, chunk_end) + VALUES (?1, ?2, ?3, ?4)", + params![path, text, start as i64, end as i64], + ) + .map_err(|e| e.to_string())?; + + let id = self.conn.last_insert_rowid() as u64; + + // Grow index if needed + if self.index.size() + 1 > self.index.capacity() { + self.index + .reserve(self.index.capacity() + 4096) + .map_err(|e| e.to_string())?; + } + + self.index.add(id, embedding).map_err(|e| e.to_string())?; + + Ok(id) + } + + pub fn chunk_count(&self) -> usize { + self.conn + .query_row("SELECT COUNT(*) FROM chunks", [], |row| { + row.get::<_, i64>(0) + }) + .unwrap_or(0) as usize + } + + // ---- query -------------------------------------------------------------- + + pub fn search( + &self, + embedding: &[f32], + limit: usize, + snippet_len: usize, + ) -> Result, String> { + let results = self + .index + .search(embedding, limit) + .map_err(|e| e.to_string())?; + + let mut hits = Vec::new(); + + for (key, distance) in results.keys.iter().zip(results.distances.iter()) { + // Convert cosine distance → similarity score (higher = better) + let score = 1.0 - distance; + + let maybe_chunk = self + .conn + .query_row( + "SELECT chunk_text, path FROM chunks WHERE id = ?1", + params![*key as i64], + |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)), + ) + .ok(); + + if let Some((chunk_text, path)) = maybe_chunk { + let snippet = truncate_to_chars(&chunk_text, snippet_len); + let title = std::path::Path::new(&path) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or(&path) + .to_string(); + + hits.push(breadsearch_shared::Hit { + title, + path, + snippet, + score, + }); + } + } + + Ok(hits) + } + + // ---- persistence -------------------------------------------------------- + + pub fn save_index(&self, state_dir: &Path) -> Result<(), String> { + let idx_path = state_dir.join("vectors.usearch"); + self.index + .save(idx_path.to_str().unwrap()) + .map_err(|e| e.to_string()) + } +} + +fn truncate_to_chars(s: &str, max_chars: usize) -> String { + if s.chars().count() <= max_chars { + return s.to_string(); + } + let truncated: String = s.chars().take(max_chars).collect(); + format!("{}…", truncated.trim_end()) +} diff --git a/breadsearch-shared/Cargo.toml b/breadsearch-shared/Cargo.toml new file mode 100644 index 0000000..703db23 --- /dev/null +++ b/breadsearch-shared/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "breadsearch-shared" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[lib] +name = "breadsearch_shared" +path = "src/lib.rs" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "0.8" diff --git a/breadsearch-shared/src/lib.rs b/breadsearch-shared/src/lib.rs new file mode 100644 index 0000000..0d02cb8 --- /dev/null +++ b/breadsearch-shared/src/lib.rs @@ -0,0 +1,268 @@ +use std::{ + env, fs, + io::{BufRead, BufReader, Write}, + os::unix::net::UnixStream, + path::PathBuf, +}; + +use serde::{Deserialize, Serialize}; + +// ---- XDG path helpers ------------------------------------------------------- + +pub fn home_dir() -> PathBuf { + PathBuf::from(env::var("HOME").unwrap_or_else(|_| "/tmp".into())) +} + +pub fn config_dir() -> PathBuf { + env::var("XDG_CONFIG_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| home_dir().join(".config")) + .join("breadsearch") +} + +pub fn state_dir() -> PathBuf { + env::var("XDG_STATE_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| home_dir().join(".local/state")) + .join("breadsearch") +} + +pub fn cache_dir() -> PathBuf { + env::var("XDG_CACHE_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| home_dir().join(".cache")) + .join("breadsearch") +} + +pub fn socket_path() -> PathBuf { + env::var("XDG_RUNTIME_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/tmp")) + .join("breadmill.sock") +} + +// ---- Config ----------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + #[serde(default)] + pub index: IndexConfig, + #[serde(default)] + pub search: SearchConfig, + #[serde(default)] + pub model: ModelConfig, + #[serde(default)] + pub power: PowerConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndexConfig { + #[serde(default = "default_roots")] + pub roots: Vec, + #[serde(default)] + pub excludes: Vec, + #[serde(default = "default_extensions")] + pub extensions: Vec, + #[serde(default = "default_max_file_mb")] + pub max_file_mb: f64, +} + +fn default_roots() -> Vec { + let home = home_dir(); + vec![ + home.join("Documents").to_string_lossy().into_owned(), + home.join("Projects").to_string_lossy().into_owned(), + home.join(".config/breadpad").to_string_lossy().into_owned(), + ] +} + +fn default_extensions() -> Vec { + ["md", "txt", "org", "pdf", "odt", "docx"] + .iter() + .map(|s| s.to_string()) + .collect() +} + +fn default_max_file_mb() -> f64 { + 10.0 +} + +impl Default for IndexConfig { + fn default() -> Self { + Self { + roots: default_roots(), + excludes: vec![], + extensions: default_extensions(), + max_file_mb: default_max_file_mb(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SearchConfig { + #[serde(default = "default_limit")] + pub limit: usize, + #[serde(default = "default_snippet_len")] + pub snippet_len: usize, +} + +fn default_limit() -> usize { + 10 +} + +fn default_snippet_len() -> usize { + 200 +} + +impl Default for SearchConfig { + fn default() -> Self { + Self { + limit: default_limit(), + snippet_len: default_snippet_len(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelConfig { + #[serde(default = "default_model_name")] + pub name: String, + #[serde(default = "default_dim")] + pub dim: usize, + /// Compute backend: "cpu" or "npu" (VitisAI/XDNA). + #[serde(default = "default_backend")] + pub backend: String, +} + +fn default_model_name() -> String { + "nomic-embed-text-v1.5".into() +} + +fn default_dim() -> usize { + 768 +} + +fn default_backend() -> String { + "cpu".into() +} + +impl Default for ModelConfig { + fn default() -> Self { + Self { + name: default_model_name(), + dim: default_dim(), + backend: default_backend(), + } + } +} + +impl Default for Config { + fn default() -> Self { + Self { + index: IndexConfig::default(), + search: SearchConfig::default(), + model: ModelConfig::default(), + power: PowerConfig::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PowerConfig { + /// Master on/off switch for indexing (embedding). When false, breadmill + /// still serves queries over the existing index but never embeds new files. + #[serde(default = "default_true")] + pub enabled: bool, + /// Whether to keep embedding while running on battery. Embedding is the + /// compute-heavy step (CPU/NPU/GPU forward pass), so this defaults to + /// false to avoid draining battery; indexing resumes automatically once + /// AC power is reconnected. + #[serde(default)] + pub run_on_battery: bool, +} + +fn default_true() -> bool { + true +} + +impl Default for PowerConfig { + fn default() -> Self { + Self { + enabled: true, + run_on_battery: false, + } + } +} + +impl Config { + pub fn load() -> Self { + let path = config_dir().join("config.toml"); + let content = match fs::read_to_string(&path) { + Ok(s) => s, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Self::default(), + Err(e) => { + eprintln!("breadsearch: could not read {}: {}", path.display(), e); + return Self::default(); + } + }; + match toml::from_str(&content) { + Ok(c) => c, + Err(e) => { + eprintln!("breadsearch: parse error in {}: {}", path.display(), e); + Self::default() + } + } + } +} + +// ---- IPC types (newline-delimited JSON over Unix socket) -------------------- + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Request { + Query { query: String, limit: usize }, + Status, + Reindex, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Response { + Hits { hits: Vec }, + StatusInfo(StatusInfo), + Ok, + Error { message: String }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Hit { + pub title: String, + pub path: String, + pub snippet: String, + pub score: f32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StatusInfo { + pub indexed: usize, + pub pending: usize, + pub model_ready: bool, +} + +// ---- Socket client ---------------------------------------------------------- + +pub fn send_request(req: &Request) -> std::io::Result { + let mut stream = UnixStream::connect(socket_path())?; + + let mut line = serde_json::to_string(req) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + line.push('\n'); + stream.write_all(line.as_bytes())?; + stream.flush()?; + + let mut response = String::new(); + BufReader::new(stream).read_line(&mut response)?; + + serde_json::from_str(response.trim()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) +} diff --git a/breadsearch/Cargo.toml b/breadsearch/Cargo.toml new file mode 100644 index 0000000..fc469e0 --- /dev/null +++ b/breadsearch/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "breadsearch" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[[bin]] +name = "breadsearch" +path = "src/main.rs" + +[dependencies] +breadsearch-shared = { path = "../breadsearch-shared" } +bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] } +gtk4 = { version = "0.11", features = ["v4_12"] } +gtk4-layer-shell = "0.8" +serde_json = "1" diff --git a/breadsearch/src/main.rs b/breadsearch/src/main.rs new file mode 100644 index 0000000..58f4b47 --- /dev/null +++ b/breadsearch/src/main.rs @@ -0,0 +1,429 @@ +use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette}; +use breadsearch_shared::{Hit, Request, Response}; +use std::{ + cell::RefCell, + env, fs, + path::PathBuf, + process::Command, + rc::Rc, + sync::mpsc, +}; + +use gtk4::{ + glib, + pango::EllipsizeMode, + prelude::*, + Application, ApplicationWindow, Box as GBox, CssProvider, EventControllerKey, Image, Label, + ListBox, Orientation, PolicyType, ScrolledWindow, SearchEntry, SelectionMode, +}; +use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; + +// ---- Theming ---------------------------------------------------------------- + +fn build_css(p: &Palette) -> String { + let bg_panel = hex_to_rgba(&p.background, 0.60); + format!( + "window {{ background-color: transparent; }}\ + .launcher-bg {{ background-color: {bg_panel}; color: {on_bg}; border-radius: 8px;\ + box-shadow: 0 8px 32px rgba(0,0,0,0.6); }}\ + searchentry {{ background-color: {surface}; color: {on_surface}; caret-color: {accent};\ + border: none; outline: none; box-shadow: none;\ + padding: 12px 16px; border-radius: 6px 6px 0 0; }}\ + listbox {{ background-color: transparent; padding: 4px; }}\ + row {{ padding: 8px 12px; color: {on_bg}; background-color: transparent;\ + border-radius: 6px; }}\ + row:hover {{ background-color: {surface}; color: {on_surface}; }}\ + row:selected {{ background-color: {surface}; color: {on_surface}; }}\ + .hit-title {{ font-size: 14px; }}\ + .hit-muted {{ opacity: 0.6; font-size: 12px; }}\ + .hit-snippet {{ opacity: 0.75; font-size: 11px; font-style: italic; }}\ + .hit-score {{ opacity: 0.5; font-size: 11px; }}\ + image {{ margin-right: 8px; }}", + bg_panel = bg_panel, + surface = p.color0, + accent = p.color4, + on_bg = ink_on(&p.background), + on_surface = ink_on(&p.color0), + ) +} + +// ---- PID file toggle -------------------------------------------------------- + +fn pid_file() -> PathBuf { + env::var("XDG_RUNTIME_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/tmp")) + .join("breadsearch.pid") +} + +fn is_breadsearch_pid(pid: u32) -> bool { + fs::read_to_string(format!("/proc/{}/comm", pid)) + .map(|s| s.trim() == "breadsearch") + .unwrap_or(false) +} + +fn toggle_or_continue() -> bool { + let pf = pid_file(); + if let Ok(content) = fs::read_to_string(&pf) { + if let Ok(pid) = content.trim().parse::() { + if is_breadsearch_pid(pid) { + let _ = Command::new("kill").arg(pid.to_string()).status(); + return false; + } + } + } + let _ = fs::write(&pf, std::process::id().to_string()); + true +} + +fn cleanup_pid() { + let _ = fs::remove_file(pid_file()); +} + +// ---- Row builder ------------------------------------------------------------ + +fn make_hit_row(hit: &Hit) -> gtk4::ListBoxRow { + let row = gtk4::ListBoxRow::new(); + let vbox = GBox::new(Orientation::Vertical, 2); + vbox.set_margin_start(6); + vbox.set_margin_end(6); + vbox.set_margin_top(4); + vbox.set_margin_bottom(4); + + let hbox = GBox::new(Orientation::Horizontal, 0); + + let (content_type, _) = + gtk4::gio::functions::content_type_guess(Some(&hit.path), None::<&[u8]>); + let gicon = gtk4::gio::content_type_get_icon(&content_type); + let img = Image::from_gicon(&gicon); + img.set_pixel_size(24); + hbox.append(&img); + + let title = Label::new(Some(&hit.title)); + title.add_css_class("hit-title"); + title.set_xalign(0.0); + title.set_hexpand(true); + title.set_ellipsize(EllipsizeMode::End); + hbox.append(&title); + + let score_lbl = Label::new(Some(&format!("{:.0}%", hit.score * 100.0))); + score_lbl.add_css_class("hit-score"); + score_lbl.set_xalign(1.0); + hbox.append(&score_lbl); + + vbox.append(&hbox); + + let path_lbl = Label::new(Some(&hit.path)); + path_lbl.add_css_class("hit-muted"); + path_lbl.set_xalign(0.0); + path_lbl.set_ellipsize(EllipsizeMode::Start); + vbox.append(&path_lbl); + + let snippet_text = hit.snippet.lines().next().unwrap_or("").trim().to_string(); + if !snippet_text.is_empty() { + let snippet = Label::new(Some(&snippet_text)); + snippet.add_css_class("hit-snippet"); + snippet.set_xalign(0.0); + snippet.set_ellipsize(EllipsizeMode::End); + vbox.append(&snippet); + } + + row.set_child(Some(&vbox)); + unsafe { row.set_data("hit_path", hit.path.clone()) }; + row +} + +fn row_path(row: >k4::ListBoxRow) -> Option { + unsafe { row.data::("hit_path").map(|p| p.as_ref().clone()) } +} + +// ---- List population -------------------------------------------------------- + +fn clear_list(list: &ListBox) { + while let Some(child) = list.first_child() { + list.remove(&child); + } +} + +fn info_row(text: &str) -> gtk4::ListBoxRow { + let row = gtk4::ListBoxRow::new(); + let lbl = Label::new(Some(text)); + lbl.add_css_class("hit-muted"); + lbl.set_margin_top(12); + lbl.set_margin_bottom(12); + row.set_child(Some(&lbl)); + row.set_activatable(false); + row.set_selectable(false); + row +} + +fn populate_list(list: &ListBox, result: std::io::Result) { + clear_list(list); + match result { + Ok(Response::Hits { hits }) => { + if hits.is_empty() { + list.append(&info_row("No results")); + } else { + for hit in &hits { + list.append(&make_hit_row(hit)); + } + if let Some(first) = list.row_at_index(0) { + list.select_row(Some(&first)); + } + } + } + Ok(Response::Error { message }) => { + list.append(&info_row(&format!("Error: {}", message))); + } + Err(e) => { + list.append(&info_row(&format!("breadmill not running: {}", e))); + } + _ => {} + } +} + +// ---- Actions ---------------------------------------------------------------- + +fn open_file(path: &str) { + let _ = Command::new("xdg-open") + .arg(path) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn(); +} + +fn open_folder(path: &str) { + let parent = std::path::Path::new(path) + .parent() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_else(|| path.to_string()); + let _ = Command::new("xdg-open") + .arg(&parent) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn(); +} + +// ---- UI --------------------------------------------------------------------- + +fn run_ui() { + let app = Application::builder() + .application_id("com.breadway.breadsearch") + .build(); + + let debounce_id: Rc>> = Rc::new(RefCell::new(None)); + + app.connect_activate(move |app| { + bread_theme::gtk::apply_shared(); + bread_theme::gtk::apply_app_css(|| build_css(&load_palette())); + + { + let user_css_path = breadsearch_shared::config_dir().join("style.css"); + let user_cell: RefCell> = RefCell::new(None); + bread_theme::gtk::apply_user_css(&user_css_path, &user_cell); + } + + let window = ApplicationWindow::builder().application(app).build(); + window.init_layer_shell(); + window.set_namespace(Some("breadsearch")); + window.set_layer(Layer::Overlay); + window.set_keyboard_mode(KeyboardMode::Exclusive); + for edge in [Edge::Top, Edge::Bottom, Edge::Left, Edge::Right] { + window.set_anchor(edge, true); + } + window.set_exclusive_zone(0); + + let close_all: Rc = Rc::new({ + let w = window.clone(); + move || { + cleanup_pid(); + w.close(); + } + }); + + let vbox = GBox::new(Orientation::Vertical, 0); + vbox.add_css_class("launcher-bg"); + vbox.set_halign(gtk4::Align::Center); + vbox.set_valign(gtk4::Align::Start); + vbox.set_margin_top(120); + vbox.set_size_request(640, -1); + + let search = SearchEntry::new(); + search.set_placeholder_text(Some("breadsearch — find anything by meaning")); + vbox.append(&search); + + let scroll = ScrolledWindow::new(); + scroll.set_policy(PolicyType::Never, PolicyType::Automatic); + scroll.set_max_content_height(520); + scroll.set_propagate_natural_height(true); + + let list = ListBox::new(); + list.set_selection_mode(SelectionMode::Browse); + list.append(&info_row("Type to search across your documents…")); + + scroll.set_child(Some(&list)); + vbox.append(&scroll); + window.set_child(Some(&vbox)); + + // Search with 150ms debounce + off-UI-thread query + let list_s = list.clone(); + let debounce = Rc::clone(&debounce_id); + + search.connect_changed(move |entry| { + let query = entry.text().to_string(); + + if let Some(id) = debounce.borrow_mut().take() { + id.remove(); + } + + if query.is_empty() { + clear_list(&list_s); + list_s.append(&info_row("Type to search across your documents…")); + return; + } + + let list_clone = list_s.clone(); + let debounce_clone = Rc::clone(&debounce); + + let id = glib::timeout_add_local(std::time::Duration::from_millis(150), move || { + debounce_clone.borrow_mut().take(); + + let q = query.clone(); + let (tx, rx) = mpsc::sync_channel::>(1); + + std::thread::spawn(move || { + let req = Request::Query { query: q, limit: 10 }; + let _ = tx.send(breadsearch_shared::send_request(&req)); + }); + + // Poll via idle_add_local until the thread delivers its result. + // Unix socket round-trips are sub-millisecond so this fires once. + let rx = Rc::new(rx); + let list_t = list_clone.clone(); + + glib::idle_add_local(move || { + match rx.try_recv() { + Ok(result) => { + populate_list(&list_t, result); + glib::ControlFlow::Break + } + Err(mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue, + Err(mpsc::TryRecvError::Disconnected) => glib::ControlFlow::Break, + } + }); + + glib::ControlFlow::Break + }); + + *debounce.borrow_mut() = Some(id); + }); + + // Keyboard handling + let key_ctrl = EventControllerKey::new(); + key_ctrl.set_propagation_phase(gtk4::PropagationPhase::Capture); + let close_k = Rc::clone(&close_all); + let list_k = list.clone(); + + key_ctrl.connect_key_pressed(move |_, key, _, mods| { + use gtk4::gdk::Key; + match key { + Key::Escape => { + close_k(); + glib::Propagation::Stop + } + Key::Return | Key::KP_Enter => { + if let Some(row) = list_k.selected_row() { + if let Some(path) = row_path(&row) { + if mods.contains(gtk4::gdk::ModifierType::CONTROL_MASK) { + open_folder(&path); + } else { + open_file(&path); + } + close_k(); + } + } + glib::Propagation::Stop + } + Key::Down => { + let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(-1); + let mut i = cur + 1; + loop { + match list_k.row_at_index(i) { + Some(r) if r.is_selectable() => { + list_k.select_row(Some(&r)); + break; + } + Some(_) => i += 1, + None => break, + } + } + glib::Propagation::Stop + } + Key::Up => { + let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(0); + let mut i = cur - 1; + loop { + if i < 0 { + break; + } + match list_k.row_at_index(i) { + Some(r) if r.is_selectable() => { + list_k.select_row(Some(&r)); + break; + } + Some(_) => i -= 1, + None => break, + } + } + glib::Propagation::Stop + } + _ => glib::Propagation::Proceed, + } + }); + window.add_controller(key_ctrl); + + // Row click opens file + let close_a = Rc::clone(&close_all); + list.connect_row_activated(move |_, row| { + if let Some(path) = row_path(row) { + open_file(&path); + close_a(); + } + }); + + // Click outside launcher panel → close + let close_outside = Rc::clone(&close_all); + let vbox_ref = vbox.clone(); + let win_ref = window.clone(); + let outside_click = gtk4::GestureClick::new(); + outside_click.connect_pressed(move |_, _, x, y| { + if let Some(b) = vbox_ref.compute_bounds(&win_ref) { + if x < b.x() as f64 + || x > (b.x() + b.width()) as f64 + || y < b.y() as f64 + || y > (b.y() + b.height()) as f64 + { + close_outside(); + } + } + }); + window.add_controller(outside_click); + + window.connect_destroy(|_| cleanup_pid()); + window.present(); + search.grab_focus(); + }); + + app.run(); +} + +// ---- Main ------------------------------------------------------------------- + +fn main() { + if !toggle_or_continue() { + return; + } + run_ui(); +} diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..b4fc17a --- /dev/null +++ b/config.example.toml @@ -0,0 +1,41 @@ +# breadsearch configuration +# Copy to ~/.config/breadsearch/config.toml + +[index] +# Directories to crawl for documents +roots = [ + "~/Documents", + "~/Projects", + "~/.config/breadpad", +] +# File extensions to index +extensions = ["md", "txt", "org", "pdf", "odt", "docx"] +# Skip files larger than this (MB) +max_file_mb = 10.0 + +[search] +# Default number of results +limit = 10 +# Max characters in snippet +snippet_len = 200 + +[model] +# Embedding model name (informational; breadmill uses the ONNX files it fetches) +name = "nomic-embed-text-v1.5" +# Embedding dimension +dim = 768 +# Compute backend: "cpu" (default), "npu" (AMD XDNA via VitisAI EP), or +# "rocm" (AMD iGPU/dGPU). NPU requires breadmill compiled with --features npu +# and the AMD Ryzen AI SDK installed; ROCm requires --features rocm. +# Override at runtime with: breadmill --npu / --rocm +backend = "cpu" + +[power] +# Master switch for indexing. When false, breadmill still answers queries +# against the existing index but never embeds new/changed files. +enabled = true +# Embedding is the compute-heavy step. By default breadmill pauses indexing +# while on battery and resumes automatically when AC power is reconnected. +# Set true to index on battery too. Machines with no battery (desktops) are +# always treated as on-power regardless of this setting. +run_on_battery = false diff --git a/docs/error-reports/2026-06-24-oom-minified-json-chunking.md b/docs/error-reports/2026-06-24-oom-minified-json-chunking.md new file mode 100644 index 0000000..53b6db4 --- /dev/null +++ b/docs/error-reports/2026-06-24-oom-minified-json-chunking.md @@ -0,0 +1,238 @@ +# Error Report: breadmill OOM loop on minified JSON `.txt` file + +**Date:** 2026-06-24 +**Reporter:** breadway +**Component:** `breadmill` (semantic search indexer) +**Version:** 0.1.0 (`/home/breadway/Projects/breadsearch/breadmill`) +**Severity:** Critical — repeated kernel OOM kills, ~28 GB RAM consumption, system instability + +--- + +## Summary + +`breadmill` enters a crash loop when indexing a 705 KB minified JSON game save file exposed as `.txt`. Word-based chunking produces ~486–616 KB chunks that are passed unbounded to the ONNX embedding model. ONNX Runtime then attempts multi-terabyte allocations in attention `MatMul`, the process balloons to ~28 GB RSS, and the kernel OOM killer terminates it. `Restart=on-failure` causes an immediate restart and the cycle repeats. + +On 2026-06-24 this produced **40 embed errors** and **76 OOM kills** in journal logs within ~45 minutes. + +--- + +## Environment + +| Item | Value | +|------|-------| +| Host | Lenovo Yoga Slim 7 14AKP10 (`83JY`) | +| OS | BOS (Arch-based), kernel `7.0.12-arch1-1` | +| RAM | 32 GB | +| Swap | 4 GB zram | +| Model | `nomic-embed-text-v1.5` (768-dim, ONNX via `ort` 2.0.0-rc.12) | +| Service | `breadmill.service` (user systemd unit) | +| Config | `~/.config/breadsearch/config.toml` | + +### Service unit + +```ini +[Service] +Type=simple +ExecStart=%h/.cargo/bin/breadmill +Restart=on-failure +RestartSec=5 +# No MemoryMax set (unlimited) +# OOMScoreAdjust=200 (default for user services — preferential OOM victim) +``` + +### Relevant config + +```toml +[index] +roots = ["~/Documents", "~/.config/breadpad"] +extensions = ["md", "txt", "org", "pdf", "odt", "docx"] +max_file_mb = 5.0 +``` + +--- + +## Triggering file + +**Path:** +`~/Documents/Gaming/Games/Spaceflight Simulator Game/Saving/Worlds/Bread/Persistent/Rockets.txt` + +| Property | Value | +|----------|-------| +| Size | 721,601 bytes (705 KB) | +| Type | JSON array (11 rocket objects), stored as `.txt` | +| Lines | **1** (single-line minified JSON) | +| Whitespace-delimited "words" | 601 | +| Longest "word" | 68,068 characters | + +The file is under `max_file_mb = 5.0` and has extension `.txt`, so it is indexed. + +--- + +## Root cause + +### 1. Word-based chunking breaks on minified JSON + +`chunk::chunk_text()` splits on whitespace only (`chunk.rs`). For minified JSON, most structure is packed into very long tokens with few spaces. + +Chunking with `words_per_chunk=400`, `overlap_words=80` (`indexer.rs:268`) yields: + +| Chunk | Word range | Character length | +|-------|------------|------------------| +| 0 | 0–399 | **486,347** | +| 1 | 320–600 | **616,582** | + +A "400-word chunk" is not ~400 natural-language words; it is hundreds of kilobytes of dense JSON. + +### 2. No token/character limit before embedding + +`embed::OrtEmbedder::embed_with_prefix()` tokenizes the full chunk and runs ONNX inference with no `max_length` truncation (`embed.rs`). The nomic model supports ~8192 tokens; these chunks are orders of magnitude larger when tokenized. + +### 3. ONNX attention allocation explodes + +Embedding fails inside ONNX Runtime: + +``` +Non-zero status code returned while running FusedMatMul node. +Name: '/encoder/layers.0/attn/MatMul/MatMulScaleFusion/' +Status Message: ... BFCArena::AllocateRawInternal ... +Failed to allocate memory for requested buffer of size 5525780084992 +``` + +Requested buffer: **~5.1 TiB** (5,525,780,084,992 bytes). + +Kernel also logs repeated allocation attempts before OOM: + +``` +__vm_enough_memory: pid: NNNN, comm: breadmill, + bytes: 8796093026304 not enough memory for the allocation (~8.0 TiB) + bytes: 7916483514368 not enough memory for the allocation (~7.2 TiB) + bytes: 7124834848768 not enough memory for the allocation (~6.5 TiB) +``` + +### 4. Restart loop amplifies damage + +`Restart=on-failure` + `RestartSec=5` restarts breadmill immediately after each OOM kill. Each restart reloads the model, rescans `~/Documents`, hits the same file, and OOMs again. Peak memory per attempt: **~27–28 GB RSS** (`anon-rss:27867432kB`). + +--- + +## Log excerpts + +### Embed error (repeats on every restart) + +``` +breadmill: embed error for .../Rockets.txt: Non-zero status code returned while running FusedMatMul node. +Name:'/encoder/layers.0/attn/MatMul/MatMulScaleFusion/' Status Message: + ... Failed to allocate memory for requested buffer of size 5525780084992 +``` + +### OOM kill + +``` +oom-kill: ... task_memcg=.../breadmill.service, task=breadmill, pid=4515, uid=1000 +Out of memory: Killed process 4515 (breadmill) + total-vm:64081848kB, anon-rss:27867432kB, ... oom_score_adj:200 +systemd[1649]: breadmill.service: Failed with result 'oom-kill'. +systemd[1649]: breadmill.service: Consumed ... 27.9G memory peak, 2.1G memory swap peak. +systemd[1649]: breadmill.service: Scheduled restart job, restart counter is at 8. +``` + +### Secondary noise (non-fatal) + +``` +breadmill: extract .../Novacana Info.txt: stream did not contain valid UTF-8 +``` + +--- + +## Reproduction + +1. Place a single-line minified JSON file (>400 KB, `.txt` extension) under a configured index root. +2. Start `breadmill serve` (or `systemctl --user start breadmill.service`). +3. Wait for initial scan to reach the file. +4. Observe embed errors, climbing RSS, and OOM kills in `journalctl --user -u breadmill.service -f`. + +**Minimal reproducer characteristics:** + +- Extension in `extensions` list (e.g. `txt`) +- File size `< max_file_mb` +- Content is minified JSON or other whitespace-sparse text on one line +- Results in chunks >> model `max_seq_len` when tokenized + +--- + +## Impact + +- **breadmill** unusable while the file is present in index roots +- **System-wide** memory pressure: swap exhaustion, unrelated process kills, journal/cache pressure +- Can be mistaken for unrelated storage or suspend issues when swap write errors appear in kernel log + +--- + +## Suggested fixes + +### Required (correctness) + +1. **Truncate before tokenization** — cap input to model `max_seq_len` (8192 tokens for nomic-embed-text-v1.5) in `embed.rs`, with explicit logging when truncation occurs. + +2. **Character-based chunk limits** — add `max_chunk_chars` (e.g. 8_000–32_000) independent of word count; split oversized chunks before embedding. + +3. **Sanity-check chunk size** — refuse to embed chunks above a byte/token threshold; log and skip rather than calling ONNX. + +### Recommended (resilience) + +4. **Per-chunk error isolation** — on embed failure for one chunk, skip that chunk but do not retry the entire file in a tight loop; mark file as `failed` in SQLite. + +5. **Service memory limit** — set `MemoryMax=4G` (or similar) on `breadmill.service` so a runaway embed cannot take the whole machine. + +6. **Lower `OOMScoreAdjust`** — use `0` or negative so breadmill is not preferentially killed while still allowing limits. + +7. **Backoff on repeated OOM** — `RestartSec=exponential` or stop after N OOM kills per file. + +### Optional (UX) + +8. **Exclude patterns** — config option for glob excludes (e.g. `**/Saving/**`, `**/*.json` even if renamed `.txt`). + +9. **Detect minified JSON** — if `serde_json::from_str` succeeds on `.txt`, skip or pretty-print/chunk differently. + +10. **`--version` flag** — aids bug reports (currently `unknown command: --version`). + +--- + +## Workaround (immediate) + +Exclude the game save directory from index roots in `~/.config/breadsearch/config.toml`: + +```toml +[index] +roots = [ + "~/Documents", + "~/.config/breadpad", +] +# Then add an exclude mechanism when available, OR temporarily narrow roots: +# roots = ["~/Documents/Creative", "~/.config/breadpad"] +``` + +Or stop the service until a fix is deployed: + +```bash +systemctl --user stop breadmill.service +``` + +--- + +## Files involved + +| File | Role | +|------|------| +| `breadmill/src/chunk.rs` | Whitespace word chunking — no char/token cap | +| `breadmill/src/indexer.rs:268` | `chunk_text(&text, 400, 80)` — hardcoded params | +| `breadmill/src/embed.rs` | No truncation before `tokenizer.encode()` / ONNX run | +| `breadmill/src/extract.rs` | Treats `.txt` as raw UTF-8 (JSON passes through) | +| `~/.config/breadsearch/config.toml` | `max_file_mb` only; no chunk/token limits | + +--- + +## Related + +- ONNX node: `/encoder/layers.0/attn/MatMul/MatMulScaleFusion/` +- Model: [nomic-embed-text-v1.5](https://huggingface.co/nomic-ai/nomic-embed-text-v1.5) (max sequence length 8192) diff --git a/original-info-signature.txt b/original-info-signature.txt new file mode 100644 index 0000000..3bfd606 --- /dev/null +++ b/original-info-signature.txt @@ -0,0 +1 @@ +6c422027bd642c5e1f409e020b7b7a9b \ No newline at end of file diff --git a/original-model-signature.txt b/original-model-signature.txt new file mode 100644 index 0000000..200c9db --- /dev/null +++ b/original-model-signature.txt @@ -0,0 +1 @@ +e9339c75bca7719f67ac377f03e28b83 \ No newline at end of file diff --git a/packaging/breadmill.service b/packaging/breadmill.service new file mode 100644 index 0000000..2b4d65e --- /dev/null +++ b/packaging/breadmill.service @@ -0,0 +1,15 @@ +[Unit] +Description=Breadmill semantic search indexer +Documentation=https://github.com/breadway/breadsearch +After=default.target + +[Service] +Type=simple +ExecStart=%h/.cargo/bin/breadmill +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=default.target diff --git a/quantize_nomic.py b/quantize_nomic.py new file mode 100644 index 0000000..e98e535 --- /dev/null +++ b/quantize_nomic.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +""" +Quantize nomic-embed-text-v1.5 with AMD Quark for NPU (XDNA) inference. +Targets int8 QDQ format with enable_npu_transformer=True so VAIML can map +attention MatMul ops to the NPU. +""" + +import json +import numpy as np +import onnxruntime +from pathlib import Path + +MODEL_PATH = Path.home() / ".cache/breadsearch/models/model.onnx" +TOKENIZER_PATH = Path.home() / ".cache/breadsearch/models/tokenizer.json" +OUTPUT_PATH = Path.home() / ".cache/breadsearch/models/model_quantized.onnx" + +CALIB_SENTENCES = [ + "search_document: The quick brown fox jumps over the lazy dog.", + "search_document: Machine learning is a branch of artificial intelligence.", + "search_document: The Eiffel Tower is located in Paris, France.", + "search_document: Python is a high-level programming language.", + "search_document: The speed of light is approximately 299,792,458 meters per second.", + "search_document: Climate change is one of the greatest challenges facing humanity.", + "search_document: Quantum computing leverages quantum mechanical phenomena.", + "search_document: The human genome contains approximately 3 billion base pairs.", + "search_document: Rust is a systems programming language focused on safety.", + "search_document: Neural networks are inspired by the structure of the brain.", + "search_document: The Milky Way galaxy contains over 100 billion stars.", + "search_document: Cryptography is the practice of secure communication.", + "search_document: The Linux kernel was first released in 1991 by Linus Torvalds.", + "search_document: Databases store and retrieve structured data efficiently.", + "search_document: The TCP/IP protocol suite is the backbone of the internet.", + "search_document: Embedded systems run on resource-constrained hardware.", + "search_document: The ONNX format provides a standard for machine learning models.", + "search_document: Transformer models use attention mechanisms for sequence tasks.", + "search_document: File systems organize and manage data storage on disks.", + "search_document: Async programming allows concurrent execution without threads.", + "search_query: What is machine learning?", + "search_query: How does attention work in transformers?", + "search_query: Where is the Eiffel Tower?", + "search_query: What programming language should I learn first?", + "search_query: How fast is the speed of light?", +] + +MAX_SEQ_LEN = 512 + +def load_tokenizer(path: Path): + with open(path) as f: + return json.load(f) + +def simple_tokenize(tok_data: dict, text: str, max_len: int) -> dict[str, np.ndarray]: + vocab = tok_data["model"]["vocab"] + unk_id = vocab.get("[UNK]", 100) + cls_id = vocab.get("[CLS]", 101) + sep_id = vocab.get("[SEP]", 102) + pad_id = vocab.get("[PAD]", 0) + + words = text.lower().split() + token_ids = [cls_id] + for w in words: + token_ids.append(vocab.get(w, unk_id)) + token_ids.append(sep_id) + + if len(token_ids) > max_len: + token_ids = token_ids[:max_len - 1] + [sep_id] + + seq_len = len(token_ids) + padded = token_ids + [pad_id] * (max_len - seq_len) + mask = [1] * seq_len + [0] * (max_len - seq_len) + type_ids = [0] * max_len + + return { + "input_ids": np.array([padded], dtype=np.int64), + "attention_mask": np.array([mask], dtype=np.int64), + "token_type_ids": np.array([type_ids], dtype=np.int64), + } + + +class NomicCalibrationReader: + def __init__(self, sentences, tok_data, max_len=MAX_SEQ_LEN): + self.inputs = [simple_tokenize(tok_data, s, max_len) for s in sentences] + self.idx = 0 + + def get_next(self): + if self.idx >= len(self.inputs): + return None + sample = self.inputs[self.idx] + self.idx += 1 + return sample + + def rewind(self): + self.idx = 0 + + +def main(): + print(f"Loading tokenizer from {TOKENIZER_PATH}") + tok_data = load_tokenizer(TOKENIZER_PATH) + + calib_reader = NomicCalibrationReader(CALIB_SENTENCES, tok_data) + + # Verify model input names match what we provide + sess = onnxruntime.InferenceSession(str(MODEL_PATH), providers=["CPUExecutionProvider"]) + input_names = [inp.name for inp in sess.get_inputs()] + print(f"Model inputs: {input_names}") + del sess + + from quark.onnx import ModelQuantizer, QuantizationConfig + from quark.onnx.quantization.config.config import Config + from onnxruntime.quantization import CalibrationMethod, QuantFormat, QuantType + + quant_config = QuantizationConfig( + calibrate_method=CalibrationMethod.MinMax, + quant_format=QuantFormat.QDQ, + activation_type=QuantType.QInt8, + weight_type=QuantType.QInt8, + per_channel=False, + reduce_range=False, + optimize_model=True, + enable_npu_transformer=True, + include_cle=True, + print_summary=True, + extra_options={ + "ActivationSymmetric": True, + "WeightSymmetric": True, + }, + ) + + config = Config(global_quant_config=quant_config) + quantizer = ModelQuantizer(config) + print(f"Quantizing {MODEL_PATH} → {OUTPUT_PATH}") + print("This may take several minutes...") + + quantizer.quantize_model( + model_input=str(MODEL_PATH), + model_output=str(OUTPUT_PATH), + calibration_data_reader=calib_reader, + ) + + print(f"\nQuantized model saved to {OUTPUT_PATH}") + print(f"Size: {OUTPUT_PATH.stat().st_size / 1024 / 1024:.1f} MB") + + +if __name__ == "__main__": + main() diff --git a/quantized_info.csv b/quantized_info.csv new file mode 100644 index 0000000..f5fcc24 --- /dev/null +++ b/quantized_info.csv @@ -0,0 +1,151 @@ +/home/breadway/.cache/breadsearch/models/model.onnx quantization quantization info,2026-06-24 18:14:20 +quantization stage,time consumed(s),sub stage,time consumed(s) +pre process,5.43 +,,calibration: collect data (onnx inference + numpy statistics),16.60 +,,calibration: compute data,0.01 +calibration (collect data + compute data),19.75 +static quantization,0.81 +post process(including finetuning),0.00 +e2e,40.621040006999465 + +Node Name,Op Type,Activation,Weights,Bias +/encoder/layers.0/attn/Wqkv/MatMul,MatMul,INT8,INT8, +/encoder/layers.0/attn/Reshape,Reshape,INT8,, +/encoder/layers.0/attn/out_proj/MatMul,MatMul,INT8,INT8, +/encoder/layers.0/Add,Add,INT8,, +/encoder/layers.0/mlp/fc12/MatMul,MatMul,INT8,INT8, +/encoder/layers.0/mlp/fc11/MatMul,MatMul,INT8,INT8, +/encoder/layers.0/mlp/Sigmoid,Sigmoid,INT8,, +/encoder/layers.0/mlp/Mul,Mul,INT8,, +/encoder/layers.0/mlp/Mul_1,Mul,INT8,, +/encoder/layers.0/mlp/fc2/MatMul,MatMul,INT8,INT8, +/encoder/layers.0/Add_1,Add,INT8,, +/encoder/layers.1/attn/Wqkv/MatMul,MatMul,INT8,INT8, +/encoder/layers.1/attn/Reshape,Reshape,INT8,, +/encoder/layers.1/attn/out_proj/MatMul,MatMul,INT8,INT8, +/encoder/layers.1/Add,Add,INT8,, +/encoder/layers.1/mlp/fc12/MatMul,MatMul,INT8,INT8, +/encoder/layers.1/mlp/fc11/MatMul,MatMul,INT8,INT8, +/encoder/layers.1/mlp/Sigmoid,Sigmoid,INT8,, +/encoder/layers.1/mlp/Mul,Mul,INT8,, +/encoder/layers.1/mlp/Mul_1,Mul,INT8,, +/encoder/layers.1/mlp/fc2/MatMul,MatMul,INT8,INT8, +/encoder/layers.1/Add_1,Add,INT8,, +/encoder/layers.2/attn/Wqkv/MatMul,MatMul,INT8,INT8, +/encoder/layers.2/attn/Reshape,Reshape,INT8,, +/encoder/layers.2/attn/out_proj/MatMul,MatMul,INT8,INT8, +/encoder/layers.2/Add,Add,INT8,, +/encoder/layers.2/mlp/fc12/MatMul,MatMul,INT8,INT8, +/encoder/layers.2/mlp/fc11/MatMul,MatMul,INT8,INT8, +/encoder/layers.2/mlp/Sigmoid,Sigmoid,INT8,, +/encoder/layers.2/mlp/Mul,Mul,INT8,, +/encoder/layers.2/mlp/Mul_1,Mul,INT8,, +/encoder/layers.2/mlp/fc2/MatMul,MatMul,INT8,INT8, +/encoder/layers.2/Add_1,Add,INT8,, +/encoder/layers.3/attn/Wqkv/MatMul,MatMul,INT8,INT8, +/encoder/layers.3/attn/Reshape,Reshape,INT8,, +/encoder/layers.3/attn/out_proj/MatMul,MatMul,INT8,INT8, +/encoder/layers.3/Add,Add,INT8,, +/encoder/layers.3/mlp/fc12/MatMul,MatMul,INT8,INT8, +/encoder/layers.3/mlp/fc11/MatMul,MatMul,INT8,INT8, +/encoder/layers.3/mlp/Sigmoid,Sigmoid,INT8,, +/encoder/layers.3/mlp/Mul,Mul,INT8,, +/encoder/layers.3/mlp/Mul_1,Mul,INT8,, +/encoder/layers.3/mlp/fc2/MatMul,MatMul,INT8,INT8, +/encoder/layers.3/Add_1,Add,INT8,, +/encoder/layers.4/attn/Wqkv/MatMul,MatMul,INT8,INT8, +/encoder/layers.4/attn/Reshape,Reshape,INT8,, +/encoder/layers.4/attn/out_proj/MatMul,MatMul,INT8,INT8, +/encoder/layers.4/Add,Add,INT8,, +/encoder/layers.4/mlp/fc12/MatMul,MatMul,INT8,INT8, +/encoder/layers.4/mlp/fc11/MatMul,MatMul,INT8,INT8, +/encoder/layers.4/mlp/Sigmoid,Sigmoid,INT8,, +/encoder/layers.4/mlp/Mul,Mul,INT8,, +/encoder/layers.4/mlp/Mul_1,Mul,INT8,, +/encoder/layers.4/mlp/fc2/MatMul,MatMul,INT8,INT8, +/encoder/layers.4/Add_1,Add,INT8,, +/encoder/layers.5/attn/Wqkv/MatMul,MatMul,INT8,INT8, +/encoder/layers.5/attn/Reshape,Reshape,INT8,, +/encoder/layers.5/attn/out_proj/MatMul,MatMul,INT8,INT8, +/encoder/layers.5/Add,Add,INT8,, +/encoder/layers.5/mlp/fc12/MatMul,MatMul,INT8,INT8, +/encoder/layers.5/mlp/fc11/MatMul,MatMul,INT8,INT8, +/encoder/layers.5/mlp/Sigmoid,Sigmoid,INT8,, +/encoder/layers.5/mlp/Mul,Mul,INT8,, +/encoder/layers.5/mlp/Mul_1,Mul,INT8,, +/encoder/layers.5/mlp/fc2/MatMul,MatMul,INT8,INT8, +/encoder/layers.5/Add_1,Add,INT8,, +/encoder/layers.6/attn/Wqkv/MatMul,MatMul,INT8,INT8, +/encoder/layers.6/attn/Reshape,Reshape,INT8,, +/encoder/layers.6/attn/out_proj/MatMul,MatMul,INT8,INT8, +/encoder/layers.6/Add,Add,INT8,, +/encoder/layers.6/mlp/fc12/MatMul,MatMul,INT8,INT8, +/encoder/layers.6/mlp/fc11/MatMul,MatMul,INT8,INT8, +/encoder/layers.6/mlp/Sigmoid,Sigmoid,INT8,, +/encoder/layers.6/mlp/Mul,Mul,INT8,, +/encoder/layers.6/mlp/Mul_1,Mul,INT8,, +/encoder/layers.6/mlp/fc2/MatMul,MatMul,INT8,INT8, +/encoder/layers.6/Add_1,Add,INT8,, +/encoder/layers.7/attn/Wqkv/MatMul,MatMul,INT8,INT8, +/encoder/layers.7/attn/Reshape,Reshape,INT8,, +/encoder/layers.7/attn/out_proj/MatMul,MatMul,INT8,INT8, +/encoder/layers.7/Add,Add,INT8,, +/encoder/layers.7/mlp/fc12/MatMul,MatMul,INT8,INT8, +/encoder/layers.7/mlp/fc11/MatMul,MatMul,INT8,INT8, +/encoder/layers.7/mlp/Sigmoid,Sigmoid,INT8,, +/encoder/layers.7/mlp/Mul,Mul,INT8,, +/encoder/layers.7/mlp/Mul_1,Mul,INT8,, +/encoder/layers.7/mlp/fc2/MatMul,MatMul,INT8,INT8, +/encoder/layers.7/Add_1,Add,INT8,, +/encoder/layers.8/attn/Wqkv/MatMul,MatMul,INT8,INT8, +/encoder/layers.8/attn/Reshape,Reshape,INT8,, +/encoder/layers.8/attn/out_proj/MatMul,MatMul,INT8,INT8, +/encoder/layers.8/Add,Add,INT8,, +/encoder/layers.8/mlp/fc12/MatMul,MatMul,INT8,INT8, +/encoder/layers.8/mlp/fc11/MatMul,MatMul,INT8,INT8, +/encoder/layers.8/mlp/Sigmoid,Sigmoid,INT8,, +/encoder/layers.8/mlp/Mul,Mul,INT8,, +/encoder/layers.8/mlp/Mul_1,Mul,INT8,, +/encoder/layers.8/mlp/fc2/MatMul,MatMul,INT8,INT8, +/encoder/layers.8/Add_1,Add,INT8,, +/encoder/layers.9/attn/Wqkv/MatMul,MatMul,INT8,INT8, +/encoder/layers.9/attn/Reshape,Reshape,INT8,, +/encoder/layers.9/attn/out_proj/MatMul,MatMul,INT8,INT8, +/encoder/layers.9/Add,Add,INT8,, +/encoder/layers.9/mlp/fc12/MatMul,MatMul,INT8,INT8, +/encoder/layers.9/mlp/fc11/MatMul,MatMul,INT8,INT8, +/encoder/layers.9/mlp/Sigmoid,Sigmoid,INT8,, +/encoder/layers.9/mlp/Mul,Mul,INT8,, +/encoder/layers.9/mlp/Mul_1,Mul,INT8,, +/encoder/layers.9/mlp/fc2/MatMul,MatMul,INT8,INT8, +/encoder/layers.9/Add_1,Add,INT8,, +/encoder/layers.10/attn/Wqkv/MatMul,MatMul,INT8,INT8, +/encoder/layers.10/attn/Reshape,Reshape,INT8,, +/encoder/layers.10/attn/out_proj/MatMul,MatMul,INT8,INT8, +/encoder/layers.10/Add,Add,INT8,, +/encoder/layers.10/mlp/fc12/MatMul,MatMul,INT8,INT8, +/encoder/layers.10/mlp/fc11/MatMul,MatMul,INT8,INT8, +/encoder/layers.10/mlp/Sigmoid,Sigmoid,INT8,, +/encoder/layers.10/mlp/Mul,Mul,INT8,, +/encoder/layers.10/mlp/Mul_1,Mul,INT8,, +/encoder/layers.10/mlp/fc2/MatMul,MatMul,INT8,INT8, +/encoder/layers.10/Add_1,Add,INT8,, +/encoder/layers.11/attn/Wqkv/MatMul,MatMul,INT8,INT8, +/encoder/layers.11/attn/Reshape,Reshape,INT8,, +/encoder/layers.11/attn/out_proj/MatMul,MatMul,INT8,INT8, +/encoder/layers.11/Add,Add,INT8,, +/encoder/layers.11/mlp/fc12/MatMul,MatMul,INT8,INT8, +/encoder/layers.11/mlp/fc11/MatMul,MatMul,INT8,INT8, +/encoder/layers.11/mlp/Sigmoid,Sigmoid,INT8,, +/encoder/layers.11/mlp/Mul,Mul,INT8,, +/encoder/layers.11/mlp/Mul_1,Mul,INT8,, +/encoder/layers.11/mlp/fc2/MatMul,MatMul,INT8,INT8, +/encoder/layers.11/Add_1,Add,INT8,, + +Op Type,Activation,Weights,Bias +MatMul,INT8(60),INT8(60), +Reshape,INT8(12),, +Add,INT8(24),, +Sigmoid,INT8(12),, +Mul,INT8(24),, + diff --git a/test_npu_quantized.py b/test_npu_quantized.py new file mode 100644 index 0000000..0f122af --- /dev/null +++ b/test_npu_quantized.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +""" +Test VitisAI EP with the quantized nomic-embed-text-v1.5 model. +Checks whether the NPU VAIML pass achieves meaningful GOPs coverage. +""" + +import os +import json +import time +import numpy as np +from pathlib import Path + +MODEL_PATH = Path.home() / ".cache/breadsearch/models/model_quantized_static.onnx" +CACHE_DIR = Path.home() / ".cache/breadsearch/npu/nomic-quantized-static" +VAIP_CONFIG = Path.home() / ".config/breadsearch/vaip_config.json" +RYZEN_AI_LIB = Path.home() / ".local/share/ryzen-ai-1.7.1/lib" + +# libvaiml.so must be discoverable +os.environ["RYZEN_AI_INSTALLATION_PATH"] = str(RYZEN_AI_LIB) +os.environ["LD_LIBRARY_PATH"] = str(RYZEN_AI_LIB) + ":" + os.environ.get("LD_LIBRARY_PATH", "") + +CACHE_DIR.mkdir(parents=True, exist_ok=True) + +if not VAIP_CONFIG.exists(): + print(f"ERROR: vaip_config.json not found at {VAIP_CONFIG}") + print("Check breadmill embed.rs for the find_vaip_config() paths") + exit(1) + +print(f"Testing quantized model: {MODEL_PATH}") +print(f"Cache dir: {CACHE_DIR}") +print(f"VAIP config: {VAIP_CONFIG}") +print(f"RYZEN_AI_INSTALLATION_PATH: {os.environ['RYZEN_AI_INSTALLATION_PATH']}") + +import onnxruntime as ort + +providers = [ + ("VitisAIExecutionProvider", { + "config_file": str(VAIP_CONFIG), + "cacheDir": str(CACHE_DIR), + "cacheKey": "nomic-quantized-static", + }), + "CPUExecutionProvider", +] + +print("\nCreating InferenceSession with VitisAI EP...") +t0 = time.time() +try: + sess = ort.InferenceSession(str(MODEL_PATH), providers=providers) + t1 = time.time() + print(f"Session created in {t1-t0:.1f}s") + print(f"Active providers: {sess.get_providers()}") +except Exception as e: + print(f"ERROR creating session: {e}") + exit(1) + +# Check for the VAIML pass summary +summary_path = CACHE_DIR / "nomic-quantized" / "preliminary-vaiml-pass-summary.txt" +if not summary_path.exists(): + # Try variations + for p in CACHE_DIR.rglob("preliminary-vaiml-pass-summary.txt"): + summary_path = p + break + +if summary_path.exists(): + print(f"\n--- VAIML Pass Summary ---") + print(summary_path.read_text()) +else: + print(f"\nNo VAIML summary found at {summary_path}") + print("Files in cache dir:") + for f in CACHE_DIR.rglob("*"): + if f.is_file(): + print(f" {f}") + +# Run a quick inference test +print("\nRunning inference test...") +seq_len = 128 +dummy_ids = np.ones((1, seq_len), dtype=np.int64) +dummy_mask = np.ones((1, seq_len), dtype=np.int64) +dummy_types = np.zeros((1, seq_len), dtype=np.int64) + +input_names = [inp.name for inp in sess.get_inputs()] +print(f"Input names: {input_names}") + +feed = {} +for name in input_names: + if "type" in name: + feed[name] = dummy_types + elif "mask" in name: + feed[name] = dummy_mask + else: + feed[name] = dummy_ids + +t0 = time.time() +outputs = sess.run(None, feed) +t1 = time.time() +print(f"Inference completed in {(t1-t0)*1000:.1f}ms") +print(f"Output shape: {outputs[0].shape}")