diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..82806b9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,50 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Mouth is a single-binary, offline speech-to-text tool. Press a global hotkey, speak, and transcribed text is pasted at your cursor. Configured via YAML, no UI. Primary target is Windows; Linux/macOS supported where possible. + +Uses Parakeet TDT 0.6B v3 (ONNX, from `istupakov/parakeet-tdt-0.6b-v3-onnx`) for transcription, Silero VAD v4 for voice activity detection. + +## Build & Run + +```bash +cargo build # debug build +cargo build --release # release build +cargo run # run daemon (default command) +cargo run -- config --show # show current config +cargo run -- config # interactive config TUI +cargo run -- config --reset # reset to defaults +cargo run -- models # list models +cargo run -- models --download # download configured model +cargo run -- status # daemon status +``` + +## Architecture + +Single-binary Rust application. Core pipeline: hotkey capture (rdev) → audio recording (cpal) → resampling to 16kHz (rubato) → VAD (Silero ONNX) → mel spectrogram → transcription (Parakeet v3 TDT decoder via ort) → clipboard/paste (arboard + enigo). Minimal native overlay window (winit + softbuffer). + +**Threading model:** Main thread owns the overlay window event loop (required by winit). Background threads: hotkey listener (rdev::listen is blocking), audio recorder (cpal stream), coordinator (state machine). All communicate via `std::sync::mpsc` channels. + +**Coordinator state machine:** Idle → Recording → Transcribing → (Pasting) → Idle. Cancel from Recording returns to Idle. + +**Parakeet v3 inference:** Two-stage ONNX model — encoder (FastConformer) produces features, decoder+joint (TDT transducer) greedily decodes tokens with duration predictions. Audio preprocessing: pre-emphasis → STFT → 128-band log-mel → per-utterance CMVN. Vocab is SentencePiece BPE with `▁` as word boundary marker. + +**ort crate (v2.0.0-rc.12) notes:** Session::run needs `&mut self`. Input values must be converted to `Value::into_dyn()` before passing. Use `SessionInputValue::Owned(value.into_dyn())` pattern. `try_extract_tensor` returns `(&Shape, &[T])` tuple. `from_shape_vec` needs `[usize; N]` not `Vec`. + +Config lives at `~/.config/mouth/config.yaml` (Linux/macOS) or `%APPDATA%\mouth\config.yaml` (Windows). Models cached via HuggingFace Hub standard cache (`~/.cache/huggingface/hub/`). + +## Cross-Compilation + +Developing on Ubuntu 24.04, targeting Windows: +```bash +cargo build --target x86_64-pc-windows-gnu +``` + +## System Dependencies (Ubuntu) + +```bash +sudo apt-get install libssl-dev libasound2-dev libpulse-dev libx11-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libwayland-dev libgtk-3-dev libxtst-dev libxdo-dev cmake +``` diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..77bc5e3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4950 @@ +# 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 = "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 = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.11.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +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.11.0", + "cc", + "jni 0.22.4", + "libc", + "log", + "ndk 0.9.0", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 2.0.18", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.11.0", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[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.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.11.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cocoa" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667fdc068627a2816b9ff831201dd9864249d6ee8d190b9532357f1fc0f61ea7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics 0.21.0", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +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 = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[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.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.7.0", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a67c4378cf203eace8fb6567847eb641fd6ff933c1145a115c6ee820ebb978" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "foreign-types 0.3.2", + "libc", +] + +[[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 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", + "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 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys 0.8.7", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys 0.8.7", + "coreaudio-rs", + "dasp_sample", + "jni 0.21.1", + "js-sys", + "libc", + "mach2", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "ctor-lite" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e162d0c2e2068eb736b71e5597eff0b9944e6b973cd9f37b6a288ab9bf20e300" + +[[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 = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +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.11.0", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[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 = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" +dependencies = [ + "drm-sys", + "rustix 1.1.4", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8" +dependencies = [ + "libc", + "linux-raw-sys 0.9.4", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enigo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cf6f550bbbdd5fe66f39d429cb2604bcdacbf00dca0f5bbe2e9306a0009b7c6" +dependencies = [ + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types-shared 0.3.1", + "libc", + "log", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "serde", + "windows 0.58.0", + "xkbcommon", + "xkeysym", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[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 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[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" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "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.4", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hf-hub" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" +dependencies = [ + "dirs", + "futures", + "http", + "indicatif", + "libc", + "log", + "native-tls", + "num_cpus", + "rand", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "ureq 2.12.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "hmac-sha256" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +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.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[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.18", + "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", +] + +[[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", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[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 = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.4", +] + +[[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.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lzma-rust2" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1670343e58806300d87950e3401e820b519b9384281bbabfb15e3636689ffd69" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mouth" +version = "0.1.0" +dependencies = [ + "anyhow", + "arboard", + "clap", + "cpal", + "dialoguer", + "dirs", + "enigo", + "hf-hub", + "indicatif", + "ndarray", + "num_cpus", + "ort", + "rdev", + "rodio", + "rubato", + "serde", + "serde_yaml", + "softbuffer", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-subscriber", + "winit", +] + +[[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 = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.11.0", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.6.0+11769913", + "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.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + +[[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 = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[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", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +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.11.0", + "block2", + "libc", + "objc2 0.5.2", + "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.11.0", + "objc2 0.6.4", + "objc2-core-graphics", + "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.11.0", + "block2", + "objc2 0.5.2", + "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", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2 0.5.2", + "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.11.0", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2 0.6.4", + "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", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[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.11.0", + "block2", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "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.11.0", + "objc2 0.6.4", + "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", + "objc2 0.5.2", + "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.11.0", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.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.11.0", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 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.2", + "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.11.0", + "block2", + "objc2 0.5.2", + "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-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2 0.5.2", + "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.11.0", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni 0.21.1", + "ndk 0.8.0", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59aed3b33578edcfa1bc96a321d590d31832b6ad55a26f0313362ce687e9abd6" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ort" +version = "2.0.0-rc.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133" +dependencies = [ + "ndarray", + "ort-sys", + "smallvec", + "tracing", + "ureq 3.3.0", +] + +[[package]] +name = "ort-sys" +version = "2.0.0-rc.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90" +dependencies = [ + "hmac-sha256", + "lzma-rust2", + "ureq 3.3.0", +] + +[[package]] +name = "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_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 = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[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.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[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.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[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.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rdev" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00552ca2dc2f93b84cd7b5581de49549411e4e41d89e1c691bcb93dc4be360c3" +dependencies = [ + "cocoa", + "core-foundation 0.7.0", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "lazy_static", + "libc", + "winapi", + "x11", +] + +[[package]] +name = "realfft" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" +dependencies = [ + "rustfft", +] + +[[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.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[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.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" +dependencies = [ + "claxon", + "cpal", + "hound", + "lewton", + "symphonia", +] + +[[package]] +name = "rubato" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustfft" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "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", + "tiny-skia", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys 0.8.7", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +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 = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.11.0", + "calloop", + "calloop-wayland-source", + "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 = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "drm", + "fastrand", + "js-sys", + "memmap2", + "ndk 0.9.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "raw-window-handle", + "redox_syscall 0.5.18", + "rustix 1.1.4", + "tiny-xlib", + "tracing", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.61.2", + "x11rb", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symphonia" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" +dependencies = [ + "lazy_static", + "symphonia-bundle-mp3", + "symphonia-core", + "symphonia-metadata", +] + +[[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-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-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 = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys 0.8.7", + "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.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[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 = "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 = "tiny-xlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "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", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "native-tls", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "socks", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64", + "der", + "log", + "native-tls", + "percent-encoding", + "rustls-pki-types", + "socks", + "ureq-proto", + "utf8-zero", + "webpki-root-certs", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "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 = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[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 = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[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.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.0", + "rustix 1.1.4", + "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.11.0", + "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.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "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.11.0", + "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.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +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.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +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 = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[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.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[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.11.0", + "block2", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk 0.9.0", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[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.4", + "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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.11.0", + "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 = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e679589 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "mouth" +version = "0.1.0" +edition = "2024" +description = "Offline speech-to-text with global hotkey and paste" + +[dependencies] +# CLI +clap = { version = "4", features = ["derive"] } + +# Config +serde = { version = "1", features = ["derive"] } +serde_yaml = "0.9" +dirs = "6" + +# Interactive config TUI +dialoguer = "0.11" + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Async +tokio = { version = "1", features = ["full"] } + +# Global hotkey +rdev = "0.5" + +# Audio capture +cpal = "0.15" + +# Audio resampling +rubato = "0.16" + +# ONNX inference (Parakeet v3 + Silero VAD) +ort = { version = "2.0.0-rc.12", features = ["download-binaries"] } +ndarray = "0.17" + +# Model download from HuggingFace +hf-hub = "0.4" +indicatif = "0.17" + +# Clipboard +arboard = "3" + +# Keyboard simulation +enigo = { version = "0.3", features = ["serde"] } + +# Overlay window +winit = "0.30" +softbuffer = "0.4" + +# Audio feedback +rodio = "0.20" + +# System info +num_cpus = "1" + +# Error handling +anyhow = "1" +thiserror = "2" diff --git a/config.yaml.example b/config.yaml.example new file mode 100644 index 0000000..aa2ff8f --- /dev/null +++ b/config.yaml.example @@ -0,0 +1,42 @@ +# Mouth configuration +# Copy to ~/.config/mouth/config.yaml (Linux/macOS) +# or %APPDATA%\mouth\config.yaml (Windows) + +# Hotkey to activate recording +hotkey: ctrl+space + +# Recording mode: push_to_talk or toggle +mode: push_to_talk + +# Cancel hotkey (only active while recording) +cancel_key: escape + +# Speech-to-text model +model: parakeet-tdt-0.6b-v3 + +# Inference accelerator: auto, cpu, cuda, directml +accelerator: auto + +# GPU device index (when accelerator is cuda/directml) +gpu_device: 0 + +# How to paste text: ctrl_v, shift_insert, ctrl_shift_v, clipboard_only +paste_method: ctrl_v + +# Keep transcribed text on clipboard after pasting +copy_to_clipboard: true + +# Overlay position: top, bottom, none +overlay_position: top + +# Play audio feedback sounds +audio_feedback: true + +# Audio input device name (null = system default) +input_device: null + +# Voice activity detection (trim silence) +vad_enabled: true + +# Language hint for model +language: en diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..c7478dd --- /dev/null +++ b/plan.md @@ -0,0 +1,287 @@ +# Mouth — Implementation Plan + +## Overview + +Mouth is a single-binary, offline speech-to-text tool for Windows (with Linux/macOS support where possible). Press a hotkey, speak, and transcribed text is pasted at your cursor. Configured entirely via YAML. + +## Architecture + +``` +┌─────────────┐ ┌───────────┐ ┌─────────────┐ ┌────────────┐ +│ Hotkey │────▶│ Recorder │────▶│ Transcriber │────▶│ Paste │ +│ Listener │ │ (cpal) │ │ (ort/ONNX) │ │ (enigo) │ +│ (rdev) │ │ │ │ │ │ │ +└─────────────┘ └───────────┘ └─────────────┘ └────────────┘ + │ │ │ │ + │ ▼ │ │ + │ ┌───────────┐ │ │ + │ │ VAD │ │ │ + │ │ (silero) │ │ │ + │ └───────────┘ │ │ + │ │ │ + ▼ ▼ ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ Overlay (winit) │ +│ State: idle → recording → transcribing → done │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +### Component Communication + +All components communicate via channels (`std::sync::mpsc` or `tokio::sync`). The main thread owns the overlay window (required by most windowing systems). A coordinator task receives events from hotkey/recorder/transcriber and drives state transitions. + +``` +HotkeyEvent(Pressed/Released) ──┐ +AudioReady(Vec) ───────────┼──▶ Coordinator ──▶ OverlayState +TranscriptionDone(String) ──────┘ ──▶ PasteAction +CancelRequested ────────────────┘ +``` + +## Crate Dependencies + +| Crate | Purpose | Notes | +|-------|---------|-------| +| `rdev` | Global hotkey capture | Cross-platform key events, no focus required | +| `cpal` | Audio capture | Cross-platform mic input | +| `rubato` | Audio resampling | Resample to 16kHz for Parakeet | +| `ort` | ONNX Runtime | Run Parakeet v3 + Silero VAD | +| `hf-hub` | Model download | Download from HuggingFace, standard cache dir | +| `enigo` | Keyboard simulation | Simulate Ctrl+V, Shift+Insert, etc. | +| `arboard` | Clipboard access | Read/write clipboard, save/restore | +| `winit` | Windowing | Minimal overlay window | +| `softbuffer` | Pixel rendering | Draw coloured overlay (no GPU needed for overlay) | +| `serde` + `serde_yaml` | Config | Deserialize YAML config | +| `clap` | CLI | Subcommands: `run`, `config`, `models` | +| `dialoguer` | Interactive TUI | `mouth config` interactive setup | +| `rodio` | Audio playback | Blip up/down sounds | +| `indicatif` | Progress bars | Model download progress | +| `dirs` | Platform dirs | Config/cache paths | +| `tracing` | Logging | Structured logging | + +## Config File + +Location: `~/.config/mouth/config.yaml` (Linux/macOS), `%APPDATA%\mouth\config.yaml` (Windows) + +```yaml +# Hotkey to activate recording +hotkey: "ctrl+space" + +# Recording mode: push_to_talk or toggle +mode: push_to_talk + +# Cancel hotkey (only active while recording) +cancel_key: "escape" + +# Speech-to-text model +model: "parakeet-tdt-0.6b-v3" + +# Inference accelerator: auto, cpu, cuda, directml +accelerator: auto + +# GPU device index (only used when accelerator is cuda/directml) +gpu_device: 0 + +# How to paste text +paste_method: ctrl_v # ctrl_v | shift_insert | ctrl_shift_v | clipboard_only + +# Also keep transcribed text on clipboard after pasting +copy_to_clipboard: true + +# Overlay position on screen +overlay_position: top # top | bottom | none + +# Audio feedback +audio_feedback: true + +# Audio input device (null = system default) +input_device: null + +# VAD: trim silence from audio before transcription +vad_enabled: true + +# Language (for model hint, if supported) +language: en +``` + +## CLI Interface + +``` +mouth run # Start the daemon (default if no subcommand) +mouth config # Interactive TUI to edit config +mouth config --show # Print current config to stdout +mouth config --reset # Reset config to defaults +mouth models # List available/downloaded models +mouth models download # Download configured model (if not cached) +mouth status # Show daemon status, loaded model, app version +``` + +## Implementation Phases + +### Phase 1: Project Skeleton + Config + +- Cargo.toml with all dependencies +- Config struct with serde, defaults, load/save +- CLI with clap (run, config, models subcommands) +- `mouth config` interactive TUI with dialoguer +- Platform-aware config/cache directory resolution + +### Phase 2: Hotkey Listener + +- Global hotkey capture using rdev +- Support configurable key combinations (parse from string like "ctrl+space") +- Push-to-talk mode: record on press, stop on release +- Toggle mode: start on first press, stop on second press +- Cancel on Escape while recording +- Debounce rapid key events (~30ms) + +### Phase 3: Audio Capture + VAD + +- Open mic input via cpal (default device or configured) +- Convert to f32 mono +- Resample to 16kHz via rubato +- Buffer audio chunks during recording +- Run Silero VAD to trim leading/trailing silence +- Produce final `Vec` of clean speech at 16kHz + +### Phase 4: Model Management + +- Use hf-hub to download Parakeet v3 ONNX model from HuggingFace +- Store in standard HF cache (`~/.cache/huggingface/hub/`) +- Show download progress with indicatif +- `mouth models` command to list/download models +- Auto-download on first run if model not cached + +### Phase 5: Transcription + +- Load Parakeet v3 ONNX model via ort +- Auto-detect GPU (DirectML on Windows, CUDA if available, CPU fallback) +- Respect accelerator override from config +- Run inference on captured audio +- Return transcribed text string + +### Phase 6: Overlay + +- Create a small always-on-top window using winit +- Render with softbuffer (simple coloured rectangle + text) +- States and colours: + - Recording: red pulsing indicator + - Transcribing: amber/yellow + - Done: brief green flash, then hide + - Error: brief red flash with error hint +- Window flags (Windows): `WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE` +- Position: centered horizontally at top or bottom of current monitor +- No focus steal, no taskbar entry + +### Phase 7: Paste System + +- Save current clipboard content (if preserving) +- Set transcribed text to clipboard via arboard +- Simulate keypress via enigo based on paste_method: + - `ctrl_v`: Ctrl+V (Cmd+V on macOS) + - `shift_insert`: Shift+Insert + - `ctrl_shift_v`: Ctrl+Shift+V + - `clipboard_only`: no keypress, just clipboard +- Restore previous clipboard content (unless copy_to_clipboard is true) +- Small delay between clipboard set and paste simulation (~50ms) + +### Phase 8: Audio Feedback + +- Bundle two short PCM blip sounds in the binary (via `include_bytes!`) +- "Blip up" on recording start +- "Blip down" on recording stop / transcription complete +- Play via rodio on a separate thread (non-blocking) +- Respect audio_feedback config flag + +### Phase 9: Coordinator + Integration + +- Wire all components together with channel-based message passing +- Main thread: overlay window event loop (winit requires this) +- Spawned threads/tasks: hotkey listener, audio recorder, transcriber +- Coordinator receives events, drives state machine: + ``` + Idle ──[hotkey press]──▶ Recording + Recording ──[hotkey release/press]──▶ Transcribing + Recording ──[cancel]──▶ Idle + Transcribing ──[result]──▶ Pasting ──▶ Idle + Transcribing ──[error]──▶ Error ──▶ Idle + ``` +- Graceful shutdown on SIGINT / tray quit + +### Phase 10: Daemon IPC + Status + +- The running daemon listens on a local Unix domain socket (Linux/macOS) or named pipe (Windows) for status queries +- Socket/pipe path: `/tmp/mouth.sock` (Linux/macOS), `\\.\pipe\mouth` (Windows) +- `mouth status` connects and requests current state; daemon responds with JSON: + ```json + { + "version": "0.1.0", + "state": "idle", + "model": "parakeet-tdt-0.6b-v3", + "accelerator": "directml", + "uptime_secs": 3420 + } + ``` +- If the daemon is not running, `mouth status` reports "Mouth is not running" and exits with code 1 +- Also used internally to prevent launching a second daemon instance (lock check) + +### Phase 11: Polish + Distribution + +- Error handling: user-friendly messages for common failures (no mic, model not found, etc.) +- Windows installer via `cargo-wix` or distribute as standalone .exe +- Test on Windows 10/11 primarily +- Test on Linux (X11 + Wayland) and macOS as secondary +- Update CLAUDE.md with build/run/test instructions +- Write user-facing README with setup instructions + +## Risks & Mitigations + +| Risk | Impact | Mitigation | +|------|--------|------------| +| Parakeet v3 ONNX model compatibility with `ort` | Blocks core functionality | Test early in Phase 5; Parakeet v2 as fallback | +| `rdev` hotkey reliability on Windows | Broken UX | Test early in Phase 2; fallback to Win32 `RegisterHotKey` | +| Overlay focus stealing | Annoying | Use proper window flags; test with various foreground apps | +| Audio resampling quality | Poor transcription | Use rubato SincInterpolation (high quality) | +| Binary size with bundled ONNX Runtime | Large download | ONNX Runtime is ~20-40MB; acceptable for a single-binary tool | +| winit event loop blocking | Unresponsive | All heavy work on background threads; overlay is lightweight | + +## File Structure + +``` +mouth/ +├── Cargo.toml +├── CLAUDE.md +├── README.md +├── plan.md +├── config.yaml.example +├── resources/ +│ ├── blip_up.pcm # bundled audio feedback +│ └── blip_down.pcm +└── src/ + ├── main.rs # CLI entry, clap setup + ├── config.rs # Config struct, YAML load/save, defaults + ├── hotkey.rs # Global hotkey listener (rdev) + ├── recorder.rs # Audio capture (cpal + rubato + VAD) + ├── vad.rs # Silero VAD wrapper + ├── transcriber.rs # ONNX inference, model loading, GPU detection + ├── model_cache.rs # HuggingFace download, cache management + ├── overlay.rs # Minimal overlay window (winit + softbuffer) + ├── paste.rs # Clipboard + paste simulation + ├── audio_feedback.rs # Blip sounds via rodio + ├── coordinator.rs # State machine, channel hub + └── cli/ + ├── mod.rs + ├── run.rs # `mouth run` handler + ├── config_cmd.rs # `mouth config` TUI + ├── models_cmd.rs # `mouth models` handler + └── status_cmd.rs # `mouth status` handler +``` + +## Not In Scope (v1) + +- LLM post-processing of transcriptions +- Transcription history / database +- Multiple model support (v1 is Parakeet v3 only, architecture supports adding more later) +- Auto-submit (Enter after paste) +- Multi-language UI +- Tray icon / system tray integration +- Translate-to-English mode diff --git a/src/audio_feedback.rs b/src/audio_feedback.rs new file mode 100644 index 0000000..f3d2e31 --- /dev/null +++ b/src/audio_feedback.rs @@ -0,0 +1,103 @@ +use anyhow::Result; +use rodio::{OutputStream, Sink}; +use std::io::Cursor; +use std::thread; +use std::time::Duration; +use tracing::{debug, warn}; + +/// Generate a simple sine wave blip sound. +fn generate_blip(freq_start: f32, freq_end: f32, duration_ms: u64) -> Vec { + let sample_rate = 44100u32; + let num_samples = (sample_rate as u64 * duration_ms / 1000) as usize; + let mut samples = Vec::with_capacity(num_samples); + + for i in 0..num_samples { + let t = i as f32 / sample_rate as f32; + let progress = i as f32 / num_samples as f32; + + // Linear frequency sweep + let freq = freq_start + (freq_end - freq_start) * progress; + + // Sine wave with envelope (fade in/out) + let envelope = if progress < 0.1 { + progress / 0.1 + } else if progress > 0.8 { + (1.0 - progress) / 0.2 + } else { + 1.0 + }; + + let sample = (envelope * 0.3 * (2.0 * std::f32::consts::PI * freq * t).sin()) * i16::MAX as f32; + samples.push(sample as i16); + } + + samples +} + +/// Encode samples as a WAV file in memory. +fn encode_wav(samples: &[i16], sample_rate: u32) -> Vec { + let mut buf = Vec::new(); + let data_len = (samples.len() * 2) as u32; + let file_len = 36 + data_len; + + // RIFF header + buf.extend_from_slice(b"RIFF"); + buf.extend_from_slice(&file_len.to_le_bytes()); + buf.extend_from_slice(b"WAVE"); + + // fmt chunk + buf.extend_from_slice(b"fmt "); + buf.extend_from_slice(&16u32.to_le_bytes()); // chunk size + buf.extend_from_slice(&1u16.to_le_bytes()); // PCM + buf.extend_from_slice(&1u16.to_le_bytes()); // mono + buf.extend_from_slice(&sample_rate.to_le_bytes()); + buf.extend_from_slice(&(sample_rate * 2).to_le_bytes()); // byte rate + buf.extend_from_slice(&2u16.to_le_bytes()); // block align + buf.extend_from_slice(&16u16.to_le_bytes()); // bits per sample + + // data chunk + buf.extend_from_slice(b"data"); + buf.extend_from_slice(&data_len.to_le_bytes()); + for sample in samples { + buf.extend_from_slice(&sample.to_le_bytes()); + } + + buf +} + +/// Play the "blip up" sound (recording started). +pub fn play_blip_up() { + thread::spawn(|| { + if let Err(e) = play_blip_internal(800.0, 1200.0, 100) { + warn!("Failed to play blip up: {e}"); + } + }); +} + +/// Play the "blip down" sound (recording stopped / transcription done). +pub fn play_blip_down() { + thread::spawn(|| { + if let Err(e) = play_blip_internal(1200.0, 800.0, 100) { + warn!("Failed to play blip down: {e}"); + } + }); +} + +fn play_blip_internal(freq_start: f32, freq_end: f32, duration_ms: u64) -> Result<()> { + let samples = generate_blip(freq_start, freq_end, duration_ms); + let wav_data = encode_wav(&samples, 44100); + + let (_stream, stream_handle) = OutputStream::try_default()?; + let sink = Sink::try_new(&stream_handle)?; + + let cursor = Cursor::new(wav_data); + let source = rodio::Decoder::new(cursor)?; + sink.append(source); + + debug!("Playing blip ({freq_start}Hz -> {freq_end}Hz, {duration_ms}ms)"); + sink.sleep_until_end(); + + // Keep stream alive briefly + thread::sleep(Duration::from_millis(50)); + Ok(()) +} diff --git a/src/cli/config_cmd.rs b/src/cli/config_cmd.rs new file mode 100644 index 0000000..2f2c827 --- /dev/null +++ b/src/cli/config_cmd.rs @@ -0,0 +1,127 @@ +use anyhow::Result; +use dialoguer::{Input, Select}; + +use crate::config::{Accelerator, Config, OverlayPosition, PasteMethod, RecordingMode}; + +pub fn show() -> Result<()> { + let config = Config::load()?; + let yaml = serde_yaml::to_string(&config)?; + println!("{yaml}"); + Ok(()) +} + +pub fn reset() -> Result<()> { + let config = Config::default(); + config.save()?; + println!("Config reset to defaults at {}", Config::path()?.display()); + Ok(()) +} + +pub fn interactive() -> Result<()> { + let mut config = Config::load()?; + + config.hotkey = Input::new() + .with_prompt("Hotkey") + .default(config.hotkey) + .interact_text()?; + + let mode_idx = Select::new() + .with_prompt("Recording mode") + .items(&["push_to_talk", "toggle"]) + .default(match config.mode { + RecordingMode::PushToTalk => 0, + RecordingMode::Toggle => 1, + }) + .interact()?; + config.mode = match mode_idx { + 0 => RecordingMode::PushToTalk, + _ => RecordingMode::Toggle, + }; + + config.cancel_key = Input::new() + .with_prompt("Cancel key") + .default(config.cancel_key) + .interact_text()?; + + config.model = Input::new() + .with_prompt("Model") + .default(config.model) + .interact_text()?; + + let accel_idx = Select::new() + .with_prompt("Accelerator") + .items(&["auto", "cpu", "cuda", "directml"]) + .default(match config.accelerator { + Accelerator::Auto => 0, + Accelerator::Cpu => 1, + Accelerator::Cuda => 2, + Accelerator::DirectMl => 3, + }) + .interact()?; + config.accelerator = match accel_idx { + 0 => Accelerator::Auto, + 1 => Accelerator::Cpu, + 2 => Accelerator::Cuda, + _ => Accelerator::DirectMl, + }; + + config.gpu_device = Input::new() + .with_prompt("GPU device index") + .default(config.gpu_device) + .interact_text()?; + + let paste_idx = Select::new() + .with_prompt("Paste method") + .items(&["ctrl_v", "shift_insert", "ctrl_shift_v", "clipboard_only"]) + .default(match config.paste_method { + PasteMethod::CtrlV => 0, + PasteMethod::ShiftInsert => 1, + PasteMethod::CtrlShiftV => 2, + PasteMethod::ClipboardOnly => 3, + }) + .interact()?; + config.paste_method = match paste_idx { + 0 => PasteMethod::CtrlV, + 1 => PasteMethod::ShiftInsert, + 2 => PasteMethod::CtrlShiftV, + _ => PasteMethod::ClipboardOnly, + }; + + let overlay_idx = Select::new() + .with_prompt("Overlay position") + .items(&["top", "bottom", "none"]) + .default(match config.overlay_position { + OverlayPosition::Top => 0, + OverlayPosition::Bottom => 1, + OverlayPosition::None => 2, + }) + .interact()?; + config.overlay_position = match overlay_idx { + 0 => OverlayPosition::Top, + 1 => OverlayPosition::Bottom, + _ => OverlayPosition::None, + }; + + let feedback_idx = Select::new() + .with_prompt("Audio feedback") + .items(&["yes", "no"]) + .default(if config.audio_feedback { 0 } else { 1 }) + .interact()?; + config.audio_feedback = feedback_idx == 0; + + let vad_idx = Select::new() + .with_prompt("VAD (voice activity detection)") + .items(&["enabled", "disabled"]) + .default(if config.vad_enabled { 0 } else { 1 }) + .interact()?; + config.vad_enabled = vad_idx == 0; + + config.language = Input::new() + .with_prompt("Language") + .default(config.language) + .interact_text()?; + + config.save()?; + println!("\nConfig saved to {}", Config::path()?.display()); + Ok(()) +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..febdfc1 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,4 @@ +pub mod config_cmd; +pub mod models_cmd; +pub mod run_cmd; +pub mod status_cmd; diff --git a/src/cli/models_cmd.rs b/src/cli/models_cmd.rs new file mode 100644 index 0000000..1207f2b --- /dev/null +++ b/src/cli/models_cmd.rs @@ -0,0 +1,30 @@ +use anyhow::Result; + +use crate::config::Config; +use crate::model_cache; + +pub fn list() -> Result<()> { + let config = Config::load()?; + + println!("Configured model: {}", config.model); + println!(); + println!("Available models:"); + for (name, cached) in model_cache::list_models() { + let status = if cached { "downloaded" } else { "not downloaded" }; + let marker = if name == config.model { " (active)" } else { "" }; + println!(" {name}{marker} [{status}]"); + } + Ok(()) +} + +pub fn download() -> Result<()> { + let config = Config::load()?; + println!("Downloading model: {}...", config.model); + + let paths = model_cache::ensure_model(&config.model)?; + println!("Model ready:"); + println!(" Encoder: {}", paths.encoder.display()); + println!(" Decoder: {}", paths.decoder.display()); + println!(" Vocab: {}", paths.vocab.display()); + Ok(()) +} diff --git a/src/cli/run_cmd.rs b/src/cli/run_cmd.rs new file mode 100644 index 0000000..73cc5f1 --- /dev/null +++ b/src/cli/run_cmd.rs @@ -0,0 +1,116 @@ +use anyhow::{Context, Result}; +use std::sync::mpsc; +use std::thread; +use tracing::info; + +use crate::config::{Config, OverlayPosition}; +use crate::coordinator::Coordinator; +use crate::hotkey; +use crate::model_cache; +use crate::overlay; +use crate::recorder; +use crate::transcriber::Transcriber; + +pub fn run() -> Result<()> { + let config = Config::load()?; + info!("Mouth v{} starting", env!("CARGO_PKG_VERSION")); + info!("Mode: {:?}", config.mode); + info!("Hotkey: {}", config.hotkey); + info!("Model: {}", config.model); + info!("Accelerator: {:?}", config.accelerator); + info!("Paste method: {:?}", config.paste_method); + + // Step 1: Ensure model is downloaded + info!("Checking model..."); + let model_paths = model_cache::ensure_model(&config.model) + .context("Failed to ensure model is available")?; + + // Step 2: Load transcriber + info!("Loading transcription engine..."); + let transcriber = Transcriber::new(&model_paths, &config.accelerator, config.gpu_device) + .context("Failed to load transcription engine")?; + + // Step 3: VAD (not yet bundled) + let vad = if config.vad_enabled { + info!("VAD enabled but Silero model not yet bundled — skipping"); + None + } else { + None + }; + + // Step 4: Parse hotkeys + let hotkey_combo = hotkey::parse_hotkey(&config.hotkey) + .with_context(|| format!("Invalid hotkey: {}", config.hotkey))?; + let cancel_combo = hotkey::parse_hotkey(&config.cancel_key) + .with_context(|| format!("Invalid cancel key: {}", config.cancel_key))?; + + // Step 5: Set up channels + let (hotkey_tx, hotkey_rx) = mpsc::channel(); + let (recorder_cmd_tx, recorder_cmd_rx) = mpsc::channel(); + let (audio_tx, audio_rx) = mpsc::channel(); + + // Step 6: Spawn background threads + let device_name = config.input_device.clone(); + thread::Builder::new() + .name("mouth-recorder".into()) + .spawn(move || { + recorder::run(device_name, recorder_cmd_rx, audio_tx); + }) + .context("Failed to spawn recorder thread")?; + + thread::Builder::new() + .name("mouth-hotkey".into()) + .spawn(move || { + hotkey::listen(hotkey_combo, cancel_combo, hotkey_tx); + }) + .context("Failed to spawn hotkey thread")?; + + // Step 7: Start overlay + coordinator + if config.overlay_position != OverlayPosition::None { + let (event_loop, proxy) = overlay::create_event_loop() + .map_err(|e| anyhow::anyhow!("Failed to create overlay event loop: {e}"))?; + + let overlay_position = config.overlay_position.clone(); + let coord_proxy = Some(proxy); + + // Coordinator runs on a background thread + let coord_config = config.clone(); + thread::Builder::new() + .name("mouth-coordinator".into()) + .spawn(move || { + let mut coordinator = Coordinator::new( + coord_config, + transcriber, + vad, + recorder_cmd_tx, + audio_rx, + hotkey_rx, + coord_proxy, + ); + coordinator.run(); + }) + .context("Failed to spawn coordinator thread")?; + + println!("Mouth is running. Press {} to record. Ctrl+C to quit.", config.hotkey); + + // Overlay event loop runs on main thread (blocking) + overlay::run_event_loop(event_loop, overlay_position) + .map_err(|e| anyhow::anyhow!("Overlay event loop error: {e}"))?; + } else { + // No overlay — coordinator runs on main thread + println!("Mouth is running. Press {} to record. Ctrl+C to quit.", config.hotkey); + + let mut coordinator = Coordinator::new( + config, + transcriber, + vad, + recorder_cmd_tx, + audio_rx, + hotkey_rx, + None, + ); + coordinator.run(); + } + + Ok(()) +} diff --git a/src/cli/status_cmd.rs b/src/cli/status_cmd.rs new file mode 100644 index 0000000..3e1b98e --- /dev/null +++ b/src/cli/status_cmd.rs @@ -0,0 +1,11 @@ +use anyhow::Result; + +pub fn status() -> Result<()> { + let version = env!("CARGO_PKG_VERSION"); + + // TODO: Phase 10 — connect to daemon IPC socket/pipe and query status + // For now, just show version info + println!("Mouth v{version}"); + println!("Status: not yet implemented (requires daemon IPC)"); + Ok(()) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..5c5b0f2 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,184 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum RecordingMode { + PushToTalk, + Toggle, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum PasteMethod { + CtrlV, + ShiftInsert, + CtrlShiftV, + ClipboardOnly, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum OverlayPosition { + Top, + Bottom, + None, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum Accelerator { + Auto, + Cpu, + Cuda, + DirectMl, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + /// Hotkey to activate recording + #[serde(default = "defaults::hotkey")] + pub hotkey: String, + + /// Recording mode + #[serde(default = "defaults::mode")] + pub mode: RecordingMode, + + /// Cancel hotkey (only active while recording) + #[serde(default = "defaults::cancel_key")] + pub cancel_key: String, + + /// Speech-to-text model identifier + #[serde(default = "defaults::model")] + pub model: String, + + /// Inference accelerator + #[serde(default = "defaults::accelerator")] + pub accelerator: Accelerator, + + /// GPU device index (when accelerator is cuda/directml) + #[serde(default)] + pub gpu_device: u32, + + /// How to paste transcribed text + #[serde(default = "defaults::paste_method")] + pub paste_method: PasteMethod, + + /// Keep transcribed text on clipboard after pasting + #[serde(default = "defaults::yes")] + pub copy_to_clipboard: bool, + + /// Overlay position on screen + #[serde(default = "defaults::overlay_position")] + pub overlay_position: OverlayPosition, + + /// Play audio feedback sounds + #[serde(default = "defaults::yes")] + pub audio_feedback: bool, + + /// Audio input device name (null = system default) + #[serde(default)] + pub input_device: Option, + + /// Enable VAD to trim silence + #[serde(default = "defaults::yes")] + pub vad_enabled: bool, + + /// Language hint for model + #[serde(default = "defaults::language")] + pub language: String, +} + +mod defaults { + use super::*; + + pub fn hotkey() -> String { + "ctrl+space".into() + } + pub fn mode() -> RecordingMode { + RecordingMode::PushToTalk + } + pub fn cancel_key() -> String { + "escape".into() + } + pub fn model() -> String { + "parakeet-tdt-0.6b-v3".into() + } + pub fn accelerator() -> Accelerator { + Accelerator::Auto + } + pub fn paste_method() -> PasteMethod { + PasteMethod::CtrlV + } + pub fn overlay_position() -> OverlayPosition { + OverlayPosition::Top + } + pub fn yes() -> bool { + true + } + pub fn language() -> String { + "en".into() + } +} + +impl Default for Config { + fn default() -> Self { + Self { + hotkey: defaults::hotkey(), + mode: defaults::mode(), + cancel_key: defaults::cancel_key(), + model: defaults::model(), + accelerator: defaults::accelerator(), + gpu_device: 0, + paste_method: defaults::paste_method(), + copy_to_clipboard: true, + overlay_position: defaults::overlay_position(), + audio_feedback: true, + input_device: None, + vad_enabled: true, + language: defaults::language(), + } + } +} + +impl Config { + /// Returns the platform-appropriate config directory. + pub fn dir() -> Result { + let dir = dirs::config_dir() + .context("Could not determine config directory")? + .join("mouth"); + Ok(dir) + } + + /// Returns the path to the config file. + pub fn path() -> Result { + Ok(Self::dir()?.join("config.yaml")) + } + + /// Load config from disk, falling back to defaults if file doesn't exist. + pub fn load() -> Result { + let path = Self::path()?; + if !path.exists() { + return Ok(Self::default()); + } + let contents = std::fs::read_to_string(&path) + .with_context(|| format!("Failed to read config from {}", path.display()))?; + let config: Config = serde_yaml::from_str(&contents) + .with_context(|| format!("Failed to parse config from {}", path.display()))?; + Ok(config) + } + + /// Save config to disk, creating the directory if needed. + pub fn save(&self) -> Result<()> { + let path = Self::path()?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("Failed to create config directory {}", parent.display()))?; + } + let yaml = serde_yaml::to_string(self).context("Failed to serialize config")?; + std::fs::write(&path, yaml) + .with_context(|| format!("Failed to write config to {}", path.display()))?; + Ok(()) + } +} diff --git a/src/coordinator.rs b/src/coordinator.rs new file mode 100644 index 0000000..9f04348 --- /dev/null +++ b/src/coordinator.rs @@ -0,0 +1,255 @@ +use std::sync::mpsc; +use std::thread; +use tracing::{debug, error, info, warn}; +use winit::event_loop::EventLoopProxy; + +use crate::audio_feedback; +use crate::config::{Config, RecordingMode}; +use crate::hotkey::HotkeyEvent; +use crate::overlay::{OverlayEvent, OverlayState}; +use crate::paste; +use crate::recorder::{AudioData, RecorderCommand}; +use crate::transcriber::Transcriber; +use crate::vad::Vad; + +/// The application state machine. +#[derive(Debug, Clone, Copy, PartialEq)] +enum State { + Idle, + Recording, + Transcribing, +} + +/// Central coordinator that wires all components together. +pub struct Coordinator { + config: Config, + state: State, + transcriber: Transcriber, + vad: Option, + recorder_tx: mpsc::Sender, + audio_rx: mpsc::Receiver, + hotkey_rx: mpsc::Receiver, + overlay_proxy: Option>, +} + +impl Coordinator { + pub fn new( + config: Config, + transcriber: Transcriber, + vad: Option, + recorder_tx: mpsc::Sender, + audio_rx: mpsc::Receiver, + hotkey_rx: mpsc::Receiver, + overlay_proxy: Option>, + ) -> Self { + Self { + config, + state: State::Idle, + transcriber, + vad, + recorder_tx, + audio_rx, + hotkey_rx, + overlay_proxy, + } + } + + /// Run the coordinator loop. This blocks until shutdown. + pub fn run(&mut self) { + info!("Coordinator started"); + + loop { + // Wait for hotkey events + let event = match self.hotkey_rx.recv() { + Ok(e) => e, + Err(_) => { + info!("Hotkey channel closed, shutting down"); + break; + } + }; + + self.handle_event(event); + } + + self.shutdown(); + } + + fn handle_event(&mut self, event: HotkeyEvent) { + debug!("Event: {:?}, State: {:?}", event, self.state); + + match (self.state, event) { + // Start recording + (State::Idle, HotkeyEvent::Pressed) => { + self.start_recording(); + } + + // Push-to-talk: stop on release + (State::Recording, HotkeyEvent::Released) => { + if self.config.mode == RecordingMode::PushToTalk { + self.stop_recording(); + } + } + + // Toggle mode: stop on second press + (State::Recording, HotkeyEvent::Pressed) => { + if self.config.mode == RecordingMode::Toggle { + self.stop_recording(); + } + } + + // Cancel recording + (State::Recording, HotkeyEvent::Cancel) => { + self.cancel_recording(); + } + + // Ignore other combinations + _ => { + debug!("Ignoring event {:?} in state {:?}", event, self.state); + } + } + } + + fn start_recording(&mut self) { + info!("Recording started"); + self.state = State::Recording; + self.set_overlay(OverlayState::Recording); + + if self.config.audio_feedback { + audio_feedback::play_blip_up(); + } + + if self.recorder_tx.send(RecorderCommand::Start).is_err() { + error!("Failed to send start command to recorder"); + self.state = State::Idle; + self.set_overlay(OverlayState::Hidden); + } + } + + fn stop_recording(&mut self) { + info!("Recording stopped, starting transcription"); + self.state = State::Transcribing; + self.set_overlay(OverlayState::Transcribing); + + if self.config.audio_feedback { + audio_feedback::play_blip_down(); + } + + if self.recorder_tx.send(RecorderCommand::Stop).is_err() { + error!("Failed to send stop command to recorder"); + self.state = State::Idle; + self.set_overlay(OverlayState::Hidden); + return; + } + + // Wait for audio data + match self.audio_rx.recv() { + Ok(audio) => { + self.process_audio(audio); + } + Err(_) => { + error!("Failed to receive audio data"); + self.state = State::Idle; + self.set_overlay(OverlayState::Error); + self.delayed_hide_overlay(); + } + } + } + + fn cancel_recording(&mut self) { + info!("Recording cancelled"); + self.state = State::Idle; + + if self.recorder_tx.send(RecorderCommand::Stop).is_err() { + warn!("Failed to send stop command to recorder"); + } + + // Drain any pending audio + while self.audio_rx.try_recv().is_ok() {} + + self.set_overlay(OverlayState::Hidden); + } + + fn process_audio(&mut self, audio: AudioData) { + let samples = if self.config.vad_enabled { + if let Some(vad) = &mut self.vad { + match vad.filter_speech(&audio.samples) { + Ok(filtered) => { + if filtered.is_empty() { + info!("No speech detected by VAD"); + self.state = State::Idle; + self.set_overlay(OverlayState::Hidden); + return; + } + filtered + } + Err(e) => { + warn!("VAD failed, using raw audio: {e}"); + audio.samples + } + } + } else { + audio.samples + } + } else { + audio.samples + }; + + // Transcribe + match self.transcriber.transcribe(&samples) { + Ok(text) => { + if text.is_empty() { + info!("Empty transcription"); + self.state = State::Idle; + self.set_overlay(OverlayState::Hidden); + return; + } + + info!("Transcribed: \"{text}\""); + self.set_overlay(OverlayState::Done); + + // Paste the text + if let Err(e) = paste::paste_text( + &text, + &self.config.paste_method, + self.config.copy_to_clipboard, + ) { + error!("Failed to paste text: {e}"); + self.set_overlay(OverlayState::Error); + } + + self.delayed_hide_overlay(); + self.state = State::Idle; + } + Err(e) => { + error!("Transcription failed: {e}"); + self.state = State::Idle; + self.set_overlay(OverlayState::Error); + self.delayed_hide_overlay(); + } + } + } + + fn set_overlay(&self, state: OverlayState) { + if let Some(proxy) = &self.overlay_proxy { + let _ = proxy.send_event(OverlayEvent::SetState(state)); + } + } + + fn delayed_hide_overlay(&self) { + if let Some(proxy) = &self.overlay_proxy { + let proxy = proxy.clone(); + thread::spawn(move || { + thread::sleep(std::time::Duration::from_millis(500)); + let _ = proxy.send_event(OverlayEvent::SetState(OverlayState::Hidden)); + }); + } + } + + fn shutdown(&self) { + info!("Coordinator shutting down"); + let _ = self.recorder_tx.send(RecorderCommand::Shutdown); + if let Some(proxy) = &self.overlay_proxy { + let _ = proxy.send_event(OverlayEvent::Shutdown); + } + } +} diff --git a/src/hotkey.rs b/src/hotkey.rs new file mode 100644 index 0000000..8c1241d --- /dev/null +++ b/src/hotkey.rs @@ -0,0 +1,240 @@ +use anyhow::{bail, Result}; +use rdev::{self, Event, EventType, Key}; +use std::sync::mpsc; +use std::time::{Duration, Instant}; +use tracing::{debug, error, info}; + +/// Events sent from the hotkey listener to the coordinator. +#[derive(Debug, Clone, Copy)] +pub enum HotkeyEvent { + /// Hotkey was pressed (start recording or toggle) + Pressed, + /// Hotkey was released (stop recording in push-to-talk mode) + Released, + /// Cancel key was pressed + Cancel, +} + +/// Parsed hotkey combination. +#[derive(Debug, Clone)] +pub struct HotkeyCombination { + pub modifiers: Vec, + pub key: Key, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Modifier { + Ctrl, + Alt, + Shift, + Meta, +} + +/// Tracks which modifier keys are currently held down. +#[derive(Debug, Default)] +struct ModifierState { + ctrl: bool, + alt: bool, + shift: bool, + meta: bool, +} + +impl ModifierState { + fn update(&mut self, key: &Key, pressed: bool) { + match key { + Key::ControlLeft | Key::ControlRight => self.ctrl = pressed, + Key::Alt | Key::AltGr => self.alt = pressed, + Key::ShiftLeft | Key::ShiftRight => self.shift = pressed, + Key::MetaLeft | Key::MetaRight => self.meta = pressed, + _ => {} + } + } + + fn is_held(&self, modifier: &Modifier) -> bool { + match modifier { + Modifier::Ctrl => self.ctrl, + Modifier::Alt => self.alt, + Modifier::Shift => self.shift, + Modifier::Meta => self.meta, + } + } + + fn all_held(&self, modifiers: &[Modifier]) -> bool { + modifiers.iter().all(|m| self.is_held(m)) + } +} + +/// Parse a hotkey string like "ctrl+space" into a HotkeyCombination. +pub fn parse_hotkey(s: &str) -> Result { + let lowered = s.to_lowercase(); + let parts: Vec<&str> = lowered.split('+').map(|p| p.trim()).collect(); + if parts.is_empty() { + bail!("Empty hotkey string"); + } + + let mut modifiers = Vec::new(); + let mut key = None; + + for part in &parts { + match *part { + "ctrl" | "control" => modifiers.push(Modifier::Ctrl), + "alt" => modifiers.push(Modifier::Alt), + "shift" => modifiers.push(Modifier::Shift), + "meta" | "super" | "win" | "cmd" => modifiers.push(Modifier::Meta), + _ => { + if key.is_some() { + bail!("Multiple non-modifier keys in hotkey: {s}"); + } + key = Some(parse_key(part)?); + } + } + } + + let key = key.unwrap_or_else(|| { + // If no non-modifier key, treat last modifier as the key + // This shouldn't happen with valid config but handle gracefully + Key::Unknown(0) + }); + + Ok(HotkeyCombination { modifiers, key }) +} + +fn parse_key(s: &str) -> Result { + let key = match s { + "space" => Key::Space, + "enter" | "return" => Key::Return, + "escape" | "esc" => Key::Escape, + "tab" => Key::Tab, + "backspace" => Key::Backspace, + "delete" | "del" => Key::Delete, + "insert" => Key::Insert, + "home" => Key::Home, + "end" => Key::End, + "pageup" => Key::PageUp, + "pagedown" => Key::PageDown, + "up" => Key::UpArrow, + "down" => Key::DownArrow, + "left" => Key::LeftArrow, + "right" => Key::RightArrow, + "f1" => Key::F1, + "f2" => Key::F2, + "f3" => Key::F3, + "f4" => Key::F4, + "f5" => Key::F5, + "f6" => Key::F6, + "f7" => Key::F7, + "f8" => Key::F8, + "f9" => Key::F9, + "f10" => Key::F10, + "f11" => Key::F11, + "f12" => Key::F12, + "a" => Key::KeyA, + "b" => Key::KeyB, + "c" => Key::KeyC, + "d" => Key::KeyD, + "e" => Key::KeyE, + "f" => Key::KeyF, + "g" => Key::KeyG, + "h" => Key::KeyH, + "i" => Key::KeyI, + "j" => Key::KeyJ, + "k" => Key::KeyK, + "l" => Key::KeyL, + "m" => Key::KeyM, + "n" => Key::KeyN, + "o" => Key::KeyO, + "p" => Key::KeyP, + "q" => Key::KeyQ, + "r" => Key::KeyR, + "s" => Key::KeyS, + "t" => Key::KeyT, + "u" => Key::KeyU, + "v" => Key::KeyV, + "w" => Key::KeyW, + "x" => Key::KeyX, + "y" => Key::KeyY, + "z" => Key::KeyZ, + "0" => Key::Num0, + "1" => Key::Num1, + "2" => Key::Num2, + "3" => Key::Num3, + "4" => Key::Num4, + "5" => Key::Num5, + "6" => Key::Num6, + "7" => Key::Num7, + "8" => Key::Num8, + "9" => Key::Num9, + _ => bail!("Unknown key: {s}"), + }; + Ok(key) +} + +/// Start the global hotkey listener on the current thread (blocking). +/// Sends HotkeyEvents to the provided channel. +pub fn listen( + hotkey: HotkeyCombination, + cancel_key: HotkeyCombination, + tx: mpsc::Sender, +) { + let debounce_duration = Duration::from_millis(30); + let mut last_event_time = Instant::now() - debounce_duration; + let mut modifier_state = ModifierState::default(); + let mut hotkey_held = false; + + info!("Hotkey listener started"); + debug!("Hotkey: {:?}", hotkey); + debug!("Cancel: {:?}", cancel_key); + + let callback = move |event: Event| { + let now = Instant::now(); + match event.event_type { + EventType::KeyPress(key) => { + modifier_state.update(&key, true); + + // Check cancel key + if key == cancel_key.key && modifier_state.all_held(&cancel_key.modifiers) { + if now.duration_since(last_event_time) >= debounce_duration { + last_event_time = now; + debug!("Cancel key pressed"); + if tx.send(HotkeyEvent::Cancel).is_err() { + error!("Failed to send cancel event"); + } + } + return; + } + + // Check hotkey + if key == hotkey.key && modifier_state.all_held(&hotkey.modifiers) { + if now.duration_since(last_event_time) >= debounce_duration && !hotkey_held { + last_event_time = now; + hotkey_held = true; + debug!("Hotkey pressed"); + if tx.send(HotkeyEvent::Pressed).is_err() { + error!("Failed to send pressed event"); + } + } + } + } + EventType::KeyRelease(key) => { + modifier_state.update(&key, false); + + // Check hotkey release (for push-to-talk) + if key == hotkey.key && hotkey_held { + if now.duration_since(last_event_time) >= debounce_duration { + last_event_time = now; + hotkey_held = false; + debug!("Hotkey released"); + if tx.send(HotkeyEvent::Released).is_err() { + error!("Failed to send released event"); + } + } + } + } + _ => {} + } + }; + + if let Err(e) = rdev::listen(callback) { + error!("Hotkey listener error: {:?}", e); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0c9d423 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,82 @@ +mod audio_feedback; +mod cli; +mod config; +mod coordinator; +mod hotkey; +mod model_cache; +mod overlay; +mod paste; +mod recorder; +mod transcriber; +mod vad; + +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command(name = "mouth", version, about = "Offline speech-to-text with global hotkey and paste")] +struct Cli { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// Start the mouth daemon + Run, + + /// View or edit configuration + Config { + /// Print current config to stdout + #[arg(long)] + show: bool, + + /// Reset config to defaults + #[arg(long)] + reset: bool, + }, + + /// Manage speech-to-text models + Models { + /// Download the configured model + #[arg(long)] + download: bool, + }, + + /// Show daemon status, loaded model, and version + Status, +} + +fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + let cli = Cli::parse(); + + match cli.command { + None | Some(Commands::Run) => cli::run_cmd::run(), + + Some(Commands::Config { show, reset }) => { + if show { + cli::config_cmd::show() + } else if reset { + cli::config_cmd::reset() + } else { + cli::config_cmd::interactive() + } + } + + Some(Commands::Models { download }) => { + if download { + cli::models_cmd::download() + } else { + cli::models_cmd::list() + } + } + + Some(Commands::Status) => cli::status_cmd::status(), + } +} diff --git a/src/model_cache.rs b/src/model_cache.rs new file mode 100644 index 0000000..6678e48 --- /dev/null +++ b/src/model_cache.rs @@ -0,0 +1,97 @@ +use anyhow::{Context, Result}; +use hf_hub::api::sync::Api; +use std::path::PathBuf; +use tracing::{debug, info}; + +/// Known model definitions. +pub struct ModelInfo { + pub repo_id: &'static str, + pub encoder_file: &'static str, + pub encoder_data_file: Option<&'static str>, + pub decoder_file: &'static str, + pub vocab_file: &'static str, + #[allow(dead_code)] + pub preprocessor_file: Option<&'static str>, +} + +/// Get model info for a model name. +pub fn get_model_info(model_name: &str) -> Result { + match model_name { + "parakeet-tdt-0.6b-v3" => Ok(ModelInfo { + repo_id: "istupakov/parakeet-tdt-0.6b-v3-onnx", + encoder_file: "encoder-model.onnx", + encoder_data_file: Some("encoder-model.onnx.data"), + decoder_file: "decoder_joint-model.onnx", + vocab_file: "vocab.txt", + preprocessor_file: None, + }), + _ => anyhow::bail!("Unknown model: {model_name}. Supported models: parakeet-tdt-0.6b-v3"), + } +} + +/// Paths to downloaded model files. +pub struct ModelPaths { + pub encoder: PathBuf, + pub decoder: PathBuf, + pub vocab: PathBuf, +} + +/// Ensure model files are downloaded and return their paths. +/// Uses the standard HuggingFace cache directory. +pub fn ensure_model(model_name: &str) -> Result { + let model_info = get_model_info(model_name)?; + let api = Api::new().context("Failed to create HuggingFace Hub API")?; + let repo = api.model(model_info.repo_id.to_string()); + + info!("Ensuring model files for '{model_name}' from {}", model_info.repo_id); + + // Download encoder + info!("Checking encoder: {}", model_info.encoder_file); + let encoder = repo + .get(model_info.encoder_file) + .with_context(|| format!("Failed to download {}", model_info.encoder_file))?; + debug!("Encoder: {}", encoder.display()); + + // Download encoder data file if present + if let Some(data_file) = model_info.encoder_data_file { + info!("Checking encoder data: {data_file}"); + let data_path = repo + .get(data_file) + .with_context(|| format!("Failed to download {data_file}"))?; + debug!("Encoder data: {}", data_path.display()); + } + + // Download decoder + info!("Checking decoder: {}", model_info.decoder_file); + let decoder = repo + .get(model_info.decoder_file) + .with_context(|| format!("Failed to download {}", model_info.decoder_file))?; + debug!("Decoder: {}", decoder.display()); + + // Download vocab + info!("Checking vocab: {}", model_info.vocab_file); + let vocab = repo + .get(model_info.vocab_file) + .with_context(|| format!("Failed to download {}", model_info.vocab_file))?; + debug!("Vocab: {}", vocab.display()); + + Ok(ModelPaths { + encoder, + decoder, + vocab, + }) +} + +/// Check if model files are already cached. +pub fn is_model_cached(model_name: &str) -> bool { + ensure_model(model_name).is_ok() +} + +/// List available models with their download status. +pub fn list_models() -> Vec<(&'static str, bool)> { + let models = ["parakeet-tdt-0.6b-v3"]; + models + .iter() + .map(|name| (*name, is_model_cached(name))) + .collect() +} diff --git a/src/overlay.rs b/src/overlay.rs new file mode 100644 index 0000000..69e428a --- /dev/null +++ b/src/overlay.rs @@ -0,0 +1,201 @@ +use std::num::NonZeroU32; +use tracing::{debug, error, info, warn}; +use winit::application::ApplicationHandler; +use winit::dpi::{LogicalSize, PhysicalPosition}; +use winit::event::WindowEvent; +use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}; +use winit::window::{Window, WindowAttributes, WindowId, WindowLevel}; + +use crate::config::OverlayPosition; + +const OVERLAY_WIDTH: u32 = 200; +const OVERLAY_HEIGHT: u32 = 36; + +/// State of the overlay display. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum OverlayState { + Hidden, + Recording, + Transcribing, + Done, + Error, +} + +/// Events sent to the overlay from the coordinator. +#[derive(Debug, Clone)] +pub enum OverlayEvent { + SetState(OverlayState), + Shutdown, +} + +/// The overlay application handler for winit. +struct OverlayApp { + window: Option>, + surface: Option, std::rc::Rc>>, + state: OverlayState, + position: OverlayPosition, +} + +impl OverlayApp { + fn draw(&mut self) { + let Some(surface) = &mut self.surface else { return }; + let Some(window) = &self.window else { return }; + + let size = window.inner_size(); + if size.width == 0 || size.height == 0 { + return; + } + + let Ok(w) = NonZeroU32::try_from(size.width) else { return }; + let Ok(h) = NonZeroU32::try_from(size.height) else { return }; + + if surface.resize(w, h).is_err() { + return; + } + + let Ok(mut buffer) = surface.buffer_mut() else { return }; + + let color = match self.state { + OverlayState::Hidden => 0x00000000, + OverlayState::Recording => 0xFFDD3333, // Red + OverlayState::Transcribing => 0xFFDDAA33, // Amber + OverlayState::Done => 0xFF33AA33, // Green + OverlayState::Error => 0xFFDD3333, // Red + }; + + let width = size.width as usize; + let height = size.height as usize; + + for y in 0..height { + for x in 0..width { + let radius = 8; + let in_corner = (x < radius || x >= width - radius) + && (y < radius || y >= height - radius); + + let pixel = if in_corner { + let cx = if x < radius { radius } else { width - radius - 1 }; + let cy = if y < radius { radius } else { height - radius - 1 }; + let dx = x as i32 - cx as i32; + let dy = y as i32 - cy as i32; + if dx * dx + dy * dy <= (radius * radius) as i32 { + color + } else { + 0x00000000 + } + } else { + color + }; + + buffer[y * width + x] = pixel; + } + } + + let _ = buffer.present(); + } + + fn update_visibility(&self) { + if let Some(window) = &self.window { + let visible = self.state != OverlayState::Hidden; + window.set_visible(visible); + } + } +} + +impl ApplicationHandler for OverlayApp { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + if self.window.is_some() { + return; + } + + let attrs = WindowAttributes::default() + .with_title("Mouth") + .with_inner_size(LogicalSize::new(OVERLAY_WIDTH, OVERLAY_HEIGHT)) + .with_resizable(false) + .with_decorations(false) + .with_transparent(true) + .with_window_level(WindowLevel::AlwaysOnTop) + .with_visible(false); + + match event_loop.create_window(attrs) { + Ok(window) => { + let window = std::rc::Rc::new(window); + + // Position at top center of primary monitor + if let Some(monitor) = window.current_monitor() { + let screen_size = monitor.size(); + let pos = match self.position { + OverlayPosition::Top => PhysicalPosition::new( + (screen_size.width - OVERLAY_WIDTH) / 2, + 10, + ), + OverlayPosition::Bottom => PhysicalPosition::new( + (screen_size.width - OVERLAY_WIDTH) / 2, + screen_size.height - OVERLAY_HEIGHT - 50, + ), + OverlayPosition::None => PhysicalPosition::new(0, 0), + }; + window.set_outer_position(pos); + } + + let context = softbuffer::Context::new(window.clone()).ok(); + let surface = context.and_then(|ctx| { + softbuffer::Surface::new(&ctx, window.clone()).ok() + }); + + if surface.is_none() { + warn!("Could not create softbuffer surface — overlay rendering disabled"); + } + + self.surface = surface; + self.window = Some(window); + info!("Overlay window created"); + } + Err(e) => { + error!("Failed to create overlay window: {e}"); + } + } + } + + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: OverlayEvent) { + match event { + OverlayEvent::SetState(state) => { + debug!("Overlay state: {:?} -> {:?}", self.state, state); + self.state = state; + self.update_visibility(); + self.draw(); + } + OverlayEvent::Shutdown => { + info!("Overlay shutting down"); + event_loop.exit(); + } + } + } + + fn window_event(&mut self, _event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { + if let WindowEvent::RedrawRequested = event { + self.draw(); + } + } +} + +/// Create an event loop and return the proxy for sending events. +pub fn create_event_loop() -> Result<(EventLoop, EventLoopProxy), winit::error::EventLoopError> { + let event_loop: EventLoop = EventLoop::with_user_event().build()?; + let proxy = event_loop.create_proxy(); + Ok((event_loop, proxy)) +} + +/// Run the event loop with the given position config. +pub fn run_event_loop( + event_loop: EventLoop, + position: OverlayPosition, +) -> Result<(), winit::error::EventLoopError> { + let mut app = OverlayApp { + window: None, + surface: None, + state: OverlayState::Hidden, + position, + }; + + event_loop.run_app(&mut app) +} diff --git a/src/paste.rs b/src/paste.rs new file mode 100644 index 0000000..ce21a52 --- /dev/null +++ b/src/paste.rs @@ -0,0 +1,71 @@ +use anyhow::{Context, Result}; +use arboard::Clipboard; +use enigo::{Direction, Enigo, Key, Keyboard, Settings}; +use std::thread; +use std::time::Duration; +use tracing::{debug, info}; + +use crate::config::PasteMethod; + +/// Paste text using the configured method. +pub fn paste_text(text: &str, method: &PasteMethod, keep_on_clipboard: bool) -> Result<()> { + let mut clipboard = Clipboard::new().context("Failed to open clipboard")?; + + // Save current clipboard content if we need to restore it + let previous = if !keep_on_clipboard { + clipboard.get_text().ok() + } else { + None + }; + + // Set text to clipboard + clipboard + .set_text(text.to_string()) + .context("Failed to set clipboard text")?; + debug!("Text set to clipboard ({} chars)", text.len()); + + if *method == PasteMethod::ClipboardOnly { + info!("Text copied to clipboard (clipboard_only mode)"); + return Ok(()); + } + + // Small delay to ensure clipboard is ready + thread::sleep(Duration::from_millis(50)); + + // Simulate paste keystroke + let mut enigo = Enigo::new(&Settings::default()).context("Failed to create enigo instance")?; + + match method { + PasteMethod::CtrlV => { + debug!("Pasting via Ctrl+V"); + enigo.key(Key::Control, Direction::Press)?; + enigo.key(Key::Unicode('v'), Direction::Click)?; + enigo.key(Key::Control, Direction::Release)?; + } + PasteMethod::ShiftInsert => { + debug!("Pasting via Shift+Insert"); + enigo.key(Key::Shift, Direction::Press)?; + enigo.key(Key::Other(0x2D), Direction::Click)?; // VK_INSERT on Windows + enigo.key(Key::Shift, Direction::Release)?; + } + PasteMethod::CtrlShiftV => { + debug!("Pasting via Ctrl+Shift+V"); + enigo.key(Key::Control, Direction::Press)?; + enigo.key(Key::Shift, Direction::Press)?; + enigo.key(Key::Unicode('v'), Direction::Click)?; + enigo.key(Key::Shift, Direction::Release)?; + enigo.key(Key::Control, Direction::Release)?; + } + PasteMethod::ClipboardOnly => unreachable!(), + } + + // Restore previous clipboard content if needed + if let Some(prev) = previous { + thread::sleep(Duration::from_millis(100)); + let _ = clipboard.set_text(prev); + debug!("Previous clipboard content restored"); + } + + info!("Text pasted ({} chars)", text.len()); + Ok(()) +} diff --git a/src/recorder.rs b/src/recorder.rs new file mode 100644 index 0000000..2be6932 --- /dev/null +++ b/src/recorder.rs @@ -0,0 +1,265 @@ +use anyhow::{Context, Result}; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{Device, SampleFormat, SampleRate, StreamConfig}; +use rubato::{FftFixedIn, Resampler}; +use std::sync::mpsc; +use std::sync::{Arc, Mutex}; +use tracing::{debug, error, info, warn}; + +const TARGET_SAMPLE_RATE: u32 = 16000; + +/// Commands sent to the recorder. +#[derive(Debug)] +pub enum RecorderCommand { + Start, + Stop, + Shutdown, +} + +/// Audio data produced by the recorder. +#[derive(Debug)] +pub struct AudioData { + /// Mono f32 samples at 16kHz + pub samples: Vec, + #[allow(dead_code)] + pub sample_rate: u32, +} + +/// Find the audio input device by name, or use the default. +fn get_input_device(name: Option<&str>) -> Result { + let host = cpal::default_host(); + + if let Some(name) = name { + let devices = host.input_devices().context("Failed to enumerate input devices")?; + for device in devices { + if let Ok(dev_name) = device.name() { + if dev_name.to_lowercase().contains(&name.to_lowercase()) { + info!("Using audio input device: {dev_name}"); + return Ok(device); + } + } + } + warn!("Audio device '{name}' not found, falling back to default"); + } + + host.default_input_device().context("No default input device available") +} + +/// Convert interleaved multi-channel samples to mono f32. +fn to_mono_f32(data: &[f32], channels: u16) -> Vec { + if channels == 1 { + return data.to_vec(); + } + data.chunks(channels as usize) + .map(|frame| frame.iter().sum::() / channels as f32) + .collect() +} + +/// Resample audio from source rate to target rate. +fn resample(samples: &[f32], from_rate: u32, to_rate: u32) -> Result> { + if from_rate == to_rate { + return Ok(samples.to_vec()); + } + + let chunk_size = 1024; + let mut resampler = FftFixedIn::::new( + from_rate as usize, + to_rate as usize, + chunk_size, + 1, // sub-chunks + 1, // mono + )?; + + let mut output = Vec::new(); + let mut pos = 0; + + while pos + chunk_size <= samples.len() { + let chunk = &samples[pos..pos + chunk_size]; + let result = resampler.process(&[chunk], None)?; + output.extend_from_slice(&result[0]); + pos += chunk_size; + } + + // Handle remaining samples by padding with zeros + if pos < samples.len() { + let mut last_chunk = samples[pos..].to_vec(); + last_chunk.resize(chunk_size, 0.0); + let result = resampler.process(&[&last_chunk], None)?; + let remaining_ratio = (samples.len() - pos) as f32 / chunk_size as f32; + let take = (result[0].len() as f32 * remaining_ratio) as usize; + output.extend_from_slice(&result[0][..take]); + } + + Ok(output) +} + +/// Run the audio recorder in a loop, responding to commands. +/// This should be called from a dedicated thread. +pub fn run( + device_name: Option, + cmd_rx: mpsc::Receiver, + audio_tx: mpsc::Sender, +) { + let device = match get_input_device(device_name.as_deref()) { + Ok(d) => d, + Err(e) => { + error!("Failed to get audio input device: {e}"); + return; + } + }; + + let dev_name = device.name().unwrap_or_else(|_| "unknown".into()); + info!("Audio recorder using device: {dev_name}"); + + let config = match device.default_input_config() { + Ok(c) => c, + Err(e) => { + error!("Failed to get default input config: {e}"); + return; + } + }; + + let source_sample_rate = config.sample_rate().0; + let channels = config.channels(); + debug!("Input config: {source_sample_rate}Hz, {channels}ch, {:?}", config.sample_format()); + + loop { + // Wait for a Start command + match cmd_rx.recv() { + Ok(RecorderCommand::Start) => { + debug!("Recording started"); + } + Ok(RecorderCommand::Shutdown) => { + info!("Recorder shutting down"); + return; + } + Ok(RecorderCommand::Stop) => { + // Ignore stop when not recording + continue; + } + Err(_) => { + info!("Recorder channel closed, shutting down"); + return; + } + } + + // Record until Stop or Shutdown + let buffer: Arc>> = Arc::new(Mutex::new(Vec::new())); + let buffer_clone = Arc::clone(&buffer); + let ch = channels; + + let stream_config = StreamConfig { + channels, + sample_rate: SampleRate(source_sample_rate), + buffer_size: cpal::BufferSize::Default, + }; + + let stream = match config.sample_format() { + SampleFormat::F32 => device.build_input_stream( + &stream_config, + move |data: &[f32], _: &cpal::InputCallbackInfo| { + let mono = to_mono_f32(data, ch); + if let Ok(mut buf) = buffer_clone.lock() { + buf.extend_from_slice(&mono); + } + }, + |err| error!("Audio stream error: {err}"), + None, + ), + SampleFormat::I16 => { + let buffer_clone = Arc::clone(&buffer); + device.build_input_stream( + &stream_config, + move |data: &[i16], _: &cpal::InputCallbackInfo| { + let f32_data: Vec = data.iter().map(|&s| s as f32 / i16::MAX as f32).collect(); + let mono = to_mono_f32(&f32_data, ch); + if let Ok(mut buf) = buffer_clone.lock() { + buf.extend_from_slice(&mono); + } + }, + |err| error!("Audio stream error: {err}"), + None, + ) + } + format => { + error!("Unsupported sample format: {format:?}"); + continue; + } + }; + + let stream = match stream { + Ok(s) => s, + Err(e) => { + error!("Failed to build input stream: {e}"); + continue; + } + }; + + if let Err(e) = stream.play() { + error!("Failed to start audio stream: {e}"); + continue; + } + + // Wait for Stop or Shutdown + let should_shutdown = loop { + match cmd_rx.recv() { + Ok(RecorderCommand::Stop) => { + debug!("Recording stopped"); + break false; + } + Ok(RecorderCommand::Shutdown) => { + info!("Recorder shutting down during recording"); + break true; + } + Ok(RecorderCommand::Start) => { + // Ignore duplicate start + continue; + } + Err(_) => { + break true; + } + } + }; + + // Stop the stream + drop(stream); + + if should_shutdown { + return; + } + + // Get recorded audio + let raw_samples = { + let buf = buffer.lock().unwrap(); + buf.clone() + }; + + if raw_samples.is_empty() { + warn!("No audio recorded"); + continue; + } + + debug!("Recorded {} samples at {}Hz", raw_samples.len(), source_sample_rate); + + // Resample to 16kHz + let samples = match resample(&raw_samples, source_sample_rate, TARGET_SAMPLE_RATE) { + Ok(s) => s, + Err(e) => { + error!("Failed to resample audio: {e}"); + continue; + } + }; + + debug!("Resampled to {} samples at {}Hz", samples.len(), TARGET_SAMPLE_RATE); + + let audio = AudioData { + samples, + sample_rate: TARGET_SAMPLE_RATE, + }; + + if audio_tx.send(audio).is_err() { + error!("Failed to send audio data"); + return; + } + } +} diff --git a/src/transcriber.rs b/src/transcriber.rs new file mode 100644 index 0000000..6e7b6d8 --- /dev/null +++ b/src/transcriber.rs @@ -0,0 +1,424 @@ +use anyhow::{Context, Result}; +use ndarray::{Array2, Array3}; +use ort::session::{Session, SessionInputValue}; +use ort::value::Value; +use std::borrow::Cow; +use std::path::Path; +use tracing::{debug, info}; + +use crate::config::Accelerator; +use crate::model_cache::ModelPaths; + +// Audio preprocessing constants (NeMo-style) +const SAMPLE_RATE: usize = 16000; +const N_FFT: usize = 512; +const WIN_LENGTH: usize = 400; +const HOP_LENGTH: usize = 160; +const N_MELS: usize = 128; +const PRE_EMPHASIS: f32 = 0.97; + +/// The transcription engine. +pub struct Transcriber { + encoder: Session, + decoder: Session, + vocab: Vec, + blank_id: i64, + vocab_size: usize, +} + +fn make_input<'a, V: Into>(name: &'static str, value: V) -> (Cow<'a, str>, SessionInputValue<'a>) { + (Cow::Borrowed(name), SessionInputValue::Owned(value.into().into_dyn())) +} + +impl Transcriber { + /// Load the transcription model from the given paths. + pub fn new(paths: &ModelPaths, accelerator: &Accelerator, gpu_device: u32) -> Result { + info!("Loading transcription model..."); + + let encoder = build_session(&paths.encoder, accelerator, gpu_device) + .context("Failed to load encoder")?; + info!("Encoder loaded"); + + let decoder = build_session(&paths.decoder, accelerator, gpu_device) + .context("Failed to load decoder")?; + info!("Decoder loaded"); + + let vocab = load_vocab(&paths.vocab)?; + let vocab_size = vocab.len(); + let blank_id = (vocab_size - 1) as i64; // is the last token + info!("Vocab loaded: {vocab_size} tokens, blank_id={blank_id}"); + + Ok(Self { + encoder, + decoder, + vocab, + blank_id, + vocab_size, + }) + } + + /// Transcribe audio samples (mono f32 at 16kHz). + pub fn transcribe(&mut self, samples: &[f32]) -> Result { + if samples.is_empty() { + return Ok(String::new()); + } + + let duration = samples.len() as f32 / SAMPLE_RATE as f32; + info!("Transcribing {duration:.1}s of audio..."); + + // Step 1: Compute mel spectrogram + let features = compute_mel_spectrogram(samples); + let num_frames = features.ncols(); + debug!("Mel spectrogram: {N_MELS}x{num_frames} frames"); + + // Step 2: Run encoder + let (encoder_output, feat_dim, encoded_length) = self.run_encoder(&features)?; + debug!("Encoded: feat_dim={feat_dim}, length={encoded_length}"); + + // Step 3: TDT greedy decode + let tokens = self.tdt_greedy_decode(&encoder_output, feat_dim, encoded_length)?; + debug!("Decoded {} tokens", tokens.len()); + + // Step 4: Convert tokens to text + let text = self.tokens_to_text(&tokens); + info!("Transcription: \"{text}\""); + + Ok(text) + } + + fn run_encoder(&mut self, features: &Array2) -> Result<(Vec, usize, usize)> { + let num_frames = features.ncols(); + + // Shape: [batch=1, n_mels=128, time_frames] + let input = features + .clone() + .into_shape_with_order((1, N_MELS, num_frames))?; + + let length_data = ndarray::Array1::from_vec(vec![num_frames as i64]); + + let input_value = Value::from_array(input)?; + let length_value = Value::from_array(length_data)?; + + let outputs = self.encoder.run(vec![ + make_input("audio_signal", input_value.into_dyn()), + make_input("length", length_value.into_dyn()), + ])?; + + let (enc_shape, enc_data) = outputs[0] + .try_extract_tensor::() + .map_err(|e| anyhow::anyhow!("Failed to extract encoder output: {e}"))?; + + let (_, len_data) = outputs[1] + .try_extract_tensor::() + .map_err(|e| anyhow::anyhow!("Failed to extract encoded lengths: {e}"))?; + + let encoded_length = len_data[0] as usize; + // Shape: [1, feat_dim, encoded_time] + let dims: Vec = enc_shape.iter().map(|&d| d as usize).collect(); + let feat_dim = if dims.len() == 3 { dims[1] } else { dims[dims.len() - 1] }; + + debug!("Encoder output shape: {:?}", dims); + Ok((enc_data.to_vec(), feat_dim, encoded_length)) + } + + fn tdt_greedy_decode(&mut self, encoder_output: &[f32], feat_dim: usize, encoded_length: usize) -> Result> { + // Determine decoder LSTM state dimensions by inspecting input metadata + // Default fallback values + let mut state_shape: [usize; 3] = [1, 1, 640]; + + for input in self.decoder.inputs() { + let name = input.name(); + if name == "input_states_1" || name == "input_states_2" { + let dtype = input.dtype(); + // Try to extract shape from the ValueType + if let ort::value::ValueType::Tensor { shape, .. } = dtype { + if shape.len() == 3 { + // Use known dims (skip dynamic ones which are -1) + for (i, &dim) in shape.iter().enumerate() { + if dim > 0 { + state_shape[i] = dim as usize; + } + } + } + } + break; // both states have same shape + } + } + + debug!("Decoder state shape: {:?}", state_shape); + + let mut state1 = Array3::::zeros(state_shape); + let mut state2 = Array3::::zeros(state_shape); + let mut prev_token = self.blank_id; + let mut tokens = Vec::new(); + let mut t = 0; + + let max_steps = encoded_length * 10; + let mut step = 0; + + while t < encoded_length && step < max_steps { + step += 1; + + // Extract current encoder frame: shape [1, feat_dim, 1] + let mut frame_data = vec![0.0f32; feat_dim]; + for f in 0..feat_dim { + // encoder_output layout: [1, feat_dim, time] -> flat index + frame_data[f] = encoder_output[f * encoded_length + t]; + } + let frame = Array3::from_shape_vec([1, feat_dim, 1], frame_data)?; + + let targets = ndarray::Array2::from_shape_vec((1, 1), vec![prev_token])?; + let target_length = ndarray::Array1::from_vec(vec![1i64]); + + let outputs = self.decoder.run(vec![ + make_input("encoder_outputs", Value::from_array(frame)?.into_dyn()), + make_input("targets", Value::from_array(targets)?.into_dyn()), + make_input("target_length", Value::from_array(target_length)?.into_dyn()), + make_input("input_states_1", Value::from_array(state1.clone())?.into_dyn()), + make_input("input_states_2", Value::from_array(state2.clone())?.into_dyn()), + ])?; + + let (_, output_data) = outputs["outputs"] + .try_extract_tensor::() + .map_err(|e| anyhow::anyhow!("Failed to extract decoder output: {e}"))?; + + // Split into token logits and duration logits + let token_logits = &output_data[..self.vocab_size]; + let duration_logits = &output_data[self.vocab_size..]; + + let token_id = argmax(token_logits) as i64; + let duration = if !duration_logits.is_empty() { + argmax(duration_logits) + } else { + 1 + }; + + if token_id != self.blank_id { + tokens.push(token_id); + prev_token = token_id; + + // Update states + let (s1_shape, s1_data) = outputs["output_states_1"] + .try_extract_tensor::() + .map_err(|e| anyhow::anyhow!("Failed to extract state 1: {e}"))?; + let s1_dims: Vec = s1_shape.iter().map(|&d| d as usize).collect(); + if s1_dims.len() == 3 { + state1 = Array3::from_shape_vec([s1_dims[0], s1_dims[1], s1_dims[2]], s1_data.to_vec())?; + } + + let (s2_shape, s2_data) = outputs["output_states_2"] + .try_extract_tensor::() + .map_err(|e| anyhow::anyhow!("Failed to extract state 2: {e}"))?; + let s2_dims: Vec = s2_shape.iter().map(|&d| d as usize).collect(); + if s2_dims.len() == 3 { + state2 = Array3::from_shape_vec([s2_dims[0], s2_dims[1], s2_dims[2]], s2_data.to_vec())?; + } + } + + if duration > 0 { + t += duration; + } else if token_id == self.blank_id { + t += 1; + } + } + + Ok(tokens) + } + + fn tokens_to_text(&self, tokens: &[i64]) -> String { + let mut text = String::new(); + for &token_id in tokens { + if token_id >= 0 && (token_id as usize) < self.vocab.len() { + let token = &self.vocab[token_id as usize]; + if token.starts_with('<') && token.ends_with('>') { + continue; + } + text.push_str(token); + } + } + + text = text.replace('\u{2581}', " "); + let text = text.trim().to_string(); + + let mut result = String::with_capacity(text.len()); + let mut prev_space = false; + for ch in text.chars() { + if ch == ' ' { + if !prev_space { + result.push(ch); + } + prev_space = true; + } else { + result.push(ch); + prev_space = false; + } + } + + result + } +} + +fn build_session(model_path: &Path, accelerator: &Accelerator, _gpu_device: u32) -> Result { + let mut builder = Session::builder() + .map_err(|e| anyhow::anyhow!("Failed to create session builder: {e}"))? + .with_intra_threads(num_cpus::get().min(4)) + .map_err(|e| anyhow::anyhow!("Failed to set thread count: {e}"))?; + + match accelerator { + Accelerator::Auto | Accelerator::Cpu => {} + Accelerator::Cuda => { + info!("CUDA requested — falling back to CPU (CUDA EP not yet configured)"); + } + Accelerator::DirectMl => { + info!("DirectML requested — falling back to CPU (DirectML EP not yet configured)"); + } + } + + let session = builder + .commit_from_file(model_path) + .map_err(|e| anyhow::anyhow!("Failed to load ONNX model from {}: {e}", model_path.display()))?; + + Ok(session) +} + +fn load_vocab(path: &Path) -> Result> { + let content = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read vocab from {}", path.display()))?; + + let mut vocab = Vec::new(); + for line in content.lines() { + let token = line.split_whitespace().next().unwrap_or("").to_string(); + vocab.push(token); + } + + Ok(vocab) +} + +fn argmax(slice: &[f32]) -> usize { + slice + .iter() + .enumerate() + .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .map(|(i, _)| i) + .unwrap_or(0) +} + +// --- Mel spectrogram computation --- + +fn compute_mel_spectrogram(samples: &[f32]) -> Array2 { + let mut emphasized = Vec::with_capacity(samples.len()); + emphasized.push(samples[0]); + for i in 1..samples.len() { + emphasized.push(samples[i] - PRE_EMPHASIS * samples[i - 1]); + } + + let num_frames = (emphasized.len().saturating_sub(WIN_LENGTH)) / HOP_LENGTH + 1; + let fft_size = N_FFT / 2 + 1; + + let window: Vec = (0..WIN_LENGTH) + .map(|i| 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / (WIN_LENGTH - 1) as f32).cos())) + .collect(); + + let mel_bank = create_mel_filterbank(SAMPLE_RATE as f32, N_FFT, N_MELS); + + let mut mel_spec = Array2::::zeros((N_MELS, num_frames)); + + for frame_idx in 0..num_frames { + let start = frame_idx * HOP_LENGTH; + + let mut windowed = vec![0.0f32; N_FFT]; + for i in 0..WIN_LENGTH { + if start + i < emphasized.len() { + windowed[i] = emphasized[start + i] * window[i]; + } + } + + let power_spectrum = compute_power_spectrum(&windowed, fft_size); + + for mel_idx in 0..N_MELS { + let mut energy = 0.0f32; + for k in 0..fft_size { + energy += mel_bank[mel_idx][k] * power_spectrum[k]; + } + mel_spec[[mel_idx, frame_idx]] = (energy + 2.0f32.powi(-24)).ln(); + } + } + + // Per-utterance CMVN + for mel_idx in 0..N_MELS { + let row = mel_spec.row(mel_idx); + let mean = row.mean().unwrap_or(0.0); + let variance = row.iter().map(|&x| (x - mean).powi(2)).sum::() / num_frames as f32; + let std = (variance + 1e-5).sqrt(); + + for frame_idx in 0..num_frames { + mel_spec[[mel_idx, frame_idx]] = (mel_spec[[mel_idx, frame_idx]] - mean) / std; + } + } + + mel_spec +} + +fn compute_power_spectrum(signal: &[f32], output_size: usize) -> Vec { + let n = signal.len(); + let mut spectrum = Vec::with_capacity(output_size); + + for k in 0..output_size { + let mut real = 0.0f32; + let mut imag = 0.0f32; + for (t, &sample) in signal.iter().enumerate() { + let angle = -2.0 * std::f32::consts::PI * k as f32 * t as f32 / n as f32; + real += sample * angle.cos(); + imag += sample * angle.sin(); + } + spectrum.push(real * real + imag * imag); + } + + spectrum +} + +fn create_mel_filterbank(sample_rate: f32, n_fft: usize, n_mels: usize) -> Vec> { + let fft_size = n_fft / 2 + 1; + + let mel_low = hz_to_mel(0.0); + let mel_high = hz_to_mel(sample_rate / 2.0); + + let mel_points: Vec = (0..n_mels + 2) + .map(|i| mel_low + (mel_high - mel_low) * i as f32 / (n_mels + 1) as f32) + .collect(); + + let hz_points: Vec = mel_points.iter().map(|&m| mel_to_hz(m)).collect(); + let bin_points: Vec = hz_points + .iter() + .map(|&hz| ((n_fft as f32 + 1.0) * hz / sample_rate).floor() as usize) + .collect(); + + let mut filterbank = vec![vec![0.0f32; fft_size]; n_mels]; + + for m in 0..n_mels { + let left = bin_points[m]; + let center = bin_points[m + 1]; + let right = bin_points[m + 2]; + + for k in left..center { + if center > left { + filterbank[m][k] = (k - left) as f32 / (center - left) as f32; + } + } + for k in center..right { + if right > center { + filterbank[m][k] = (right - k) as f32 / (right - center) as f32; + } + } + } + + filterbank +} + +fn hz_to_mel(hz: f32) -> f32 { + 2595.0 * (1.0 + hz / 700.0).log10() +} + +fn mel_to_hz(mel: f32) -> f32 { + 700.0 * (10.0f32.powf(mel / 2595.0) - 1.0) +} diff --git a/src/vad.rs b/src/vad.rs new file mode 100644 index 0000000..ba8dce2 --- /dev/null +++ b/src/vad.rs @@ -0,0 +1,149 @@ +use anyhow::Result; +use ndarray::{Array1, Array2, Array3}; +use ort::session::{Session, SessionInputValue}; +use ort::value::Value; +use std::borrow::Cow; +use tracing::{debug, info}; + +const SILERO_SAMPLE_RATE: i64 = 16000; +const WINDOW_SIZE: usize = 512; // 32ms at 16kHz +const THRESHOLD: f32 = 0.5; +const MIN_SPEECH_DURATION_MS: usize = 250; +const MIN_SILENCE_DURATION_MS: usize = 300; +const SPEECH_PAD_MS: usize = 100; + +fn make_input<'a, V: Into>(name: &'static str, value: V) -> (Cow<'a, str>, SessionInputValue<'a>) { + (Cow::Borrowed(name), SessionInputValue::Owned(value.into().into_dyn())) +} + +/// Voice Activity Detector using Silero VAD v4. +pub struct Vad { + session: Session, +} + +impl Vad { + /// Create a new VAD instance from an ONNX model file. + pub fn new(model_path: &str) -> Result { + let session = Session::builder() + .map_err(|e| anyhow::anyhow!("Failed to create VAD session builder: {e}"))? + .with_intra_threads(1) + .map_err(|e| anyhow::anyhow!("Failed to set VAD threads: {e}"))? + .commit_from_file(model_path) + .map_err(|e| anyhow::anyhow!("Failed to load VAD model from {model_path}: {e}"))?; + + info!("Silero VAD loaded from {model_path}"); + Ok(Self { session }) + } + + /// Filter audio to keep only speech segments. + pub fn filter_speech(&mut self, samples: &[f32]) -> Result> { + if samples.is_empty() { + return Ok(Vec::new()); + } + + let probabilities = self.get_speech_probabilities(samples)?; + let segments = self.find_speech_segments(&probabilities, samples.len()); + + if segments.is_empty() { + debug!("VAD: no speech detected"); + return Ok(Vec::new()); + } + + let mut result = Vec::new(); + for (start, end) in &segments { + debug!("VAD: speech segment {start}..{end} ({:.1}s..{:.1}s)", + *start as f32 / SILERO_SAMPLE_RATE as f32, + *end as f32 / SILERO_SAMPLE_RATE as f32); + result.extend_from_slice(&samples[*start..*end]); + } + + let original_duration = samples.len() as f32 / SILERO_SAMPLE_RATE as f32; + let filtered_duration = result.len() as f32 / SILERO_SAMPLE_RATE as f32; + debug!("VAD: {original_duration:.1}s -> {filtered_duration:.1}s ({:.0}% kept)", + filtered_duration / original_duration * 100.0); + + Ok(result) + } + + fn get_speech_probabilities(&mut self, samples: &[f32]) -> Result> { + let mut probabilities = Vec::new(); + + let mut state = Array3::::zeros((2, 1, 128)); + let sr = Array1::from_vec(vec![SILERO_SAMPLE_RATE]); + + for chunk in samples.chunks(WINDOW_SIZE) { + let mut window = chunk.to_vec(); + if window.len() < WINDOW_SIZE { + window.resize(WINDOW_SIZE, 0.0); + } + + let input = Array2::from_shape_vec((1, WINDOW_SIZE), window)?; + + let outputs = self.session.run(vec![ + make_input("input", Value::from_array(input)?.into_dyn()), + make_input("state", Value::from_array(state.clone())?.into_dyn()), + make_input("sr", Value::from_array(sr.clone())?.into_dyn()), + ])?; + + let (_, output_data) = outputs["output"] + .try_extract_tensor::() + .map_err(|e| anyhow::anyhow!("Failed to extract VAD output: {e}"))?; + probabilities.push(output_data[0]); + + let (state_shape, state_data) = outputs["stateN"] + .try_extract_tensor::() + .map_err(|e| anyhow::anyhow!("Failed to extract VAD state: {e}"))?; + let dims: Vec = state_shape.iter().map(|&d| d as usize).collect(); + if dims.len() == 3 { + state = Array3::from_shape_vec([dims[0], dims[1], dims[2]], state_data.to_vec())?; + } + } + + Ok(probabilities) + } + + fn find_speech_segments(&self, probabilities: &[f32], total_samples: usize) -> Vec<(usize, usize)> { + let samples_per_window = WINDOW_SIZE; + let min_speech_windows = MIN_SPEECH_DURATION_MS * SILERO_SAMPLE_RATE as usize / 1000 / samples_per_window; + let min_silence_windows = MIN_SILENCE_DURATION_MS * SILERO_SAMPLE_RATE as usize / 1000 / samples_per_window; + let pad_samples = SPEECH_PAD_MS * SILERO_SAMPLE_RATE as usize / 1000; + + let mut segments = Vec::new(); + let mut in_speech = false; + let mut speech_start = 0; + let mut silence_count = 0; + let mut speech_count = 0; + + for (i, &prob) in probabilities.iter().enumerate() { + if prob >= THRESHOLD { + if !in_speech { + speech_start = i; + speech_count = 0; + } + in_speech = true; + speech_count += 1; + silence_count = 0; + } else if in_speech { + silence_count += 1; + if silence_count >= min_silence_windows { + if speech_count >= min_speech_windows { + let start = (speech_start * samples_per_window).saturating_sub(pad_samples); + let end = ((i - silence_count + 1) * samples_per_window + pad_samples).min(total_samples); + segments.push((start, end)); + } + in_speech = false; + silence_count = 0; + speech_count = 0; + } + } + } + + if in_speech && speech_count >= min_speech_windows { + let start = (speech_start * samples_per_window).saturating_sub(pad_samples); + let end = total_samples; + segments.push((start, end)); + } + + segments + } +}