diff options
| author | Malte Voos <git@mal.tc> | 2026-01-01 19:26:01 +0100 |
|---|---|---|
| committer | Malte Voos <git@mal.tc> | 2026-01-04 00:38:38 +0100 |
| commit | c8b942b1fbe8fdab1db0e0f56d3ed86a7486b578 (patch) | |
| tree | cf344838c96ad9bd7bd97d0216c43d6a858f4a60 | |
| parent | 80a1c8234fc5b6f56bd1f2df4e6118e57631f523 (diff) | |
| download | lleap-c8b942b1fbe8fdab1db0e0f56d3ed86a7486b578.tar.gz lleap-c8b942b1fbe8fdab1db0e0f56d3ed86a7486b578.zip | |
| -rw-r--r-- | Cargo.lock | 408 | ||||
| -rw-r--r-- | Cargo.toml | 4 | ||||
| -rw-r--r-- | src/app.rs | 91 | ||||
| -rw-r--r-- | src/open_dialog.rs | 4 | ||||
| -rw-r--r-- | src/player.rs | 5 | ||||
| -rw-r--r-- | src/subtitles/extraction/embedded.rs | 3 | ||||
| -rw-r--r-- | src/subtitles/extraction/mod.rs | 33 | ||||
| -rw-r--r-- | src/subtitles/extraction/whisper.rs | 3 | ||||
| -rw-r--r-- | src/subtitles/mod.rs | 7 | ||||
| -rw-r--r-- | src/translation/deepl.rs | 65 | ||||
| -rw-r--r-- | src/util/cache.rs | 24 | ||||
| -rw-r--r-- | src/util/mod.rs | 2 |
12 files changed, 580 insertions, 69 deletions
diff --git a/Cargo.lock b/Cargo.lock index 6a00a96..5353f66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,18 @@ 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", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -18,6 +30,12 @@ dependencies = [ ] [[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -101,6 +119,17 @@ dependencies = [ ] [[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -130,7 +159,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -144,6 +173,12 @@ dependencies = [ [[package]] name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" @@ -161,18 +196,64 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] +name = "cached" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c" +dependencies = [ + "ahash", + "async-trait", + "cached_proc_macro", + "cached_proc_macro_types", + "directories", + "futures", + "hashbrown 0.15.5", + "once_cell", + "rmp-serde", + "serde", + "sled", + "thiserror", + "tokio", + "web-time", +] + +[[package]] +name = "cached_proc_macro" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + +[[package]] name = "cairo-rs" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfe4354df4da648870e363387679081f8f9fc538ec8b55901e3740c6a0ef81b1" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cairo-sys-rs", "glib", "libc", @@ -252,7 +333,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" dependencies = [ - "bitflags", + "bitflags 2.10.0", "block", "cocoa-foundation", "core-foundation 0.10.1", @@ -268,7 +349,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" dependencies = [ - "bitflags", + "bitflags 2.10.0", "block", "core-foundation 0.10.1", "core-graphics-types", @@ -322,7 +403,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", @@ -335,7 +416,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation 0.10.1", "libc", ] @@ -350,12 +431,56 @@ dependencies = [ ] [[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.110", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.110", +] + +[[package]] name = "deepl" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -393,7 +518,7 @@ dependencies = [ "arrayvec", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 2.0.110", ] @@ -413,6 +538,27 @@ dependencies = [ ] [[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +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.60.2", +] + +[[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -516,7 +662,7 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d658424d233cbd993a972dd73a66ca733acd12a494c68995c9ac32ae1fe65b40" dependencies = [ - "bitflags", + "bitflags 2.10.0", "ffmpeg-sys-next", "libc", ] @@ -580,6 +726,12 @@ 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" @@ -637,6 +789,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" [[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -726,6 +888,15 @@ dependencies = [ ] [[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] name = "gdk-pixbuf" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -868,7 +1039,7 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b9dbecb1c33e483a98be4acfea2ab369e1c28f517c6eadb674537409c25c4b2" dependencies = [ - "bitflags", + "bitflags 2.10.0", "futures-channel", "futures-core", "futures-executor", @@ -1041,6 +1212,8 @@ dependencies = [ "option-operations", "pastey", "pin-project-lite", + "serde", + "serde_bytes", "smallvec", "thiserror", ] @@ -1259,6 +1432,17 @@ dependencies = [ [[package]] name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" @@ -1506,6 +1690,12 @@ dependencies = [ ] [[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1539,7 +1729,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", ] [[package]] @@ -1687,6 +1886,16 @@ dependencies = [ ] [[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + +[[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1704,8 +1913,10 @@ version = "0.1.0" dependencies = [ "anyhow", "async-channel", + "cached", "cocoa", "deepl", + "directories", "env_logger", "ffmpeg-next", "gsettings-macro", @@ -1870,6 +2081,7 @@ checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ "num-integer", "num-traits", + "serde", ] [[package]] @@ -1918,7 +2130,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -1957,6 +2169,12 @@ dependencies = [ ] [[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] name = "option-operations" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1996,6 +2214,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[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 0.9.12", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[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 = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2161,6 +2427,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +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.10.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.16", + "libredox", + "thiserror", +] + +[[package]] name = "regex" version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2315,6 +2610,25 @@ dependencies = [ ] [[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2335,7 +2649,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -2417,7 +2731,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -2451,6 +2765,15 @@ dependencies = [ ] [[package]] +name = "serde_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + +[[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2529,6 +2852,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + +[[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2572,6 +2911,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[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" @@ -2624,7 +2969,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2710,6 +3055,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot 0.12.5", "pin-project-lite", "socket2", "tokio-macros", @@ -2860,7 +3206,7 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -3143,6 +3489,32 @@ dependencies = [ ] [[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 = "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" @@ -3152,6 +3524,12 @@ dependencies = [ ] [[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-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index 5f7af0f..4cf357a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -gst = { version = "0.24.3", package = "gstreamer", features = ["v1_26"] } +gst = { version = "0.24.3", package = "gstreamer", features = ["v1_26", "serde"] } gst-video = { version = "0.24.3", package = "gstreamer-video", features = ["v1_26"] } gst-play = { version = "0.24.2", package = "gstreamer-play", features = ["v1_26"] } gst-plugin-gtk4 = { version = "0.14", features = ["gtk_v4_20"] } @@ -25,6 +25,8 @@ serde_json = "1.0.145" relm4-icons = "0.10" deepl = "0.7.3" gsettings-macro = "0.2.2" +cached = { version = "0.56.0", features = ["disk_store", "async"] } +directories = "6.0.0" [target.'cfg(target_os = "macos")'.dependencies] cocoa = "0.26" diff --git a/src/app.rs b/src/app.rs index 49efd49..35a501e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,5 @@ use adw::prelude::*; +use cached::{DiskCache, IOCached}; use relm4::{WorkerController, prelude::*}; use crate::{ @@ -13,13 +14,15 @@ use crate::{ subtitle_view::{SubtitleView, SubtitleViewMsg, SubtitleViewOutput}, subtitles::{ MetadataCollection, SUBTITLE_TRACKS, StreamIndex, SubtitleCue, SubtitleTrack, - TrackMetadata, - extraction::{SubtitleExtractor, SubtitleExtractorMsg, SubtitleExtractorOutput}, + SubtitleTrackCollection, TrackMetadata, + extraction::{ + ExtractionArgs, SubtitleExtractor, SubtitleExtractorMsg, SubtitleExtractorOutput, + }, state::SubtitleState, }, transcript::{Transcript, TranscriptMsg, TranscriptOutput}, - translation::{DeeplTranslator, deepl::DeeplTranslatorMsg}, - util::Tracker, + translation::{DeeplTranslator, TRANSLATIONS, deepl::DeeplTranslatorMsg}, + util::{self, Tracker}, }; pub struct App { @@ -34,6 +37,9 @@ pub struct App { open_url_dialog: Controller<OpenDialog>, subtitle_selection_dialog: Option<Controller<SubtitleSelectionDialog>>, + subtitle_extraction_args: Option<ExtractionArgs>, + subtitle_cache: DiskCache<ExtractionArgs, SubtitleTrackCollection>, + primary_subtitle_state: SubtitleState, secondary_subtitle_state: SubtitleState, @@ -153,6 +159,8 @@ impl SimpleComponent for App { }, ); + let subtitle_cache = util::make_cache("subtitles"); + let model = Self { root: root.clone(), player, @@ -165,6 +173,9 @@ impl SimpleComponent for App { open_url_dialog, subtitle_selection_dialog: None, + subtitle_extraction_args: None, + subtitle_cache, + primary_subtitle_state: SubtitleState::default(), secondary_subtitle_state: SubtitleState::default(), @@ -193,6 +204,14 @@ impl SimpleComponent for App { } AppMsg::SubtitleExtractionComplete => { log::info!("Subtitle extraction complete"); + if let Some(ref args) = self.subtitle_extraction_args { + if let Err(e) = self + .subtitle_cache + .cache_set(args.clone(), SUBTITLE_TRACKS.read().clone()) + { + log::error!("error caching extracted subtitles: {}", e); + } + } } AppMsg::ApplySubtitleSettings(settings) => { self.primary_subtitle_state @@ -249,6 +268,8 @@ impl SimpleComponent for App { mut metadata, whisper_stream_index, } => { + self.reset(); + if let Some(ix) = whisper_stream_index { let audio_metadata = metadata.audio.get(&ix).unwrap(); let subs_metadata = TrackMetadata { @@ -263,17 +284,33 @@ impl SimpleComponent for App { metadata.subtitles.insert(ix, subs_metadata); } - self.player - .sender() - .send(PlayerMsg::SetUrl(url.clone())) - .unwrap(); - self.extractor - .sender() - .send(SubtitleExtractorMsg::ExtractFromUrl { - url, - whisper_stream_index, - }) - .unwrap(); + let extraction_args = ExtractionArgs { + url: url.clone(), + whisper_stream_index, + }; + + match self.subtitle_cache.cache_get(&extraction_args) { + Ok(Some(track_collection)) => { + log::debug!("subtitle cache hit"); + *(SUBTITLE_TRACKS.write()) = track_collection; + } + Ok(None) => { + log::debug!("subtitle cache miss"); + self.extractor + .sender() + .send(SubtitleExtractorMsg::Extract(extraction_args.clone())) + .unwrap(); + } + Err(e) => { + log::error!("error querying subtitle cache: {}", e); + self.extractor + .sender() + .send(SubtitleExtractorMsg::Extract(extraction_args.clone())) + .unwrap(); + } + } + + self.subtitle_extraction_args = Some(extraction_args); let subtitle_selection_dialog = SubtitleSelectionDialog::builder() .launch((self.root.clone().into(), metadata)) @@ -283,12 +320,28 @@ impl SimpleComponent for App { } }); self.subtitle_selection_dialog = Some(subtitle_selection_dialog); + + self.player.sender().send(PlayerMsg::SetUrl(url)).unwrap(); } } } } impl App { + fn reset(&mut self) { + SUBTITLE_TRACKS.write().clear(); + TRANSLATIONS.write().clear(); + + self.subtitle_selection_dialog = None; + self.subtitle_extraction_args = None; + self.primary_subtitle_state = SubtitleState::default(); + self.secondary_subtitle_state = SubtitleState::default(); + self.autopaused = false; + self.hovering_primary_cue = false; + + // TODO also clear transcript? + } + fn update_subtitle_states(&mut self, position: gst::ClockTime) { self.update_primary_subtitle_state(position); self.update_secondary_subtitle_state(position); @@ -356,10 +409,10 @@ impl App { fn update_subtitle_state(state: &mut SubtitleState, position: gst::ClockTime) { if let Some(stream_ix) = state.stream_ix { let lock = SUBTITLE_TRACKS.read(); - let track = lock.get(&stream_ix).unwrap(); - - update_last_time_ix(&track.start_times, &mut state.last_started_cue_ix, position); - update_last_time_ix(&track.end_times, &mut state.last_ended_cue_ix, position); + if let Some(track) = lock.get(&stream_ix) { + update_last_time_ix(&track.start_times, &mut state.last_started_cue_ix, position); + update_last_time_ix(&track.end_times, &mut state.last_ended_cue_ix, position); + } } } diff --git a/src/open_dialog.rs b/src/open_dialog.rs index 3b822be..b84ff3b 100644 --- a/src/open_dialog.rs +++ b/src/open_dialog.rs @@ -68,6 +68,7 @@ impl Component for OpenDialog { set_child = &adw::NavigationView { add = &adw::NavigationPage { set_title: "Open File or Stream", + set_tag: Some("file_selection"), #[wrap(Some)] set_child = &adw::ToolbarView { @@ -304,6 +305,9 @@ impl OpenDialog { self.url.get_mut().clear(); self.do_whisper_extraction = false; self.whisper_stream_index = None; + if let Some(ref nav) = self.navigation_view { + nav.pop_to_tag("file_selection"); + } } fn fetch_metadata(&mut self, sender: ComponentSender<Self>) { diff --git a/src/player.rs b/src/player.rs index d533f48..0804fed 100644 --- a/src/player.rs +++ b/src/player.rs @@ -205,9 +205,12 @@ impl SimpleComponent for Player { } } PlayMessage::Buffering(_) => { + // println!("buffering") // TODO } - _ => {} + msg => { + // println!("msg: {:?}", msg); + } } glib::ControlFlow::Continue diff --git a/src/subtitles/extraction/embedded.rs b/src/subtitles/extraction/embedded.rs index 920f52b..39698cf 100644 --- a/src/subtitles/extraction/embedded.rs +++ b/src/subtitles/extraction/embedded.rs @@ -5,8 +5,7 @@ use anyhow::Context; use crate::{subtitles::SubtitleCue, subtitles::extraction::*}; pub fn extract_embedded_subtitles( - // stream index to use when storing extracted subtitles, this index already - // has to be in TRACKS when this function is called! + // stream index to use when storing extracted subtitles stream_ix: StreamIndex, context: ffmpeg::codec::Context, time_base: ffmpeg::Rational, diff --git a/src/subtitles/extraction/mod.rs b/src/subtitles/extraction/mod.rs index 5070fdb..6495b62 100644 --- a/src/subtitles/extraction/mod.rs +++ b/src/subtitles/extraction/mod.rs @@ -3,22 +3,30 @@ mod embedded; /// Synthesis of subtitles from audio using whisper.cpp mod whisper; -use std::{collections::BTreeMap, sync::mpsc, thread}; +use std::{collections::BTreeMap, fmt::Display, sync::mpsc, thread}; use ffmpeg::Rational; use relm4::{ComponentSender, Worker}; -use crate::subtitles::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue}; +use crate::subtitles::{StreamIndex, SubtitleCue}; pub struct SubtitleExtractor {} +#[derive(Debug, Clone)] +pub struct ExtractionArgs { + pub url: String, + pub whisper_stream_index: Option<usize>, +} + +impl Display for ExtractionArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {:?}", self.url, self.whisper_stream_index) + } +} + #[derive(Debug)] pub enum SubtitleExtractorMsg { - ExtractFromUrl { - url: String, - // the index of the audio stream on which to run a whisper transcription - whisper_stream_index: Option<usize>, - }, + Extract(ExtractionArgs), } #[derive(Debug)] @@ -38,10 +46,10 @@ impl Worker for SubtitleExtractor { fn update(&mut self, msg: SubtitleExtractorMsg, sender: ComponentSender<Self>) { match msg { - SubtitleExtractorMsg::ExtractFromUrl { + SubtitleExtractorMsg::Extract(ExtractionArgs { url, whisper_stream_index: whisper_audio_stream_ix, - } => { + }) => { self.handle_extract_from_url(url, whisper_audio_stream_ix, sender); } } @@ -55,12 +63,8 @@ impl SubtitleExtractor { whisper_audio_stream_ix: Option<usize>, sender: ComponentSender<Self>, ) { - // Clear existing tracks - SUBTITLE_TRACKS.write().clear(); - match self.extract_subtitles(&url, whisper_audio_stream_ix, sender.clone()) { Ok(_) => { - log::info!("Subtitle extraction completed successfully"); sender .output(SubtitleExtractorOutput::ExtractionComplete) .unwrap(); @@ -125,7 +129,8 @@ impl SubtitleExtractor { } // wait for extraction to complete - for (_, (_, join_handle)) in subtitle_extractors { + for (packet_tx, join_handle) in subtitle_extractors.into_values() { + drop(packet_tx); join_handle .join() .unwrap() diff --git a/src/subtitles/extraction/whisper.rs b/src/subtitles/extraction/whisper.rs index bd6fba7..be4346a 100644 --- a/src/subtitles/extraction/whisper.rs +++ b/src/subtitles/extraction/whisper.rs @@ -21,8 +21,7 @@ struct WhisperCue { } pub fn generate_whisper_subtitles( - // stream index to use when storing generated subtitles, this index - // already has to be in TRACKS when this function is called! + // stream index to use when storing generated subtitles stream_ix: StreamIndex, context: ffmpeg::codec::Context, time_base: ffmpeg::Rational, diff --git a/src/subtitles/mod.rs b/src/subtitles/mod.rs index acb73dc..de747f1 100644 --- a/src/subtitles/mod.rs +++ b/src/subtitles/mod.rs @@ -4,6 +4,7 @@ pub mod state; use std::collections::BTreeMap; use relm4::SharedState; +use serde::{Deserialize, Serialize}; pub type StreamIndex = usize; @@ -26,7 +27,7 @@ pub struct SubtitleCue { pub end_time: gst::ClockTime, } -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct SubtitleTrack { // SoA of cue text, start timestamp, end timestamp pub texts: Vec<String>, @@ -34,7 +35,9 @@ pub struct SubtitleTrack { pub end_times: Vec<gst::ClockTime>, } -pub static SUBTITLE_TRACKS: SharedState<BTreeMap<StreamIndex, SubtitleTrack>> = SharedState::new(); +pub type SubtitleTrackCollection = BTreeMap<StreamIndex, SubtitleTrack>; + +pub static SUBTITLE_TRACKS: SharedState<SubtitleTrackCollection> = SharedState::new(); impl TrackMetadata { pub fn from_ffmpeg_stream(stream: &ffmpeg::Stream) -> Self { diff --git a/src/translation/deepl.rs b/src/translation/deepl.rs index f2e84d7..11510d5 100644 --- a/src/translation/deepl.rs +++ b/src/translation/deepl.rs @@ -1,6 +1,9 @@ -use std::{collections::BTreeMap, time::Duration}; +use std::{collections::BTreeMap, fmt::Display, time::Duration}; +use crate::util; +use cached::proc_macro::io_cached; use deepl::DeepLApi; +use deepl::ModelType; use relm4::prelude::*; use crate::{ @@ -66,30 +69,28 @@ impl AsyncComponent for DeeplTranslator { impl DeeplTranslator { async fn do_translate(&mut self, sender: AsyncComponentSender<Self>) { if let Some(stream_ix) = self.stream_ix { - let deepl = DeepLApi::with(&Settings::default().deepl_api_key()).new(); - let next_cue_to_translate = self.next_cues_to_translate.entry(stream_ix).or_insert(0); - if let Some(cue) = { + while let Some(cue) = { SUBTITLE_TRACKS .read() .get(&stream_ix) - .unwrap() - .texts - .get(*next_cue_to_translate) + .and_then(|stream| stream.texts.get(*next_cue_to_translate)) .cloned() } { - match deepl - .translate_text(cue, deepl::Lang::EN) - .model_type(deepl::ModelType::PreferQualityOptimized) - .await + match translate_text(TranslateRequest { + input: cue.clone(), + source_lang: None, // TODO + target_lang: deepl::Lang::EN, // TODO + }) + .await { - Ok(mut resp) => { + Ok(translated) => { TRANSLATIONS .write() .entry(stream_ix) .or_insert(Vec::new()) - .push(resp.translations.pop().unwrap().text); + .push(translated); *next_cue_to_translate = *next_cue_to_translate + 1; } @@ -100,7 +101,45 @@ impl DeeplTranslator { } } + // check every second for new cues to be translated relm4::tokio::time::sleep(Duration::from_secs(1)).await; sender.input(DeeplTranslatorMsg::DoTranslate); } } + +#[derive(Clone)] +struct TranslateRequest { + input: String, + source_lang: Option<deepl::Lang>, + target_lang: deepl::Lang, +} + +impl Display for TranslateRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:?}-{}:{}", + self.source_lang, self.target_lang, self.input + ) + } +} + +#[io_cached( + disk = true, + create = r#"{ util::make_cache("deepl_translations") }"#, + map_error = r#"|err| err"# +)] +async fn translate_text(req: TranslateRequest) -> anyhow::Result<String> { + let deepl = DeepLApi::with(&Settings::default().deepl_api_key()).new(); + let mut requester = deepl.translate_text(req.input, req.target_lang); + requester.model_type(ModelType::PreferQualityOptimized); + if let Some(source_lang) = req.source_lang { + requester.source_lang(source_lang); + } + let translated = requester.await?.translations.pop().unwrap().text; + + // try to respect deepl's rate-limit + relm4::tokio::time::sleep(Duration::from_millis(500)).await; + + Ok(translated) +} diff --git a/src/util/cache.rs b/src/util/cache.rs new file mode 100644 index 0000000..6e4672f --- /dev/null +++ b/src/util/cache.rs @@ -0,0 +1,24 @@ +use std::env; + +use cached::DiskCache; +use directories::BaseDirs; +use serde::{Serialize, de::DeserializeOwned}; + +pub fn make_cache<K, V>(name: &str) -> DiskCache<K, V> +where + K: ToString, + V: Serialize + DeserializeOwned, +{ + let dir = match BaseDirs::new() { + Some(base_dirs) => base_dirs.cache_dir().join("lleap"), + None => env::current_dir() + .expect("unable to determine current directory") + .join("lleap_cache"), + }; + + DiskCache::new(name) + .set_disk_directory(dir) + .set_sync_to_disk_on_cache_change(true) + .build() + .expect("unable to open disk cache") +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 4d19eff..2098a35 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,3 +1,5 @@ +mod cache; mod tracker; +pub use cache::make_cache; pub use tracker::Tracker; |