[READ-ONLY] Mirror of https://github.com/mrgnw/spatial-maker.
0

Configure Feed

Select the types of activity you want to include in your feed.

Restructure as publishable crate with CoreML-first depth estimation

Replace candle_onnx with CoreML Swift bridge (default) and ort ONNX
(optional fallback). Port Frame's full API surface: async video pipeline,
multi-format image loader, SBS/TAB/Separate output with MV-HEVC, proper
error types, and model management with correct download URLs.

Feature-gated: coreml (default), onnx (optional), avif/jxl/heic.
Video pipeline now caches the depth model across frames.
Frame can use this as a direct path dep with zero worker.rs changes.

+3122 -509
+1417 -61
Cargo.lock
··· 115 115 ] 116 116 117 117 [[package]] 118 + name = "atomic-waker" 119 + version = "1.1.2" 120 + source = "registry+https://github.com/rust-lang/crates.io-index" 121 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 122 + 123 + [[package]] 118 124 name = "autocfg" 119 125 version = "1.5.0" 120 126 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 174 180 version = "1.8.3" 175 181 source = "registry+https://github.com/rust-lang/crates.io-index" 176 182 checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" 177 - 178 - [[package]] 179 - name = "bit_field" 180 - version = "0.10.3" 181 - source = "registry+https://github.com/rust-lang/crates.io-index" 182 - checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" 183 183 184 184 [[package]] 185 185 name = "bitflags" ··· 245 245 ] 246 246 247 247 [[package]] 248 + name = "cfg-expr" 249 + version = "0.20.6" 250 + source = "registry+https://github.com/rust-lang/crates.io-index" 251 + checksum = "78cef5b5a1a6827c7322ae2a636368a573006b27cfa76c7ebd53e834daeaab6a" 252 + dependencies = [ 253 + "smallvec", 254 + "target-lexicon", 255 + ] 256 + 257 + [[package]] 248 258 name = "cfg-if" 249 259 version = "1.0.4" 250 260 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 304 314 305 315 [[package]] 306 316 name = "core-foundation" 317 + version = "0.9.4" 318 + source = "registry+https://github.com/rust-lang/crates.io-index" 319 + checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 320 + dependencies = [ 321 + "core-foundation-sys", 322 + "libc", 323 + ] 324 + 325 + [[package]] 326 + name = "core-foundation" 307 327 version = "0.10.1" 308 328 source = "registry+https://github.com/rust-lang/crates.io-index" 309 329 checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" ··· 399 419 ] 400 420 401 421 [[package]] 422 + name = "displaydoc" 423 + version = "0.2.5" 424 + source = "registry+https://github.com/rust-lang/crates.io-index" 425 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 426 + dependencies = [ 427 + "proc-macro2", 428 + "quote", 429 + "syn", 430 + ] 431 + 432 + [[package]] 402 433 name = "either" 403 434 version = "1.15.0" 404 435 source = "registry+https://github.com/rust-lang/crates.io-index" 405 436 checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 406 437 407 438 [[package]] 439 + name = "encoding_rs" 440 + version = "0.8.35" 441 + source = "registry+https://github.com/rust-lang/crates.io-index" 442 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 443 + dependencies = [ 444 + "cfg-if", 445 + ] 446 + 447 + [[package]] 448 + name = "enumn" 449 + version = "0.1.14" 450 + source = "registry+https://github.com/rust-lang/crates.io-index" 451 + checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" 452 + dependencies = [ 453 + "proc-macro2", 454 + "quote", 455 + "syn", 456 + ] 457 + 458 + [[package]] 408 459 name = "equator" 409 460 version = "0.4.2" 410 461 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 441 492 ] 442 493 443 494 [[package]] 444 - name = "exr" 445 - version = "1.74.0" 446 - source = "registry+https://github.com/rust-lang/crates.io-index" 447 - checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" 448 - dependencies = [ 449 - "bit_field", 450 - "half", 451 - "lebe", 452 - "miniz_oxide", 453 - "rayon-core", 454 - "smallvec", 455 - "zune-inflate", 456 - ] 457 - 458 - [[package]] 459 495 name = "fastrand" 460 496 version = "2.3.0" 461 497 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 507 543 ] 508 544 509 545 [[package]] 546 + name = "fnv" 547 + version = "1.0.7" 548 + source = "registry+https://github.com/rust-lang/crates.io-index" 549 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 550 + 551 + [[package]] 510 552 name = "foldhash" 511 553 version = "0.1.5" 512 554 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 528 570 checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 529 571 530 572 [[package]] 573 + name = "form_urlencoded" 574 + version = "1.2.2" 575 + source = "registry+https://github.com/rust-lang/crates.io-index" 576 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 577 + dependencies = [ 578 + "percent-encoding", 579 + ] 580 + 581 + [[package]] 582 + name = "four-cc" 583 + version = "0.4.0" 584 + source = "registry+https://github.com/rust-lang/crates.io-index" 585 + checksum = "795cbfc56d419a7ce47ccbb7504dd9a5b7c484c083c356e797de08bd988d9629" 586 + 587 + [[package]] 588 + name = "futures-channel" 589 + version = "0.3.31" 590 + source = "registry+https://github.com/rust-lang/crates.io-index" 591 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 592 + dependencies = [ 593 + "futures-core", 594 + ] 595 + 596 + [[package]] 597 + name = "futures-core" 598 + version = "0.3.31" 599 + source = "registry+https://github.com/rust-lang/crates.io-index" 600 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 601 + 602 + [[package]] 603 + name = "futures-io" 604 + version = "0.3.31" 605 + source = "registry+https://github.com/rust-lang/crates.io-index" 606 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 607 + 608 + [[package]] 609 + name = "futures-macro" 610 + version = "0.3.31" 611 + source = "registry+https://github.com/rust-lang/crates.io-index" 612 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 613 + dependencies = [ 614 + "proc-macro2", 615 + "quote", 616 + "syn", 617 + ] 618 + 619 + [[package]] 620 + name = "futures-sink" 621 + version = "0.3.31" 622 + source = "registry+https://github.com/rust-lang/crates.io-index" 623 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 624 + 625 + [[package]] 626 + name = "futures-task" 627 + version = "0.3.31" 628 + source = "registry+https://github.com/rust-lang/crates.io-index" 629 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 630 + 631 + [[package]] 632 + name = "futures-util" 633 + version = "0.3.31" 634 + source = "registry+https://github.com/rust-lang/crates.io-index" 635 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 636 + dependencies = [ 637 + "futures-core", 638 + "futures-io", 639 + "futures-macro", 640 + "futures-sink", 641 + "futures-task", 642 + "memchr", 643 + "pin-project-lite", 644 + "pin-utils", 645 + "slab", 646 + ] 647 + 648 + [[package]] 531 649 name = "getrandom" 532 650 version = "0.2.17" 533 651 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 574 692 ] 575 693 576 694 [[package]] 695 + name = "h2" 696 + version = "0.4.13" 697 + source = "registry+https://github.com/rust-lang/crates.io-index" 698 + checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" 699 + dependencies = [ 700 + "atomic-waker", 701 + "bytes", 702 + "fnv", 703 + "futures-core", 704 + "futures-sink", 705 + "http", 706 + "indexmap", 707 + "slab", 708 + "tokio", 709 + "tokio-util", 710 + "tracing", 711 + ] 712 + 713 + [[package]] 577 714 name = "half" 578 715 version = "2.7.1" 579 716 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 622 759 ] 623 760 624 761 [[package]] 762 + name = "http-body" 763 + version = "1.0.1" 764 + source = "registry+https://github.com/rust-lang/crates.io-index" 765 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 766 + dependencies = [ 767 + "bytes", 768 + "http", 769 + ] 770 + 771 + [[package]] 772 + name = "http-body-util" 773 + version = "0.1.3" 774 + source = "registry+https://github.com/rust-lang/crates.io-index" 775 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 776 + dependencies = [ 777 + "bytes", 778 + "futures-core", 779 + "http", 780 + "http-body", 781 + "pin-project-lite", 782 + ] 783 + 784 + [[package]] 625 785 name = "httparse" 626 786 version = "1.10.1" 627 787 source = "registry+https://github.com/rust-lang/crates.io-index" 628 788 checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 629 789 630 790 [[package]] 791 + name = "hyper" 792 + version = "1.8.1" 793 + source = "registry+https://github.com/rust-lang/crates.io-index" 794 + checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 795 + dependencies = [ 796 + "atomic-waker", 797 + "bytes", 798 + "futures-channel", 799 + "futures-core", 800 + "h2", 801 + "http", 802 + "http-body", 803 + "httparse", 804 + "itoa", 805 + "pin-project-lite", 806 + "pin-utils", 807 + "smallvec", 808 + "tokio", 809 + "want", 810 + ] 811 + 812 + [[package]] 813 + name = "hyper-rustls" 814 + version = "0.27.7" 815 + source = "registry+https://github.com/rust-lang/crates.io-index" 816 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 817 + dependencies = [ 818 + "http", 819 + "hyper", 820 + "hyper-util", 821 + "rustls", 822 + "rustls-pki-types", 823 + "tokio", 824 + "tokio-rustls", 825 + "tower-service", 826 + ] 827 + 828 + [[package]] 829 + name = "hyper-tls" 830 + version = "0.6.0" 831 + source = "registry+https://github.com/rust-lang/crates.io-index" 832 + checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 833 + dependencies = [ 834 + "bytes", 835 + "http-body-util", 836 + "hyper", 837 + "hyper-util", 838 + "native-tls", 839 + "tokio", 840 + "tokio-native-tls", 841 + "tower-service", 842 + ] 843 + 844 + [[package]] 845 + name = "hyper-util" 846 + version = "0.1.20" 847 + source = "registry+https://github.com/rust-lang/crates.io-index" 848 + checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" 849 + dependencies = [ 850 + "base64", 851 + "bytes", 852 + "futures-channel", 853 + "futures-util", 854 + "http", 855 + "http-body", 856 + "hyper", 857 + "ipnet", 858 + "libc", 859 + "percent-encoding", 860 + "pin-project-lite", 861 + "socket2", 862 + "system-configuration", 863 + "tokio", 864 + "tower-service", 865 + "tracing", 866 + "windows-registry", 867 + ] 868 + 869 + [[package]] 870 + name = "icu_collections" 871 + version = "2.1.1" 872 + source = "registry+https://github.com/rust-lang/crates.io-index" 873 + checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 874 + dependencies = [ 875 + "displaydoc", 876 + "potential_utf", 877 + "yoke", 878 + "zerofrom", 879 + "zerovec", 880 + ] 881 + 882 + [[package]] 883 + name = "icu_locale_core" 884 + version = "2.1.1" 885 + source = "registry+https://github.com/rust-lang/crates.io-index" 886 + checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 887 + dependencies = [ 888 + "displaydoc", 889 + "litemap", 890 + "tinystr", 891 + "writeable", 892 + "zerovec", 893 + ] 894 + 895 + [[package]] 896 + name = "icu_normalizer" 897 + version = "2.1.1" 898 + source = "registry+https://github.com/rust-lang/crates.io-index" 899 + checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 900 + dependencies = [ 901 + "icu_collections", 902 + "icu_normalizer_data", 903 + "icu_properties", 904 + "icu_provider", 905 + "smallvec", 906 + "zerovec", 907 + ] 908 + 909 + [[package]] 910 + name = "icu_normalizer_data" 911 + version = "2.1.1" 912 + source = "registry+https://github.com/rust-lang/crates.io-index" 913 + checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 914 + 915 + [[package]] 916 + name = "icu_properties" 917 + version = "2.1.2" 918 + source = "registry+https://github.com/rust-lang/crates.io-index" 919 + checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" 920 + dependencies = [ 921 + "icu_collections", 922 + "icu_locale_core", 923 + "icu_properties_data", 924 + "icu_provider", 925 + "zerotrie", 926 + "zerovec", 927 + ] 928 + 929 + [[package]] 930 + name = "icu_properties_data" 931 + version = "2.1.2" 932 + source = "registry+https://github.com/rust-lang/crates.io-index" 933 + checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" 934 + 935 + [[package]] 936 + name = "icu_provider" 937 + version = "2.1.1" 938 + source = "registry+https://github.com/rust-lang/crates.io-index" 939 + checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 940 + dependencies = [ 941 + "displaydoc", 942 + "icu_locale_core", 943 + "writeable", 944 + "yoke", 945 + "zerofrom", 946 + "zerotrie", 947 + "zerovec", 948 + ] 949 + 950 + [[package]] 631 951 name = "id-arena" 632 952 version = "2.3.0" 633 953 source = "registry+https://github.com/rust-lang/crates.io-index" 634 954 checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" 635 955 636 956 [[package]] 957 + name = "idna" 958 + version = "1.1.0" 959 + source = "registry+https://github.com/rust-lang/crates.io-index" 960 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 961 + dependencies = [ 962 + "idna_adapter", 963 + "smallvec", 964 + "utf8_iter", 965 + ] 966 + 967 + [[package]] 968 + name = "idna_adapter" 969 + version = "1.2.1" 970 + source = "registry+https://github.com/rust-lang/crates.io-index" 971 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 972 + dependencies = [ 973 + "icu_normalizer", 974 + "icu_properties", 975 + ] 976 + 977 + [[package]] 637 978 name = "image" 638 979 version = "0.25.9" 639 980 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 642 983 "bytemuck", 643 984 "byteorder-lite", 644 985 "color_quant", 645 - "exr", 646 986 "gif", 647 987 "image-webp", 648 988 "moxcms", 649 989 "num-traits", 650 990 "png", 651 - "qoi", 652 991 "ravif", 653 - "rayon", 654 992 "rgb", 655 993 "tiff", 656 994 "zune-core 0.5.1", ··· 697 1035 ] 698 1036 699 1037 [[package]] 1038 + name = "ipnet" 1039 + version = "2.11.0" 1040 + source = "registry+https://github.com/rust-lang/crates.io-index" 1041 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1042 + 1043 + [[package]] 1044 + name = "iri-string" 1045 + version = "0.7.10" 1046 + source = "registry+https://github.com/rust-lang/crates.io-index" 1047 + checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" 1048 + dependencies = [ 1049 + "memchr", 1050 + "serde", 1051 + ] 1052 + 1053 + [[package]] 700 1054 name = "is_terminal_polyfill" 701 1055 version = "1.70.2" 702 1056 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 728 1082 ] 729 1083 730 1084 [[package]] 731 - name = "leb128fmt" 732 - version = "0.1.0" 1085 + name = "js-sys" 1086 + version = "0.3.85" 1087 + source = "registry+https://github.com/rust-lang/crates.io-index" 1088 + checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" 1089 + dependencies = [ 1090 + "once_cell", 1091 + "wasm-bindgen", 1092 + ] 1093 + 1094 + [[package]] 1095 + name = "jxl-bitstream" 1096 + version = "0.4.1" 1097 + source = "registry+https://github.com/rust-lang/crates.io-index" 1098 + checksum = "5855ff16398ffbcf81fee52c41ca65326499c8764b21bb9952c367ace98995fb" 1099 + dependencies = [ 1100 + "tracing", 1101 + ] 1102 + 1103 + [[package]] 1104 + name = "jxl-coding" 1105 + version = "0.4.1" 733 1106 source = "registry+https://github.com/rust-lang/crates.io-index" 734 - checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" 1107 + checksum = "da5b5093904e940bc11ef50e872c7bdf7b6e88653f012b925f8479daf212b5c9" 1108 + dependencies = [ 1109 + "jxl-bitstream", 1110 + "tracing", 1111 + ] 735 1112 736 1113 [[package]] 737 - name = "lebe" 1114 + name = "jxl-color" 1115 + version = "0.8.1" 1116 + source = "registry+https://github.com/rust-lang/crates.io-index" 1117 + checksum = "c8ec11695f2091e50531c970ad7eb4819989a20a2c89d68ae1b4f74f48454c10" 1118 + dependencies = [ 1119 + "jxl-bitstream", 1120 + "jxl-coding", 1121 + "jxl-grid", 1122 + "jxl-threadpool", 1123 + "tracing", 1124 + ] 1125 + 1126 + [[package]] 1127 + name = "jxl-frame" 1128 + version = "0.10.0" 1129 + source = "registry+https://github.com/rust-lang/crates.io-index" 1130 + checksum = "4719f285ebfff5e64f352d0ef149a5244aef4f8e6b5aa666ba6241e90b50632f" 1131 + dependencies = [ 1132 + "jxl-bitstream", 1133 + "jxl-coding", 1134 + "jxl-grid", 1135 + "jxl-image", 1136 + "jxl-modular", 1137 + "jxl-threadpool", 1138 + "jxl-vardct", 1139 + "tracing", 1140 + ] 1141 + 1142 + [[package]] 1143 + name = "jxl-grid" 738 1144 version = "0.5.3" 739 1145 source = "registry+https://github.com/rust-lang/crates.io-index" 740 - checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" 1146 + checksum = "5739f02add3d5c00320140bec6f5a80fac4baa630f88fe4c6a55a0d719718ce3" 1147 + dependencies = [ 1148 + "tracing", 1149 + ] 1150 + 1151 + [[package]] 1152 + name = "jxl-image" 1153 + version = "0.10.0" 1154 + source = "registry+https://github.com/rust-lang/crates.io-index" 1155 + checksum = "e3824c81613c05c19a9e4329d569145d3f460c0fcadb3965bd8418162d43f7f4" 1156 + dependencies = [ 1157 + "jxl-bitstream", 1158 + "jxl-color", 1159 + "jxl-grid", 1160 + "tracing", 1161 + ] 1162 + 1163 + [[package]] 1164 + name = "jxl-modular" 1165 + version = "0.8.1" 1166 + source = "registry+https://github.com/rust-lang/crates.io-index" 1167 + checksum = "d7e10bbc6041d9ea64bcfc6931ed89f0192954ac0a02bdbad624aa43b345e613" 1168 + dependencies = [ 1169 + "jxl-bitstream", 1170 + "jxl-coding", 1171 + "jxl-grid", 1172 + "jxl-threadpool", 1173 + "tracing", 1174 + ] 1175 + 1176 + [[package]] 1177 + name = "jxl-oxide" 1178 + version = "0.9.1" 1179 + source = "registry+https://github.com/rust-lang/crates.io-index" 1180 + checksum = "71f3b9dbe4adefadac57b25a15bf7735202ba58c0e5500c6bfb2d63398bf21c2" 1181 + dependencies = [ 1182 + "jxl-bitstream", 1183 + "jxl-color", 1184 + "jxl-frame", 1185 + "jxl-grid", 1186 + "jxl-image", 1187 + "jxl-render", 1188 + "jxl-threadpool", 1189 + "tracing", 1190 + ] 1191 + 1192 + [[package]] 1193 + name = "jxl-render" 1194 + version = "0.9.1" 1195 + source = "registry+https://github.com/rust-lang/crates.io-index" 1196 + checksum = "e4259446ca029587f2b7850d223d57b4f69665dd8628e83bcb0a6c2ab964f1ef" 1197 + dependencies = [ 1198 + "jxl-bitstream", 1199 + "jxl-coding", 1200 + "jxl-color", 1201 + "jxl-frame", 1202 + "jxl-grid", 1203 + "jxl-image", 1204 + "jxl-modular", 1205 + "jxl-threadpool", 1206 + "jxl-vardct", 1207 + "tracing", 1208 + ] 1209 + 1210 + [[package]] 1211 + name = "jxl-threadpool" 1212 + version = "0.1.2" 1213 + source = "registry+https://github.com/rust-lang/crates.io-index" 1214 + checksum = "ad9c78eaf899cce165e266300f9963d8d376d4ed95cf4d12dd7066f05542cd88" 1215 + dependencies = [ 1216 + "rayon", 1217 + "rayon-core", 1218 + "tracing", 1219 + ] 1220 + 1221 + [[package]] 1222 + name = "jxl-vardct" 1223 + version = "0.8.0" 1224 + source = "registry+https://github.com/rust-lang/crates.io-index" 1225 + checksum = "15da4b49b832b3d8a67329f47e2a1732e0847667938bb9b4a37d99a4668775c2" 1226 + dependencies = [ 1227 + "jxl-bitstream", 1228 + "jxl-coding", 1229 + "jxl-grid", 1230 + "jxl-modular", 1231 + "jxl-threadpool", 1232 + "tracing", 1233 + ] 1234 + 1235 + [[package]] 1236 + name = "leb128fmt" 1237 + version = "0.1.0" 1238 + source = "registry+https://github.com/rust-lang/crates.io-index" 1239 + checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" 741 1240 742 1241 [[package]] 743 1242 name = "libc" ··· 756 1255 ] 757 1256 758 1257 [[package]] 1258 + name = "libheif-rs" 1259 + version = "2.6.1" 1260 + source = "registry+https://github.com/rust-lang/crates.io-index" 1261 + checksum = "5de2912bca071d0f609cd6cb8d348c475439f69e808726374bea9e079591559e" 1262 + dependencies = [ 1263 + "cfg-if", 1264 + "enumn", 1265 + "four-cc", 1266 + "libc", 1267 + "libheif-sys", 1268 + ] 1269 + 1270 + [[package]] 1271 + name = "libheif-sys" 1272 + version = "5.1.1+1.21.2" 1273 + source = "registry+https://github.com/rust-lang/crates.io-index" 1274 + checksum = "03b69722f9386e24ae14655f8faddff28cb6a9a5511d7ffdde3a4499b51cca07" 1275 + dependencies = [ 1276 + "cfg-if", 1277 + "libc", 1278 + "system-deps", 1279 + "vcpkg", 1280 + "walkdir", 1281 + ] 1282 + 1283 + [[package]] 759 1284 name = "libredox" 760 1285 version = "0.1.12" 761 1286 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 772 1297 checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 773 1298 774 1299 [[package]] 1300 + name = "litemap" 1301 + version = "0.8.1" 1302 + source = "registry+https://github.com/rust-lang/crates.io-index" 1303 + checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 1304 + 1305 + [[package]] 1306 + name = "lock_api" 1307 + version = "0.4.14" 1308 + source = "registry+https://github.com/rust-lang/crates.io-index" 1309 + checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 1310 + dependencies = [ 1311 + "scopeguard", 1312 + ] 1313 + 1314 + [[package]] 775 1315 name = "log" 776 1316 version = "0.4.29" 777 1317 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 809 1349 checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" 810 1350 dependencies = [ 811 1351 "cfg-if", 812 - "rayon", 813 1352 ] 814 1353 815 1354 [[package]] ··· 819 1358 checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" 820 1359 821 1360 [[package]] 1361 + name = "mime" 1362 + version = "0.3.17" 1363 + source = "registry+https://github.com/rust-lang/crates.io-index" 1364 + checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1365 + 1366 + [[package]] 822 1367 name = "miniz_oxide" 823 1368 version = "0.8.9" 824 1369 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 826 1371 dependencies = [ 827 1372 "adler2", 828 1373 "simd-adler32", 1374 + ] 1375 + 1376 + [[package]] 1377 + name = "mio" 1378 + version = "1.1.1" 1379 + source = "registry+https://github.com/rust-lang/crates.io-index" 1380 + checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" 1381 + dependencies = [ 1382 + "libc", 1383 + "wasi", 1384 + "windows-sys 0.61.2", 829 1385 ] 830 1386 831 1387 [[package]] ··· 853 1409 "security-framework", 854 1410 "security-framework-sys", 855 1411 "tempfile", 1412 + ] 1413 + 1414 + [[package]] 1415 + name = "ndarray" 1416 + version = "0.16.1" 1417 + source = "registry+https://github.com/rust-lang/crates.io-index" 1418 + checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" 1419 + dependencies = [ 1420 + "matrixmultiply", 1421 + "num-complex", 1422 + "num-integer", 1423 + "num-traits", 1424 + "portable-atomic", 1425 + "portable-atomic-util", 1426 + "rawpointer", 856 1427 ] 857 1428 858 1429 [[package]] ··· 1019 1590 checksum = "4a5df903c0d2c07b56950f1058104ab0c8557159f2741782223704de9be73c3c" 1020 1591 dependencies = [ 1021 1592 "half", 1022 - "ndarray", 1593 + "ndarray 0.17.2", 1023 1594 "ort-sys", 1024 1595 "smallvec", 1025 1596 "tracing", ··· 1038 1609 ] 1039 1610 1040 1611 [[package]] 1612 + name = "parking_lot" 1613 + version = "0.12.5" 1614 + source = "registry+https://github.com/rust-lang/crates.io-index" 1615 + checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 1616 + dependencies = [ 1617 + "lock_api", 1618 + "parking_lot_core", 1619 + ] 1620 + 1621 + [[package]] 1622 + name = "parking_lot_core" 1623 + version = "0.9.12" 1624 + source = "registry+https://github.com/rust-lang/crates.io-index" 1625 + checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 1626 + dependencies = [ 1627 + "cfg-if", 1628 + "libc", 1629 + "redox_syscall", 1630 + "smallvec", 1631 + "windows-link", 1632 + ] 1633 + 1634 + [[package]] 1041 1635 name = "paste" 1042 1636 version = "1.0.15" 1043 1637 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1071 1665 checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1072 1666 1073 1667 [[package]] 1668 + name = "pin-utils" 1669 + version = "0.1.0" 1670 + source = "registry+https://github.com/rust-lang/crates.io-index" 1671 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1672 + 1673 + [[package]] 1074 1674 name = "pkg-config" 1075 1675 version = "0.3.32" 1076 1676 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1105 1705 ] 1106 1706 1107 1707 [[package]] 1708 + name = "potential_utf" 1709 + version = "0.1.4" 1710 + source = "registry+https://github.com/rust-lang/crates.io-index" 1711 + checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 1712 + dependencies = [ 1713 + "zerovec", 1714 + ] 1715 + 1716 + [[package]] 1108 1717 name = "ppv-lite86" 1109 1718 version = "0.2.21" 1110 1719 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1158 1767 checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" 1159 1768 dependencies = [ 1160 1769 "num-traits", 1161 - ] 1162 - 1163 - [[package]] 1164 - name = "qoi" 1165 - version = "0.4.1" 1166 - source = "registry+https://github.com/rust-lang/crates.io-index" 1167 - checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" 1168 - dependencies = [ 1169 - "bytemuck", 1170 1770 ] 1171 1771 1172 1772 [[package]] ··· 1265 1865 "loop9", 1266 1866 "quick-error", 1267 1867 "rav1e", 1268 - "rayon", 1269 1868 "rgb", 1270 1869 ] 1271 1870 ··· 1296 1895 ] 1297 1896 1298 1897 [[package]] 1898 + name = "redox_syscall" 1899 + version = "0.5.18" 1900 + source = "registry+https://github.com/rust-lang/crates.io-index" 1901 + checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 1902 + dependencies = [ 1903 + "bitflags", 1904 + ] 1905 + 1906 + [[package]] 1299 1907 name = "redox_users" 1300 1908 version = "0.4.6" 1301 1909 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1307 1915 ] 1308 1916 1309 1917 [[package]] 1918 + name = "reqwest" 1919 + version = "0.12.28" 1920 + source = "registry+https://github.com/rust-lang/crates.io-index" 1921 + checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 1922 + dependencies = [ 1923 + "base64", 1924 + "bytes", 1925 + "encoding_rs", 1926 + "futures-core", 1927 + "futures-util", 1928 + "h2", 1929 + "http", 1930 + "http-body", 1931 + "http-body-util", 1932 + "hyper", 1933 + "hyper-rustls", 1934 + "hyper-tls", 1935 + "hyper-util", 1936 + "js-sys", 1937 + "log", 1938 + "mime", 1939 + "native-tls", 1940 + "percent-encoding", 1941 + "pin-project-lite", 1942 + "rustls-pki-types", 1943 + "serde", 1944 + "serde_json", 1945 + "serde_urlencoded", 1946 + "sync_wrapper", 1947 + "tokio", 1948 + "tokio-native-tls", 1949 + "tokio-util", 1950 + "tower", 1951 + "tower-http", 1952 + "tower-service", 1953 + "url", 1954 + "wasm-bindgen", 1955 + "wasm-bindgen-futures", 1956 + "wasm-streams", 1957 + "web-sys", 1958 + ] 1959 + 1960 + [[package]] 1310 1961 name = "rgb" 1311 1962 version = "0.8.52" 1312 1963 source = "registry+https://github.com/rust-lang/crates.io-index" 1313 1964 checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" 1314 1965 1315 1966 [[package]] 1967 + name = "ring" 1968 + version = "0.17.14" 1969 + source = "registry+https://github.com/rust-lang/crates.io-index" 1970 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1971 + dependencies = [ 1972 + "cc", 1973 + "cfg-if", 1974 + "getrandom 0.2.17", 1975 + "libc", 1976 + "untrusted", 1977 + "windows-sys 0.52.0", 1978 + ] 1979 + 1980 + [[package]] 1316 1981 name = "rustix" 1317 1982 version = "1.1.3" 1318 1983 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1326 1991 ] 1327 1992 1328 1993 [[package]] 1994 + name = "rustls" 1995 + version = "0.23.36" 1996 + source = "registry+https://github.com/rust-lang/crates.io-index" 1997 + checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" 1998 + dependencies = [ 1999 + "once_cell", 2000 + "rustls-pki-types", 2001 + "rustls-webpki", 2002 + "subtle", 2003 + "zeroize", 2004 + ] 2005 + 2006 + [[package]] 1329 2007 name = "rustls-pki-types" 1330 2008 version = "1.14.0" 1331 2009 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1335 2013 ] 1336 2014 1337 2015 [[package]] 2016 + name = "rustls-webpki" 2017 + version = "0.103.9" 2018 + source = "registry+https://github.com/rust-lang/crates.io-index" 2019 + checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" 2020 + dependencies = [ 2021 + "ring", 2022 + "rustls-pki-types", 2023 + "untrusted", 2024 + ] 2025 + 2026 + [[package]] 1338 2027 name = "rustversion" 1339 2028 version = "1.0.22" 1340 2029 source = "registry+https://github.com/rust-lang/crates.io-index" 1341 2030 checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 2031 + 2032 + [[package]] 2033 + name = "ryu" 2034 + version = "1.0.23" 2035 + source = "registry+https://github.com/rust-lang/crates.io-index" 2036 + checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" 2037 + 2038 + [[package]] 2039 + name = "same-file" 2040 + version = "1.0.6" 2041 + source = "registry+https://github.com/rust-lang/crates.io-index" 2042 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 2043 + dependencies = [ 2044 + "winapi-util", 2045 + ] 1342 2046 1343 2047 [[package]] 1344 2048 name = "schannel" ··· 1350 2054 ] 1351 2055 1352 2056 [[package]] 2057 + name = "scopeguard" 2058 + version = "1.2.0" 2059 + source = "registry+https://github.com/rust-lang/crates.io-index" 2060 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 2061 + 2062 + [[package]] 1353 2063 name = "security-framework" 1354 2064 version = "3.6.0" 1355 2065 source = "registry+https://github.com/rust-lang/crates.io-index" 1356 2066 checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" 1357 2067 dependencies = [ 1358 2068 "bitflags", 1359 - "core-foundation", 2069 + "core-foundation 0.10.1", 1360 2070 "core-foundation-sys", 1361 2071 "libc", 1362 2072 "security-framework-sys", ··· 1385 2095 checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 1386 2096 dependencies = [ 1387 2097 "serde_core", 2098 + "serde_derive", 1388 2099 ] 1389 2100 1390 2101 [[package]] ··· 1421 2132 ] 1422 2133 1423 2134 [[package]] 2135 + name = "serde_spanned" 2136 + version = "1.0.4" 2137 + source = "registry+https://github.com/rust-lang/crates.io-index" 2138 + checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" 2139 + dependencies = [ 2140 + "serde_core", 2141 + ] 2142 + 2143 + [[package]] 2144 + name = "serde_urlencoded" 2145 + version = "0.7.1" 2146 + source = "registry+https://github.com/rust-lang/crates.io-index" 2147 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 2148 + dependencies = [ 2149 + "form_urlencoded", 2150 + "itoa", 2151 + "ryu", 2152 + "serde", 2153 + ] 2154 + 2155 + [[package]] 1424 2156 name = "shlex" 1425 2157 version = "1.3.0" 1426 2158 source = "registry+https://github.com/rust-lang/crates.io-index" 1427 2159 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1428 2160 1429 2161 [[package]] 2162 + name = "signal-hook-registry" 2163 + version = "1.4.8" 2164 + source = "registry+https://github.com/rust-lang/crates.io-index" 2165 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 2166 + dependencies = [ 2167 + "errno", 2168 + "libc", 2169 + ] 2170 + 2171 + [[package]] 1430 2172 name = "simd-adler32" 1431 2173 version = "0.3.8" 1432 2174 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1440 2182 dependencies = [ 1441 2183 "quote", 1442 2184 ] 2185 + 2186 + [[package]] 2187 + name = "slab" 2188 + version = "0.4.12" 2189 + source = "registry+https://github.com/rust-lang/crates.io-index" 2190 + checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" 1443 2191 1444 2192 [[package]] 1445 2193 name = "smallvec" ··· 1448 2196 checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1449 2197 1450 2198 [[package]] 2199 + name = "socket2" 2200 + version = "0.6.2" 2201 + source = "registry+https://github.com/rust-lang/crates.io-index" 2202 + checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" 2203 + dependencies = [ 2204 + "libc", 2205 + "windows-sys 0.60.2", 2206 + ] 2207 + 2208 + [[package]] 1451 2209 name = "socks" 1452 2210 version = "0.3.4" 1453 2211 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1462 2220 name = "spatial-maker" 1463 2221 version = "0.1.0" 1464 2222 dependencies = [ 1465 - "anyhow", 1466 2223 "clap", 1467 2224 "dirs", 2225 + "futures-util", 1468 2226 "image", 1469 - "ndarray", 2227 + "jxl-oxide", 2228 + "libheif-rs", 2229 + "ndarray 0.16.1", 1470 2230 "ort", 2231 + "reqwest", 2232 + "serde", 2233 + "tempfile", 2234 + "thiserror 2.0.18", 2235 + "tokio", 2236 + "tracing", 1471 2237 ] 1472 2238 1473 2239 [[package]] ··· 1483 2249 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1484 2250 1485 2251 [[package]] 2252 + name = "subtle" 2253 + version = "2.6.1" 2254 + source = "registry+https://github.com/rust-lang/crates.io-index" 2255 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 2256 + 2257 + [[package]] 1486 2258 name = "syn" 1487 2259 version = "2.0.115" 1488 2260 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1494 2266 ] 1495 2267 1496 2268 [[package]] 2269 + name = "sync_wrapper" 2270 + version = "1.0.2" 2271 + source = "registry+https://github.com/rust-lang/crates.io-index" 2272 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 2273 + dependencies = [ 2274 + "futures-core", 2275 + ] 2276 + 2277 + [[package]] 2278 + name = "synstructure" 2279 + version = "0.13.2" 2280 + source = "registry+https://github.com/rust-lang/crates.io-index" 2281 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 2282 + dependencies = [ 2283 + "proc-macro2", 2284 + "quote", 2285 + "syn", 2286 + ] 2287 + 2288 + [[package]] 2289 + name = "system-configuration" 2290 + version = "0.7.0" 2291 + source = "registry+https://github.com/rust-lang/crates.io-index" 2292 + checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" 2293 + dependencies = [ 2294 + "bitflags", 2295 + "core-foundation 0.9.4", 2296 + "system-configuration-sys", 2297 + ] 2298 + 2299 + [[package]] 2300 + name = "system-configuration-sys" 2301 + version = "0.6.0" 2302 + source = "registry+https://github.com/rust-lang/crates.io-index" 2303 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 2304 + dependencies = [ 2305 + "core-foundation-sys", 2306 + "libc", 2307 + ] 2308 + 2309 + [[package]] 2310 + name = "system-deps" 2311 + version = "7.0.7" 2312 + source = "registry+https://github.com/rust-lang/crates.io-index" 2313 + checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" 2314 + dependencies = [ 2315 + "cfg-expr", 2316 + "heck", 2317 + "pkg-config", 2318 + "toml", 2319 + "version-compare", 2320 + ] 2321 + 2322 + [[package]] 2323 + name = "target-lexicon" 2324 + version = "0.13.3" 2325 + source = "registry+https://github.com/rust-lang/crates.io-index" 2326 + checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" 2327 + 2328 + [[package]] 1497 2329 name = "tempfile" 1498 2330 version = "3.25.0" 1499 2331 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1561 2393 ] 1562 2394 1563 2395 [[package]] 2396 + name = "tinystr" 2397 + version = "0.8.2" 2398 + source = "registry+https://github.com/rust-lang/crates.io-index" 2399 + checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 2400 + dependencies = [ 2401 + "displaydoc", 2402 + "zerovec", 2403 + ] 2404 + 2405 + [[package]] 2406 + name = "tokio" 2407 + version = "1.49.0" 2408 + source = "registry+https://github.com/rust-lang/crates.io-index" 2409 + checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" 2410 + dependencies = [ 2411 + "bytes", 2412 + "libc", 2413 + "mio", 2414 + "parking_lot", 2415 + "pin-project-lite", 2416 + "signal-hook-registry", 2417 + "socket2", 2418 + "tokio-macros", 2419 + "windows-sys 0.61.2", 2420 + ] 2421 + 2422 + [[package]] 2423 + name = "tokio-macros" 2424 + version = "2.6.0" 2425 + source = "registry+https://github.com/rust-lang/crates.io-index" 2426 + checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 2427 + dependencies = [ 2428 + "proc-macro2", 2429 + "quote", 2430 + "syn", 2431 + ] 2432 + 2433 + [[package]] 2434 + name = "tokio-native-tls" 2435 + version = "0.3.1" 2436 + source = "registry+https://github.com/rust-lang/crates.io-index" 2437 + checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2438 + dependencies = [ 2439 + "native-tls", 2440 + "tokio", 2441 + ] 2442 + 2443 + [[package]] 2444 + name = "tokio-rustls" 2445 + version = "0.26.4" 2446 + source = "registry+https://github.com/rust-lang/crates.io-index" 2447 + checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 2448 + dependencies = [ 2449 + "rustls", 2450 + "tokio", 2451 + ] 2452 + 2453 + [[package]] 2454 + name = "tokio-util" 2455 + version = "0.7.18" 2456 + source = "registry+https://github.com/rust-lang/crates.io-index" 2457 + checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" 2458 + dependencies = [ 2459 + "bytes", 2460 + "futures-core", 2461 + "futures-sink", 2462 + "pin-project-lite", 2463 + "tokio", 2464 + ] 2465 + 2466 + [[package]] 2467 + name = "toml" 2468 + version = "0.9.12+spec-1.1.0" 2469 + source = "registry+https://github.com/rust-lang/crates.io-index" 2470 + checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" 2471 + dependencies = [ 2472 + "indexmap", 2473 + "serde_core", 2474 + "serde_spanned", 2475 + "toml_datetime", 2476 + "toml_parser", 2477 + "toml_writer", 2478 + "winnow", 2479 + ] 2480 + 2481 + [[package]] 2482 + name = "toml_datetime" 2483 + version = "0.7.5+spec-1.1.0" 2484 + source = "registry+https://github.com/rust-lang/crates.io-index" 2485 + checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" 2486 + dependencies = [ 2487 + "serde_core", 2488 + ] 2489 + 2490 + [[package]] 2491 + name = "toml_parser" 2492 + version = "1.0.8+spec-1.1.0" 2493 + source = "registry+https://github.com/rust-lang/crates.io-index" 2494 + checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc" 2495 + dependencies = [ 2496 + "winnow", 2497 + ] 2498 + 2499 + [[package]] 2500 + name = "toml_writer" 2501 + version = "1.0.6+spec-1.1.0" 2502 + source = "registry+https://github.com/rust-lang/crates.io-index" 2503 + checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" 2504 + 2505 + [[package]] 2506 + name = "tower" 2507 + version = "0.5.3" 2508 + source = "registry+https://github.com/rust-lang/crates.io-index" 2509 + checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" 2510 + dependencies = [ 2511 + "futures-core", 2512 + "futures-util", 2513 + "pin-project-lite", 2514 + "sync_wrapper", 2515 + "tokio", 2516 + "tower-layer", 2517 + "tower-service", 2518 + ] 2519 + 2520 + [[package]] 2521 + name = "tower-http" 2522 + version = "0.6.8" 2523 + source = "registry+https://github.com/rust-lang/crates.io-index" 2524 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 2525 + dependencies = [ 2526 + "bitflags", 2527 + "bytes", 2528 + "futures-util", 2529 + "http", 2530 + "http-body", 2531 + "iri-string", 2532 + "pin-project-lite", 2533 + "tower", 2534 + "tower-layer", 2535 + "tower-service", 2536 + ] 2537 + 2538 + [[package]] 2539 + name = "tower-layer" 2540 + version = "0.3.3" 2541 + source = "registry+https://github.com/rust-lang/crates.io-index" 2542 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2543 + 2544 + [[package]] 2545 + name = "tower-service" 2546 + version = "0.3.3" 2547 + source = "registry+https://github.com/rust-lang/crates.io-index" 2548 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2549 + 2550 + [[package]] 1564 2551 name = "tracing" 1565 2552 version = "0.1.44" 1566 2553 source = "registry+https://github.com/rust-lang/crates.io-index" 1567 2554 checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 1568 2555 dependencies = [ 1569 2556 "pin-project-lite", 2557 + "tracing-attributes", 1570 2558 "tracing-core", 1571 2559 ] 1572 2560 1573 2561 [[package]] 2562 + name = "tracing-attributes" 2563 + version = "0.1.31" 2564 + source = "registry+https://github.com/rust-lang/crates.io-index" 2565 + checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 2566 + dependencies = [ 2567 + "proc-macro2", 2568 + "quote", 2569 + "syn", 2570 + ] 2571 + 2572 + [[package]] 1574 2573 name = "tracing-core" 1575 2574 version = "0.1.36" 1576 2575 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1580 2579 ] 1581 2580 1582 2581 [[package]] 2582 + name = "try-lock" 2583 + version = "0.2.5" 2584 + source = "registry+https://github.com/rust-lang/crates.io-index" 2585 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2586 + 2587 + [[package]] 1583 2588 name = "unicode-ident" 1584 2589 version = "1.0.23" 1585 2590 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1592 2597 checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 1593 2598 1594 2599 [[package]] 2600 + name = "untrusted" 2601 + version = "0.9.0" 2602 + source = "registry+https://github.com/rust-lang/crates.io-index" 2603 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2604 + 2605 + [[package]] 1595 2606 name = "ureq" 1596 2607 version = "3.2.0" 1597 2608 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1622 2633 ] 1623 2634 1624 2635 [[package]] 2636 + name = "url" 2637 + version = "2.5.8" 2638 + source = "registry+https://github.com/rust-lang/crates.io-index" 2639 + checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" 2640 + dependencies = [ 2641 + "form_urlencoded", 2642 + "idna", 2643 + "percent-encoding", 2644 + "serde", 2645 + ] 2646 + 2647 + [[package]] 1625 2648 name = "utf-8" 1626 2649 version = "0.7.6" 1627 2650 source = "registry+https://github.com/rust-lang/crates.io-index" 1628 2651 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 2652 + 2653 + [[package]] 2654 + name = "utf8_iter" 2655 + version = "1.0.4" 2656 + source = "registry+https://github.com/rust-lang/crates.io-index" 2657 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1629 2658 1630 2659 [[package]] 1631 2660 name = "utf8parse" ··· 1651 2680 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1652 2681 1653 2682 [[package]] 2683 + name = "version-compare" 2684 + version = "0.2.1" 2685 + source = "registry+https://github.com/rust-lang/crates.io-index" 2686 + checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" 2687 + 2688 + [[package]] 2689 + name = "walkdir" 2690 + version = "2.5.0" 2691 + source = "registry+https://github.com/rust-lang/crates.io-index" 2692 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 2693 + dependencies = [ 2694 + "same-file", 2695 + "winapi-util", 2696 + ] 2697 + 2698 + [[package]] 2699 + name = "want" 2700 + version = "0.3.1" 2701 + source = "registry+https://github.com/rust-lang/crates.io-index" 2702 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2703 + dependencies = [ 2704 + "try-lock", 2705 + ] 2706 + 2707 + [[package]] 1654 2708 name = "wasi" 1655 2709 version = "0.11.1+wasi-snapshot-preview1" 1656 2710 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1685 2739 "rustversion", 1686 2740 "wasm-bindgen-macro", 1687 2741 "wasm-bindgen-shared", 2742 + ] 2743 + 2744 + [[package]] 2745 + name = "wasm-bindgen-futures" 2746 + version = "0.4.58" 2747 + source = "registry+https://github.com/rust-lang/crates.io-index" 2748 + checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" 2749 + dependencies = [ 2750 + "cfg-if", 2751 + "futures-util", 2752 + "js-sys", 2753 + "once_cell", 2754 + "wasm-bindgen", 2755 + "web-sys", 1688 2756 ] 1689 2757 1690 2758 [[package]] ··· 1742 2810 ] 1743 2811 1744 2812 [[package]] 2813 + name = "wasm-streams" 2814 + version = "0.4.2" 2815 + source = "registry+https://github.com/rust-lang/crates.io-index" 2816 + checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" 2817 + dependencies = [ 2818 + "futures-util", 2819 + "js-sys", 2820 + "wasm-bindgen", 2821 + "wasm-bindgen-futures", 2822 + "web-sys", 2823 + ] 2824 + 2825 + [[package]] 1745 2826 name = "wasmparser" 1746 2827 version = "0.244.0" 1747 2828 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1751 2832 "hashbrown 0.15.5", 1752 2833 "indexmap", 1753 2834 "semver", 2835 + ] 2836 + 2837 + [[package]] 2838 + name = "web-sys" 2839 + version = "0.3.85" 2840 + source = "registry+https://github.com/rust-lang/crates.io-index" 2841 + checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" 2842 + dependencies = [ 2843 + "js-sys", 2844 + "wasm-bindgen", 1754 2845 ] 1755 2846 1756 2847 [[package]] ··· 1785 2876 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1786 2877 1787 2878 [[package]] 2879 + name = "winapi-util" 2880 + version = "0.1.11" 2881 + source = "registry+https://github.com/rust-lang/crates.io-index" 2882 + checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 2883 + dependencies = [ 2884 + "windows-sys 0.61.2", 2885 + ] 2886 + 2887 + [[package]] 1788 2888 name = "winapi-x86_64-pc-windows-gnu" 1789 2889 version = "0.4.0" 1790 2890 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1797 2897 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1798 2898 1799 2899 [[package]] 2900 + name = "windows-registry" 2901 + version = "0.6.1" 2902 + source = "registry+https://github.com/rust-lang/crates.io-index" 2903 + checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" 2904 + dependencies = [ 2905 + "windows-link", 2906 + "windows-result", 2907 + "windows-strings", 2908 + ] 2909 + 2910 + [[package]] 2911 + name = "windows-result" 2912 + version = "0.4.1" 2913 + source = "registry+https://github.com/rust-lang/crates.io-index" 2914 + checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 2915 + dependencies = [ 2916 + "windows-link", 2917 + ] 2918 + 2919 + [[package]] 2920 + name = "windows-strings" 2921 + version = "0.5.1" 2922 + source = "registry+https://github.com/rust-lang/crates.io-index" 2923 + checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 2924 + dependencies = [ 2925 + "windows-link", 2926 + ] 2927 + 2928 + [[package]] 1800 2929 name = "windows-sys" 1801 2930 version = "0.48.0" 1802 2931 source = "registry+https://github.com/rust-lang/crates.io-index" 1803 2932 checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1804 2933 dependencies = [ 1805 - "windows-targets", 2934 + "windows-targets 0.48.5", 2935 + ] 2936 + 2937 + [[package]] 2938 + name = "windows-sys" 2939 + version = "0.52.0" 2940 + source = "registry+https://github.com/rust-lang/crates.io-index" 2941 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2942 + dependencies = [ 2943 + "windows-targets 0.52.6", 2944 + ] 2945 + 2946 + [[package]] 2947 + name = "windows-sys" 2948 + version = "0.60.2" 2949 + source = "registry+https://github.com/rust-lang/crates.io-index" 2950 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 2951 + dependencies = [ 2952 + "windows-targets 0.53.5", 1806 2953 ] 1807 2954 1808 2955 [[package]] ··· 1820 2967 source = "registry+https://github.com/rust-lang/crates.io-index" 1821 2968 checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1822 2969 dependencies = [ 1823 - "windows_aarch64_gnullvm", 1824 - "windows_aarch64_msvc", 1825 - "windows_i686_gnu", 1826 - "windows_i686_msvc", 1827 - "windows_x86_64_gnu", 1828 - "windows_x86_64_gnullvm", 1829 - "windows_x86_64_msvc", 2970 + "windows_aarch64_gnullvm 0.48.5", 2971 + "windows_aarch64_msvc 0.48.5", 2972 + "windows_i686_gnu 0.48.5", 2973 + "windows_i686_msvc 0.48.5", 2974 + "windows_x86_64_gnu 0.48.5", 2975 + "windows_x86_64_gnullvm 0.48.5", 2976 + "windows_x86_64_msvc 0.48.5", 2977 + ] 2978 + 2979 + [[package]] 2980 + name = "windows-targets" 2981 + version = "0.52.6" 2982 + source = "registry+https://github.com/rust-lang/crates.io-index" 2983 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2984 + dependencies = [ 2985 + "windows_aarch64_gnullvm 0.52.6", 2986 + "windows_aarch64_msvc 0.52.6", 2987 + "windows_i686_gnu 0.52.6", 2988 + "windows_i686_gnullvm 0.52.6", 2989 + "windows_i686_msvc 0.52.6", 2990 + "windows_x86_64_gnu 0.52.6", 2991 + "windows_x86_64_gnullvm 0.52.6", 2992 + "windows_x86_64_msvc 0.52.6", 2993 + ] 2994 + 2995 + [[package]] 2996 + name = "windows-targets" 2997 + version = "0.53.5" 2998 + source = "registry+https://github.com/rust-lang/crates.io-index" 2999 + checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 3000 + dependencies = [ 3001 + "windows-link", 3002 + "windows_aarch64_gnullvm 0.53.1", 3003 + "windows_aarch64_msvc 0.53.1", 3004 + "windows_i686_gnu 0.53.1", 3005 + "windows_i686_gnullvm 0.53.1", 3006 + "windows_i686_msvc 0.53.1", 3007 + "windows_x86_64_gnu 0.53.1", 3008 + "windows_x86_64_gnullvm 0.53.1", 3009 + "windows_x86_64_msvc 0.53.1", 1830 3010 ] 1831 3011 1832 3012 [[package]] ··· 1836 3016 checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1837 3017 1838 3018 [[package]] 3019 + name = "windows_aarch64_gnullvm" 3020 + version = "0.52.6" 3021 + source = "registry+https://github.com/rust-lang/crates.io-index" 3022 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 3023 + 3024 + [[package]] 3025 + name = "windows_aarch64_gnullvm" 3026 + version = "0.53.1" 3027 + source = "registry+https://github.com/rust-lang/crates.io-index" 3028 + checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 3029 + 3030 + [[package]] 1839 3031 name = "windows_aarch64_msvc" 1840 3032 version = "0.48.5" 1841 3033 source = "registry+https://github.com/rust-lang/crates.io-index" 1842 3034 checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1843 3035 1844 3036 [[package]] 3037 + name = "windows_aarch64_msvc" 3038 + version = "0.52.6" 3039 + source = "registry+https://github.com/rust-lang/crates.io-index" 3040 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 3041 + 3042 + [[package]] 3043 + name = "windows_aarch64_msvc" 3044 + version = "0.53.1" 3045 + source = "registry+https://github.com/rust-lang/crates.io-index" 3046 + checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 3047 + 3048 + [[package]] 1845 3049 name = "windows_i686_gnu" 1846 3050 version = "0.48.5" 1847 3051 source = "registry+https://github.com/rust-lang/crates.io-index" 1848 3052 checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1849 3053 1850 3054 [[package]] 3055 + name = "windows_i686_gnu" 3056 + version = "0.52.6" 3057 + source = "registry+https://github.com/rust-lang/crates.io-index" 3058 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 3059 + 3060 + [[package]] 3061 + name = "windows_i686_gnu" 3062 + version = "0.53.1" 3063 + source = "registry+https://github.com/rust-lang/crates.io-index" 3064 + checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 3065 + 3066 + [[package]] 3067 + name = "windows_i686_gnullvm" 3068 + version = "0.52.6" 3069 + source = "registry+https://github.com/rust-lang/crates.io-index" 3070 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 3071 + 3072 + [[package]] 3073 + name = "windows_i686_gnullvm" 3074 + version = "0.53.1" 3075 + source = "registry+https://github.com/rust-lang/crates.io-index" 3076 + checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 3077 + 3078 + [[package]] 1851 3079 name = "windows_i686_msvc" 1852 3080 version = "0.48.5" 1853 3081 source = "registry+https://github.com/rust-lang/crates.io-index" 1854 3082 checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1855 3083 1856 3084 [[package]] 3085 + name = "windows_i686_msvc" 3086 + version = "0.52.6" 3087 + source = "registry+https://github.com/rust-lang/crates.io-index" 3088 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 3089 + 3090 + [[package]] 3091 + name = "windows_i686_msvc" 3092 + version = "0.53.1" 3093 + source = "registry+https://github.com/rust-lang/crates.io-index" 3094 + checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 3095 + 3096 + [[package]] 1857 3097 name = "windows_x86_64_gnu" 1858 3098 version = "0.48.5" 1859 3099 source = "registry+https://github.com/rust-lang/crates.io-index" 1860 3100 checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1861 3101 1862 3102 [[package]] 3103 + name = "windows_x86_64_gnu" 3104 + version = "0.52.6" 3105 + source = "registry+https://github.com/rust-lang/crates.io-index" 3106 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 3107 + 3108 + [[package]] 3109 + name = "windows_x86_64_gnu" 3110 + version = "0.53.1" 3111 + source = "registry+https://github.com/rust-lang/crates.io-index" 3112 + checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 3113 + 3114 + [[package]] 1863 3115 name = "windows_x86_64_gnullvm" 1864 3116 version = "0.48.5" 1865 3117 source = "registry+https://github.com/rust-lang/crates.io-index" 1866 3118 checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1867 3119 1868 3120 [[package]] 3121 + name = "windows_x86_64_gnullvm" 3122 + version = "0.52.6" 3123 + source = "registry+https://github.com/rust-lang/crates.io-index" 3124 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 3125 + 3126 + [[package]] 3127 + name = "windows_x86_64_gnullvm" 3128 + version = "0.53.1" 3129 + source = "registry+https://github.com/rust-lang/crates.io-index" 3130 + checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 3131 + 3132 + [[package]] 1869 3133 name = "windows_x86_64_msvc" 1870 3134 version = "0.48.5" 1871 3135 source = "registry+https://github.com/rust-lang/crates.io-index" 1872 3136 checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 3137 + 3138 + [[package]] 3139 + name = "windows_x86_64_msvc" 3140 + version = "0.52.6" 3141 + source = "registry+https://github.com/rust-lang/crates.io-index" 3142 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3143 + 3144 + [[package]] 3145 + name = "windows_x86_64_msvc" 3146 + version = "0.53.1" 3147 + source = "registry+https://github.com/rust-lang/crates.io-index" 3148 + checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 3149 + 3150 + [[package]] 3151 + name = "winnow" 3152 + version = "0.7.14" 3153 + source = "registry+https://github.com/rust-lang/crates.io-index" 3154 + checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" 1873 3155 1874 3156 [[package]] 1875 3157 name = "wit-bindgen" ··· 1960 3242 ] 1961 3243 1962 3244 [[package]] 3245 + name = "writeable" 3246 + version = "0.6.2" 3247 + source = "registry+https://github.com/rust-lang/crates.io-index" 3248 + checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 3249 + 3250 + [[package]] 1963 3251 name = "y4m" 1964 3252 version = "0.8.0" 1965 3253 source = "registry+https://github.com/rust-lang/crates.io-index" 1966 3254 checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" 1967 3255 1968 3256 [[package]] 3257 + name = "yoke" 3258 + version = "0.8.1" 3259 + source = "registry+https://github.com/rust-lang/crates.io-index" 3260 + checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 3261 + dependencies = [ 3262 + "stable_deref_trait", 3263 + "yoke-derive", 3264 + "zerofrom", 3265 + ] 3266 + 3267 + [[package]] 3268 + name = "yoke-derive" 3269 + version = "0.8.1" 3270 + source = "registry+https://github.com/rust-lang/crates.io-index" 3271 + checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 3272 + dependencies = [ 3273 + "proc-macro2", 3274 + "quote", 3275 + "syn", 3276 + "synstructure", 3277 + ] 3278 + 3279 + [[package]] 1969 3280 name = "zerocopy" 1970 3281 version = "0.8.39" 1971 3282 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1986 3297 ] 1987 3298 1988 3299 [[package]] 3300 + name = "zerofrom" 3301 + version = "0.1.6" 3302 + source = "registry+https://github.com/rust-lang/crates.io-index" 3303 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 3304 + dependencies = [ 3305 + "zerofrom-derive", 3306 + ] 3307 + 3308 + [[package]] 3309 + name = "zerofrom-derive" 3310 + version = "0.1.6" 3311 + source = "registry+https://github.com/rust-lang/crates.io-index" 3312 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 3313 + dependencies = [ 3314 + "proc-macro2", 3315 + "quote", 3316 + "syn", 3317 + "synstructure", 3318 + ] 3319 + 3320 + [[package]] 1989 3321 name = "zeroize" 1990 3322 version = "1.8.2" 1991 3323 source = "registry+https://github.com/rust-lang/crates.io-index" 1992 3324 checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 1993 3325 1994 3326 [[package]] 3327 + name = "zerotrie" 3328 + version = "0.2.3" 3329 + source = "registry+https://github.com/rust-lang/crates.io-index" 3330 + checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 3331 + dependencies = [ 3332 + "displaydoc", 3333 + "yoke", 3334 + "zerofrom", 3335 + ] 3336 + 3337 + [[package]] 3338 + name = "zerovec" 3339 + version = "0.11.5" 3340 + source = "registry+https://github.com/rust-lang/crates.io-index" 3341 + checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 3342 + dependencies = [ 3343 + "yoke", 3344 + "zerofrom", 3345 + "zerovec-derive", 3346 + ] 3347 + 3348 + [[package]] 3349 + name = "zerovec-derive" 3350 + version = "0.11.2" 3351 + source = "registry+https://github.com/rust-lang/crates.io-index" 3352 + checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 3353 + dependencies = [ 3354 + "proc-macro2", 3355 + "quote", 3356 + "syn", 3357 + ] 3358 + 3359 + [[package]] 1995 3360 name = "zmij" 1996 3361 version = "1.0.21" 1997 3362 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2008 3373 version = "0.5.1" 2009 3374 source = "registry+https://github.com/rust-lang/crates.io-index" 2010 3375 checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" 2011 - 2012 - [[package]] 2013 - name = "zune-inflate" 2014 - version = "0.2.54" 2015 - source = "registry+https://github.com/rust-lang/crates.io-index" 2016 - checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" 2017 - dependencies = [ 2018 - "simd-adler32", 2019 - ] 2020 3376 2021 3377 [[package]] 2022 3378 name = "zune-jpeg"
+62 -5
Cargo.toml
··· 5 5 license = "MIT" 6 6 description = "Convert 2D images and videos to stereoscopic 3D spatial content for Apple Vision Pro using AI depth estimation" 7 7 keywords = ["spatial-video", "3d", "stereoscopic", "apple-vision-pro", "depth-estimation"] 8 + categories = ["multimedia::images", "computer-vision"] 9 + repository = "https://github.com/mrgnw/spatial-maker" 10 + readme = "RUST_README.md" 11 + rust-version = "1.75" 12 + exclude = [ 13 + "spatial-maker-rust/", 14 + "test_*.jpg", 15 + "*.py", 16 + "benchmarking/", 17 + "samples/", 18 + "todos/", 19 + "output/", 20 + "checkpoints/", 21 + "__pycache__/", 22 + ".venv/", 23 + "scripts/", 24 + "research/", 25 + "pyproject.toml", 26 + "uv.lock", 27 + "pipeline.py", 28 + ] 29 + 30 + [lib] 31 + name = "spatial_maker" 32 + path = "src/lib.rs" 33 + 34 + [features] 35 + default = ["coreml"] 36 + coreml = [] 37 + onnx = ["dep:ort"] 38 + 39 + # Native format decoders (optional - falls back to ffmpeg if not enabled) 40 + avif = ["image/avif"] 41 + jxl = ["dep:jxl-oxide"] 42 + heic = ["dep:libheif-rs"] 43 + native-formats = ["avif", "jxl", "heic"] 8 44 9 45 [dependencies] 10 - ort = { version = "2.0.0-rc.11", features = ["coreml", "ndarray", "half", "download-binaries"] } 11 - image = "0.25" 12 - ndarray = "0.17" 13 - anyhow = "1.0" 46 + image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "bmp", "tiff", "webp"] } 47 + ndarray = "0.16" 48 + thiserror = "2" 49 + serde = { version = "1.0", features = ["derive"] } 50 + tokio = { version = "1.0", features = ["full"] } 51 + reqwest = { version = "0.12", features = ["stream"] } 52 + futures-util = "0.3" 14 53 dirs = "5.0" 54 + tracing = "0.1" 55 + 56 + # Optional: ONNX Runtime backend 57 + ort = { version = "2.0.0-rc.11", features = ["half", "download-binaries"], optional = true } 15 58 16 - [build-dependencies] 59 + # Optional native decoders 60 + jxl-oxide = { version = "0.9", optional = true } 61 + libheif-rs = { version = "2.1", optional = true } 17 62 18 63 [dev-dependencies] 19 64 clap = { version = "4", features = ["derive"] } 65 + tempfile = "3.8" 66 + tokio = { version = "1.0", features = ["full", "macros"] } 67 + 68 + [[example]] 69 + name = "photo_coreml" 70 + path = "examples/photo_coreml.rs" 71 + required-features = ["coreml"] 72 + 73 + [[example]] 74 + name = "photo" 75 + path = "examples/photo.rs" 76 + required-features = ["onnx"]
+41 -43
build.rs
··· 2 2 use std::process::Command; 3 3 4 4 fn main() { 5 - // Only build Swift bridge on macOS 6 - #[cfg(target_os = "macos")] 7 - { 8 - println!("cargo:rerun-if-changed=swift-bridge/CoreMLDepth.swift"); 5 + #[cfg(target_os = "macos")] 6 + { 7 + if env::var("CARGO_FEATURE_COREML").is_ok() { 8 + println!("cargo:rerun-if-changed=swift-bridge/CoreMLDepth.swift"); 9 9 10 - let out_dir = env::var("OUT_DIR").unwrap(); 11 - let lib_path = format!("{}/libCoreMLDepth.a", out_dir); 10 + let out_dir = env::var("OUT_DIR").unwrap(); 11 + let lib_path = format!("{}/libCoreMLDepth.a", out_dir); 12 12 13 - // Compile Swift to static library 14 - let status = Command::new("swiftc") 15 - .args(&[ 16 - "-emit-library", 17 - "-static", 18 - "-module-name", 19 - "CoreMLDepth", 20 - "-O", 21 - "swift-bridge/CoreMLDepth.swift", 22 - "-o", 23 - &lib_path, 24 - ]) 25 - .status() 26 - .expect("Failed to compile Swift code. Is Xcode Command Line Tools installed?"); 13 + let status = Command::new("swiftc") 14 + .args(&[ 15 + "-emit-library", 16 + "-static", 17 + "-module-name", 18 + "CoreMLDepth", 19 + "-O", 20 + "swift-bridge/CoreMLDepth.swift", 21 + "-o", 22 + &lib_path, 23 + ]) 24 + .status() 25 + .expect("Failed to compile Swift code. Is Xcode Command Line Tools installed?"); 27 26 28 - if !status.success() { 29 - panic!("swiftc compilation failed"); 30 - } 27 + if !status.success() { 28 + panic!("swiftc compilation failed"); 29 + } 31 30 32 - println!("cargo:rustc-link-search=native={}", out_dir); 33 - println!("cargo:rustc-link-lib=static=CoreMLDepth"); 31 + println!("cargo:rustc-link-search=native={}", out_dir); 32 + println!("cargo:rustc-link-lib=static=CoreMLDepth"); 34 33 35 - // Link Apple frameworks 36 - println!("cargo:rustc-link-lib=framework=CoreML"); 37 - println!("cargo:rustc-link-lib=framework=Foundation"); 38 - println!("cargo:rustc-link-lib=framework=CoreVideo"); 39 - println!("cargo:rustc-link-lib=framework=Accelerate"); 34 + println!("cargo:rustc-link-lib=framework=CoreML"); 35 + println!("cargo:rustc-link-lib=framework=Foundation"); 36 + println!("cargo:rustc-link-lib=framework=CoreVideo"); 37 + println!("cargo:rustc-link-lib=framework=Accelerate"); 40 38 41 - // Link Swift runtime 42 - let sdk_path = String::from_utf8( 43 - Command::new("xcrun") 44 - .args(&["--show-sdk-path"]) 45 - .output() 46 - .expect("Failed to find SDK path") 47 - .stdout, 48 - ) 49 - .unwrap(); 39 + let sdk_path = String::from_utf8( 40 + Command::new("xcrun") 41 + .args(&["--show-sdk-path"]) 42 + .output() 43 + .expect("Failed to find SDK path") 44 + .stdout, 45 + ) 46 + .unwrap(); 50 47 51 - let sdk_path = sdk_path.trim(); 52 - println!("cargo:rustc-link-search=native={}/usr/lib/swift", sdk_path); 53 - println!("cargo:rustc-link-search=native=/usr/lib/swift"); 54 - } 48 + let sdk_path = sdk_path.trim(); 49 + println!("cargo:rustc-link-search=native={}/usr/lib/swift", sdk_path); 50 + println!("cargo:rustc-link-search=native=/usr/lib/swift"); 51 + } 52 + } 55 53 }
+24 -34
examples/photo.rs
··· 1 - use anyhow::Result; 2 1 use clap::Parser; 3 - use spatial_maker::{create_stereo_pair, model, output, DepthEstimator}; 2 + use spatial_maker::{generate_stereo_pair, model, output, OnnxDepthEstimator}; 4 3 5 4 #[derive(Parser)] 6 - #[command(name = "spatial-maker-photo")] 7 - #[command(about = "Convert a 2D photo to side-by-side stereo", long_about = None)] 5 + #[command(name = "spatial-maker-onnx")] 6 + #[command(about = "Convert a 2D photo to side-by-side stereo using ONNX Runtime")] 8 7 struct Args { 9 - /// Input image file 10 - input: String, 8 + input: String, 11 9 12 - /// Output image file 13 - #[arg(short, long)] 14 - output: String, 10 + #[arg(short, long)] 11 + output: String, 15 12 16 - /// Maximum pixel disparity for 3D effect (default: 30) 17 - #[arg(long, default_value = "30.0")] 18 - max_disparity: f32, 13 + #[arg(long, default_value = "30")] 14 + max_disparity: u32, 19 15 20 - /// Model checkpoint name (default: depth_anything_v2_vits.onnx) 21 - #[arg(long, default_value = "depth_anything_v2_vits.onnx")] 22 - model: String, 16 + #[arg(long, default_value = "s")] 17 + encoder_size: String, 23 18 } 24 19 25 - fn main() -> Result<()> { 26 - let args = Args::parse(); 20 + fn main() -> Result<(), Box<dyn std::error::Error>> { 21 + let args = Args::parse(); 27 22 28 - println!("Loading image: {}", args.input); 29 - let input_image = image::open(&args.input)?; 23 + let input_image = image::open(&args.input)?; 30 24 31 - println!("Finding model checkpoint: {}", args.model); 32 - let model_path = model::find_checkpoint(&args.model)?; 33 - println!("Using model: {}", model_path.display()); 25 + let model_path = model::find_model(&args.encoder_size)?; 34 26 35 - println!("Initializing depth estimator..."); 36 - let mut estimator = DepthEstimator::new(model_path.to_str().unwrap())?; 27 + let estimator = OnnxDepthEstimator::new(model_path.to_str().unwrap())?; 37 28 38 - println!("Estimating depth..."); 39 - let depth_map = estimator.estimate(&input_image)?; 29 + let start = std::time::Instant::now(); 30 + let depth_map = estimator.estimate(&input_image)?; 31 + let depth_time = start.elapsed(); 32 + eprintln!("Depth estimation: {:?}", depth_time); 40 33 41 - println!("Creating stereo pair..."); 42 - let (left, right) = create_stereo_pair(&input_image, &depth_map, args.max_disparity)?; 34 + let (left, right) = generate_stereo_pair(&input_image, &depth_map, args.max_disparity)?; 43 35 44 - println!("Creating SBS image..."); 45 - let sbs = output::create_sbs_image(left, right); 36 + let sbs = output::create_sbs_image(&left, &right); 37 + sbs.save(&args.output)?; 46 38 47 - println!("Saving to: {}", args.output); 48 - sbs.save(&args.output)?; 39 + eprintln!("Total time: {:?}", start.elapsed()); 49 40 50 - println!("Done!"); 51 - Ok(()) 41 + Ok(()) 52 42 }
+26 -41
examples/photo_coreml.rs
··· 1 - use anyhow::Result; 2 1 use clap::Parser; 3 - use spatial_maker::{create_stereo_pair, model, output, CoreMLDepthEstimator}; 2 + use spatial_maker::{generate_stereo_pair, model, output, CoreMLDepthEstimator}; 4 3 5 4 #[derive(Parser)] 6 5 #[command(name = "spatial-maker-coreml")] 7 - #[command(about = "Convert a 2D photo to side-by-side stereo using CoreML (macOS only)", long_about = None)] 6 + #[command(about = "Convert a 2D photo to side-by-side stereo using CoreML (macOS only)")] 8 7 struct Args { 9 - /// Input image file 10 - input: String, 8 + input: String, 11 9 12 - /// Output image file 13 - #[arg(short, long)] 14 - output: String, 10 + #[arg(short, long)] 11 + output: String, 15 12 16 - /// Maximum pixel disparity for 3D effect (default: 30) 17 - #[arg(long, default_value = "30.0")] 18 - max_disparity: f32, 13 + #[arg(long, default_value = "30")] 14 + max_disparity: u32, 19 15 20 - /// Model checkpoint name (default: DepthAnythingV2BaseF16.mlpackage) 21 - #[arg(long, default_value = "DepthAnythingV2BaseF16.mlpackage")] 22 - model: String, 16 + #[arg(long, default_value = "DepthAnythingV2BaseF16.mlpackage")] 17 + model: String, 23 18 } 24 19 25 - fn main() -> Result<()> { 26 - let args = Args::parse(); 20 + fn main() -> Result<(), Box<dyn std::error::Error>> { 21 + let args = Args::parse(); 27 22 28 - println!("🍎 Using CoreML backend (Apple Neural Engine + GPU + CPU)"); 29 - println!("Loading image: {}", args.input); 30 - let input_image = image::open(&args.input)?; 23 + let input_image = image::open(&args.input)?; 31 24 32 - println!("Finding model checkpoint: {}", args.model); 33 - let model_path = model::find_checkpoint(&args.model)?; 34 - println!("Using model: {}", model_path.display()); 25 + let model_path = model::find_model("b").unwrap_or_else(|_| { 26 + let checkpoint_dir = model::get_checkpoint_dir().expect("Could not determine checkpoint dir"); 27 + checkpoint_dir.join(&args.model) 28 + }); 35 29 36 - println!("Initializing CoreML depth estimator..."); 37 - let estimator = CoreMLDepthEstimator::new(model_path.to_str().unwrap())?; 30 + let estimator = CoreMLDepthEstimator::new(model_path.to_str().unwrap())?; 38 31 39 - println!("Estimating depth (this should be ~60-90ms on M4 Pro)..."); 40 - let start = std::time::Instant::now(); 41 - let depth_map = estimator.estimate(&input_image)?; 42 - let depth_time = start.elapsed(); 43 - println!("✓ Depth estimation: {:?}", depth_time); 32 + let start = std::time::Instant::now(); 33 + let depth_map = estimator.estimate(&input_image)?; 34 + let depth_time = start.elapsed(); 35 + eprintln!("Depth estimation: {:?}", depth_time); 44 36 45 - println!("Creating stereo pair..."); 46 - let (left, right) = create_stereo_pair(&input_image, &depth_map, args.max_disparity)?; 37 + let (left, right) = generate_stereo_pair(&input_image, &depth_map, args.max_disparity)?; 47 38 48 - println!("Creating SBS image..."); 49 - let sbs = output::create_sbs_image(left, right); 50 - 51 - println!("Saving to: {}", args.output); 52 - sbs.save(&args.output)?; 39 + let sbs = output::create_sbs_image(&left, &right); 40 + sbs.save(&args.output)?; 53 41 54 - let total_time = start.elapsed(); 55 - println!("\n✅ Done!"); 56 - println!("Total time: {:?}", total_time); 57 - println!("Depth time: {:?}", depth_time); 42 + eprintln!("Total time: {:?}", start.elapsed()); 58 43 59 - Ok(()) 44 + Ok(()) 60 45 }
+75 -72
src/depth.rs
··· 1 - use anyhow::{Context, Result}; 2 - use image::{DynamicImage, ImageBuffer, Luma}; 3 - use ndarray::{Array, Array4, Axis}; 4 - use ort::{ 5 - session::{builder::GraphOptimizationLevel, Session}, 6 - value::Value, 7 - }; 1 + #[cfg(feature = "onnx")] 2 + use crate::error::{SpatialError, SpatialResult}; 3 + #[cfg(feature = "onnx")] 4 + use image::DynamicImage; 5 + #[cfg(feature = "onnx")] 6 + use ndarray::Array2; 7 + #[cfg(feature = "onnx")] 8 + use ort::session::{builder::GraphOptimizationLevel, Session}; 8 9 10 + #[cfg(feature = "onnx")] 9 11 const INPUT_SIZE: u32 = 518; 12 + #[cfg(feature = "onnx")] 10 13 const IMAGENET_MEAN: [f32; 3] = [0.485, 0.456, 0.406]; 14 + #[cfg(feature = "onnx")] 11 15 const IMAGENET_STD: [f32; 3] = [0.229, 0.224, 0.225]; 12 16 13 - pub struct DepthEstimator { 14 - session: Session, 17 + #[cfg(feature = "onnx")] 18 + pub struct OnnxDepthEstimator { 19 + session: Session, 15 20 } 16 21 17 - impl DepthEstimator { 18 - pub fn new(model_path: &str) -> Result<Self> { 19 - // Note: CoreML EP tested but is slower than CPU on M4 Pro (9.5s vs 566ms) 20 - // Likely due to compilation overhead or unsupported ops. CPU is fast enough. 21 - let session = Session::builder()? 22 - .with_optimization_level(GraphOptimizationLevel::Level3)? 23 - .with_intra_threads(4)? 24 - .commit_from_file(model_path) 25 - .context("Failed to load ONNX model")?; 22 + #[cfg(feature = "onnx")] 23 + impl OnnxDepthEstimator { 24 + pub fn new(model_path: &str) -> SpatialResult<Self> { 25 + let session = Session::builder() 26 + .map_err(|e| SpatialError::ModelError(format!("Failed to create session: {}", e)))? 27 + .with_optimization_level(GraphOptimizationLevel::Level3) 28 + .map_err(|e| SpatialError::ModelError(format!("Failed to set opt level: {}", e)))? 29 + .with_intra_threads(4) 30 + .map_err(|e| SpatialError::ModelError(format!("Failed to set threads: {}", e)))? 31 + .commit_from_file(model_path) 32 + .map_err(|e| SpatialError::ModelError(format!("Failed to load ONNX model: {}", e)))?; 26 33 27 - Ok(Self { session }) 28 - } 34 + Ok(Self { session }) 35 + } 36 + 37 + pub fn estimate(&mut self, image: &DynamicImage) -> SpatialResult<Array2<f32>> { 38 + let (orig_width, orig_height) = (image.width(), image.height()); 39 + let size = INPUT_SIZE as usize; 29 40 30 - /// Estimate depth from an image, returning a normalized depth map 31 - pub fn estimate(&mut self, image: &DynamicImage) -> Result<ImageBuffer<Luma<f32>, Vec<f32>>> { 32 - let (orig_width, orig_height) = (image.width(), image.height()); 41 + let resized = image.resize_exact( 42 + INPUT_SIZE, 43 + INPUT_SIZE, 44 + image::imageops::FilterType::Lanczos3, 45 + ); 33 46 34 - // Preprocess: resize to 518x518 35 - let resized = image.resize_exact( 36 - INPUT_SIZE, 37 - INPUT_SIZE, 38 - image::imageops::FilterType::Lanczos3, 39 - ); 47 + let rgb = resized.to_rgb8(); 48 + let mut input_data = vec![0.0f32; 1 * 3 * size * size]; 40 49 41 - // Convert to RGB and create NCHW tensor 42 - let rgb = resized.to_rgb8(); 43 - let mut input_tensor = 44 - Array4::<f32>::zeros((1, 3, INPUT_SIZE as usize, INPUT_SIZE as usize)); 50 + for (i, pixel) in rgb.pixels().enumerate() { 51 + for c in 0..3 { 52 + let normalized = (pixel[c] as f32 / 255.0 - IMAGENET_MEAN[c]) / IMAGENET_STD[c]; 53 + input_data[c * size * size + i] = normalized; 54 + } 55 + } 45 56 46 - for (y, row) in rgb.enumerate_rows() { 47 - for (x, _, pixel) in row { 48 - for c in 0..3 { 49 - let normalized = (pixel[c] as f32 / 255.0 - IMAGENET_MEAN[c]) / IMAGENET_STD[c]; 50 - input_tensor[[0, c, y as usize, x as usize]] = normalized; 51 - } 52 - } 53 - } 57 + let input_value = ort::value::Value::from_array(([1usize, 3, size, size], input_data)) 58 + .map_err(|e| SpatialError::TensorError(format!("Failed to create input: {}", e)))?; 54 59 55 - // Run inference 56 - let input_value = Value::from_array(input_tensor)?; 57 - let outputs = self.session.run(ort::inputs![input_value])?; 60 + let outputs = self.session.run(ort::inputs![input_value]) 61 + .map_err(|e| SpatialError::ModelError(format!("Inference failed: {}", e)))?; 58 62 59 - // Extract the depth output tensor - returns (shape, data) 60 - let (shape, data) = outputs[0].try_extract_tensor::<f32>()?; 61 - let dims: Vec<usize> = shape.iter().map(|&d| d as usize).collect(); 63 + let (shape, data) = outputs[0].try_extract_tensor::<f32>() 64 + .map_err(|e| SpatialError::TensorError(format!("Failed to extract output: {}", e)))?; 62 65 63 - // Reconstruct as ndarray - shape should be [1, H, W] 64 - let depth_3d = Array::from_shape_vec((dims[0], dims[1], dims[2]), data.to_vec())?; 66 + let dims: Vec<usize> = shape.iter().map(|&d| d as usize).collect(); 67 + let h = dims[1]; 68 + let w = dims[2]; 65 69 66 - // Extract to [H, W] 67 - let depth_2d = depth_3d.index_axis(Axis(0), 0).to_owned(); 70 + let depth_data: Vec<f32> = data.to_vec(); 68 71 69 - // Normalize to 0-1 range 70 - let min_val = depth_2d.iter().copied().fold(f32::INFINITY, f32::min); 71 - let max_val = depth_2d.iter().copied().fold(f32::NEG_INFINITY, f32::max); 72 - let range = max_val - min_val; 72 + let min_val = depth_data.iter().copied().fold(f32::INFINITY, f32::min); 73 + let max_val = depth_data.iter().copied().fold(f32::NEG_INFINITY, f32::max); 74 + let range = max_val - min_val; 73 75 74 - let normalized = if range > 1e-6 { 75 - depth_2d.mapv(|v| (v - min_val) / range) 76 - } else { 77 - Array::zeros(depth_2d.dim()) 78 - }; 76 + let normalized: Vec<f32> = if range > 1e-6 { 77 + depth_data.iter().map(|&v| (v - min_val) / range).collect() 78 + } else { 79 + vec![0.5; depth_data.len()] 80 + }; 79 81 80 - // Resize back to original dimensions 81 - let depth_image = ImageBuffer::from_fn(INPUT_SIZE, INPUT_SIZE, |x, y| { 82 - Luma([normalized[[y as usize, x as usize]]]) 83 - }); 82 + let depth_image = image::ImageBuffer::from_fn(w as u32, h as u32, |x, y| { 83 + image::Luma([normalized[y as usize * w + x as usize]]) 84 + }); 84 85 85 - let resized_depth = image::imageops::resize( 86 - &depth_image, 87 - orig_width, 88 - orig_height, 89 - image::imageops::FilterType::Lanczos3, 90 - ); 86 + let resized_depth = image::imageops::resize( 87 + &depth_image, 88 + orig_width, 89 + orig_height, 90 + image::imageops::FilterType::Lanczos3, 91 + ); 91 92 92 - Ok(resized_depth) 93 - } 93 + let data: Vec<f32> = resized_depth.pixels().map(|p| p[0]).collect(); 94 + Array2::from_shape_vec((orig_height as usize, orig_width as usize), data) 95 + .map_err(|e| SpatialError::TensorError(format!("Failed to reshape depth: {}", e))) 96 + } 94 97 }
+94 -88
src/depth_coreml.rs
··· 1 - use anyhow::Result; 1 + use crate::error::{SpatialError, SpatialResult}; 2 2 use image::{DynamicImage, ImageBuffer, Luma}; 3 + use ndarray::Array2; 3 4 use std::ffi::CString; 4 5 5 6 const INPUT_SIZE: u32 = 518; 6 7 const IMAGENET_MEAN: [f32; 3] = [0.485, 0.456, 0.406]; 7 8 const IMAGENET_STD: [f32; 3] = [0.229, 0.224, 0.225]; 8 9 9 - // FFI declarations for Swift CoreML bridge 10 10 extern "C" { 11 - fn coreml_load_model(path: *const std::os::raw::c_char) -> *mut std::os::raw::c_void; 12 - fn coreml_unload_model(model: *mut std::os::raw::c_void); 13 - fn coreml_infer_depth( 14 - model: *mut std::os::raw::c_void, 15 - rgb_data: *const f32, 16 - width: i32, 17 - height: i32, 18 - output: *mut f32, 19 - ) -> i32; 11 + fn coreml_load_model(path: *const std::os::raw::c_char) -> *mut std::os::raw::c_void; 12 + fn coreml_unload_model(model: *mut std::os::raw::c_void); 13 + fn coreml_infer_depth( 14 + model: *mut std::os::raw::c_void, 15 + rgb_data: *const f32, 16 + width: i32, 17 + height: i32, 18 + output: *mut f32, 19 + ) -> i32; 20 20 } 21 21 22 22 pub struct CoreMLDepthEstimator { 23 - model: *mut std::os::raw::c_void, 23 + model: *mut std::os::raw::c_void, 24 24 } 25 25 26 26 impl CoreMLDepthEstimator { 27 - pub fn new(model_path: &str) -> Result<Self> { 28 - let c_path = CString::new(model_path)?; 27 + pub fn new(model_path: &str) -> SpatialResult<Self> { 28 + let c_path = CString::new(model_path) 29 + .map_err(|e| SpatialError::ModelError(format!("Invalid model path: {}", e)))?; 29 30 30 - let model = unsafe { coreml_load_model(c_path.as_ptr()) }; 31 + let model = unsafe { coreml_load_model(c_path.as_ptr()) }; 31 32 32 - if model.is_null() { 33 - anyhow::bail!("Failed to load CoreML model: {}", model_path); 34 - } 33 + if model.is_null() { 34 + return Err(SpatialError::ModelError(format!( 35 + "Failed to load CoreML model: {}", 36 + model_path 37 + ))); 38 + } 35 39 36 - println!("✓ CoreML model loaded: {}", model_path); 40 + tracing::info!("CoreML model loaded: {}", model_path); 37 41 38 - Ok(Self { model }) 39 - } 42 + Ok(Self { model }) 43 + } 40 44 41 - /// Estimate depth from an image, returning a normalized depth map 42 - pub fn estimate(&self, image: &DynamicImage) -> Result<ImageBuffer<Luma<f32>, Vec<f32>>> { 43 - let (orig_width, orig_height) = (image.width(), image.height()); 45 + pub fn estimate_raw(&self, image: &DynamicImage) -> SpatialResult<ImageBuffer<Luma<f32>, Vec<f32>>> { 46 + let (orig_width, orig_height) = (image.width(), image.height()); 44 47 45 - // Preprocess: resize to 518x518 46 - let resized = image.resize_exact( 47 - INPUT_SIZE, 48 - INPUT_SIZE, 49 - image::imageops::FilterType::Lanczos3, 50 - ); 48 + let resized = image.resize_exact( 49 + INPUT_SIZE, 50 + INPUT_SIZE, 51 + image::imageops::FilterType::Lanczos3, 52 + ); 51 53 52 - // Convert to RGB and normalize to float32 NCHW layout 53 - let rgb = resized.to_rgb8(); 54 - let mut input_data = Vec::with_capacity(3 * INPUT_SIZE as usize * INPUT_SIZE as usize); 54 + let rgb = resized.to_rgb8(); 55 + let mut input_data = Vec::with_capacity(3 * INPUT_SIZE as usize * INPUT_SIZE as usize); 55 56 56 - // NCHW layout: all R channel, then G, then B 57 - for c in 0..3 { 58 - for pixel in rgb.pixels() { 59 - let normalized = (pixel[c] as f32 / 255.0 - IMAGENET_MEAN[c]) / IMAGENET_STD[c]; 60 - input_data.push(normalized); 61 - } 62 - } 57 + for c in 0..3 { 58 + for pixel in rgb.pixels() { 59 + let normalized = (pixel[c] as f32 / 255.0 - IMAGENET_MEAN[c]) / IMAGENET_STD[c]; 60 + input_data.push(normalized); 61 + } 62 + } 63 63 64 - // Allocate output buffer 65 - let output_size = (INPUT_SIZE * INPUT_SIZE) as usize; 66 - let mut output_data = vec![0.0f32; output_size]; 64 + let output_size = (INPUT_SIZE * INPUT_SIZE) as usize; 65 + let mut output_data = vec![0.0f32; output_size]; 67 66 68 - // Run inference via Swift CoreML 69 - let result = unsafe { 70 - coreml_infer_depth( 71 - self.model, 72 - input_data.as_ptr(), 73 - INPUT_SIZE as i32, 74 - INPUT_SIZE as i32, 75 - output_data.as_mut_ptr(), 76 - ) 77 - }; 67 + let result = unsafe { 68 + coreml_infer_depth( 69 + self.model, 70 + input_data.as_ptr(), 71 + INPUT_SIZE as i32, 72 + INPUT_SIZE as i32, 73 + output_data.as_mut_ptr(), 74 + ) 75 + }; 78 76 79 - if result != 0 { 80 - anyhow::bail!("CoreML inference failed with error code: {}", result); 81 - } 77 + if result != 0 { 78 + return Err(SpatialError::ModelError(format!( 79 + "CoreML inference failed with error code: {}", 80 + result 81 + ))); 82 + } 82 83 83 - // Normalize depth to 0-1 range 84 - let min_val = output_data.iter().copied().fold(f32::INFINITY, f32::min); 85 - let max_val = output_data 86 - .iter() 87 - .copied() 88 - .fold(f32::NEG_INFINITY, f32::max); 89 - let range = max_val - min_val; 84 + let min_val = output_data.iter().copied().fold(f32::INFINITY, f32::min); 85 + let max_val = output_data 86 + .iter() 87 + .copied() 88 + .fold(f32::NEG_INFINITY, f32::max); 89 + let range = max_val - min_val; 90 + 91 + if range > 1e-6 { 92 + for v in &mut output_data { 93 + *v = (*v - min_val) / range; 94 + } 95 + } 90 96 91 - if range > 1e-6 { 92 - for v in &mut output_data { 93 - *v = (*v - min_val) / range; 94 - } 95 - } 97 + let depth_image = ImageBuffer::from_fn(INPUT_SIZE, INPUT_SIZE, |x, y| { 98 + let idx = (y * INPUT_SIZE + x) as usize; 99 + Luma([output_data[idx]]) 100 + }); 96 101 97 - // Create depth image 98 - let depth_image = ImageBuffer::from_fn(INPUT_SIZE, INPUT_SIZE, |x, y| { 99 - let idx = (y * INPUT_SIZE + x) as usize; 100 - Luma([output_data[idx]]) 101 - }); 102 + let resized_depth = image::imageops::resize( 103 + &depth_image, 104 + orig_width, 105 + orig_height, 106 + image::imageops::FilterType::Lanczos3, 107 + ); 102 108 103 - // Resize back to original dimensions 104 - let resized_depth = image::imageops::resize( 105 - &depth_image, 106 - orig_width, 107 - orig_height, 108 - image::imageops::FilterType::Lanczos3, 109 - ); 109 + Ok(resized_depth) 110 + } 110 111 111 - Ok(resized_depth) 112 - } 112 + pub fn estimate(&self, image: &DynamicImage) -> SpatialResult<Array2<f32>> { 113 + let depth_image = self.estimate_raw(image)?; 114 + let (width, height) = depth_image.dimensions(); 115 + let data: Vec<f32> = depth_image.pixels().map(|p| p[0]).collect(); 116 + let depth_2d = Array2::from_shape_vec((height as usize, width as usize), data) 117 + .map_err(|e| SpatialError::TensorError(format!("Failed to reshape depth: {}", e)))?; 118 + Ok(depth_2d) 119 + } 113 120 } 114 121 115 122 impl Drop for CoreMLDepthEstimator { 116 - fn drop(&mut self) { 117 - unsafe { 118 - coreml_unload_model(self.model); 119 - } 120 - } 123 + fn drop(&mut self) { 124 + unsafe { 125 + coreml_unload_model(self.model); 126 + } 127 + } 121 128 } 122 129 123 - // Make it Send + Sync for multithreading (CoreML models are thread-safe) 124 130 unsafe impl Send for CoreMLDepthEstimator {} 125 131 unsafe impl Sync for CoreMLDepthEstimator {}
+40
src/error.rs
··· 1 + use std::fmt; 2 + 3 + pub type SpatialResult<T> = Result<T, SpatialError>; 4 + 5 + #[derive(Debug)] 6 + pub enum SpatialError { 7 + ModelError(String), 8 + ImageError(String), 9 + TensorError(String), 10 + IoError(String), 11 + ConfigError(String), 12 + Other(String), 13 + } 14 + 15 + impl fmt::Display for SpatialError { 16 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 + match self { 18 + SpatialError::ModelError(msg) => write!(f, "Model error: {}", msg), 19 + SpatialError::ImageError(msg) => write!(f, "Image error: {}", msg), 20 + SpatialError::TensorError(msg) => write!(f, "Tensor error: {}", msg), 21 + SpatialError::IoError(msg) => write!(f, "I/O error: {}", msg), 22 + SpatialError::ConfigError(msg) => write!(f, "Configuration error: {}", msg), 23 + SpatialError::Other(msg) => write!(f, "Error: {}", msg), 24 + } 25 + } 26 + } 27 + 28 + impl std::error::Error for SpatialError {} 29 + 30 + impl From<std::io::Error> for SpatialError { 31 + fn from(e: std::io::Error) -> Self { 32 + SpatialError::IoError(e.to_string()) 33 + } 34 + } 35 + 36 + impl From<image::ImageError> for SpatialError { 37 + fn from(e: image::ImageError) -> Self { 38 + SpatialError::ImageError(e.to_string()) 39 + } 40 + }
+230
src/image_loader.rs
··· 1 + use crate::error::{SpatialError, SpatialResult}; 2 + use image::DynamicImage; 3 + use std::path::Path; 4 + use std::process::Command; 5 + 6 + pub async fn load_image(path: impl AsRef<Path>) -> SpatialResult<DynamicImage> { 7 + let path = path.as_ref(); 8 + 9 + if !path.exists() { 10 + return Err(SpatialError::ImageError(format!( 11 + "Image file not found: {:?}", 12 + path 13 + ))); 14 + } 15 + 16 + let extension = path 17 + .extension() 18 + .and_then(|ext| ext.to_str()) 19 + .map(|s| s.to_lowercase()) 20 + .ok_or_else(|| SpatialError::ImageError(format!("File has no extension: {:?}", path)))?; 21 + 22 + match extension.as_str() { 23 + "avif" => load_avif(path).await, 24 + "jxl" => load_jxl(path).await, 25 + "heic" | "heif" => load_heic(path).await, 26 + "jpg" | "jpeg" | "png" | "gif" | "bmp" | "tiff" | "tif" | "webp" => load_standard(path), 27 + _ => Err(SpatialError::ImageError(format!( 28 + "Unsupported image format: .{}", 29 + extension 30 + ))), 31 + } 32 + } 33 + 34 + fn load_standard(path: impl AsRef<Path>) -> SpatialResult<DynamicImage> { 35 + let path = path.as_ref(); 36 + let img = image::open(path) 37 + .map_err(|e| SpatialError::ImageError(format!("Failed to load image {:?}: {}", path, e)))?; 38 + Ok(img) 39 + } 40 + 41 + async fn load_avif(path: &Path) -> SpatialResult<DynamicImage> { 42 + #[cfg(feature = "avif")] 43 + { 44 + match image::open(path) { 45 + Ok(img) => return Ok(img), 46 + Err(e) => { 47 + tracing::warn!("Native AVIF decoder failed: {}, falling back to ffmpeg", e); 48 + } 49 + } 50 + } 51 + load_with_ffmpeg(path, "avif").await 52 + } 53 + 54 + async fn load_jxl(path: &Path) -> SpatialResult<DynamicImage> { 55 + #[cfg(feature = "jxl")] 56 + { 57 + match load_jxl_native(path) { 58 + Ok(img) => return Ok(img), 59 + Err(e) => { 60 + tracing::warn!("Native JXL decoder failed: {}, falling back to ffmpeg", e); 61 + } 62 + } 63 + } 64 + load_with_ffmpeg(path, "jxl").await 65 + } 66 + 67 + async fn load_heic(path: &Path) -> SpatialResult<DynamicImage> { 68 + #[cfg(feature = "heic")] 69 + { 70 + match load_heic_native(path) { 71 + Ok(img) => return Ok(img), 72 + Err(e) => { 73 + tracing::warn!("Native HEIC decoder failed: {}, falling back to ffmpeg", e); 74 + } 75 + } 76 + } 77 + load_with_ffmpeg(path, "heic").await 78 + } 79 + 80 + #[cfg(feature = "jxl")] 81 + fn load_jxl_native(path: &Path) -> SpatialResult<DynamicImage> { 82 + use jxl_oxide::JxlImage; 83 + 84 + let data = std::fs::read(path) 85 + .map_err(|e| SpatialError::IoError(format!("Failed to read JXL file: {}", e)))?; 86 + 87 + let jxl_image = JxlImage::builder() 88 + .read(&data[..]) 89 + .map_err(|e| SpatialError::ImageError(format!("JXL decode failed: {:?}", e)))?; 90 + 91 + let width = jxl_image.width(); 92 + let height = jxl_image.height(); 93 + 94 + let render = jxl_image 95 + .render_frame(0) 96 + .map_err(|e| SpatialError::ImageError(format!("JXL render failed: {:?}", e)))?; 97 + 98 + let planar = render.image_planar(); 99 + if planar.is_empty() { 100 + return Err(SpatialError::ImageError( 101 + "JXL image has no color channels".to_string(), 102 + )); 103 + } 104 + 105 + let mut rgb_data = Vec::with_capacity((width * height * 3) as usize); 106 + for y in 0..height { 107 + for x in 0..width { 108 + let idx = (y * width + x) as usize; 109 + let r = (planar[0].buf()[idx] * 255.0).clamp(0.0, 255.0) as u8; 110 + let g = if planar.len() > 1 { 111 + (planar[1].buf()[idx] * 255.0).clamp(0.0, 255.0) as u8 112 + } else { 113 + r 114 + }; 115 + let b = if planar.len() > 2 { 116 + (planar[2].buf()[idx] * 255.0).clamp(0.0, 255.0) as u8 117 + } else { 118 + r 119 + }; 120 + rgb_data.push(r); 121 + rgb_data.push(g); 122 + rgb_data.push(b); 123 + } 124 + } 125 + 126 + let img_buffer = image::RgbImage::from_raw(width, height, rgb_data).ok_or_else(|| { 127 + SpatialError::ImageError("Failed to create image buffer from JXL data".to_string()) 128 + })?; 129 + 130 + Ok(DynamicImage::ImageRgb8(img_buffer)) 131 + } 132 + 133 + #[cfg(feature = "heic")] 134 + fn load_heic_native(path: &Path) -> SpatialResult<DynamicImage> { 135 + use libheif_rs::{ColorSpace, HeifContext, LibHeif, RgbChroma}; 136 + 137 + let lib_heif = LibHeif::new(); 138 + let ctx = HeifContext::read_from_file( 139 + path.to_str() 140 + .ok_or_else(|| SpatialError::IoError("Invalid path encoding".to_string()))?, 141 + ) 142 + .map_err(|e| SpatialError::ImageError(format!("Failed to load HEIC file: {:?}", e)))?; 143 + 144 + let handle = ctx.primary_image_handle().map_err(|e| { 145 + SpatialError::ImageError(format!("Failed to get HEIC image handle: {:?}", e)) 146 + })?; 147 + 148 + let width = handle.width(); 149 + let height = handle.height(); 150 + 151 + let image = lib_heif 152 + .decode(&handle, ColorSpace::Rgb(RgbChroma::Rgb), None) 153 + .map_err(|e| SpatialError::ImageError(format!("HEIC decode failed: {:?}", e)))?; 154 + 155 + let planes = image.planes(); 156 + let interleaved = planes.interleaved.ok_or_else(|| { 157 + SpatialError::ImageError("No interleaved plane in HEIC image".to_string()) 158 + })?; 159 + 160 + let mut rgb_data = Vec::with_capacity((width * height * 3) as usize); 161 + for y in 0..height { 162 + let row_start = (y * interleaved.stride as u32) as usize; 163 + let row_end = row_start + (width * 3) as usize; 164 + rgb_data.extend_from_slice(&interleaved.data[row_start..row_end]); 165 + } 166 + 167 + let img_buffer = image::RgbImage::from_raw(width, height, rgb_data).ok_or_else(|| { 168 + SpatialError::ImageError("Failed to create image buffer from HEIC data".to_string()) 169 + })?; 170 + 171 + Ok(DynamicImage::ImageRgb8(img_buffer)) 172 + } 173 + 174 + async fn load_with_ffmpeg(path: &Path, format: &str) -> SpatialResult<DynamicImage> { 175 + if !is_ffmpeg_available() { 176 + return Err(SpatialError::ImageError(format!( 177 + "{} format requires ffmpeg for conversion (not installed or not in PATH)", 178 + format.to_uppercase() 179 + ))); 180 + } 181 + 182 + let temp_dir = std::env::temp_dir(); 183 + let temp_filename = format!( 184 + "spatial_maker_convert_{}_{}.jpg", 185 + format, 186 + std::time::SystemTime::now() 187 + .duration_since(std::time::UNIX_EPOCH) 188 + .unwrap_or_default() 189 + .as_millis() 190 + ); 191 + let temp_path = temp_dir.join(temp_filename); 192 + 193 + let input_str = path 194 + .to_str() 195 + .ok_or_else(|| SpatialError::IoError("Invalid input path".to_string()))?; 196 + let output_str = temp_path 197 + .to_str() 198 + .ok_or_else(|| SpatialError::IoError("Invalid output path".to_string()))?; 199 + 200 + let output = Command::new("ffmpeg") 201 + .args(&["-i", input_str, "-c:v", "libjpeg", "-q:v", "2", "-y", output_str]) 202 + .output() 203 + .map_err(|e| SpatialError::IoError(format!("Failed to run ffmpeg: {}", e)))?; 204 + 205 + if !output.status.success() { 206 + let stderr = String::from_utf8_lossy(&output.stderr); 207 + return Err(SpatialError::ImageError(format!( 208 + "ffmpeg conversion failed for {} format:\n{}", 209 + format.to_uppercase(), 210 + stderr 211 + ))); 212 + } 213 + 214 + let img = image::open(&temp_path).map_err(|e| { 215 + let _ = std::fs::remove_file(&temp_path); 216 + SpatialError::ImageError(format!("Failed to load converted image: {}", e)) 217 + })?; 218 + 219 + let _ = std::fs::remove_file(&temp_path); 220 + 221 + Ok(img) 222 + } 223 + 224 + fn is_ffmpeg_available() -> bool { 225 + Command::new("ffmpeg") 226 + .arg("-version") 227 + .output() 228 + .map(|output| output.status.success()) 229 + .unwrap_or(false) 230 + }
+82 -27
src/lib.rs
··· 1 1 pub mod depth; 2 + pub mod error; 3 + pub mod image_loader; 2 4 pub mod model; 3 5 pub mod output; 4 6 pub mod stereo; 7 + pub mod video; 5 8 6 - #[cfg(target_os = "macos")] 9 + #[cfg(all(target_os = "macos", feature = "coreml"))] 7 10 pub mod depth_coreml; 8 11 9 - pub use depth::DepthEstimator; 10 - pub use stereo::create_stereo_pair; 12 + pub use error::{SpatialError, SpatialResult}; 13 + pub use image_loader::load_image; 14 + pub use model::{find_model, get_checkpoint_dir, model_exists}; 15 + pub use output::{create_sbs_image, save_stereo_image, ImageEncoding, MVHEVCConfig, OutputFormat, OutputOptions}; 16 + pub use stereo::generate_stereo_pair; 17 + pub use video::{get_video_metadata, process_video, ProgressCallback, VideoMetadata, VideoProgress}; 11 18 12 - #[cfg(target_os = "macos")] 19 + #[cfg(all(target_os = "macos", feature = "coreml"))] 13 20 pub use depth_coreml::CoreMLDepthEstimator; 14 21 15 - use anyhow::Result; 16 - use image::DynamicImage; 22 + #[cfg(feature = "onnx")] 23 + pub use depth::OnnxDepthEstimator; 24 + 25 + use std::path::Path; 26 + 27 + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] 28 + pub struct SpatialConfig { 29 + pub encoder_size: String, 30 + pub max_disparity: u32, 31 + pub target_depth_size: u32, 32 + } 33 + 34 + pub type StereoOutputFormat = OutputFormat; 35 + 36 + impl Default for SpatialConfig { 37 + fn default() -> Self { 38 + Self { 39 + encoder_size: "b".to_string(), 40 + max_disparity: 30, 41 + target_depth_size: 518, 42 + } 43 + } 44 + } 45 + 46 + pub async fn process_photo( 47 + input_path: &Path, 48 + output_path: &Path, 49 + config: SpatialConfig, 50 + output_options: OutputOptions, 51 + ) -> SpatialResult<()> { 52 + let input_image = load_image(input_path).await?; 53 + 54 + model::ensure_model_exists::<fn(u64, u64)>(&config.encoder_size, None).await?; 55 + 56 + #[cfg(all(target_os = "macos", feature = "coreml"))] 57 + let depth_map = { 58 + let model_path = model::find_model(&config.encoder_size)?; 59 + let model_str = model_path.to_str().ok_or_else(|| { 60 + SpatialError::ModelError("Invalid model path encoding".to_string()) 61 + })?; 62 + let estimator = CoreMLDepthEstimator::new(model_str)?; 63 + estimator.estimate(&input_image)? 64 + }; 17 65 18 - /// Process a photo to create a side-by-side stereo image (ONNX backend) 19 - pub fn process_photo( 20 - input_image: DynamicImage, 21 - model_path: &str, 22 - max_disparity: f32, 23 - ) -> Result<DynamicImage> { 24 - let mut estimator = DepthEstimator::new(model_path)?; 25 - let depth_map = estimator.estimate(&input_image)?; 26 - let (left, right) = create_stereo_pair(&input_image, &depth_map, max_disparity)?; 27 - Ok(output::create_sbs_image(left, right)) 66 + #[cfg(not(all(target_os = "macos", feature = "coreml")))] 67 + let depth_map = { 68 + #[cfg(feature = "onnx")] 69 + { 70 + let model_path = model::find_model(&config.encoder_size)?; 71 + let estimator = OnnxDepthEstimator::new(model_path.to_str().unwrap())?; 72 + estimator.estimate(&input_image)? 73 + } 74 + #[cfg(not(feature = "onnx"))] 75 + { 76 + return Err(SpatialError::ConfigError( 77 + "No depth backend enabled. Enable 'coreml' (macOS) or 'onnx' feature.".to_string(), 78 + )); 79 + } 80 + }; 81 + 82 + let (left, right) = generate_stereo_pair(&input_image, &depth_map, config.max_disparity)?; 83 + 84 + save_stereo_image(&left, &right, output_path, output_options)?; 85 + 86 + Ok(()) 28 87 } 29 88 30 - /// Process a photo to create a side-by-side stereo image (CoreML backend, macOS only) 31 - #[cfg(target_os = "macos")] 32 - pub fn process_photo_coreml( 33 - input_image: DynamicImage, 34 - model_path: &str, 35 - max_disparity: f32, 36 - ) -> Result<DynamicImage> { 37 - let estimator = CoreMLDepthEstimator::new(model_path)?; 38 - let depth_map = estimator.estimate(&input_image)?; 39 - let (left, right) = create_stereo_pair(&input_image, &depth_map, max_disparity)?; 40 - Ok(output::create_sbs_image(left, right)) 89 + pub async fn process_video_sbs( 90 + input_path: &Path, 91 + output_path: &Path, 92 + config: SpatialConfig, 93 + progress_cb: Option<ProgressCallback>, 94 + ) -> SpatialResult<()> { 95 + video::process_video(input_path, output_path, config, progress_cb).await 41 96 }
+263 -52
src/model.rs
··· 1 - use anyhow::Result; 2 - use std::path::PathBuf; 1 + use crate::error::{SpatialError, SpatialResult}; 2 + use std::path::{Path, PathBuf}; 3 + use tokio::io::AsyncWriteExt; 4 + 5 + pub fn get_checkpoint_dir() -> SpatialResult<PathBuf> { 6 + if let Ok(custom_dir) = std::env::var("SPATIAL_MAKER_CHECKPOINTS") { 7 + Ok(PathBuf::from(custom_dir)) 8 + } else { 9 + let home = dirs::home_dir().ok_or_else(|| { 10 + SpatialError::ConfigError("Could not determine home directory".to_string()) 11 + })?; 12 + Ok(home.join(".spatial-maker").join("checkpoints")) 13 + } 14 + } 15 + 16 + #[derive(Clone, Debug)] 17 + pub struct ModelMetadata { 18 + pub name: String, 19 + pub filename: String, 20 + pub url: String, 21 + pub size_mb: u32, 22 + } 23 + 24 + impl ModelMetadata { 25 + pub fn coreml(encoder_size: &str) -> SpatialResult<Self> { 26 + match encoder_size { 27 + "s" | "small" => Ok(ModelMetadata { 28 + name: "depth-anything-v2-small".to_string(), 29 + filename: "DepthAnythingV2SmallF16.mlpackage".to_string(), 30 + url: "https://huggingface.co/mrgnw/depth-anything-v2-coreml/resolve/main/DepthAnythingV2SmallF16.mlpackage.tar.gz".to_string(), 31 + size_mb: 48, 32 + }), 33 + "b" | "base" => Ok(ModelMetadata { 34 + name: "depth-anything-v2-base".to_string(), 35 + filename: "DepthAnythingV2BaseF16.mlpackage".to_string(), 36 + url: "https://huggingface.co/mrgnw/depth-anything-v2-coreml/resolve/main/DepthAnythingV2BaseF16.mlpackage.tar.gz".to_string(), 37 + size_mb: 186, 38 + }), 39 + "l" | "large" => Ok(ModelMetadata { 40 + name: "depth-anything-v2-large".to_string(), 41 + filename: "DepthAnythingV2LargeF16.mlpackage".to_string(), 42 + url: "https://huggingface.co/mrgnw/depth-anything-v2-coreml/resolve/main/DepthAnythingV2LargeF16.mlpackage.tar.gz".to_string(), 43 + size_mb: 638, 44 + }), 45 + other => Err(SpatialError::ConfigError( 46 + format!("Unknown encoder size: '{}'. Use 's', 'b', or 'l'", other) 47 + )), 48 + } 49 + } 50 + 51 + #[cfg(feature = "onnx")] 52 + pub fn onnx(encoder_size: &str) -> SpatialResult<Self> { 53 + match encoder_size { 54 + "s" | "small" => Ok(ModelMetadata { 55 + name: "depth-anything-v2-small".to_string(), 56 + filename: "depth_anything_v2_small.onnx".to_string(), 57 + url: "https://huggingface.co/onnx-community/depth-anything-v2-small/resolve/main/onnx/model.onnx".to_string(), 58 + size_mb: 99, 59 + }), 60 + "b" | "base" => Ok(ModelMetadata { 61 + name: "depth-anything-v2-base".to_string(), 62 + filename: "depth_anything_v2_base.onnx".to_string(), 63 + url: "https://huggingface.co/onnx-community/depth-anything-v2-base/resolve/main/onnx/model.onnx".to_string(), 64 + size_mb: 380, 65 + }), 66 + "l" | "large" => Ok(ModelMetadata { 67 + name: "depth-anything-v2-large".to_string(), 68 + filename: "depth_anything_v2_large.onnx".to_string(), 69 + url: "https://huggingface.co/onnx-community/depth-anything-v2-large/resolve/main/onnx/model.onnx".to_string(), 70 + size_mb: 1300, 71 + }), 72 + other => Err(SpatialError::ConfigError( 73 + format!("Unknown encoder size: '{}'. Use 's', 'b', or 'l'", other) 74 + )), 75 + } 76 + } 77 + } 78 + 79 + pub fn find_model(encoder_size: &str) -> SpatialResult<PathBuf> { 80 + let checkpoint_dir = get_checkpoint_dir()?; 81 + 82 + #[cfg(all(target_os = "macos", feature = "coreml"))] 83 + { 84 + let meta = ModelMetadata::coreml(encoder_size)?; 85 + let model_path = checkpoint_dir.join(&meta.filename); 86 + if model_path.exists() { 87 + return Ok(model_path); 88 + } 89 + } 90 + 91 + #[cfg(feature = "onnx")] 92 + { 93 + let meta = ModelMetadata::onnx(encoder_size)?; 94 + let model_path = checkpoint_dir.join(&meta.filename); 95 + if model_path.exists() { 96 + return Ok(model_path); 97 + } 98 + } 99 + 100 + // Also check development paths 101 + let dev_paths = [ 102 + PathBuf::from("checkpoints"), 103 + dirs::home_dir() 104 + .unwrap_or_default() 105 + .join(".spatial-maker") 106 + .join("checkpoints"), 107 + ]; 108 + 109 + for dir in &dev_paths { 110 + if dir.exists() { 111 + if let Ok(entries) = std::fs::read_dir(dir) { 112 + for entry in entries.flatten() { 113 + let name = entry.file_name().to_string_lossy().to_string(); 114 + if name.contains("DepthAnything") || name.contains("depth_anything") { 115 + let lower_size = encoder_size.to_lowercase(); 116 + let name_lower = name.to_lowercase(); 117 + let matches = match lower_size.as_str() { 118 + "s" | "small" => name_lower.contains("small"), 119 + "b" | "base" => name_lower.contains("base"), 120 + "l" | "large" => name_lower.contains("large"), 121 + _ => false, 122 + }; 123 + if matches { 124 + return Ok(entry.path()); 125 + } 126 + } 127 + } 128 + } 129 + } 130 + } 131 + 132 + Err(SpatialError::ModelError(format!( 133 + "Model not found for encoder size '{}'. Run download first.", 134 + encoder_size 135 + ))) 136 + } 137 + 138 + pub fn model_exists(encoder_size: &str) -> bool { 139 + find_model(encoder_size).is_ok() 140 + } 141 + 142 + pub async fn ensure_model_exists<F>( 143 + encoder_size: &str, 144 + progress_fn: Option<F>, 145 + ) -> SpatialResult<PathBuf> 146 + where 147 + F: FnMut(u64, u64), 148 + { 149 + if let Ok(path) = find_model(encoder_size) { 150 + return Ok(path); 151 + } 152 + 153 + let checkpoint_dir = get_checkpoint_dir()?; 154 + tokio::fs::create_dir_all(&checkpoint_dir) 155 + .await 156 + .map_err(|e| { 157 + SpatialError::IoError(format!("Failed to create checkpoint directory: {}", e)) 158 + })?; 159 + 160 + #[cfg(all(target_os = "macos", feature = "coreml"))] 161 + { 162 + let meta = ModelMetadata::coreml(encoder_size)?; 163 + let model_path = checkpoint_dir.join(&meta.filename); 164 + download_model(&meta, &model_path, progress_fn).await?; 165 + return Ok(model_path); 166 + } 167 + 168 + #[cfg(all(feature = "onnx", not(all(target_os = "macos", feature = "coreml"))))] 169 + { 170 + let meta = ModelMetadata::onnx(encoder_size)?; 171 + let model_path = checkpoint_dir.join(&meta.filename); 172 + download_model(&meta, &model_path, progress_fn).await?; 173 + return Ok(model_path); 174 + } 175 + 176 + #[cfg(not(any(all(target_os = "macos", feature = "coreml"), feature = "onnx")))] 177 + { 178 + let _ = progress_fn; 179 + Err(SpatialError::ConfigError( 180 + "No depth backend enabled. Enable 'coreml' (macOS) or 'onnx' feature.".to_string(), 181 + )) 182 + } 183 + } 184 + 185 + async fn download_model<F>( 186 + metadata: &ModelMetadata, 187 + destination: &Path, 188 + mut progress_fn: Option<F>, 189 + ) -> SpatialResult<()> 190 + where 191 + F: FnMut(u64, u64), 192 + { 193 + tracing::info!("Downloading model: {} from {}", metadata.name, metadata.url); 194 + 195 + let response = reqwest::get(&metadata.url) 196 + .await 197 + .map_err(|e| SpatialError::Other(format!("Failed to download model: {}", e)))?; 198 + 199 + let total_bytes = response 200 + .content_length() 201 + .unwrap_or(metadata.size_mb as u64 * 1_000_000); 202 + 203 + let is_tar_gz = metadata.url.ends_with(".tar.gz"); 204 + 205 + if is_tar_gz { 206 + let temp_path = destination.with_extension("tar.gz"); 207 + let mut file = tokio::fs::File::create(&temp_path) 208 + .await 209 + .map_err(|e| SpatialError::IoError(format!("Failed to create file: {}", e)))?; 210 + 211 + let mut downloaded = 0u64; 212 + let mut stream = response.bytes_stream(); 213 + use futures_util::StreamExt; 214 + 215 + while let Some(chunk) = stream.next().await { 216 + let chunk = chunk.map_err(|e| SpatialError::Other(format!("Download interrupted: {}", e)))?; 217 + file.write_all(&chunk) 218 + .await 219 + .map_err(|e| SpatialError::IoError(format!("Failed to write to file: {}", e)))?; 220 + downloaded += chunk.len() as u64; 221 + if let Some(ref mut f) = progress_fn { 222 + f(downloaded, total_bytes); 223 + } 224 + } 225 + drop(file); 226 + 227 + let parent = destination 228 + .parent() 229 + .ok_or_else(|| SpatialError::IoError("Invalid destination path".to_string()))?; 230 + 231 + let output = std::process::Command::new("tar") 232 + .args(&["xzf"]) 233 + .arg(&temp_path) 234 + .arg("-C") 235 + .arg(parent) 236 + .output() 237 + .map_err(|e| SpatialError::IoError(format!("Failed to extract tar.gz: {}", e)))?; 3 238 4 - /// Find the checkpoint file for a given model name 5 - /// 6 - /// Searches in order: 7 - /// 1. Relative to source directory (for development) 8 - /// 2. User's home directory ~/.spatial-maker/checkpoints/ 9 - /// 3. XDG data directory 10 - /// 4. Current working directory ./checkpoints/ 11 - /// 5. SPATIAL_MAKER_CHECKPOINTS env var 12 - pub fn find_checkpoint(checkpoint_name: &str) -> Result<PathBuf> { 13 - let search_paths = vec![ 14 - // Development: relative to source file 15 - PathBuf::from(env!("CARGO_MANIFEST_DIR")) 16 - .join("checkpoints") 17 - .join(checkpoint_name), 18 - // User home directory 19 - dirs::home_dir() 20 - .unwrap_or_default() 21 - .join(".spatial-maker") 22 - .join("checkpoints") 23 - .join(checkpoint_name), 24 - // XDG data home 25 - dirs::data_dir() 26 - .unwrap_or_default() 27 - .join("spatial-maker") 28 - .join("checkpoints") 29 - .join(checkpoint_name), 30 - // Current working directory 31 - PathBuf::from("checkpoints").join(checkpoint_name), 32 - ]; 239 + if !output.status.success() { 240 + let stderr = String::from_utf8_lossy(&output.stderr); 241 + return Err(SpatialError::IoError(format!("tar extraction failed: {}", stderr))); 242 + } 33 243 34 - // Check env var first 35 - if let Ok(env_dir) = std::env::var("SPATIAL_MAKER_CHECKPOINTS") { 36 - let env_path = PathBuf::from(env_dir).join(checkpoint_name); 37 - if env_path.exists() { 38 - return Ok(env_path); 39 - } 40 - } 244 + let _ = tokio::fs::remove_file(&temp_path).await; 245 + } else { 246 + let mut file = tokio::fs::File::create(destination) 247 + .await 248 + .map_err(|e| SpatialError::IoError(format!("Failed to create file: {}", e)))?; 41 249 42 - for path in &search_paths { 43 - if path.exists() { 44 - return Ok(path.clone()); 45 - } 46 - } 250 + let mut downloaded = 0u64; 251 + let mut stream = response.bytes_stream(); 252 + use futures_util::StreamExt; 253 + 254 + while let Some(chunk) = stream.next().await { 255 + let chunk = chunk.map_err(|e| SpatialError::Other(format!("Download interrupted: {}", e)))?; 256 + file.write_all(&chunk) 257 + .await 258 + .map_err(|e| SpatialError::IoError(format!("Failed to write to file: {}", e)))?; 259 + downloaded += chunk.len() as u64; 260 + if let Some(ref mut f) = progress_fn { 261 + f(downloaded, total_bytes); 262 + } 263 + } 264 + } 47 265 48 - anyhow::bail!( 49 - "Checkpoint '{}' not found.\nSearched locations:\n{}", 50 - checkpoint_name, 51 - search_paths 52 - .iter() 53 - .map(|p| format!(" - {}", p.display())) 54 - .collect::<Vec<_>>() 55 - .join("\n") 56 - ); 266 + tracing::info!("Model downloaded: {:?}", destination); 267 + Ok(()) 57 268 }
+274 -18
src/output.rs
··· 1 - use image::{DynamicImage, ImageBuffer, RgbImage}; 1 + use crate::error::{SpatialError, SpatialResult}; 2 + use image::DynamicImage; 3 + use std::path::{Path, PathBuf}; 4 + use std::process::Command; 5 + 6 + #[derive(Clone, Copy, Debug, PartialEq, Eq)] 7 + pub enum OutputFormat { 8 + SideBySide, 9 + TopAndBottom, 10 + Separate, 11 + } 12 + 13 + impl OutputFormat { 14 + pub fn name(&self) -> &'static str { 15 + match self { 16 + OutputFormat::SideBySide => "side-by-side", 17 + OutputFormat::TopAndBottom => "top-and-bottom", 18 + OutputFormat::Separate => "separate", 19 + } 20 + } 21 + } 22 + 23 + #[derive(Clone, Copy, Debug, PartialEq, Eq)] 24 + pub enum ImageEncoding { 25 + Jpeg { quality: u8 }, 26 + Png, 27 + } 2 28 3 - /// Create a side-by-side stereo image by horizontally stacking left and right views 4 - pub fn create_sbs_image(left: RgbImage, right: RgbImage) -> DynamicImage { 5 - let (width, height) = left.dimensions(); 6 - let mut sbs = ImageBuffer::new(width * 2, height); 29 + impl ImageEncoding { 30 + pub fn extension(&self) -> &'static str { 31 + match self { 32 + ImageEncoding::Jpeg { .. } => "jpg", 33 + ImageEncoding::Png => "png", 34 + } 35 + } 36 + 37 + pub fn from_path<P: AsRef<Path>>(path: P) -> Self { 38 + let ext = path 39 + .as_ref() 40 + .extension() 41 + .and_then(|e| e.to_str()) 42 + .unwrap_or("") 43 + .to_lowercase(); 7 44 8 - // Copy left eye to left half 9 - for y in 0..height { 10 - for x in 0..width { 11 - sbs.put_pixel(x, y, *left.get_pixel(x, y)); 12 - } 13 - } 45 + match ext.as_str() { 46 + "png" => ImageEncoding::Png, 47 + _ => ImageEncoding::Jpeg { quality: 95 }, 48 + } 49 + } 50 + } 14 51 15 - // Copy right eye to right half 16 - for y in 0..height { 17 - for x in 0..width { 18 - sbs.put_pixel(width + x, y, *right.get_pixel(x, y)); 19 - } 20 - } 52 + #[derive(Clone, Debug)] 53 + pub struct MVHEVCConfig { 54 + pub spatial_cli_path: Option<PathBuf>, 55 + pub enabled: bool, 56 + pub quality: u8, 57 + pub keep_intermediate: bool, 58 + } 21 59 22 - DynamicImage::ImageRgb8(sbs) 60 + impl Default for MVHEVCConfig { 61 + fn default() -> Self { 62 + Self { 63 + spatial_cli_path: None, 64 + enabled: false, 65 + quality: 95, 66 + keep_intermediate: false, 67 + } 68 + } 69 + } 70 + 71 + #[derive(Clone, Debug)] 72 + pub struct OutputOptions { 73 + pub layout: OutputFormat, 74 + pub image_format: ImageEncoding, 75 + pub mvhevc: Option<MVHEVCConfig>, 76 + } 77 + 78 + impl Default for OutputOptions { 79 + fn default() -> Self { 80 + Self { 81 + layout: OutputFormat::SideBySide, 82 + image_format: ImageEncoding::Jpeg { quality: 95 }, 83 + mvhevc: None, 84 + } 85 + } 86 + } 87 + 88 + pub fn create_sbs_image(left: &DynamicImage, right: &DynamicImage) -> DynamicImage { 89 + let left_width = left.width(); 90 + let left_height = left.height(); 91 + 92 + let combined_width = left_width + right.width(); 93 + let mut combined = DynamicImage::new_rgb8(combined_width, left_height); 94 + 95 + image::imageops::overlay(&mut combined, left, 0, 0); 96 + image::imageops::overlay(&mut combined, right, left_width as i64, 0); 97 + 98 + combined 99 + } 100 + 101 + pub fn save_stereo_image( 102 + left: &DynamicImage, 103 + right: &DynamicImage, 104 + output_path: impl AsRef<Path>, 105 + options: OutputOptions, 106 + ) -> SpatialResult<()> { 107 + let output_path = output_path.as_ref(); 108 + 109 + if let Some(parent) = output_path.parent() { 110 + std::fs::create_dir_all(parent).map_err(|e| { 111 + SpatialError::ImageError(format!("Failed to create output directory: {}", e)) 112 + })?; 113 + } 114 + 115 + match options.layout { 116 + OutputFormat::SideBySide => { 117 + save_side_by_side(left, right, output_path, options.image_format)?; 118 + } 119 + OutputFormat::TopAndBottom => { 120 + save_top_and_bottom(left, right, output_path, options.image_format)?; 121 + } 122 + OutputFormat::Separate => { 123 + save_separate(left, right, output_path, options.image_format)?; 124 + } 125 + } 126 + 127 + if let Some(mvhevc_config) = options.mvhevc { 128 + if mvhevc_config.enabled { 129 + encode_mvhevc(output_path, &mvhevc_config)?; 130 + if !mvhevc_config.keep_intermediate { 131 + let _ = std::fs::remove_file(output_path); 132 + } 133 + } 134 + } 135 + 136 + Ok(()) 137 + } 138 + 139 + fn save_side_by_side( 140 + left: &DynamicImage, 141 + right: &DynamicImage, 142 + output_path: &Path, 143 + encoding: ImageEncoding, 144 + ) -> SpatialResult<()> { 145 + if left.height() != right.height() { 146 + return Err(SpatialError::ImageError(format!( 147 + "Left and right images must have the same height: {} != {}", 148 + left.height(), 149 + right.height() 150 + ))); 151 + } 152 + 153 + let combined = create_sbs_image(left, right); 154 + save_image(&combined, output_path, encoding) 155 + } 156 + 157 + fn save_top_and_bottom( 158 + left: &DynamicImage, 159 + right: &DynamicImage, 160 + output_path: &Path, 161 + encoding: ImageEncoding, 162 + ) -> SpatialResult<()> { 163 + if left.width() != right.width() { 164 + return Err(SpatialError::ImageError(format!( 165 + "Left and right images must have the same width: {} != {}", 166 + left.width(), 167 + right.width() 168 + ))); 169 + } 170 + 171 + let combined_height = left.height() + right.height(); 172 + let mut combined = DynamicImage::new_rgb8(left.width(), combined_height); 173 + 174 + image::imageops::overlay(&mut combined, left, 0, 0); 175 + image::imageops::overlay(&mut combined, right, 0, left.height() as i64); 176 + 177 + save_image(&combined, output_path, encoding) 178 + } 179 + 180 + fn save_separate( 181 + left: &DynamicImage, 182 + right: &DynamicImage, 183 + output_path: &Path, 184 + encoding: ImageEncoding, 185 + ) -> SpatialResult<()> { 186 + let stem = output_path 187 + .file_stem() 188 + .and_then(|s| s.to_str()) 189 + .ok_or_else(|| SpatialError::ImageError("Invalid output path".to_string()))?; 190 + 191 + let parent = output_path.parent().unwrap_or_else(|| Path::new(".")); 192 + let ext = encoding.extension(); 193 + 194 + let left_path = parent.join(format!("{}_L.{}", stem, ext)); 195 + let right_path = parent.join(format!("{}_R.{}", stem, ext)); 196 + 197 + save_image(left, &left_path, encoding)?; 198 + save_image(right, &right_path, encoding)?; 199 + 200 + Ok(()) 201 + } 202 + 203 + fn save_image(image: &DynamicImage, path: &Path, encoding: ImageEncoding) -> SpatialResult<()> { 204 + match encoding { 205 + ImageEncoding::Jpeg { quality } => { 206 + let rgb_image = image.to_rgb8(); 207 + let file = std::fs::File::create(path).map_err(|e| { 208 + SpatialError::ImageError(format!("Failed to create output file: {}", e)) 209 + })?; 210 + 211 + let mut jpeg_encoder = 212 + image::codecs::jpeg::JpegEncoder::new_with_quality(file, quality); 213 + jpeg_encoder 214 + .encode( 215 + rgb_image.as_ref(), 216 + rgb_image.width(), 217 + rgb_image.height(), 218 + image::ExtendedColorType::Rgb8, 219 + ) 220 + .map_err(|e| SpatialError::ImageError(format!("Failed to encode JPEG: {}", e)))?; 221 + } 222 + ImageEncoding::Png => { 223 + image 224 + .save(path) 225 + .map_err(|e| SpatialError::ImageError(format!("Failed to save PNG: {}", e)))?; 226 + } 227 + } 228 + 229 + Ok(()) 230 + } 231 + 232 + fn encode_mvhevc(stereo_path: &Path, config: &MVHEVCConfig) -> SpatialResult<()> { 233 + let spatial_path = config 234 + .spatial_cli_path 235 + .as_ref() 236 + .map(|p| p.as_path()) 237 + .unwrap_or_else(|| Path::new("spatial")); 238 + 239 + let hevc_path = stereo_path.with_extension("heic"); 240 + 241 + let format = if stereo_path.to_string_lossy().contains("top-bottom") 242 + || stereo_path.to_string_lossy().contains("_tb_") 243 + { 244 + "hou" 245 + } else { 246 + "sbs" 247 + }; 248 + 249 + let quality_normalized = (config.quality as f32 / 100.0).clamp(0.0, 1.0); 250 + 251 + let mut cmd = Command::new(spatial_path); 252 + cmd.arg("make") 253 + .arg("--input") 254 + .arg(stereo_path) 255 + .arg("--output") 256 + .arg(&hevc_path) 257 + .arg("--format") 258 + .arg(format) 259 + .arg("--quality") 260 + .arg(quality_normalized.to_string()) 261 + .arg("--overwrite"); 262 + 263 + let output = cmd.output().map_err(|e| { 264 + SpatialError::ImageError(format!( 265 + "Failed to run `spatial` CLI: {}. Ensure the `spatial` tool is installed and in PATH.", 266 + e 267 + )) 268 + })?; 269 + 270 + if !output.status.success() { 271 + let stderr = String::from_utf8_lossy(&output.stderr); 272 + return Err(SpatialError::ImageError(format!( 273 + "MV-HEVC encoding failed: {}", 274 + stderr 275 + ))); 276 + } 277 + 278 + Ok(()) 23 279 }
+96 -68
src/stereo.rs
··· 1 - use anyhow::Result; 2 - use image::{DynamicImage, ImageBuffer, Luma, Rgb, RgbImage}; 1 + use crate::error::SpatialResult; 2 + use image::{DynamicImage, ImageBuffer, Rgb}; 3 + use ndarray::Array2; 3 4 4 - /// Create left and right eye views from RGB frame and depth map using DIBR 5 - /// 6 - /// This ports the `create_stereo_pair` function from depth_to_stereo.py 7 - pub fn create_stereo_pair( 8 - rgb_frame: &DynamicImage, 9 - depth_frame: &ImageBuffer<Luma<f32>, Vec<f32>>, 10 - max_disparity: f32, 11 - ) -> Result<(RgbImage, RgbImage)> { 12 - let rgb = rgb_frame.to_rgb8(); 13 - let (width, height) = rgb.dimensions(); 5 + pub fn generate_stereo_pair( 6 + image: &DynamicImage, 7 + depth: &Array2<f32>, 8 + max_disparity: u32, 9 + ) -> SpatialResult<(DynamicImage, DynamicImage)> { 10 + let img_rgb = image.to_rgb8(); 11 + let width = img_rgb.width() as usize; 12 + let height = img_rgb.height() as usize; 14 13 15 - // Create disparity map (closer = higher value = more shift) 16 - let disparity: Vec<f32> = depth_frame.pixels().map(|p| p[0] * max_disparity).collect(); 14 + let mut right_rgb: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::new(width as u32, height as u32); 17 15 18 - let left_eye = remap_image(&rgb, &disparity, width, height, 0.5)?; 19 - let right_eye = remap_image(&rgb, &disparity, width, height, -0.5)?; 16 + let bg = Rgb([64u8, 64u8, 64u8]); 17 + for pixel in right_rgb.pixels_mut() { 18 + *pixel = bg; 19 + } 20 20 21 - Ok((left_eye, right_eye)) 22 - } 21 + for y in 0..height { 22 + for x in 0..width { 23 + let depth_val = get_depth_at(depth, x, y, width, height); 24 + let disparity = (depth_val * max_disparity as f32).round() as i32; 25 + let x_right = x as i32 - disparity; 23 26 24 - /// Remap an image based on disparity values 25 - /// 26 - /// This is equivalent to cv2.remap with INTER_LINEAR and BORDER_REPLICATE 27 - fn remap_image( 28 - source: &RgbImage, 29 - disparity: &[f32], 30 - width: u32, 31 - height: u32, 32 - shift_factor: f32, 33 - ) -> Result<RgbImage> { 34 - let mut output = ImageBuffer::new(width, height); 27 + if x_right >= 0 && x_right < width as i32 { 28 + if let Some(pixel) = img_rgb.get_pixel_checked(x as u32, y as u32) { 29 + right_rgb.put_pixel(x_right as u32, y as u32, *pixel); 30 + } 31 + } 32 + } 33 + } 35 34 36 - for y in 0..height { 37 - for x in 0..width { 38 - let idx = (y * width + x) as usize; 39 - let shift = disparity[idx] * shift_factor; 40 - let src_x = x as f32 + shift; 35 + fill_disocclusions(&mut right_rgb); 41 36 42 - // Bilinear interpolation 43 - let pixel = sample_bilinear(source, src_x, y as f32); 44 - output.put_pixel(x, y, pixel); 45 - } 46 - } 37 + let left_image = image.clone(); 38 + let right_image = DynamicImage::ImageRgb8(right_rgb); 47 39 48 - Ok(output) 40 + Ok((left_image, right_image)) 49 41 } 50 42 51 - /// Sample a pixel using bilinear interpolation 52 - fn sample_bilinear(image: &RgbImage, x: f32, y: f32) -> Rgb<u8> { 53 - let (width, height) = image.dimensions(); 43 + fn get_depth_at( 44 + depth: &Array2<f32>, 45 + x: usize, 46 + y: usize, 47 + img_width: usize, 48 + img_height: usize, 49 + ) -> f32 { 50 + let (depth_height, depth_width) = depth.dim(); 54 51 55 - // Clamp to image bounds (BORDER_REPLICATE) 56 - let x_clamped = x.clamp(0.0, (width - 1) as f32); 57 - let y_clamped = y.clamp(0.0, (height - 1) as f32); 52 + if depth_height == img_height && depth_width == img_width { 53 + depth[[y, x]] 54 + } else { 55 + let scaled_x = (x as f32 * depth_width as f32 / img_width as f32) 56 + .min(depth_width as f32 - 1.0) as usize; 57 + let scaled_y = (y as f32 * depth_height as f32 / img_height as f32) 58 + .min(depth_height as f32 - 1.0) as usize; 58 59 59 - let x0 = x_clamped.floor() as u32; 60 - let x1 = (x0 + 1).min(width - 1); 61 - let y0 = y_clamped.floor() as u32; 62 - let y1 = (y0 + 1).min(height - 1); 60 + if scaled_y < depth_height && scaled_x < depth_width { 61 + depth[[scaled_y, scaled_x]] 62 + } else { 63 + 0.5 64 + } 65 + } 66 + } 63 67 64 - let fx = x_clamped - x0 as f32; 65 - let fy = y_clamped - y0 as f32; 68 + fn fill_disocclusions(image: &mut ImageBuffer<Rgb<u8>, Vec<u8>>) { 69 + let width = image.width() as usize; 70 + let height = image.height() as usize; 71 + let bg = Rgb([64u8, 64u8, 64u8]); 66 72 67 - let p00 = image.get_pixel(x0, y0); 68 - let p10 = image.get_pixel(x1, y0); 69 - let p01 = image.get_pixel(x0, y1); 70 - let p11 = image.get_pixel(x1, y1); 73 + let original = image.clone(); 71 74 72 - let mut result = [0u8; 3]; 73 - for c in 0..3 { 74 - let v00 = p00[c] as f32; 75 - let v10 = p10[c] as f32; 76 - let v01 = p01[c] as f32; 77 - let v11 = p11[c] as f32; 75 + for y in 0..height { 76 + for x in 0..width { 77 + let pixel = original.get_pixel(x as u32, y as u32); 78 + if pixel[0] == bg[0] && pixel[1] == bg[1] && pixel[2] == bg[2] { 79 + if let Some(nearest) = find_nearest_valid(&original, x, y, bg) { 80 + image.put_pixel(x as u32, y as u32, nearest); 81 + } 82 + } 83 + } 84 + } 85 + } 78 86 79 - let v0 = v00 * (1.0 - fx) + v10 * fx; 80 - let v1 = v01 * (1.0 - fx) + v11 * fx; 81 - let v = v0 * (1.0 - fy) + v1 * fy; 87 + fn find_nearest_valid( 88 + image: &ImageBuffer<Rgb<u8>, Vec<u8>>, 89 + cx: usize, 90 + cy: usize, 91 + bg: Rgb<u8>, 92 + ) -> Option<Rgb<u8>> { 93 + let width = image.width() as usize; 94 + let height = image.height() as usize; 82 95 83 - result[c] = v.round().clamp(0.0, 255.0) as u8; 84 - } 96 + for radius in 1..=20 { 97 + for dy in -(radius as i32)..=(radius as i32) { 98 + for dx in -(radius as i32)..=(radius as i32) { 99 + if dx.abs() != radius as i32 && dy.abs() != radius as i32 { 100 + continue; 101 + } 102 + let nx = (cx as i32 + dx) as usize; 103 + let ny = (cy as i32 + dy) as usize; 104 + if nx < width && ny < height { 105 + let pixel = image.get_pixel(nx as u32, ny as u32); 106 + if pixel[0] != bg[0] || pixel[1] != bg[1] || pixel[2] != bg[2] { 107 + return Some(*pixel); 108 + } 109 + } 110 + } 111 + } 112 + } 85 113 86 - Rgb(result) 114 + None 87 115 }
+398
src/video.rs
··· 1 + use crate::error::{SpatialError, SpatialResult}; 2 + use crate::stereo::generate_stereo_pair; 3 + use crate::SpatialConfig; 4 + use image::{DynamicImage, ImageBuffer, RgbImage}; 5 + use std::path::Path; 6 + use std::process::Stdio; 7 + use tokio::io::{AsyncReadExt, AsyncWriteExt}; 8 + use tokio::process::Command; 9 + use tokio::sync::mpsc; 10 + 11 + #[derive(Clone, Debug)] 12 + pub struct VideoProgress { 13 + pub current_frame: u32, 14 + pub total_frames: u32, 15 + pub stage: String, 16 + pub percent: f64, 17 + } 18 + 19 + impl VideoProgress { 20 + pub fn new(current_frame: u32, total_frames: u32, stage: String) -> Self { 21 + let percent = if total_frames > 0 { 22 + (current_frame as f64 / total_frames as f64 * 100.0).min(100.0) 23 + } else { 24 + 0.0 25 + }; 26 + Self { 27 + current_frame, 28 + total_frames, 29 + stage, 30 + percent, 31 + } 32 + } 33 + } 34 + 35 + #[derive(Clone, Debug)] 36 + pub struct VideoMetadata { 37 + pub width: u32, 38 + pub height: u32, 39 + pub fps: f64, 40 + pub total_frames: u32, 41 + pub duration: f64, 42 + pub has_audio: bool, 43 + } 44 + 45 + pub type ProgressCallback = Box<dyn Fn(VideoProgress) + Send + Sync>; 46 + 47 + pub async fn get_video_metadata(input_path: &Path) -> SpatialResult<VideoMetadata> { 48 + let output = Command::new("ffprobe") 49 + .args([ 50 + "-v", 51 + "error", 52 + "-select_streams", 53 + "v:0", 54 + "-count_frames", 55 + "-show_entries", 56 + "stream=width,height,r_frame_rate,nb_read_frames,duration", 57 + "-show_entries", 58 + "format=duration", 59 + "-of", 60 + "csv=p=0", 61 + input_path.to_str().unwrap(), 62 + ]) 63 + .output() 64 + .await 65 + .map_err(|e| { 66 + SpatialError::Other(format!( 67 + "Failed to run ffprobe (is ffmpeg installed?): {}", 68 + e 69 + )) 70 + })?; 71 + 72 + if !output.status.success() { 73 + let stderr = String::from_utf8_lossy(&output.stderr); 74 + return Err(SpatialError::Other(format!("ffprobe failed: {}", stderr))); 75 + } 76 + 77 + let stdout = String::from_utf8_lossy(&output.stdout); 78 + let parts: Vec<&str> = stdout.trim().split(',').collect(); 79 + 80 + if parts.len() < 4 { 81 + return Err(SpatialError::Other(format!( 82 + "Unexpected ffprobe output: {}", 83 + stdout 84 + ))); 85 + } 86 + 87 + let width = parts[0] 88 + .parse::<u32>() 89 + .map_err(|_| SpatialError::Other("Failed to parse width".to_string()))?; 90 + let height = parts[1] 91 + .parse::<u32>() 92 + .map_err(|_| SpatialError::Other("Failed to parse height".to_string()))?; 93 + 94 + let fps = if parts[2].contains('/') { 95 + let fps_parts: Vec<&str> = parts[2].split('/').collect(); 96 + let num: f64 = fps_parts[0].parse().unwrap_or(30.0); 97 + let den: f64 = fps_parts[1].parse().unwrap_or(1.0); 98 + num / den 99 + } else { 100 + parts[2].parse().unwrap_or(30.0) 101 + }; 102 + 103 + let total_frames = parts[3] 104 + .parse::<u32>() 105 + .map_err(|_| SpatialError::Other("Failed to parse frame count".to_string()))?; 106 + 107 + let duration = parts 108 + .get(4) 109 + .and_then(|s| s.parse::<f64>().ok()) 110 + .unwrap_or(total_frames as f64 / fps); 111 + 112 + let audio_output = Command::new("ffprobe") 113 + .args([ 114 + "-v", 115 + "error", 116 + "-select_streams", 117 + "a:0", 118 + "-show_entries", 119 + "stream=codec_type", 120 + "-of", 121 + "csv=p=0", 122 + input_path.to_str().unwrap(), 123 + ]) 124 + .output() 125 + .await 126 + .map_err(|e| SpatialError::Other(format!("Failed to check audio: {}", e)))?; 127 + 128 + let has_audio = String::from_utf8_lossy(&audio_output.stdout) 129 + .trim() 130 + .contains("audio"); 131 + 132 + Ok(VideoMetadata { 133 + width, 134 + height, 135 + fps, 136 + total_frames, 137 + duration, 138 + has_audio, 139 + }) 140 + } 141 + 142 + async fn extract_frames( 143 + input_path: &Path, 144 + metadata: &VideoMetadata, 145 + ) -> SpatialResult<mpsc::Receiver<Vec<u8>>> { 146 + let (tx, rx) = mpsc::channel::<Vec<u8>>(10); 147 + 148 + let width = metadata.width; 149 + let height = metadata.height; 150 + let frame_size = (width * height * 3) as usize; 151 + 152 + let input_path = input_path.to_path_buf(); 153 + 154 + tokio::spawn(async move { 155 + let mut child = Command::new("ffmpeg") 156 + .args([ 157 + "-i", 158 + input_path.to_str().unwrap(), 159 + "-f", 160 + "rawvideo", 161 + "-pix_fmt", 162 + "rgb24", 163 + "-vsync", 164 + "0", 165 + "-", 166 + ]) 167 + .stdout(Stdio::piped()) 168 + .stderr(Stdio::null()) 169 + .spawn() 170 + .expect("Failed to spawn ffmpeg"); 171 + 172 + let stdout = child.stdout.take().expect("Failed to capture stdout"); 173 + let mut reader = tokio::io::BufReader::new(stdout); 174 + let mut frame_buffer = vec![0u8; frame_size]; 175 + 176 + loop { 177 + match reader.read_exact(&mut frame_buffer).await { 178 + Ok(_) => { 179 + if tx.send(frame_buffer.clone()).await.is_err() { 180 + break; 181 + } 182 + } 183 + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break, 184 + Err(_) => break, 185 + } 186 + } 187 + 188 + let _ = child.wait().await; 189 + }); 190 + 191 + Ok(rx) 192 + } 193 + 194 + fn frame_to_image(data: &[u8], width: u32, height: u32) -> SpatialResult<DynamicImage> { 195 + let rgb_image = RgbImage::from_raw(width, height, data.to_vec()).ok_or_else(|| { 196 + SpatialError::ImageError(format!( 197 + "Failed to create image from frame data ({}x{})", 198 + width, height 199 + )) 200 + })?; 201 + Ok(DynamicImage::ImageRgb8(rgb_image)) 202 + } 203 + 204 + async fn encode_stereo_video( 205 + output_path: std::path::PathBuf, 206 + metadata: VideoMetadata, 207 + mut rx: mpsc::Receiver<(DynamicImage, DynamicImage)>, 208 + ) -> SpatialResult<()> { 209 + let width = metadata.width; 210 + let height = metadata.height; 211 + let fps = metadata.fps; 212 + 213 + let output_width = width * 2; 214 + let output_height = height; 215 + 216 + let mut child = Command::new("ffmpeg") 217 + .args([ 218 + "-f", 219 + "rawvideo", 220 + "-pix_fmt", 221 + "rgb24", 222 + "-s", 223 + &format!("{}x{}", output_width, output_height), 224 + "-r", 225 + &format!("{}", fps), 226 + "-i", 227 + "-", 228 + "-c:v", 229 + "libx264", 230 + "-preset", 231 + "medium", 232 + "-crf", 233 + "23", 234 + "-pix_fmt", 235 + "yuv420p", 236 + "-y", 237 + output_path.to_str().unwrap(), 238 + ]) 239 + .stdin(Stdio::piped()) 240 + .stdout(Stdio::null()) 241 + .stderr(Stdio::null()) 242 + .spawn() 243 + .map_err(|e| SpatialError::Other(format!("Failed to spawn ffmpeg encoder: {}", e)))?; 244 + 245 + let mut stdin = child.stdin.take().expect("Failed to capture stdin"); 246 + 247 + while let Some((left, right)) = rx.recv().await { 248 + let mut sbs_image = ImageBuffer::new(output_width, output_height); 249 + 250 + let left_rgb = left.to_rgb8(); 251 + for y in 0..height { 252 + for x in 0..width { 253 + let pixel = left_rgb.get_pixel(x, y); 254 + sbs_image.put_pixel(x, y, *pixel); 255 + } 256 + } 257 + 258 + let right_rgb = right.to_rgb8(); 259 + for y in 0..height { 260 + for x in 0..width { 261 + let pixel = right_rgb.get_pixel(x, y); 262 + sbs_image.put_pixel(width + x, y, *pixel); 263 + } 264 + } 265 + 266 + stdin 267 + .write_all(&sbs_image.into_raw()) 268 + .await 269 + .map_err(|e| SpatialError::IoError(format!("Failed to write frame: {}", e)))?; 270 + } 271 + 272 + drop(stdin); 273 + 274 + let status = child 275 + .wait() 276 + .await 277 + .map_err(|e| SpatialError::Other(format!("ffmpeg encoding failed: {}", e)))?; 278 + 279 + if !status.success() { 280 + return Err(SpatialError::Other( 281 + "ffmpeg encoding exited with error".to_string(), 282 + )); 283 + } 284 + 285 + Ok(()) 286 + } 287 + 288 + pub async fn process_video( 289 + input_path: &Path, 290 + output_path: &Path, 291 + config: SpatialConfig, 292 + progress_cb: Option<ProgressCallback>, 293 + ) -> SpatialResult<()> { 294 + if !input_path.exists() { 295 + return Err(SpatialError::IoError(format!( 296 + "Input file not found: {:?}", 297 + input_path 298 + ))); 299 + } 300 + 301 + let metadata = get_video_metadata(input_path).await?; 302 + 303 + crate::model::ensure_model_exists::<fn(u64, u64)>(&config.encoder_size, None).await?; 304 + 305 + #[cfg(all(target_os = "macos", feature = "coreml"))] 306 + let estimator = { 307 + let model_path = crate::model::find_model(&config.encoder_size)?; 308 + let model_str = model_path.to_str().ok_or_else(|| { 309 + SpatialError::ModelError("Invalid model path encoding".to_string()) 310 + })?; 311 + std::sync::Arc::new(crate::depth_coreml::CoreMLDepthEstimator::new(model_str)?) 312 + }; 313 + 314 + let mut frame_rx = extract_frames(input_path, &metadata).await?; 315 + 316 + let (processed_tx, processed_rx) = mpsc::channel::<(DynamicImage, DynamicImage)>(10); 317 + 318 + let encode_handle = tokio::spawn(encode_stereo_video( 319 + output_path.to_path_buf(), 320 + metadata.clone(), 321 + processed_rx, 322 + )); 323 + 324 + let mut frame_count = 0u32; 325 + let total_frames = metadata.total_frames; 326 + 327 + if let Some(ref cb) = progress_cb { 328 + cb(VideoProgress::new(0, total_frames, "extracting".to_string())); 329 + } 330 + 331 + while let Some(frame_data) = frame_rx.recv().await { 332 + let frame = frame_to_image(&frame_data, metadata.width, metadata.height)?; 333 + 334 + frame_count += 1; 335 + if let Some(ref cb) = progress_cb { 336 + if frame_count % 10 == 0 || frame_count == total_frames { 337 + cb(VideoProgress::new( 338 + frame_count, 339 + total_frames, 340 + "processing".to_string(), 341 + )); 342 + } 343 + } 344 + 345 + #[cfg(all(target_os = "macos", feature = "coreml"))] 346 + let depth_map = estimator.estimate(&frame)?; 347 + 348 + #[cfg(not(all(target_os = "macos", feature = "coreml")))] 349 + let depth_map = { 350 + #[cfg(feature = "onnx")] 351 + { 352 + // For ONNX, we'd need to cache the estimator too 353 + // For now, this is a placeholder - ONNX video is not the primary path 354 + let model_path = crate::model::find_model(&config.encoder_size)?; 355 + let est = crate::depth::OnnxDepthEstimator::new(model_path.to_str().unwrap())?; 356 + est.estimate(&frame)? 357 + } 358 + #[cfg(not(feature = "onnx"))] 359 + { 360 + return Err(SpatialError::ConfigError( 361 + "No depth backend enabled. Enable 'coreml' or 'onnx' feature.".to_string(), 362 + )); 363 + } 364 + }; 365 + 366 + let (left, right) = generate_stereo_pair(&frame, &depth_map, config.max_disparity)?; 367 + 368 + if processed_tx.send((left, right)).await.is_err() { 369 + return Err(SpatialError::Other( 370 + "Encoder stopped unexpectedly".to_string(), 371 + )); 372 + } 373 + } 374 + 375 + drop(processed_tx); 376 + 377 + if let Some(ref cb) = progress_cb { 378 + cb(VideoProgress::new( 379 + total_frames, 380 + total_frames, 381 + "encoding".to_string(), 382 + )); 383 + } 384 + 385 + encode_handle 386 + .await 387 + .map_err(|e| SpatialError::Other(format!("Encoding task failed: {}", e)))??; 388 + 389 + if let Some(ref cb) = progress_cb { 390 + cb(VideoProgress::new( 391 + total_frames, 392 + total_frames, 393 + "complete".to_string(), 394 + )); 395 + } 396 + 397 + Ok(()) 398 + }