diff --git a/tts-studio/.gitignore b/tts-studio/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/tts-studio/.gitignore @@ -0,0 +1 @@ +/target diff --git a/tts-studio/Cargo.lock b/tts-studio/Cargo.lock new file mode 100644 index 0000000..57e531a --- /dev/null +++ b/tts-studio/Cargo.lock @@ -0,0 +1,5253 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "accesskit" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad442f58ee04714aaa0ba0a2768c1ea1935b29507bb22ddece8cbbc76db02932" +dependencies = [ + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c182f9c282ac9c5638d876d551d15e5f7d397ec263349a0c6a2b61595dd5e4" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "phf", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a45e2bb300bea6d5f33a82c3e051c3ddc6f41c209750f68a0acbe991a284344" +dependencies = [ + "accesskit", + "hashbrown 0.17.1", +] + +[[package]] +name = "accesskit_ios" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28e539d64b27fd4c14de8f78ada8f7c944c092f52f7e49fb63316507a57f6937" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", +] + +[[package]] +name = "accesskit_macos" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5701624bf6a11efb6d4c8e922bf44126ed96250e5faa9a7a0468c979635839a4" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202f24df034a7476d07b7f74284de84f6d62aabd858dbe7ee9cad3b7ad6f8f9d" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce63f35d6bdcf59f26b76b3379063f738e6412cef46999cc772d46aa3de35adb" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "static_assertions", + "windows", + "windows-core", +] + +[[package]] +name = "accesskit_winit" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee96dd150375c05912d8a473f98c2fb155fb3234fac88292d396548a2460fa77" +dependencies = [ + "accesskit", + "accesskit_ios", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "alsa" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" +dependencies = [ + "alsa-sys", + "bitflags 2.13.2", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.2", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.21", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "arboard" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb9553e62312c8756a5979254fc6d7da8bbeb5986a1e17037fab932f7037917" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.61.2", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.5", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.5", +] + +[[package]] +name = "async-recursion" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.5", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.3", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.5", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.2", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" +dependencies = [ + "bitflags 2.13.2", + "polling", + "rustix 1.1.5", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.5", + "rustix 1.1.5", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "clipboard-win" +version = "5.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052af7878466feccda12ba7a6a67d019d712f296809559b47c12c8cc196d3a9a" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "coreaudio-rs" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" +dependencies = [ + "bitflags 2.13.2", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "cpal" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f02e8d0327b42d3e2e4ab2119af397344eb9fc54a34bf0ddeaa1277af8681f1" +dependencies = [ + "alsa", + "block2 0.6.2", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2 0.6.5", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "web-sys", + "windows", + "windows-core", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.5", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "ecolor" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31bcbef0d4263a63eb32c4834e5d711613035ce1333468e107d928923e21e6e2" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5642501a65ca657d05f00c643a5c14ff2346cb3818032bfe23d58e1f8e5f84" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "egui_system_font_provider", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "pollster 1.0.1", + "profiling", + "raw-window-handle", + "send_wrapper", + "static_assertions", + "wasm-bindgen", + "web-sys", + "web-time", + "wgpu", + "windows-sys 0.61.2", + "winit", +] + +[[package]] +name = "egui" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9144f49b7a28b5f395d3147e325b4fe3e77ba686eac11d1bdf0a7da3f375f" +dependencies = [ + "accesskit", + "ahash", + "bitflags 2.13.2", + "emath", + "epaint", + "itertools", + "log", + "nohash-hasher", + "profiling", + "smallvec", + "unicode-segmentation", + "web-sys", +] + +[[package]] +name = "egui-wgpu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0309cbe1febed48af5b974e4bcc424b35e9664863a4d402697147b57a488c864" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 2.0.21", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e449f4c4a2a60c7ccb4ba7fa4078417bfe1f9ddc03b5e05ce75748e33a70de" +dependencies = [ + "accesskit_winit", + "arboard", + "block2 0.6.2", + "bytemuck", + "egui", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", + "profiling", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "windows-sys 0.61.2", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9275ca5cc35a6b7c94e0e8cedf22542f66cb0cf45ed8ab2657c78ff49fec7268" +dependencies = [ + "bytemuck", + "egui", + "glow", + "log", + "profiling", + "winit", +] + +[[package]] +name = "egui_system_font_provider" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2a2fdfe3c05dd9f691d2b1388b2312d6dce1af502f217c22136d22d9bd76f6" +dependencies = [ + "epaint", + "fontique", + "log", + "poll-promise", + "skrifa", + "sys-locale", + "unicode-script", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "emath" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e73ce90f54750be8672c02be0743fe47530e6f251e066f0ee2869b59eb297af5" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "epaint" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97bac86ae3dde93fc3951e790951caea739ff39a6c89d909cdaf1d9278dbb1bd" +dependencies = [ + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "font-types", + "harfrust", + "log", + "nohash-hasher", + "parking_lot", + "profiling", + "self_cell", + "skrifa", + "smallvec", + "unicode-general-category", + "unicode-properties", + "unicode-segmentation", + "vello_cpu", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc1cc505827f4c23eedc297975a0fd038268d41293f389dd8ff3b574aefab19" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fearless_simd" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontique" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6688bc1294fe7117d788937b6c53480169b29c566954af490830d4c09da9516a" +dependencies = [ + "hashbrown 0.17.1", + "linebender_resource_handle", + "memmap2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-text", + "objc2-foundation 0.3.2", + "parlance", + "read-fonts", + "roxmltree", + "smallvec", + "windows", + "windows-core", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.5", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glifo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a26c1e23de04bdab3e34a21b6f877a479b96737792516b9b8b8f69b6661be" +dependencies = [ + "bytemuck", + "foldhash 0.2.0", + "hashbrown 0.17.1", + "log", + "peniko", + "png", + "skrifa", + "smallvec", + "vello_common", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.13.2", + "cfg_aliases", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.21", + "windows", +] + +[[package]] +name = "guillotiere" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" +dependencies = [ + "euclid", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "read-fonts", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" + +[[package]] +name = "icu_properties" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags 2.13.2", + "libc", + "plain", + "redox_syscall 0.9.5", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "litrs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0ca1155a9ecc25aa12e8313a119f32edf0255fea2f44839c4ac0a12b2ac157" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "naga" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.17.1", + "indexmap", + "libm", + "log", + "naga-types", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.21", + "unicode-ident", +] + +[[package]] +name = "naga-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" +dependencies = [ + "hashbrown 0.17.1", + "indexmap", + "rustc-hash 1.1.0", + "thiserror 2.0.21", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d5490aaf8f1d7cf7688dfa9b0ce07900e168852c45cd2c03f534dfd27cfd0b" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.3", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.13.2", + "libc", + "objc2 0.6.5", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.5", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "dispatch2", + "libc", + "objc2 0.6.5", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.3", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.5", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "parlance" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73072a9470554c8e794778acece2312661218aaabb635600957d952cf01c716e" + +[[package]] +name = "peniko" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" +dependencies = [ + "bytemuck", + "color", + "kurbo", + "linebender_resource_handle", + "smallvec", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "poll-promise" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6a58fecbf9da8965bcdb20ce4fd29788d1acee68ddbb64f0ba1b81bccdb7df" +dependencies = [ + "document-features", + "static_assertions", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "pollster" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_distr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types", + "once_cell", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_syscall" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "580c875845b41ecb1564ad1ae4703044cef774145e3d7a94dc2b3fa7c97868fd" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rfd" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" +dependencies = [ + "block2 0.6.2", + "dispatch2", + "js-sys", + "libc", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "percent-encoding", + "pollster 0.4.0", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rodio" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e3ee0a5ca418bac59a15d5506e498ab138359edbb67257a6b8a3b93d411f39" +dependencies = [ + "cpal", + "dasp_sample", + "num-rational", + "rand", + "rand_distr", + "rtrb", + "symphonia", + "thiserror 2.0.21", +] + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rtrb" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fae8ee26b0371a29a77d2b2d6b3ae13aa81def6f9bf1b1b92a32d279a5e709b7" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.2", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.13.2", + "calloop 0.14.5", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.5", + "thiserror 2.0.21", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symphonia" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-ogg", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "tts-studio" +version = "0.1.0" +dependencies = [ + "base64", + "dirs", + "eframe", + "hound", + "rfd", + "rodio", + "serde", + "serde_json", + "toml", + "ureq", +] + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.3", +] + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde93ff05758a36a851a398a772b0b79a82fbe3c186a46f2696baaeaf84c966b" +dependencies = [ + "base64", + "cookie_store", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vello_common" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "log", + "peniko", + "png", + "smallvec", + "thiserror 2.0.21", +] + +[[package]] +name = "vello_cpu" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace506ca414548966fcf3c283be94eb1cad8254de488791ee698bf438b0890a9" +dependencies = [ + "bytemuck", + "glifo", + "hashbrown 0.17.1", + "png", + "vello_common", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.5", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix 1.1.5", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.13.2", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.5", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c8ddfbbf66e501434657400268cefe51b95140a0266236415cda7627732df7" +dependencies = [ + "jni", + "log", + "ndk-context", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + +[[package]] +name = "webpki-roots" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.13.2", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "indexmap", + "log", + "naga", + "naga-types", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.21", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d260ca2ca8adc654f513a869bf3cac0b47f5b8d3f87803d6942015f8de597aae" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.13.2", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "hashbrown 0.17.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "naga-types", + "ndk-sys", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "static_assertions", + "thiserror 2.0.21", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows", + "windows-core", + "windows-result", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "js-sys", + "log", + "naga-types", + "raw-window-handle", + "static_assertions", + "web-sys", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.2", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wl-clipboard-rs" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d7888ccd4896447b2d14d3a9350a85df2aeb6f181e2e7a31349d104ac46cac1" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.1.5", + "thiserror 2.0.21", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.5", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.2", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.5", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.9", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812a12be4ead7a59e18ea8f047d08add0066b73bc62fc1467066a2472ae088dd" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bf6d6cd6a4b601acb64cfb8284cdc643fcfa75ef26ae3c46ec8aa4e8a1fbb7" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2672456ba1cb54b5eaec136e63135b3c7495fb938e53b28a62e633496d486d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.9", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.9", + "winnow", +] diff --git a/tts-studio/Cargo.toml b/tts-studio/Cargo.toml new file mode 100644 index 0000000..c201e02 --- /dev/null +++ b/tts-studio/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tts-studio" +version = "0.1.0" +edition = "2024" + +[dependencies] +base64 = "0.23" +dirs = "7" +eframe = "0.37" +hound = "3.5" +rfd = "0.17" +rodio = "0.23" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "1" +ureq = { version = "3.5", features = ["json"] } diff --git a/tts-studio/config.example.toml b/tts-studio/config.example.toml new file mode 100644 index 0000000..dd9d7b2 --- /dev/null +++ b/tts-studio/config.example.toml @@ -0,0 +1,20 @@ +# Optionale Konfiguration fuer TTS Studio. +# Nach ~/.config/tts-studio/config.toml kopieren und anpassen. Ohne Datei gelten +# die Standardwerte unten (Pfade relativ zum Repo, Server auf der DGX). + +voice_dir = "/home/renato/projects/tts/voice_clone" # Klonstimmen: .wav + .txt +reading_text_file = "/home/renato/projects/tts/voice_clone/referenztext.txt" # Vorlesetext beim Aufnehmen +music_file = "/home/renato/projects/tts/background.wav" +output_dir = "/home/renato/projects/tts" # Startordner beim Speichern + +whisper_url = "http://10.146.97.103:8020/v1/audio/transcriptions" +whisper_model = "whisper-turbo-de" + +# Modellauswahl: jeder Eintrag braucht einen OpenAI-kompatiblen /v1/audio/speech-Endpunkt. +[[models]] +name = "Qwen3-TTS 1.7B (DGX)" +clone_url = "http://10.146.97.103:8012/v1/audio/speech" # Base-Modell, Klonstimmen (ref_audio) +voice_url = "http://10.146.97.103:8011/v1/audio/speech" # CustomVoice-Modell, feste Stimmen +voices = ["serena", "vivian", "ryan", "aiden", "uncle_fu"] +language = "German" +max_ref_seconds = 30.0 # laengste Vorlage, die der Server annimmt diff --git a/tts-studio/src/audio.rs b/tts-studio/src/audio.rs new file mode 100644 index 0000000..8335a13 --- /dev/null +++ b/tts-studio/src/audio.rs @@ -0,0 +1,152 @@ +//! Vorhoeren (Wiedergabe) und Mikrofonaufnahme ueber rodio. + +use crate::media; +use rodio::microphone::MicrophoneBuilder; +use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink}; +use std::path::Path; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::{Arc, Mutex, mpsc}; +use std::time::{Duration, Instant}; + +#[derive(Default)] +pub struct Player { + // beide muessen leben, solange etwas spielt + sink: Option, + current: Option, + error: Option, +} + +impl Player { + pub fn play(&mut self, path: &Path) { + self.stop(); + if let Err(e) = self.try_play(path) { + self.error = Some(format!("Wiedergabe nicht möglich: {e}")); + } + } + + fn try_play(&mut self, path: &Path) -> Result<(), String> { + if self.sink.is_none() { + let mut sink = DeviceSinkBuilder::open_default_sink().map_err(|e| e.to_string())?; + sink.log_on_drop(false); + self.sink = Some(sink); + } + let sink = self.sink.as_ref().expect("sink"); + let file = std::fs::File::open(path).map_err(|e| format!("{}: {e}", path.display()))?; + let player = rodio::Player::connect_new(sink.mixer()); + player.append(Decoder::try_from(file).map_err(|e| e.to_string())?); + self.current = Some(player); + Ok(()) + } + + pub fn stop(&mut self) { + if let Some(p) = self.current.take() { + p.stop(); + } + } + + pub fn is_playing(&self) -> bool { + self.current.as_ref().is_some_and(|p| !p.empty()) + } + + pub fn take_error(&mut self) -> Option { + self.error.take() + } +} + +/// Laufende Mikrofonaufnahme. Das Mikrofon puffert nur ~100 ms, deshalb liest ein +/// eigener Thread laufend mit und sammelt die Samples hier ein. +pub struct Recording { + stop: Arc, + level: Arc, + samples: Arc>>, + sample_rate: u32, + channels: u16, + started: Instant, +} + +impl Recording { + pub fn start() -> Result { + let stop = Arc::new(AtomicBool::new(false)); + let level = Arc::new(AtomicU32::new(0)); + let samples = Arc::new(Mutex::new(Vec::new())); + let (tx, rx) = mpsc::channel(); + let (stop_t, level_t, samples_t) = (stop.clone(), level.clone(), samples.clone()); + std::thread::spawn(move || { + let opened = MicrophoneBuilder::new() + .default_device() + .and_then(|b| b.default_config()) + .map_err(|e| e.to_string()) + .and_then(|b| b.open_stream().map_err(|e| e.to_string())) + .map_err(|e| format!("Mikrofon nicht verfügbar: {e}")); + let mut mic = match opened { + Ok(m) => m, + Err(e) => { + let _ = tx.send(Err(e)); + return; + } + }; + let cfg = *mic.config(); + let (sr, ch) = (cfg.sample_rate.get(), cfg.channel_count.get()); + let _ = tx.send(Ok((sr, ch))); + let block = (sr as usize * ch as usize / 20).max(1); // 50 ms + let mut buf = Vec::with_capacity(block); + // next() blockiert, bis Samples da sind; Stopp wird beim naechsten Sample bemerkt + while !stop_t.load(Ordering::Relaxed) { + let Some(s) = mic.next() else { break }; + buf.push(s); + if buf.len() >= block { + let rms = (buf.iter().map(|s| s * s).sum::() / buf.len() as f32).sqrt(); + level_t.store(rms.to_bits(), Ordering::Relaxed); + samples_t.lock().expect("samples").append(&mut buf); + } + } + samples_t.lock().expect("samples").append(&mut buf); + }); + let (sample_rate, channels) = rx.recv_timeout(Duration::from_secs(5)).map_err(|_| "Mikrofon antwortet nicht.".to_string())??; + Ok(Recording { stop, level, samples, sample_rate, channels, started: Instant::now() }) + } + + pub fn elapsed(&self) -> f32 { + self.started.elapsed().as_secs_f32() + } + + /// Pegel fuer die Anzeige, 0..1 ueber -60..0 dBFS + pub fn level(&self) -> f32 { + let rms = f32::from_bits(self.level.load(Ordering::Relaxed)); + ((20.0 * (rms + 1e-9).log10() + 60.0) / 60.0).clamp(0.0, 1.0) + } + + /// Aufnahme beenden und als Mono-WAV speichern. + pub fn stop(self, file: &Path) -> Result<(), String> { + self.stop.store(true, Ordering::Relaxed); + std::thread::sleep(Duration::from_millis(100)); // letzten Block einsammeln lassen + let interleaved = std::mem::take(&mut *self.samples.lock().map_err(|e| e.to_string())?); + let ch = self.channels.max(1) as usize; + let mono: Vec = interleaved.chunks(ch).map(|c| c.iter().sum::() / ch as f32).collect(); + if media::seconds(mono.len(), self.sample_rate) < 1.0 { + return Err("Aufnahme ist leer oder zu kurz – Mikrofon prüfen.".into()); + } + media::write_wav(file, &mono, self.sample_rate) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Echte Hardware: cargo test -- --ignored audio + #[test] + #[ignore] + fn audio_geraete() { + let mut sink = DeviceSinkBuilder::open_default_sink().expect("Ausgabegerät"); + sink.log_on_drop(false); + let rec = Recording::start().expect("Mikrofon"); + std::thread::sleep(Duration::from_millis(1500)); + println!("Mikrofon: {} Hz, {} Kanal/Kanäle, Pegel {:.2}", rec.sample_rate, rec.channels, rec.level()); + let file = std::env::temp_dir().join("tts-studio-mic-test.wav"); + rec.stop(&file).expect("Aufnahme"); + let (s, sr) = media::read_wav_bytes(&std::fs::read(&file).unwrap()).unwrap(); + println!("aufgenommen: {:.2} s", media::seconds(s.len(), sr)); + std::fs::remove_file(&file).unwrap(); + } +} diff --git a/tts-studio/src/config.rs b/tts-studio/src/config.rs new file mode 100644 index 0000000..0d5f55e --- /dev/null +++ b/tts-studio/src/config.rs @@ -0,0 +1,106 @@ +//! Einstellungen: eingebaute Defaults, optional ueberschrieben durch +//! ~/.config/tts-studio/config.toml (siehe config.example.toml). + +use serde::Deserialize; +use std::path::{Path, PathBuf}; + +const DGX: &str = "http://10.146.97.103"; + +/// Repo-Wurzel (Elternordner von tts-studio/), dort liegen voice_clone/ und background.wav. +fn repo_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).parent().map(Path::to_path_buf).unwrap_or_default() +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(default)] +pub struct Config { + /// Klonstimmen: .wav + .txt (Transkript) + pub voice_dir: PathBuf, + /// Vorlesetext, der beim Aufnehmen einer neuen Stimme angezeigt wird + pub reading_text_file: PathBuf, + pub music_file: PathBuf, + pub output_dir: PathBuf, + /// Whisper (OpenAI-kompatibel) zum Transkribieren neuer Stimmen + pub whisper_url: String, + pub whisper_model: String, + pub models: Vec, +} + +#[derive(Deserialize, Clone, Debug)] +pub struct Model { + pub name: String, + /// /v1/audio/speech mit ref_audio + ref_text (Klonstimmen) + pub clone_url: Option, + /// /v1/audio/speech mit voice= (feste Stimmen) + pub voice_url: Option, + #[serde(default)] + pub voices: Vec, + #[serde(default = "default_language")] + pub language: String, + /// Laengste Vorlage, die der Server annimmt (Qwen3-TTS: 30 s) + #[serde(default = "default_max_ref")] + pub max_ref_seconds: f32, +} + +fn default_language() -> String { + "German".into() +} + +fn default_max_ref() -> f32 { + 30.0 +} + +impl Default for Config { + fn default() -> Self { + let repo = repo_dir(); + Config { + voice_dir: repo.join("voice_clone"), + reading_text_file: repo.join("voice_clone/referenztext.txt"), + music_file: repo.join("background.wav"), + output_dir: repo.clone(), + whisper_url: format!("{DGX}:8020/v1/audio/transcriptions"), + whisper_model: "whisper-turbo-de".into(), + models: vec![Model { + name: "Qwen3-TTS 1.7B (DGX)".into(), + clone_url: Some(format!("{DGX}:8012/v1/audio/speech")), + voice_url: Some(format!("{DGX}:8011/v1/audio/speech")), + voices: ["serena", "vivian", "ryan", "aiden", "uncle_fu"].map(String::from).to_vec(), + language: default_language(), + max_ref_seconds: default_max_ref(), + }], + } + } +} + +pub fn config_path() -> Option { + dirs::config_dir().map(|d| d.join("tts-studio/config.toml")) +} + +/// Laedt die Konfiguration; Fehler in der Datei werden gemeldet, nicht verschluckt. +pub fn load() -> (Config, Option) { + let Some(path) = config_path().filter(|p| p.exists()) else { + return (Config::default(), None); + }; + match std::fs::read_to_string(&path).map_err(|e| e.to_string()).and_then(|s| toml::from_str(&s).map_err(|e| e.to_string())) { + Ok(cfg) => (cfg, None), + Err(e) => (Config::default(), Some(format!("{}: {e} – verwende Standardwerte", path.display()))), + } +} + +/// Basis-URL eines Endpunkts fuer den Erreichbarkeits-Check (".../v1/audio/speech" -> ".../v1/models"). +pub fn models_url(endpoint: &str) -> String { + match endpoint.find("/v1/") { + Some(i) => format!("{}/v1/models", &endpoint[..i]), + None => endpoint.to_string(), + } +} + +#[cfg(test)] +mod tests { + #[test] + fn beispielkonfiguration_ist_gueltig() { + let cfg: super::Config = toml::from_str(include_str!("../config.example.toml")).unwrap(); + assert_eq!(cfg.models.len(), 1); + assert_eq!(super::models_url(cfg.models[0].clone_url.as_deref().unwrap()), "http://10.146.97.103:8012/v1/models"); + } +} diff --git a/tts-studio/src/http.rs b/tts-studio/src/http.rs new file mode 100644 index 0000000..be73dd1 --- /dev/null +++ b/tts-studio/src/http.rs @@ -0,0 +1,67 @@ +//! Aufrufe an die Server auf der DGX: Sprache erzeugen (Qwen3-TTS), +//! transkribieren (Whisper), Erreichbarkeit pruefen. + +use std::time::Duration; + +fn agent(timeout_secs: u64) -> ureq::Agent { + ureq::Agent::config_builder() + .timeout_global(Some(Duration::from_secs(timeout_secs))) + // Fehlerantworten selbst auswerten, der Server erklaert im Body, was nicht passt + .http_status_as_error(false) + .build() + .into() +} + +fn net_err(url: &str, e: ureq::Error) -> String { + format!("{url} nicht erreichbar: {e}") +} + +/// Liest die Antwort; bei Fehlerstatus wird die Meldung des Servers zurueckgegeben. +fn read_body(url: &str, mut resp: ureq::http::Response) -> Result, String> { + let status = resp.status(); + let body = resp.body_mut().with_config().limit(100 * 1024 * 1024).read_to_vec().map_err(|e| net_err(url, e))?; + if status.is_success() { + return Ok(body); + } + let text = String::from_utf8_lossy(&body); + // vLLM: {"error": {"message": "..."}} + let msg = serde_json::from_str::(&text) + .ok() + .and_then(|v| v["error"]["message"].as_str().map(String::from)) + .unwrap_or_else(|| text.trim().to_string()); + Err(format!("Server meldet {status}: {msg}")) +} + +/// POST /v1/audio/speech -> WAV-Bytes. Erster Aufruf nach dem Serverstart dauert ~20 s. +pub fn speech(url: &str, body: &serde_json::Value) -> Result, String> { + let resp = agent(300).post(url).send_json(body).map_err(|e| net_err(url, e))?; + read_body(url, resp) +} + +/// POST /v1/audio/transcriptions (multipart) -> erkannter Text. +pub fn transcribe(url: &str, model: &str, wav: &[u8]) -> Result { + let boundary = format!("----tts-studio-{}", std::process::id()); + let mut body = Vec::with_capacity(wav.len() + 512); + for (name, value) in [("model", model), ("language", "de"), ("response_format", "json")] { + body.extend(format!("--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n{value}\r\n").as_bytes()); + } + body.extend( + format!("--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"stimme.wav\"\r\nContent-Type: audio/wav\r\n\r\n") + .as_bytes(), + ); + body.extend(wav); + body.extend(format!("\r\n--{boundary}--\r\n").as_bytes()); + + let resp = agent(180) + .post(url) + .header("Content-Type", format!("multipart/form-data; boundary={boundary}")) + .send(&body[..]) + .map_err(|e| net_err(url, e))?; + let json: serde_json::Value = serde_json::from_slice(&read_body(url, resp)?).map_err(|e| format!("Whisper-Antwort unlesbar: {e}"))?; + json["text"].as_str().map(|t| t.trim().to_string()).ok_or_else(|| "Whisper hat keinen Text geliefert.".into()) +} + +/// Kurzer Check, ob ein Server antwortet (GET .../v1/models). +pub fn reachable(models_url: &str) -> bool { + agent(3).get(models_url).call().is_ok_and(|r| r.status().is_success()) +} diff --git a/tts-studio/src/main.rs b/tts-studio/src/main.rs new file mode 100644 index 0000000..60e68e5 --- /dev/null +++ b/tts-studio/src/main.rs @@ -0,0 +1,564 @@ +//! TTS Studio: Ansagen mit Qwen3-TTS (DGX) erzeugen – Stimme waehlen oder klonen, +//! Text eingeben, optional Hintergrundmusik, fuer Asterisk oder WhatsApp speichern. + +mod audio; +mod config; +mod http; +mod media; +mod synth; +mod voices; + +use config::Config; +use eframe::egui::{self, Color32, RichText}; +use std::path::PathBuf; +use std::sync::mpsc::{Receiver, Sender, channel}; +use synth::{Job, Music, Rendered, Target}; +use voices::{Prepared, Voice}; + +fn main() -> eframe::Result { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_title("TTS Studio") + .with_app_id("tts-studio") + .with_inner_size([780.0, 760.0]) + .with_min_inner_size([600.0, 560.0]), + ..Default::default() + }; + eframe::run_native("TTS Studio", options, Box::new(|cc| Ok(Box::new(App::new(&cc.egui_ctx))))) +} + +/// Nachrichten der Hintergrund-Threads an die Oberflaeche +enum Msg { + Progress(String), + Rendered(Result), + Server { url: String, online: bool }, + Prepared(Result), +} + +enum Status { + None, + Info(String), + Error(String), +} + +/// Fenster "Neue Stimme anlegen" +#[derive(Default)] +struct NewVoice { + name: String, + recording: Option, + busy: bool, + prepared: Option, + transcript: String, + error: Option, +} + +struct App { + cfg: Config, + model_idx: usize, + voices: Vec, + voice_idx: usize, + text: String, + cursor: Option, + music_on: bool, + music_file: PathBuf, + music_volume: f32, + intro_secs: f32, + target: Target, + speed: f32, + sentence_pause: f32, + comma_pause: f32, + busy: Option, + status: Status, + result: Option, + servers: Vec<(String, String, Option)>, // Bezeichnung, URL, online? + reading_text: String, + new_voice: Option, + player: audio::Player, + work: PathBuf, + tx: Sender, + rx: Receiver, + ctx: egui::Context, +} + +impl App { + fn new(ctx: &egui::Context) -> Self { + let (cfg, config_error) = config::load(); + let (tx, rx) = channel(); + let work = std::env::temp_dir().join(format!("tts-studio-{}", std::process::id())); + let _ = std::fs::create_dir_all(&work); + let reading_text = std::fs::read_to_string(&cfg.reading_text_file).unwrap_or_default().trim().to_string(); + let mut app = App { + music_file: cfg.music_file.clone(), + cfg, + model_idx: 0, + voices: Vec::new(), + voice_idx: 0, + text: String::new(), + cursor: None, + music_on: false, + music_volume: 0.15, + intro_secs: 2.0, + speed: 1.0, + sentence_pause: 0.8, + comma_pause: 0.0, + target: Target::AsteriskWav, + busy: None, + status: config_error.map_or(Status::None, Status::Error), + result: None, + servers: Vec::new(), + reading_text, + new_voice: None, + player: audio::Player::default(), + work, + tx, + rx, + ctx: ctx.clone(), + }; + app.select_model(0); + app + } + + fn model(&self) -> &config::Model { + &self.cfg.models[self.model_idx] + } + + fn select_model(&mut self, idx: usize) { + self.model_idx = idx; + self.reload_voices(None); + self.check_servers(); + } + + /// Klonstimmen + feste Stimmen des Modells; `select` waehlt eine Stimme nach Namen aus. + fn reload_voices(&mut self, select: Option<&str>) { + let previous = self.voices.get(self.voice_idx).map(Voice::label); + let model = self.model().clone(); + self.voices = Vec::new(); + if model.clone_url.is_some() { + self.voices.extend(voices::list_clones(&self.cfg.voice_dir)); + } + if model.voice_url.is_some() { + self.voices.extend(model.voices.iter().cloned().map(Voice::Preset)); + } + let wanted = select.map(String::from).or(previous); + self.voice_idx = wanted.and_then(|w| self.voices.iter().position(|v| v.label() == w || matches!(v, Voice::Clone { name, .. } if *name == w))).unwrap_or(0); + } + + fn check_servers(&mut self) { + let model = self.model().clone(); + self.servers = [("Klonstimmen", model.clone_url), ("Feste Stimmen", model.voice_url)] + .into_iter() + .filter_map(|(label, url)| url.map(|u| (label.to_string(), u, None))) + .collect(); + for (_, url, _) in &self.servers { + let (tx, ctx, url) = (self.tx.clone(), self.ctx.clone(), url.clone()); + std::thread::spawn(move || { + let online = http::reachable(&config::models_url(&url)); + let _ = tx.send(Msg::Server { url, online }); + ctx.request_repaint(); + }); + } + } + + fn poll(&mut self) { + while let Ok(msg) = self.rx.try_recv() { + match msg { + Msg::Progress(p) => self.busy = Some(p), + Msg::Rendered(r) => { + self.busy = None; + match r { + Ok(r) => { + self.status = Status::Info(format!("Fertig: {:.1} s ({}). Jetzt anhören oder speichern.", r.secs, r.target.label())); + self.player.play(&r.preview); + self.result = Some(r); + } + Err(e) => self.status = Status::Error(e), + } + } + Msg::Server { url, online } => { + for s in self.servers.iter_mut().filter(|s| s.1 == url) { + s.2 = Some(online); + } + } + Msg::Prepared(r) => { + if let Some(nv) = &mut self.new_voice { + nv.busy = false; + match r { + Ok(p) => { + nv.transcript = p.transcript.clone(); + nv.prepared = Some(p); + } + Err(e) => nv.error = Some(e), + } + } + } + } + } + } + + fn generate(&mut self) { + let Some(voice) = self.voices.get(self.voice_idx).cloned() else { + self.status = Status::Error("Keine Stimme verfügbar.".into()); + return; + }; + let job = Job { + model: self.model().clone(), + voice, + text: self.text.clone(), + music: self.music_on.then(|| Music { file: self.music_file.clone(), volume: self.music_volume, intro_secs: self.intro_secs }), + target: self.target, + speed: self.speed, + sentence_pause_ms: (self.sentence_pause * 1000.0).round() as u32, + comma_pause_ms: (self.comma_pause * 1000.0).round() as u32, + }; + self.player.stop(); + self.busy = Some("Starte …".into()); + self.status = Status::None; + let (tx, ctx, work) = (self.tx.clone(), self.ctx.clone(), self.work.clone()); + std::thread::spawn(move || { + let progress = |m: String| { + let _ = tx.send(Msg::Progress(m)); + ctx.request_repaint(); + }; + let r = synth::render(&job, &work, &progress); + let _ = tx.send(Msg::Rendered(r)); + ctx.request_repaint(); + }); + } + + fn save_result(&mut self) { + let Some(r) = &self.result else { return }; + let ext = r.target.extension(); + let voice = self.voices.get(self.voice_idx).map(|v| match v { + Voice::Clone { name, .. } | Voice::Preset(name) => name.clone(), + }); + let suggested = format!("ansage_{}.{ext}", voice.unwrap_or_default()); + let Some(mut path) = rfd::FileDialog::new() + .set_title("Ansage speichern") + .set_directory(&self.cfg.output_dir) + .set_file_name(&suggested) + .add_filter(r.target.label(), &[ext]) + .save_file() + else { + return; + }; + if path.extension().and_then(|e| e.to_str()) != Some(ext) { + path.set_extension(ext); + } + self.status = match std::fs::copy(&r.output, &path) { + Ok(_) => { + if let Some(dir) = path.parent() { + self.cfg.output_dir = dir.to_path_buf(); + } + Status::Info(format!("Gespeichert: {}", path.display())) + } + Err(e) => Status::Error(format!("Speichern fehlgeschlagen: {e}")), + }; + } + + fn insert_pause(&mut self, tag: &str) { + let chars: Vec = self.text.chars().collect(); + let at = self.cursor.unwrap_or(chars.len()).min(chars.len()); + let before: String = chars[..at].iter().collect(); + let after: String = chars[at..].iter().collect(); + let pad_l = if before.is_empty() || before.ends_with(' ') { "" } else { " " }; + let pad_r = if after.starts_with(' ') { "" } else { " " }; + self.text = format!("{before}{pad_l}{tag}{pad_r}{after}"); + self.cursor = Some(at + pad_l.len() + tag.chars().count() + pad_r.len()); + } + + fn main_ui(&mut self, ui: &mut egui::Ui) { + let busy = self.busy.is_some(); + egui::Grid::new("form").num_columns(2).spacing([14.0, 10.0]).show(ui, |ui| { + ui.label("Modell"); + ui.horizontal(|ui| { + let mut idx = self.model_idx; + egui::ComboBox::from_id_salt("model").width(260.0).selected_text(&self.model().name).show_ui(ui, |ui| { + for (i, m) in self.cfg.models.iter().enumerate() { + ui.selectable_value(&mut idx, i, &m.name); + } + }); + if idx != self.model_idx { + self.select_model(idx); + } + for (label, _, online) in &self.servers { + let (color, tip) = match online { + None => (Color32::GRAY, "wird geprüft"), + Some(true) => (Color32::from_rgb(40, 160, 70), "erreichbar"), + Some(false) => (Color32::from_rgb(200, 50, 50), "nicht erreichbar – Server auf der DGX starten"), + }; + ui.label(RichText::new(format!("● {label}")).color(color)).on_hover_text(tip); + } + if ui.small_button("🔄").on_hover_text("Server erneut prüfen").clicked() { + self.check_servers(); + } + }); + ui.end_row(); + + ui.label("Stimme"); + ui.horizontal(|ui| { + let selected = self.voices.get(self.voice_idx).map(Voice::label).unwrap_or_else(|| "– keine –".into()); + egui::ComboBox::from_id_salt("voice").width(260.0).selected_text(selected).show_ui(ui, |ui| { + for (i, v) in self.voices.iter().enumerate() { + ui.selectable_value(&mut self.voice_idx, i, v.label()); + } + }); + if let Some(Voice::Clone { audio, .. }) = self.voices.get(self.voice_idx) + && ui.button("▶ Vorlage").on_hover_text("Aufnahme anhören, aus der die Stimme geklont wird").clicked() { + self.player.play(audio); + } + if ui.button("+ Neue Stimme …").clicked() && self.new_voice.is_none() { + self.new_voice = Some(NewVoice::default()); + } + }); + ui.end_row(); + + ui.label("Musik"); + ui.horizontal(|ui| { + ui.checkbox(&mut self.music_on, "Hintergrundmusik"); + ui.add_enabled_ui(self.music_on, |ui| { + let name = self.music_file.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); + if ui.button(format!("📂 {name}")).on_hover_text(self.music_file.display().to_string()).clicked() + && let Some(p) = rfd::FileDialog::new().add_filter("Audio", &["wav", "mp3", "ogg", "flac", "m4a"]).pick_file() { + self.music_file = p; + } + ui.add(egui::Slider::new(&mut self.music_volume, 0.02..=0.5).text("Lautstärke").fixed_decimals(2)); + ui.add(egui::Slider::new(&mut self.intro_secs, 0.0..=5.0).text("s Vorlauf").fixed_decimals(1)); + }); + }); + ui.end_row(); + + ui.label("Sprechweise"); + ui.horizontal(|ui| { + ui.add(egui::Slider::new(&mut self.speed, 0.75..=1.25).step_by(0.05).fixed_decimals(2).suffix(" ×").text("Tempo")) + .on_hover_text("Tonhöhe bleibt gleich; Pausen sind davon nicht betroffen"); + ui.add(egui::Slider::new(&mut self.sentence_pause, 0.0..=2.0).step_by(0.1).fixed_decimals(1).suffix(" s").text("nach Satzende")) + .on_hover_text("Pause nach . ! ? – 0 = Pausen wie vom Modell"); + ui.add(egui::Slider::new(&mut self.comma_pause, 0.0..=1.0).step_by(0.1).fixed_decimals(1).suffix(" s").text("nach Komma")) + .on_hover_text("Pause nach , ; : – teilt den Satz dort, das Modell betont das Teilstück dann wie ein Satzende. 0 = aus"); + }); + ui.end_row(); + + ui.label("Ziel"); + ui.horizontal(|ui| { + for t in Target::ALL { + ui.radio_value(&mut self.target, t, t.label()); + } + }); + ui.end_row(); + }); + + ui.add_space(8.0); + ui.horizontal(|ui| { + ui.label(RichText::new("Text").strong()); + ui.add_space(8.0); + for (label, tag) in [("+ Pause 0,5 s", ""), ("+ Pause 1 s", "")] { + if ui.small_button(label).on_hover_text("an der Cursorposition einfügen").clicked() { + self.insert_pause(tag); + } + } + ui.label(RichText::new("Zahlen am besten ausschreiben („acht Uhr dreißig“)").weak().small()); + }); + let editor = egui::TextEdit::multiline(&mut self.text) + .desired_rows(9) + .desired_width(f32::INFINITY) + .hint_text("Text der Ansage …") + .show(ui); + if let Some(range) = editor.cursor_range { + self.cursor = Some(range.primary.index.0); + } + + ui.add_space(8.0); + ui.horizontal(|ui| { + let ctrl_enter = ui.input(|i| i.modifiers.command && i.key_pressed(egui::Key::Enter)); + let can = !busy && !self.text.trim().is_empty() && !self.voices.is_empty(); + if ui.add_enabled(can, egui::Button::new(RichText::new("🔊 Erzeugen").strong()).min_size([130.0, 30.0].into())).on_hover_text("Strg+Enter").clicked() || (can && ctrl_enter) { + self.generate(); + } + if let Some(p) = &self.busy { + ui.spinner(); + ui.label(p); + } + }); + + if let Some(r) = &self.result { + ui.add_space(6.0); + let (secs, label, preview) = (r.secs, r.target.label(), r.preview.clone()); + ui.group(|ui| { + ui.horizontal(|ui| { + ui.label(RichText::new(format!("Ergebnis: {secs:.1} s · {label}")).strong()); + if self.player.is_playing() { + if ui.button("⏹ Stopp").clicked() { + self.player.stop(); + } + } else if ui.button("▶ Abspielen").clicked() { + self.player.play(&preview); + } + if ui.button("💾 Speichern unter …").clicked() { + self.save_result(); + } + }); + }); + } + + ui.add_space(6.0); + match &self.status { + Status::None => {} + Status::Info(m) => { + ui.label(m); + } + Status::Error(m) => { + ui.colored_label(Color32::from_rgb(200, 50, 50), m); + } + } + if let Some(e) = self.player.take_error() { + self.status = Status::Error(e); + } + } + + fn new_voice_window(&mut self, ctx: &egui::Context) { + let Some(mut nv) = self.new_voice.take() else { return }; + let mut open = true; + let mut saved: Option = None; + egui::Window::new("Neue Stimme anlegen").open(&mut open).collapsible(false).default_width(600.0).show(ctx, |ui| { + ui.label( + "Aufnahme von ca. 15–25 s in ruhiger Umgebung. So sprechen, wie die Ansagen klingen sollen – \ + das Modell übernimmt Tonfall und Tempo. Längere Aufnahmen werden automatisch in einer Sprechpause gekürzt.", + ); + ui.add_space(6.0); + ui.horizontal(|ui| { + ui.label("Name"); + ui.add(egui::TextEdit::singleline(&mut nv.name).hint_text("z. B. martina").desired_width(200.0)); + }); + ui.add_space(6.0); + + let idle = !nv.busy && nv.recording.is_none(); + ui.horizontal(|ui| { + if ui.add_enabled(idle, egui::Button::new("📂 Datei importieren …")).clicked() + && let Some(p) = rfd::FileDialog::new().add_filter("Audio", &["wav", "mp3", "m4a", "ogg", "opus", "flac", "aac", "g722"]).pick_file() { + self.start_prepare(&mut nv, p); + } + ui.label("oder"); + match &nv.recording { + None => { + if ui.add_enabled(idle, egui::Button::new("⏺ Mit Mikrofon aufnehmen")).clicked() { + self.player.stop(); + match audio::Recording::start() { + Ok(r) => { + nv.recording = Some(r); + nv.error = None; + } + Err(e) => nv.error = Some(e), + } + } + } + Some(rec) => { + let secs = rec.elapsed(); + let stop = ui.button(RichText::new("⏹ Aufnahme beenden").color(Color32::from_rgb(200, 50, 50))).clicked(); + ui.label(format!("{:02}:{:02}", secs as u32 / 60, secs as u32 % 60)); + ui.add(egui::ProgressBar::new(rec.level()).desired_width(140.0)).on_hover_text("Pegel"); + ctx.request_repaint_after(std::time::Duration::from_millis(50)); + if stop { + let rec = nv.recording.take().expect("recording"); + let file = self.work.join("mikrofon.wav"); + match rec.stop(&file) { + Ok(()) => self.start_prepare(&mut nv, file), + Err(e) => nv.error = Some(e), + } + } + } + } + }); + + if nv.recording.is_some() && !self.reading_text.is_empty() { + ui.add_space(6.0); + ui.label(RichText::new("Bitte vorlesen:").strong()); + egui::Frame::group(ui.style()).show(ui, |ui| { + ui.label(RichText::new(&self.reading_text).size(16.0)); + }); + } + + if nv.busy { + ui.horizontal(|ui| { + ui.spinner(); + ui.label("Bereite Aufnahme auf und transkribiere …"); + }); + } + + if let Some(p) = &nv.prepared { + ui.add_space(6.0); + ui.separator(); + ui.horizontal(|ui| { + let info = if p.shortened { + format!("Vorlage: {:.1} s (gekürzt von {:.1} s)", p.secs, p.original_secs) + } else { + format!("Vorlage: {:.1} s", p.secs) + }; + ui.label(RichText::new(info).strong()); + if ui.button("▶ Anhören").clicked() { + self.player.play(&p.wav); + } + if self.player.is_playing() && ui.button("⏹").clicked() { + self.player.stop(); + } + }); + ui.label("Transkript – muss genau dem Gesprochenen entsprechen (bitte anhören und korrigieren):"); + ui.add(egui::TextEdit::multiline(&mut nv.transcript).desired_rows(5).desired_width(f32::INFINITY)); + ui.add_space(4.0); + if ui.add_enabled(!nv.name.trim().is_empty(), egui::Button::new(RichText::new("💾 Stimme speichern").strong())).clicked() { + match voices::save(&self.cfg.voice_dir, &nv.name, &p.wav, &nv.transcript) { + Ok(v) => saved = Some(v), + Err(e) => nv.error = Some(e), + } + } + } + + if let Some(e) = &nv.error { + ui.colored_label(Color32::from_rgb(200, 50, 50), e); + } + }); + + if let Some(Voice::Clone { name, .. }) = saved { + self.reload_voices(Some(&name)); + self.status = Status::Info(format!("Stimme \"{name}\" angelegt und ausgewählt.")); + } else if open { + self.new_voice = Some(nv); + } + } + + fn start_prepare(&self, nv: &mut NewVoice, file: PathBuf) { + nv.busy = true; + nv.error = None; + nv.prepared = None; + if nv.name.trim().is_empty() { + nv.name = file.file_stem().map(|s| voices::sanitize_name(&s.to_string_lossy())).filter(|n| n != "mikrofon").unwrap_or_default(); + } + let (tx, ctx, cfg, work) = (self.tx.clone(), self.ctx.clone(), self.cfg.clone(), self.work.clone()); + let max = self.model().max_ref_seconds; + std::thread::spawn(move || { + let r = voices::prepare(&file, &cfg, max, &work); + let _ = tx.send(Msg::Prepared(r)); + ctx.request_repaint(); + }); + } +} + +impl eframe::App for App { + fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + self.poll(); + if self.player.is_playing() { + ctx.request_repaint_after(std::time::Duration::from_millis(200)); + } + } + + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ui, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| self.main_ui(ui)); + }); + let ctx = ui.ctx().clone(); + self.new_voice_window(&ctx); + } + + fn on_exit(&mut self) { + let _ = std::fs::remove_dir_all(&self.work); + } +} diff --git a/tts-studio/src/media.rs b/tts-studio/src/media.rs new file mode 100644 index 0000000..8782667 --- /dev/null +++ b/tts-studio/src/media.rs @@ -0,0 +1,334 @@ +//! ffmpeg-Aufrufe, WAV-Ein-/Ausgabe und einfache Signalanalyse +//! (Stille abschneiden, Vorlagen in einer Sprechpause kuerzen). + +use std::io::{Cursor, Write}; +use std::path::Path; +use std::process::{Command, Stdio}; + +pub fn ffmpeg(args: &[&str]) -> Result<(), String> { + let out = Command::new("ffmpeg") + .args(["-y", "-v", "error"]) + .args(args) + .output() + .map_err(|e| format!("ffmpeg nicht startbar: {e}"))?; + if out.status.success() { + Ok(()) + } else { + Err(format!("ffmpeg: {}", String::from_utf8_lossy(&out.stderr).trim())) + } +} + +/// Beliebige Audiodatei -> mono f32 mit `sr`. `.g722` wird als rohes G.722 gelesen. +pub fn decode(input: &Path, sr: u32, filter: Option<&str>) -> Result, String> { + let mut cmd = Command::new("ffmpeg"); + cmd.args(["-v", "error"]); + if is_g722(input) { + cmd.args(["-f", "g722"]); + } + cmd.arg("-i").arg(input); + if let Some(f) = filter { + cmd.args(["-af", f]); + } + let sr = sr.to_string(); + cmd.args(["-ac", "1", "-ar", &sr, "-f", "f32le", "-"]); + let out = cmd.output().map_err(|e| format!("ffmpeg nicht startbar: {e}"))?; + if !out.status.success() { + return Err(format!("ffmpeg: {}", String::from_utf8_lossy(&out.stderr).trim())); + } + Ok(out.stdout.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect()) +} + +/// Mono-Samples aus dem Speicher auf eine andere Abtastrate bringen. +pub fn resample(samples: &[f32], from: u32, to: u32) -> Result, String> { + if from == to { + return Ok(samples.to_vec()); + } + pipe(samples, from, to, None) +} + +/// Tempo aendern, Tonhoehe bleibt (ffmpeg atempo, fuer Sprache gut geeignet). +pub fn change_tempo(samples: &[f32], sr: u32, factor: f32) -> Result, String> { + if (factor - 1.0).abs() < 0.005 { + return Ok(samples.to_vec()); + } + pipe(samples, sr, sr, Some(&format!("atempo={factor:.3}"))) +} + +/// Samples aus dem Speicher durch ffmpeg schicken (Abtastrate und/oder Filter). +fn pipe(samples: &[f32], from: u32, to: u32, filter: Option<&str>) -> Result, String> { + let (from_s, to_s) = (from.to_string(), to.to_string()); + let mut cmd = Command::new("ffmpeg"); + cmd.args(["-v", "error", "-f", "f32le", "-ar", &from_s, "-ac", "1", "-i", "-"]); + if let Some(f) = filter { + cmd.args(["-af", f]); + } + let mut child = cmd + .args(["-ac", "1", "-ar", &to_s, "-f", "f32le", "-"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| format!("ffmpeg nicht startbar: {e}"))?; + let bytes: Vec = samples.iter().flat_map(|s| s.to_le_bytes()).collect(); + let mut stdin = child.stdin.take().expect("stdin"); + // Schreiben in eigenem Thread, sonst Deadlock, wenn stdout voll laeuft + let writer = std::thread::spawn(move || stdin.write_all(&bytes)); + let out = child.wait_with_output().map_err(|e| e.to_string())?; + let _ = writer.join(); + if !out.status.success() { + return Err(format!("ffmpeg: {}", String::from_utf8_lossy(&out.stderr).trim())); + } + Ok(out.stdout.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect()) +} + +pub fn is_g722(p: &Path) -> bool { + p.extension().is_some_and(|e| e.eq_ignore_ascii_case("g722")) +} + +fn spec(sr: u32) -> hound::WavSpec { + hound::WavSpec { channels: 1, sample_rate: sr, bits_per_sample: 16, sample_format: hound::SampleFormat::Int } +} + +fn to_i16(s: f32) -> i16 { + (s.clamp(-1.0, 1.0) * i16::MAX as f32).round() as i16 +} + +pub fn write_wav(path: &Path, samples: &[f32], sr: u32) -> Result<(), String> { + let mut w = hound::WavWriter::create(path, spec(sr)).map_err(|e| e.to_string())?; + for &s in samples { + w.write_sample(to_i16(s)).map_err(|e| e.to_string())?; + } + w.finalize().map_err(|e| e.to_string()) +} + +pub fn wav_bytes(samples: &[f32], sr: u32) -> Vec { + let mut cur = Cursor::new(Vec::new()); + { + let mut w = hound::WavWriter::new(&mut cur, spec(sr)).expect("wav header"); + for &s in samples { + w.write_sample(to_i16(s)).expect("wav sample"); + } + w.finalize().expect("wav finalize"); + } + cur.into_inner() +} + +/// WAV aus dem Speicher (Server-Antwort) -> mono f32 + Abtastrate. +pub fn read_wav_bytes(bytes: &[u8]) -> Result<(Vec, u32), String> { + let mut r = hound::WavReader::new(Cursor::new(bytes)).map_err(|e| format!("Antwort ist kein WAV: {e}"))?; + let spec = r.spec(); + let interleaved: Vec = match spec.sample_format { + hound::SampleFormat::Float => r.samples::().collect::>(), + hound::SampleFormat::Int => { + let scale = (1i64 << (spec.bits_per_sample - 1)) as f32; + r.samples::().map(|s| s.map(|v| v as f32 / scale)).collect::>() + } + } + .map_err(|e| e.to_string())?; + let ch = spec.channels.max(1) as usize; + let mono = interleaved.chunks(ch).map(|c| c.iter().sum::() / ch as f32).collect(); + Ok((mono, spec.sample_rate)) +} + +pub fn seconds(samples: usize, sr: u32) -> f32 { + samples as f32 / sr as f32 +} + +const FRAME_MS: usize = 20; + +/// Kurzzeitpegel je 20-ms-Frame in dBFS. +fn frame_db(x: &[f32], sr: u32) -> Vec { + let n = (sr as usize * FRAME_MS / 1000).max(1); + x.chunks(n) + .map(|c| { + let rms = (c.iter().map(|s| s * s).sum::() / c.len() as f32).sqrt(); + 20.0 * (rms + 1e-9).log10() + }) + .collect() +} + +fn percentile(sorted: &[f32], p: f32) -> f32 { + if sorted.is_empty() { + return -90.0; + } + sorted[((sorted.len() - 1) as f32 * p).round() as usize] +} + +/// Schwelle zwischen Sprache und Pause: Grundrauschen (ohne digitale Stille) + 8 dB, +/// aber mindestens 6 dB unter dem Median, damit leise Silben nicht als Pause zaehlen. +fn silence_threshold(db: &[f32]) -> f32 { + let mut audible: Vec = db.iter().copied().filter(|&d| d > -80.0).collect(); + audible.sort_by(f32::total_cmp); + let floor = percentile(&audible, 0.2); + let median = percentile(&audible, 0.5); + (floor + 8.0).min(median - 6.0) +} + +/// Pausen als (Start, Ende) in Frames, mindestens `min_ms` lang. +fn pauses(db: &[f32], thr: f32, min_ms: usize) -> Vec<(usize, usize)> { + let min_frames = (min_ms / FRAME_MS).max(1); + let mut out = Vec::new(); + let mut start = None; + for (i, &d) in db.iter().chain(std::iter::once(&0.0)).enumerate() { + match (d < thr, start) { + (true, None) => start = Some(i), + (false, Some(s)) => { + if i - s >= min_frames { + out.push((s, i)); + } + start = None; + } + _ => {} + } + } + out +} + +/// Stille am Anfang und Ende eines erzeugten Abschnitts entfernen (60 ms Rest bleiben), +/// damit die eingestellten Pausen exakt gelten. +pub fn trim_silence(x: &[f32], sr: u32) -> Vec { + let frame = sr as usize * FRAME_MS / 1000; + let db = frame_db(x, sr); + let Some(first) = db.iter().position(|&d| d > -45.0) else { return Vec::new() }; + let last = db.iter().rposition(|&d| d > -45.0).unwrap_or(first); + let pad = 60 / FRAME_MS; + let start = first.saturating_sub(pad) * frame; + let end = ((last + 1 + pad) * frame).min(x.len()); + x[start.min(end)..end].to_vec() +} + +/// Raumklang einer Stimme: die leisesten Stellen zwischen den Woertern aller Abschnitte +/// (ohne digitale Stille und ohne die Raender, die das Modell oft ganz stumm erzeugt). +pub fn room_tone_pool(segments: &[Vec], sr: u32) -> Vec { + let frame = sr as usize * FRAME_MS / 1000; + let dbs: Vec> = segments.iter().map(|s| frame_db(s, sr)).collect(); + let mut audible: Vec = dbs.iter().flatten().copied().filter(|&d| d > -75.0).collect(); + audible.sort_by(f32::total_cmp); + let thr = percentile(&audible, 0.08); + let fade = sr as usize * 5 / 1000; + let mut pool: Vec = Vec::new(); + for (s, db) in segments.iter().zip(&dbs) { + let edge = 3; // Frames am Rand auslassen + let mut i = edge; + while i + edge < db.len() { + if db[i] > -75.0 && db[i] <= thr { + let start = i; + while i + edge < db.len() && db[i] > -75.0 && db[i] <= thr + 1.0 { + i += 1; + } + if i - start >= 2 { + append_crossfaded(&mut pool, &s[start * frame..(i * frame).min(s.len())], fade); + } + } + i += 1; + } + } + pool +} + +/// Haengt `piece` an `out` an, mit gleichleistungs-Ueberblendung (fuer Rauschen pegeltreu). +fn append_crossfaded(out: &mut Vec, piece: &[f32], fade: usize) { + let fade = fade.min(piece.len() / 2).min(out.len()); + let start = out.len() - fade; + for (k, &p) in piece.iter().enumerate() { + if k < fade { + let t = (k as f32 + 0.5) / fade as f32 * std::f32::consts::FRAC_PI_2; + out[start + k] = out[start + k] * t.cos() + p * t.sin(); + } else { + out.push(p); + } + } +} + +/// Durchgehender Grundpegel der Laenge `len` aus zufaelligen Koernern (80–200 ms) des Pools. +/// Ohne brauchbaren Pool (< 60 ms) bleibt es still. +pub fn room_tone_bed(pool: &[f32], len: usize, sr: u32) -> Vec { + let ms = |m: usize| sr as usize * m / 1000; + if pool.len() < ms(60) { + return vec![0.0; len]; + } + let mut rng: u64 = 0x9E37_79B9_7F4A_7C15; // fest: gleiche Eingabe -> gleiche Ausgabe + let mut next = |n: usize| { + rng ^= rng << 13; + rng ^= rng >> 7; + rng ^= rng << 17; + (rng % n.max(1) as u64) as usize + }; + let mut bed = Vec::with_capacity(len + ms(200)); + while bed.len() < len { + let grain = (ms(80) + next(ms(120))).min(pool.len()); + let start = next(pool.len() - grain + 1); + append_crossfaded(&mut bed, &pool[start..start + grain], ms(10)); + } + bed.truncate(len); + bed +} + +pub struct Trimmed { + pub samples: Vec, + pub original_secs: f32, + pub shortened: bool, +} + +/// Stille am Anfang/Ende entfernen und auf `max_secs` kuerzen. Geschnitten wird in der +/// letzten deutlichen Sprechpause vor der Grenze, damit kein Wort zerteilt wird. +pub fn trim_reference(x: &[f32], sr: u32, max_secs: f32) -> Trimmed { + let original_secs = seconds(x.len(), sr); + let frame = sr as usize * FRAME_MS / 1000; + let db = frame_db(x, sr); + let thr = silence_threshold(&db); + + let first = db.iter().position(|&d| d >= thr).unwrap_or(0); + let last = db.iter().rposition(|&d| d >= thr).unwrap_or(db.len().saturating_sub(1)); + let pad_start = 150 / FRAME_MS; + let pad_end = 250 / FRAME_MS; + let start_f = first.saturating_sub(pad_start); + let mut end_f = (last + 1 + pad_end).min(db.len()); + + // 1 s Reserve unter dem Server-Limit + let limit_f = ((max_secs - 1.0).max(3.0) * 1000.0) as usize / FRAME_MS; + let shortened = end_f - start_f > limit_f; + if shortened { + let window = start_f + limit_f; + let candidates: Vec<(usize, usize)> = + pauses(&db, thr, 150).into_iter().filter(|&(s, e)| s > start_f + limit_f / 4 && (s + e) / 2 <= window).collect(); + // bevorzugt eine Satzpause (>= 0,4 s) im hinteren Drittel, sonst die letzte Pause ueberhaupt + let long = candidates.iter().rev().find(|&&(s, e)| e - s >= 400 / FRAME_MS && (s + e) / 2 >= start_f + limit_f * 2 / 3); + end_f = match long.or(candidates.last()) { + Some(&(s, e)) => (s + (e - s) / 2).min(s + pad_end), + None => window, + }; + } + + let mut y = x[(start_f * frame).min(x.len())..(end_f * frame).min(x.len())].to_vec(); + let fade = (sr as usize * 15 / 1000).min(y.len() / 2); + for i in 0..fade { + let g = i as f32 / fade as f32; + y[i] *= g; + let j = y.len() - 1 - i; + y[j] *= g; + } + Trimmed { samples: y, original_secs, shortened } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn kuerzt_in_pause() { + let sr = 16000; + // 4 x (7 s "Sprache" + 0,6 s Pause) = 30,4 s + let mut x = Vec::new(); + for _ in 0..4 { + x.extend((0..sr * 7).map(|i| 0.3 * ((i as f32) * 0.05).sin())); + x.extend((0..sr * 6 / 10).map(|_| 0.0005)); + } + let t = trim_reference(&x, sr as u32, 30.0); + let secs = seconds(t.samples.len(), sr as u32); + assert!(t.shortened); + assert!(secs <= 29.0, "{secs}"); + // Schnitt in der 3. Pause (nach 3 x 7,6 s = 22,8 s, Mitte der Pause ~22,5 s) + assert!((21.5..23.5).contains(&secs), "{secs}"); + } +} diff --git a/tts-studio/src/synth.rs b/tts-studio/src/synth.rs new file mode 100644 index 0000000..754c56e --- /dev/null +++ b/tts-studio/src/synth.rs @@ -0,0 +1,428 @@ +//! Text -> fertige Ansage: Pausen-Tags auswerten, Abschnitte beim Modell anfragen, +//! zusammensetzen, optional Hintergrundmusik, ins Zielformat wandeln. + +use crate::config::Model; +use crate::voices::{REF_SR, Voice}; +use crate::{http, media}; +use base64::Engine; +use serde_json::json; +use std::path::{Path, PathBuf}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Target { + AsteriskWav, + AsteriskG722, + WhatsApp, +} + +impl Target { + pub const ALL: [Target; 3] = [Target::AsteriskWav, Target::AsteriskG722, Target::WhatsApp]; + + pub fn label(self) -> &'static str { + match self { + Target::AsteriskWav => "Asterisk – WAV 8 kHz", + Target::AsteriskG722 => "Asterisk – G.722", + Target::WhatsApp => "WhatsApp – MP3", + } + } + + pub fn extension(self) -> &'static str { + match self { + Target::AsteriskWav => "wav", + Target::AsteriskG722 => "g722", + Target::WhatsApp => "mp3", + } + } + + fn ffmpeg_out(self) -> &'static [&'static str] { + match self { + Target::AsteriskWav => &["-acodec", "pcm_s16le", "-ar", "8000", "-ac", "1"], + Target::AsteriskG722 => &["-ac", "1", "-ar", "16000", "-c:a", "g722", "-f", "g722"], + Target::WhatsApp => &["-ac", "1", "-ar", "44100", "-c:a", "libmp3lame", "-b:a", "96k"], + } + } + + /// Rate der Vorhoer-Datei: so klingt es beim Anrufer bzw. Empfaenger + fn preview_rate(self) -> u32 { + match self { + Target::AsteriskWav => 8000, + Target::AsteriskG722 => 16000, + Target::WhatsApp => 44100, + } + } +} + +pub struct Music { + pub file: PathBuf, + pub volume: f32, + pub intro_secs: f32, +} + +pub struct Job { + pub model: Model, + pub voice: Voice, + pub text: String, + pub music: Option, + pub target: Target, + /// Sprechtempo (1.0 = normal), Pausen bleiben davon unberuehrt + pub speed: f32, + /// Pause nach . ! ? (0 = Text nicht an Satzenden teilen) + pub sentence_pause_ms: u32, + /// Pause nach , ; : (0 = nicht teilen) + pub comma_pause_ms: u32, +} + +/// Gleichzeitige Anfragen (Server verarbeitet bis zu 8 parallel) +const PARALLEL: usize = 8; + +pub struct Rendered { + pub output: PathBuf, + pub preview: PathBuf, + pub secs: f32, + pub target: Target, +} + +#[derive(Debug, PartialEq)] +pub enum Part { + Text(String), + Pause(u32), +} + +/// Text mit bzw. in Abschnitte und Pausen zerlegen. +/// Andere Tags (SSML aus Azure-Zeiten) werden zu Leerzeichen. +pub fn parse_text(input: &str) -> Vec { + fn flush(parts: &mut Vec, text: &mut String) { + let t = text.split_whitespace().collect::>().join(" "); + if !t.is_empty() { + parts.push(Part::Text(t)); + } + text.clear(); + } + let mut parts = Vec::new(); + let mut text = String::new(); + let mut rest = input; + while let Some(lt) = rest.find('<') { + text.push_str(&rest[..lt]); + let Some(gt) = rest[lt..].find('>') else { + rest = &rest[lt..]; // "<" ohne ">" bleibt normaler Text + break; + }; + let tag = &rest[lt + 1..lt + gt]; + rest = &rest[lt + gt + 1..]; + match break_ms(tag) { + Some(ms) => { + flush(&mut parts, &mut text); + parts.push(Part::Pause(ms)); + } + None => text.push(' '), + } + } + text.push_str(rest); + flush(&mut parts, &mut text); + parts +} + +fn break_ms(tag: &str) -> Option { + let t = tag.trim().trim_end_matches('/').trim().to_ascii_lowercase(); + let t = t.strip_prefix("break")?; + let Some((_, after)) = t.split_once("time") else { + return Some(500); // ohne Zeitangabe + }; + let v = after.trim_start().strip_prefix('=')?.trim().trim_matches(|c| c == '"' || c == '\''); + let split = v.find(|c: char| !(c.is_ascii_digit() || c == '.')).unwrap_or(v.len()); + let n: f32 = v[..split].parse().ok()?; + let factor = match v[split..].trim() { + "ms" | "" => 1.0, + "s" => 1000.0, + _ => return None, + }; + Some((n * factor).round() as u32) +} + +/// Abkuerzungen, deren Punkt kein Satzende ist (klein, ohne Punkt) +const ABBREVIATIONS: &[&str] = &[ + "dr", "prof", "mag", "dipl", "ing", "med", "univ", "nr", "str", "tel", "fax", "bzw", "ca", "usw", "etc", "inkl", "exkl", "evtl", "ggf", + "vgl", "zb", "hr", "fr", "mo", "di", "mi", "do", "sa", "jan", "feb", "mär", "apr", "jun", "jul", "aug", "sep", "sept", "okt", + "nov", "dez", "abt", "geb", "max", "min", "std", "bzgl", "lt", "u.a", "z.b", "d.h", "u.ä", +]; + +/// Ist das Satzzeichen an Byte-Position `i` in `t` eine Grenze? Liefert true fuer Satzende. +fn boundary_at(t: &str, i: usize, c: char) -> Option { + let after = &t[i + c.len_utf8()..]; + // nur vor Leerraum: "8,30", "8:30", "www.x.at" bleiben ganz; Textende braucht keinen Schnitt + if !after.starts_with(char::is_whitespace) || after.trim().is_empty() { + return None; + } + match c { + '!' | '?' | '…' => Some(true), + ',' | ';' | ':' => Some(false), + '.' => { + let word = t[..i].rsplit(char::is_whitespace).next().unwrap_or(""); + let w = word.trim_start_matches(|c: char| !c.is_alphanumeric()).to_lowercase(); + let next = after.trim_start().chars().next(); + let abbreviation = w.chars().count() == 1 || ABBREVIATIONS.contains(&w.as_str()); + // "22. Dezember", "Dr. Ahmad", "z. B. morgen" sind kein Satzende + let ordinal = !w.is_empty() && w.chars().all(|c| c.is_ascii_digit()); + (!abbreviation && !ordinal && !next.is_some_and(char::is_lowercase)).then_some(true) + } + _ => None, + } +} + +fn push_text(out: &mut Vec, t: &str) { + if !t.trim().is_empty() { + out.push(Part::Text(t.trim().to_string())); + } +} + +/// Textabschnitte an Satzenden (und optional Kommas) teilen und dort Pausen einsetzen. +pub fn split_sentences(parts: Vec, sentence_ms: u32, comma_ms: u32) -> Vec { + let mut out = Vec::new(); + for part in parts { + let Part::Text(t) = part else { + out.push(part); + continue; + }; + let mut start = 0; + for (i, c) in t.char_indices() { + let pause = match boundary_at(&t, i, c) { + Some(true) if sentence_ms > 0 => sentence_ms, + Some(false) if comma_ms > 0 => comma_ms, + _ => continue, + }; + let end = i + c.len_utf8(); + push_text(&mut out, &t[start..end]); + out.push(Part::Pause(pause)); + start = end; + } + push_text(&mut out, &t[start..]); + } + out +} + +/// Einen Abschnitt sprechen lassen: Stille an den Raendern weg, Tempo anpassen. +fn speak(url: &str, request: &serde_json::Value, text: &str, job: &Job) -> Result, String> { + let mut body = request.clone(); + body["input"] = json!(text); + body["language"] = json!(job.model.language); + body["response_format"] = json!("wav"); + body["sample_rate"] = json!(REF_SR); + let (s, sr) = media::read_wav_bytes(&http::speech(url, &body)?)?; + let s = media::trim_silence(&media::resample(&s, sr, REF_SR)?, REF_SR); + media::change_tempo(&s, REF_SR, job.speed) +} + +/// Erzeugt die Ansage im Arbeitsordner `work`; `progress` meldet Zwischenstaende. +pub fn render(job: &Job, work: &Path, progress: &dyn Fn(String)) -> Result { + let parts = split_sentences(parse_text(&job.text), job.sentence_pause_ms, job.comma_pause_ms); + let texts: Vec<&str> = parts.iter().filter_map(|p| if let Part::Text(t) = p { Some(t.as_str()) } else { None }).collect(); + if texts.is_empty() { + return Err("Kein Text eingegeben.".into()); + } + + let (url, request) = match &job.voice { + Voice::Clone { audio, transcript, name } => { + let url = job.model.clone_url.clone().ok_or("Dieses Modell unterstützt keine Klonstimmen.")?; + let ref_samples = media::decode(audio, REF_SR, None)?; + let ref_secs = media::seconds(ref_samples.len(), REF_SR); + if ref_secs > job.model.max_ref_seconds { + return Err(format!( + "Vorlage von \"{name}\" ist {ref_secs:.1} s lang, erlaubt sind {:.0} s. Bitte über \"Neue Stimme\" neu anlegen (wird automatisch gekürzt).", + job.model.max_ref_seconds + )); + } + let ref_text = std::fs::read_to_string(transcript).map_err(|e| format!("{}: {e}", transcript.display()))?; + let data = base64::engine::general_purpose::STANDARD.encode(media::wav_bytes(&ref_samples, REF_SR)); + (url, json!({ "task_type": "Base", "ref_audio": format!("data:audio/wav;base64,{data}"), "ref_text": ref_text.trim() })) + } + Voice::Preset(name) => { + let url = job.model.voice_url.clone().ok_or("Dieses Modell hat keine festen Stimmen.")?; + (url, json!({ "voice": name })) + } + }; + + // Abschnitte parallel anfragen, Reihenfolge bleibt erhalten + let mut spoken: Vec> = Vec::with_capacity(texts.len()); + for chunk in texts.chunks(PARALLEL) { + progress(format!("Spreche Abschnitt {}–{} von {} …", spoken.len() + 1, spoken.len() + chunk.len(), texts.len())); + let results: Vec, String>> = std::thread::scope(|s| { + let handles: Vec<_> = chunk.iter().map(|t| s.spawn(|| speak(&url, &request, t, job))).collect(); + handles.into_iter().map(|h| h.join().unwrap_or_else(|_| Err("Abschnitt abgestürzt".into()))).collect() + }); + for r in results { + spoken.push(r?); + } + } + let mut segments = spoken.iter(); + let mut speech: Vec = Vec::new(); + for part in &parts { + match part { + Part::Text(_) => speech.extend(segments.next().into_iter().flatten()), + Part::Pause(ms) => speech.extend(std::iter::repeat_n(0.0, (REF_SR as u64 * *ms as u64 / 1000) as usize)), + } + } + // kurzer Ausklang, sonst endet die Ansage hart nach dem letzten Wort + speech.extend(std::iter::repeat_n(0.0, REF_SR as usize * 4 / 10)); + // Raumklang der Stimme durchgehend unterlegen: Pausen sind sonst digitale Stille, + // was gegenueber dem Grundrauschen zwischen den Woertern auffaellt + let bed = media::room_tone_bed(&media::room_tone_pool(&spoken, REF_SR), speech.len(), REF_SR); + for (s, b) in speech.iter_mut().zip(bed) { + *s += b; + } + + progress("Wandle ins Zielformat um …".into()); + // eindeutige Namen: eine laufende Wiedergabe der vorigen Datei stoert nicht + let stamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis()).unwrap_or(0); + let raw = work.join(format!("{stamp}_raw.wav")); + let output = work.join(format!("{stamp}_ansage.{}", job.target.extension())); + let preview = work.join(format!("{stamp}_vorhoeren.wav")); + media::write_wav(&raw, &speech, REF_SR)?; + + let raw_s = raw.to_string_lossy().into_owned(); + let mut args: Vec = vec!["-i".into(), raw_s]; + if let Some(m) = &job.music { + if !m.file.exists() { + return Err(format!("Musikdatei nicht gefunden: {}", m.file.display())); + } + // Mix wie im bisherigen Azure-Skript, aber mit 2 s Nachlauf: der Fade-out am Ende + // soll nur die Musik ausblenden, nicht die letzten Woerter + let filter = format!( + "[0:a]adelay={}:all=1,apad=pad_dur=2[sp];[1:a]volume={}[bg];\ + [sp][bg]amix=inputs=2:duration=first:dropout_transition=0,areverse,afade=t=in:d=2,areverse", + (m.intro_secs * 1000.0).round() as u32, + m.volume + ); + args.extend(["-stream_loop".into(), "-1".into(), "-i".into(), m.file.to_string_lossy().into_owned(), "-filter_complex".into(), filter]); + } + args.extend(job.target.ffmpeg_out().iter().map(|s| s.to_string())); + args.push(output.to_string_lossy().into_owned()); + media::ffmpeg(&args.iter().map(String::as_str).collect::>())?; + let _ = std::fs::remove_file(&raw); + + let rate = job.target.preview_rate(); + let heard = media::decode(&output, rate, None)?; + media::write_wav(&preview, &heard, rate)?; + Ok(Rendered { output, preview, secs: media::seconds(heard.len(), rate), target: job.target }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pausen_tags() { + assert_eq!( + parse_text("Hallo Welt Ende"), + vec![Part::Text("Hallo".into()), Part::Pause(500), Part::Text("Welt".into()), Part::Pause(1500), Part::Text("Ende".into())] + ); + assert_eq!(parse_text("Grüß Gott"), vec![Part::Text("Grüß Gott".into())]); + assert_eq!(parse_text("A B"), vec![Part::Text("A".into()), Part::Pause(500), Part::Text("B".into())]); + assert_eq!(parse_text(" "), vec![Part::Pause(1000)]); + } +} + +#[cfg(test)] +mod dgx_tests { + use super::*; + use crate::{config::Config, voices}; + + /// Gegen die echten Server: cargo test -- --ignored --nocapture + #[test] + #[ignore] + fn ansage_mit_klonstimme_alle_ziele() { + let cfg = Config::default(); + let voice = voices::list_clones(&cfg.voice_dir).into_iter().find(|v| v.label() == "martina").expect("martina"); + let work = std::env::temp_dir().join("tts-studio-test"); + std::fs::create_dir_all(&work).unwrap(); + for (target, music) in [(Target::AsteriskWav, false), (Target::AsteriskG722, true), (Target::WhatsApp, false)] { + let job = Job { + model: cfg.models[0].clone(), + voice: voice.clone(), + text: "Guten Tag. Bitte haben Sie einen Moment Geduld.".into(), + music: music.then(|| Music { file: cfg.music_file.clone(), volume: 0.15, intro_secs: 2.0 }), + target, + speed: 1.0, + sentence_pause_ms: 800, + comma_pause_ms: 0, + }; + let t0 = std::time::Instant::now(); + let r = render(&job, &work, &|m| println!(" {m}")).unwrap(); + println!("{:?}: {:.1} s Audio in {:.1} s -> {}", target, r.secs, t0.elapsed().as_secs_f32(), r.output.display()); + assert!(r.secs > 2.0 && r.output.exists() && r.preview.exists()); + } + } + + #[test] + #[ignore] + fn neue_stimme_wird_gekuerzt_und_transkribiert() { + let cfg = Config::default(); + let work = std::env::temp_dir().join("tts-studio-test"); + std::fs::create_dir_all(&work).unwrap(); + let p = voices::prepare(&cfg.voice_dir.join("alex.wav"), &cfg, 30.0, &work).unwrap(); + println!("{:.1} s -> {:.1} s (gekuerzt: {})\n{}", p.original_secs, p.secs, p.shortened, p.transcript); + assert!(p.shortened && p.secs <= 29.0 && p.secs > 15.0); + assert!(p.transcript.contains("Nussbaumer")); + } +} + +#[cfg(test)] +mod satz_tests { + use super::*; + + fn split(t: &str, s: u32, c: u32) -> Vec { + split_sentences(parse_text(t), s, c) + } + + #[test] + fn satzenden() { + let t = "Willkommen bei Dr. Ahmad. Wir haben vom 22. Dezember bis 6. Jänner zu, z. B. morgen. Rufen Sie an! Danke"; + assert_eq!( + split(t, 800, 0), + vec![ + Part::Text("Willkommen bei Dr. Ahmad.".into()), + Part::Pause(800), + Part::Text("Wir haben vom 22. Dezember bis 6. Jänner zu, z. B. morgen.".into()), + Part::Pause(800), + Part::Text("Rufen Sie an!".into()), + Part::Pause(800), + Part::Text("Danke".into()), + ] + ); + } + + #[test] + fn kommas_und_uhrzeiten() { + assert_eq!( + split("Montag von 8:30 bis 17,30 Uhr, Dienstag geschlossen. Ende.", 0, 300), + vec![Part::Text("Montag von 8:30 bis 17,30 Uhr,".into()), Part::Pause(300), Part::Text("Dienstag geschlossen. Ende.".into())] + ); + // ohne Pausen bleibt alles ein Abschnitt; explizite Pausen bleiben erhalten + assert_eq!(split("A. B. C.", 0, 0), vec![Part::Text("A. B.".into()), Part::Pause(1000), Part::Text("C.".into())]); + } +} + +#[cfg(test)] +mod tempo_tests { + use super::*; + use crate::{config::Config, voices}; + + #[test] + #[ignore] + fn tempo_und_satzpausen() { + let cfg = Config::default(); + let voice = voices::list_clones(&cfg.voice_dir).into_iter().find(|v| v.label() == "martina").expect("martina"); + let work = std::env::temp_dir().join("tts-studio-tempo"); + std::fs::create_dir_all(&work).unwrap(); + let text = "Herzlich willkommen in der Ordination von Doktor Ahmad und Doktor Nussbaumer, Gruppenpraxis für Psychiatrie und psychotherapeutische Medizin. \ + Leider rufen Sie außerhalb der Ordinationszeiten an. Unsere Ordinationszeiten sind Montag und Dienstag von acht Uhr dreißig bis siebzehn Uhr dreißig. \ + Mittwoch und Freitag von acht Uhr dreißig bis vierzehn Uhr. Vielen Dank!"; + for (speed, sentence, comma) in [(1.0, 0, 0), (1.0, 800, 0), (0.9, 800, 0), (1.0, 800, 300)] { + let job = Job { model: cfg.models[0].clone(), voice: voice.clone(), text: text.into(), music: None, target: Target::WhatsApp, speed, sentence_pause_ms: sentence, comma_pause_ms: comma }; + let t0 = std::time::Instant::now(); + let r = render(&job, &work, &|_| {}).unwrap(); + let name = work.join(format!("tempo{speed}_satz{sentence}_komma{comma}.wav")); + std::fs::copy(&r.preview, &name).unwrap(); + println!("{:>4}x Satz {sentence:>3} ms Komma {comma:>3} ms: {:5.1} s Audio in {:4.1} s -> {}", speed, r.secs, t0.elapsed().as_secs_f32(), name.display()); + } + } +} diff --git a/tts-studio/src/voices.rs b/tts-studio/src/voices.rs new file mode 100644 index 0000000..6ab4958 --- /dev/null +++ b/tts-studio/src/voices.rs @@ -0,0 +1,107 @@ +//! Stimmen: Klonstimmen aus dem Stimmenordner und feste Stimmen des Modells, +//! sowie das Anlegen neuer Klonstimmen (aufbereiten, kuerzen, transkribieren). + +use crate::{config::Config, http, media}; +use std::path::{Path, PathBuf}; + +/// Abtastrate der Vorlagen (= Ausgaberate von Qwen3-TTS) +pub const REF_SR: u32 = 24000; +const AUDIO_EXT: [&str; 6] = ["wav", "mp3", "m4a", "ogg", "flac", "opus"]; + +#[derive(Clone, Debug, PartialEq)] +pub enum Voice { + Clone { name: String, audio: PathBuf, transcript: PathBuf }, + Preset(String), +} + +impl Voice { + pub fn label(&self) -> String { + match self { + Voice::Clone { name, .. } => name.clone(), + Voice::Preset(name) => format!("{name} (feste Stimme)"), + } + } +} + +/// Alle Audiodateien mit gleichnamigem Transkript (.txt). "alex_ref.wav" erscheint als "alex". +pub fn list_clones(dir: &Path) -> Vec { + let Ok(entries) = std::fs::read_dir(dir) else { return Vec::new() }; + let mut voices: Vec = entries + .filter_map(|e| e.ok().map(|e| e.path())) + .filter(|p| p.extension().and_then(|e| e.to_str()).is_some_and(|e| AUDIO_EXT.contains(&e.to_ascii_lowercase().as_str()))) + .filter_map(|audio| { + let transcript = audio.with_extension("txt"); + let stem = audio.file_stem()?.to_str()?; + let name = stem.strip_suffix("_ref").unwrap_or(stem).to_string(); + transcript.exists().then_some(Voice::Clone { name, audio, transcript }) + }) + .collect(); + voices.sort_by_key(Voice::label); + voices +} + +/// Dateiname aus frei eingegebenem Namen: klein, nur a-z 0-9 _ -, Umlaute ausgeschrieben. +pub fn sanitize_name(s: &str) -> String { + let mut out = String::new(); + for c in s.trim().to_lowercase().chars() { + match c { + 'a'..='z' | '0'..='9' | '_' | '-' => out.push(c), + 'ä' => out.push_str("ae"), + 'ö' => out.push_str("oe"), + 'ü' => out.push_str("ue"), + 'ß' => out.push_str("ss"), + ' ' | '.' => out.push('_'), + _ => {} + } + } + out +} + +pub struct Prepared { + pub wav: PathBuf, + pub secs: f32, + pub original_secs: f32, + pub shortened: bool, + pub transcript: String, +} + +/// Aufnahme -> Vorlage: Brummen raus (Hochpass 80 Hz), Pegel angleichen, Stille an den +/// Raendern entfernen, auf das Server-Limit kuerzen, per Whisper transkribieren. +/// Bewusst kein Entrauschen: Artefakte wuerden mitgeklont. +pub fn prepare(input: &Path, cfg: &Config, max_secs: f32, work: &Path) -> Result { + let x = media::decode(input, REF_SR, Some("highpass=f=80,loudnorm=I=-20:TP=-2:LRA=11"))?; + if media::seconds(x.len(), REF_SR) < 3.0 { + return Err("Aufnahme ist zu kurz (mindestens 3 Sekunden Sprache).".into()); + } + let t = media::trim_reference(&x, REF_SR, max_secs); + let wav = work.join("neue_stimme.wav"); + media::write_wav(&wav, &t.samples, REF_SR)?; + let transcript = http::transcribe(&cfg.whisper_url, &cfg.whisper_model, &media::wav_bytes(&t.samples, REF_SR))?; + Ok(Prepared { + wav, + secs: media::seconds(t.samples.len(), REF_SR), + original_secs: t.original_secs, + shortened: t.shortened, + transcript, + }) +} + +/// Vorlage + Transkript in den Stimmenordner legen. Ueberschreibt nichts. +pub fn save(dir: &Path, name: &str, prepared_wav: &Path, transcript: &str) -> Result { + let name = sanitize_name(name); + if name.is_empty() { + return Err("Bitte einen Namen eingeben.".into()); + } + if transcript.trim().is_empty() { + return Err("Das Transkript ist leer.".into()); + } + std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; + let audio = dir.join(format!("{name}.wav")); + let txt = dir.join(format!("{name}.txt")); + if audio.exists() || txt.exists() || dir.join(format!("{name}_ref.wav")).exists() { + return Err(format!("Im Stimmenordner gibt es bereits \"{name}\" – bitte anderen Namen wählen.")); + } + std::fs::copy(prepared_wav, &audio).map_err(|e| e.to_string())?; + std::fs::write(&txt, format!("{}\n", transcript.trim())).map_err(|e| e.to_string())?; + Ok(Voice::Clone { name, audio, transcript: txt }) +}