From 73692b0a0286d7410a1c09287e4ddb0f80a35d51 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 13 Apr 2026 16:37:52 +0200 Subject: [PATCH] feat: Add ffmpeg remuxing for faststart --- server/Cargo.lock | 535 +++++++++++++++++-------------- server/Cargo.toml | 4 +- server/src/lib.rs | 1 + server/src/transcribe/ffmpeg.rs | 70 ++++ server/src/transcribe/mod.rs | 1 + server/tests/fixtures/sample.m4a | Bin 0 -> 17581 bytes server/tests/transcribe_test.rs | 45 +++ 7 files changed, 420 insertions(+), 236 deletions(-) create mode 100644 server/src/transcribe/ffmpeg.rs create mode 100644 server/src/transcribe/mod.rs create mode 100644 server/tests/fixtures/sample.m4a create mode 100644 server/tests/transcribe_test.rs diff --git a/server/Cargo.lock b/server/Cargo.lock index d17c85f..99a9e6b 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -61,6 +61,16 @@ dependencies = [ "nom", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -226,6 +236,12 @@ 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 = "cipher" version = "0.4.4" @@ -247,32 +263,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -298,6 +288,24 @@ dependencies = [ "typenum", ] +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "deranged" version = "0.5.8" @@ -327,10 +335,11 @@ dependencies = [ "axum-extra", "bcrypt", "dotenvy", - "rand", + "rand 0.8.5", "reqwest", "serde", "serde_json", + "tempfile", "tokio", "toml", "tower", @@ -339,6 +348,7 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "uuid", + "wiremock", ] [[package]] @@ -396,21 +406,6 @@ 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", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -420,6 +415,21 @@ 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" @@ -427,6 +437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -435,6 +446,34 @@ 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" @@ -453,8 +492,13 @@ 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", ] @@ -476,8 +520,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", ] [[package]] @@ -488,7 +548,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -533,6 +593,12 @@ 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 = "http" version = "1.4.0" @@ -622,22 +688,7 @@ dependencies = [ "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", + "webpki-roots", ] [[package]] @@ -658,11 +709,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -880,6 +929,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -951,23 +1006,6 @@ dependencies = [ "version_check", ] -[[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 = "nom" version = "7.1.3" @@ -1002,56 +1040,22 @@ 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 = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -[[package]] -name = "openssl" -version = "0.10.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "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.113" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "parking_lot" version = "0.12.5" @@ -1087,12 +1091,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - [[package]] name = "potential_utf" version = "0.1.5" @@ -1136,6 +1134,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.3", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.45" @@ -1145,6 +1198,12 @@ 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" @@ -1158,8 +1217,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1169,7 +1238,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[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 0.9.5", ] [[package]] @@ -1181,6 +1260,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[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 = "redox_syscall" version = "0.5.18" @@ -1190,6 +1278,18 @@ dependencies = [ "bitflags", ] +[[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" @@ -1215,31 +1315,28 @@ 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", "mime_guess", - "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -1247,6 +1344,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", ] [[package]] @@ -1263,6 +1361,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustix" version = "1.1.4" @@ -1283,6 +1387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1295,6 +1400,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] @@ -1321,44 +1427,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[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 = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.28" @@ -1536,27 +1610,6 @@ dependencies = [ "syn", ] -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags", - "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", - "libc", -] - [[package]] name = "tempfile" version = "3.27.0" @@ -1640,6 +1693,21 @@ dependencies = [ "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" @@ -1668,16 +1736,6 @@ dependencies = [ "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" @@ -1935,12 +1993,6 @@ 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" @@ -2079,41 +2131,31 @@ dependencies = [ "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-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[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", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -2205,6 +2247,29 @@ dependencies = [ "memchr", ] +[[package]] +name = "wiremock" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" +dependencies = [ + "assert-json-diff", + "base64", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/server/Cargo.toml b/server/Cargo.toml index c85d4aa..553a3ea 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -6,7 +6,8 @@ edition = "2024" [dependencies] axum = { version = "0.8", features = ["multipart"] } tokio = { version = "1", features = ["full"] } -reqwest = { version = "0.12", features = ["json", "multipart"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls"] } +tempfile = "3" askama = "0.12" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } @@ -23,3 +24,4 @@ axum-extra = { version = "0.12", features = ["cookie"] } [dev-dependencies] tower = { version = "0.5", features = ["util"] } +wiremock = "0.6" diff --git a/server/src/lib.rs b/server/src/lib.rs index b155e3a..91a4032 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -3,6 +3,7 @@ pub mod config; pub mod error; pub mod models; pub mod routes; +pub mod transcribe; use std::sync::Arc; diff --git a/server/src/transcribe/ffmpeg.rs b/server/src/transcribe/ffmpeg.rs new file mode 100644 index 0000000..fe1d472 --- /dev/null +++ b/server/src/transcribe/ffmpeg.rs @@ -0,0 +1,70 @@ +use std::path::Path; +use std::process::Stdio; + +use tempfile::NamedTempFile; +use tokio::process::Command; +use tracing::debug; + +#[derive(Debug)] +pub enum FfmpegError { + Spawn(std::io::Error), + NonZeroExit { code: Option, stderr: String }, + TempFile(std::io::Error), +} + +impl std::fmt::Display for FfmpegError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Spawn(e) => write!(f, "failed to spawn ffmpeg: {e}"), + Self::NonZeroExit { code, stderr } => { + write!(f, "ffmpeg exited with {code:?}: {stderr}") + } + Self::TempFile(e) => write!(f, "failed to create temp file: {e}"), + } + } +} + +impl std::error::Error for FfmpegError {} + +/// Remux an m4a file with `-movflags faststart` so the `moov` atom sits at the +/// start of the file. Returns a NamedTempFile that is auto-deleted on drop. +/// +/// Lossless — just a container-level rewrite, typically <1s for dictation-sized files. +pub async fn remux_faststart(input: &Path) -> Result { + let output = tempfile::Builder::new() + .prefix("doctate-remux-") + .suffix(".m4a") + .tempfile() + .map_err(FfmpegError::TempFile)?; + + let output_path = output.path().to_owned(); + + let result = Command::new("ffmpeg") + .arg("-hide_banner") + .arg("-loglevel") + .arg("error") + .arg("-y") + .arg("-i") + .arg(input) + .arg("-c") + .arg("copy") + .arg("-movflags") + .arg("faststart") + .arg(&output_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .output() + .await + .map_err(FfmpegError::Spawn)?; + + if !result.status.success() { + return Err(FfmpegError::NonZeroExit { + code: result.status.code(), + stderr: String::from_utf8_lossy(&result.stderr).into_owned(), + }); + } + + debug!(?output_path, "ffmpeg remux completed"); + Ok(output) +} diff --git a/server/src/transcribe/mod.rs b/server/src/transcribe/mod.rs new file mode 100644 index 0000000..0620ccc --- /dev/null +++ b/server/src/transcribe/mod.rs @@ -0,0 +1 @@ +pub mod ffmpeg; diff --git a/server/tests/fixtures/sample.m4a b/server/tests/fixtures/sample.m4a new file mode 100644 index 0000000000000000000000000000000000000000..f1deb50339df481bfd7d96bcf533eac79fcb4aa8 GIT binary patch literal 17581 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 literal 0 HcmV?d00001 diff --git a/server/tests/transcribe_test.rs b/server/tests/transcribe_test.rs new file mode 100644 index 0000000..feac4d8 --- /dev/null +++ b/server/tests/transcribe_test.rs @@ -0,0 +1,45 @@ +use std::path::PathBuf; + +use doctate_server::transcribe::ffmpeg::remux_faststart; + +fn fixture(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures") + .join(name) +} + +#[tokio::test] +async fn ffmpeg_remux_produces_valid_output() { + let input = fixture("sample.m4a"); + assert!(input.exists(), "fixture missing: {}", input.display()); + + let output = remux_faststart(&input) + .await + .expect("remux failed"); + + let meta = std::fs::metadata(output.path()).expect("output missing"); + assert!(meta.len() > 0, "output file is empty"); + + // Sanity check: ffprobe should still recognize it as an m4a/mp4 container. + let probe = std::process::Command::new("ffprobe") + .arg("-hide_banner") + .arg("-loglevel") + .arg("error") + .arg("-show_entries") + .arg("format=format_name") + .arg("-of") + .arg("default=nw=1:nk=1") + .arg(output.path()) + .output() + .expect("ffprobe spawn failed"); + assert!(probe.status.success(), "ffprobe failed"); + let fmt = String::from_utf8_lossy(&probe.stdout); + assert!(fmt.contains("mp4") || fmt.contains("m4a"), "unexpected format: {fmt}"); +} + +#[tokio::test] +async fn ffmpeg_remux_fails_on_missing_input() { + let input = PathBuf::from("/nonexistent/does-not-exist.m4a"); + let result = remux_faststart(&input).await; + assert!(result.is_err(), "expected error for missing input"); +}