commit 2778f145743392f19673615d03e3278567998839 Author: Breadway Date: Thu Jul 2 20:59:18 2026 +0800 Initial commit 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}")