feat: video renderer adapter w/ slides + charts + ffmpeg

This commit is contained in:
2026-06-02 22:31:45 +02:00
parent f00a2cbbb8
commit d45d8aa913
8 changed files with 820 additions and 8 deletions

518
Cargo.lock generated
View File

@@ -2,6 +2,22 @@
# 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 0.25.1",
]
[[package]]
name = "ab_glyph_rasterizer"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "activitypub"
version = "0.1.0"
@@ -246,6 +262,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "aligned"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
dependencies = [
"as-slice",
]
[[package]]
name = "aligned-vec"
version = "0.6.4"
@@ -319,12 +344,22 @@ dependencies = [
"futures",
"hex",
"rand 0.9.4",
"serde_json",
"sha2",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]]
name = "arbitrary"
version = "1.4.2"
@@ -363,6 +398,15 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "as-slice"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
dependencies = [
"stable_deref_trait",
]
[[package]]
name = "askama"
version = "0.16.0"
@@ -649,6 +693,26 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "av-scenechange"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
dependencies = [
"aligned",
"anyhow",
"arg_enum_proc_macro",
"arrayvec",
"log",
"num-rational",
"num-traits",
"pastey",
"rayon",
"thiserror 2.0.18",
"v_frame",
"y4m",
]
[[package]]
name = "av1-grain"
version = "0.2.5"
@@ -815,6 +879,12 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bit_field"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -836,6 +906,15 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
[[package]]
name = "bitstream-io"
version = "4.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f"
dependencies = [
"no_std_io2",
]
[[package]]
name = "blake2"
version = "0.10.6"
@@ -882,6 +961,12 @@ version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
[[package]]
name = "built"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9"
[[package]]
name = "bumpalo"
version = "3.20.3"
@@ -1057,6 +1142,12 @@ dependencies = [
"encoding_rs",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "combine"
version = "4.6.7"
@@ -1182,6 +1273,16 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
@@ -1233,6 +1334,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -1580,6 +1687,7 @@ dependencies = [
"chrono",
"email_address",
"futures",
"serde",
"thiserror 2.0.18",
"uuid",
]
@@ -1827,6 +1935,21 @@ dependencies = [
"uuid",
]
[[package]]
name = "exr"
version = "1.74.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
dependencies = [
"bit_field",
"half",
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
]
[[package]]
name = "fancy-regex"
version = "0.11.0"
@@ -1849,6 +1972,12 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "fax"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
[[package]]
name = "fdeflate"
version = "0.3.7"
@@ -2137,6 +2266,16 @@ dependencies = [
"wasip3",
]
[[package]]
name = "gif"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "glob"
version = "0.3.3"
@@ -2182,6 +2321,17 @@ dependencies = [
"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.14.5"
@@ -2576,6 +2726,20 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "image"
version = "0.24.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"jpeg-decoder",
"num-traits",
"png 0.17.16",
]
[[package]]
name = "image"
version = "0.25.10"
@@ -2584,10 +2748,18 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"color_quant",
"exr",
"gif",
"image-webp",
"moxcms",
"num-traits",
"png",
"png 0.18.1",
"qoi",
"ravif 0.13.0",
"rayon",
"rgb",
"tiff",
"zune-core",
"zune-jpeg",
]
@@ -2599,10 +2771,10 @@ dependencies = [
"anyhow",
"async-trait",
"domain",
"image",
"image 0.25.10",
"image-storage",
"object_store",
"ravif",
"ravif 0.11.20",
"tokio",
"tracing",
"uuid",
@@ -2633,6 +2805,24 @@ dependencies = [
"quick-error",
]
[[package]]
name = "imageproc"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "602b4e8a4cc3e98372b766cd184ab532999bc0e839b7469e759511ccabc65d77"
dependencies = [
"ab_glyph",
"approx",
"getrandom 0.2.17",
"image 0.25.10",
"itertools 0.12.1",
"nalgebra",
"num",
"rand 0.8.6",
"rand_distr",
"rayon",
]
[[package]]
name = "imgref"
version = "1.12.1"
@@ -2826,6 +3016,12 @@ dependencies = [
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
[[package]]
name = "js-sys"
version = "0.3.99"
@@ -2923,6 +3119,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lebe"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
name = "libc"
version = "0.2.186"
@@ -3109,6 +3311,16 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "matrixmultiply"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "maybe-rayon"
version = "0.1.1"
@@ -3116,6 +3328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
dependencies = [
"cfg-if",
"rayon",
]
[[package]]
@@ -3251,6 +3464,21 @@ dependencies = [
"version_check",
]
[[package]]
name = "nalgebra"
version = "0.32.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4"
dependencies = [
"approx",
"matrixmultiply",
"num-complex",
"num-rational",
"num-traits",
"simba",
"typenum",
]
[[package]]
name = "nats"
version = "0.1.0"
@@ -3302,6 +3530,15 @@ dependencies = [
"signatory",
]
[[package]]
name = "no_std_io2"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003"
dependencies = [
"memchr",
]
[[package]]
name = "nom"
version = "7.1.3"
@@ -3540,6 +3777,24 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "owned_ttf_parser"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05e6affeb1632d6ff6a23d2cd40ffed138e82f1532571a26f527c8a284bb2fbb"
dependencies = [
"ttf-parser 0.15.2",
]
[[package]]
name = "owned_ttf_parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
dependencies = [
"ttf-parser 0.25.1",
]
[[package]]
name = "parking"
version = "2.2.1"
@@ -3586,6 +3841,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pastey"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "pem"
version = "3.0.6"
@@ -3785,6 +4046,48 @@ dependencies = [
"serde_json",
]
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-bitmap",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-bitmap"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ce181e3f6bf82d6c1dc569103ca7b1bd964c60ba03d7e6cdfbb3e3eb7f7405"
dependencies = [
"image 0.24.9",
"plotters-backend",
]
[[package]]
name = "png"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "png"
version = "0.18.1"
@@ -4028,6 +4331,15 @@ version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]]
name = "quick-error"
version = "2.0.1"
@@ -4207,6 +4519,16 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_distr"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
dependencies = [
"num-traits",
"rand 0.8.6",
]
[[package]]
name = "ratatui"
version = "0.30.0"
@@ -4302,8 +4624,8 @@ dependencies = [
"arg_enum_proc_macro",
"arrayvec",
"av1-grain",
"bitstream-io",
"built",
"bitstream-io 2.6.0",
"built 0.7.7",
"cfg-if",
"interpolate_name",
"itertools 0.12.1",
@@ -4327,6 +4649,41 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "rav1e"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
dependencies = [
"aligned-vec",
"arbitrary",
"arg_enum_proc_macro",
"arrayvec",
"av-scenechange",
"av1-grain",
"bitstream-io 4.10.0",
"built 0.8.1",
"cfg-if",
"interpolate_name",
"itertools 0.14.0",
"libc",
"libfuzzer-sys",
"log",
"maybe-rayon",
"new_debug_unreachable",
"noop_proc_macro",
"num-derive",
"num-traits",
"paste",
"profiling",
"rand 0.9.4",
"rand_chacha 0.9.0",
"simd_helpers",
"thiserror 2.0.18",
"v_frame",
"wasm-bindgen",
]
[[package]]
name = "ravif"
version = "0.11.20"
@@ -4337,10 +4694,51 @@ dependencies = [
"imgref",
"loop9",
"quick-error",
"rav1e",
"rav1e 0.7.1",
"rgb",
]
[[package]]
name = "ravif"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
dependencies = [
"avif-serialize",
"imgref",
"loop9",
"quick-error",
"rav1e 0.8.1",
"rayon",
"rgb",
]
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -4703,6 +5101,16 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rusttype"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff8374aa04134254b7995b63ad3dc41c7f7236f69528b28553da7d72efaa967"
dependencies = [
"ab_glyph_rasterizer",
"owned_ttf_parser 0.15.2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -4715,6 +5123,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "safe_arch"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
dependencies = [
"bytemuck",
]
[[package]]
name = "same-file"
version = "1.0.6"
@@ -4979,6 +5396,19 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "simba"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae"
dependencies = [
"approx",
"num-complex",
"num-traits",
"paste",
"wide",
]
[[package]]
name = "simd-adler32"
version = "0.3.9"
@@ -5685,6 +6115,20 @@ 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 = "time"
version = "0.3.47"
@@ -6040,6 +6484,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "ttf-parser"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd"
[[package]]
name = "ttf-parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
name = "tui"
version = "0.1.0"
@@ -6511,7 +6967,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c071456adef4aca59bf6a583c46b90ff5eb0b4f758fc347cea81290288f37ce1"
dependencies = [
"image",
"image 0.25.10",
"libwebp-sys",
]
@@ -6542,6 +6998,12 @@ 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 = "wezterm-bidi"
version = "0.2.3"
@@ -6624,6 +7086,16 @@ dependencies = [
"wasite",
]
[[package]]
name = "wide"
version = "0.7.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
dependencies = [
"bytemuck",
"safe_arch",
]
[[package]]
name = "winapi"
version = "0.3.9"
@@ -6646,7 +7118,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -7106,12 +7578,33 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "wrapup-renderer"
version = "0.1.0"
dependencies = [
"async-trait",
"domain",
"image 0.25.10",
"imageproc",
"plotters",
"rusttype",
"tempfile",
"tokio",
"tracing",
]
[[package]]
name = "writeable"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "y4m"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
[[package]]
name = "yoke"
version = "0.8.2"
@@ -7360,6 +7853,15 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]
[[package]]
name = "zune-jpeg"
version = "0.5.15"

View File

@@ -31,6 +31,7 @@ members = [
"crates/adapters/plex",
"crates/adapters/sqlite-search",
"crates/adapters/postgres-search",
"crates/adapters/wrapup-renderer",
]
resolver = "2"
@@ -90,3 +91,4 @@ plex = { path = "crates/adapters/plex" }
image-converter = { path = "crates/adapters/image-converter" }
sqlite-search = { path = "crates/adapters/sqlite-search" }
postgres-search = { path = "crates/adapters/postgres-search" }
wrapup-renderer = { path = "crates/adapters/wrapup-renderer" }

View File

@@ -0,0 +1,15 @@
[package]
name = "wrapup-renderer"
version = "0.1.0"
edition = "2024"
[dependencies]
domain = { workspace = true }
async-trait = { workspace = true }
tracing = { workspace = true }
image = "0.25"
imageproc = "0.25"
rusttype = "0.9"
plotters = { version = "0.3", default-features = false, features = ["bitmap_backend", "bitmap_encoder"] }
tokio = { workspace = true, features = ["process"] }
tempfile = "3"

View File

@@ -0,0 +1,71 @@
use domain::errors::DomainError;
use domain::models::wrapup::WrapUpReport;
use plotters::prelude::*;
pub fn render_genre_chart(
report: &WrapUpReport,
width: u32,
height: u32,
) -> Result<Vec<u8>, DomainError> {
let mut buf = vec![0u8; (width * height * 3) as usize];
{
let root =
BitMapBackend::with_buffer(&mut buf, (width, height)).into_drawing_area();
root.fill(&RGBColor(26, 26, 36))
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let max_count = report
.top_genres
.iter()
.map(|g| g.count)
.max()
.unwrap_or(1);
let mut chart = ChartBuilder::on(&root)
.margin(40)
.x_label_area_size(60)
.y_label_area_size(60)
.build_cartesian_2d(
0u32..max_count + 1,
(0..report.top_genres.len() as i32).into_segmented(),
)
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
chart
.configure_mesh()
.disable_mesh()
.label_style(("sans-serif", 14, &WHITE))
.axis_style(&RGBColor(100, 100, 100))
.draw()
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
chart
.draw_series(report.top_genres.iter().enumerate().map(|(i, g)| {
let color = RGBColor(229, 192, 52);
Rectangle::new(
[
(0, SegmentValue::Exact(i as i32)),
(g.count, SegmentValue::Exact(i as i32 + 1)),
],
color.filled(),
)
}))
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
root.present()
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
}
// Convert raw RGB to PNG via image crate
let img = image::RgbImage::from_raw(width, height, buf)
.ok_or_else(|| DomainError::InfrastructureError("invalid image buffer".into()))?;
let rgba = image::DynamicImage::ImageRgb8(img).to_rgba8();
let mut png_buf = Vec::new();
rgba.write_to(
&mut std::io::Cursor::new(&mut png_buf),
image::ImageFormat::Png,
)
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
Ok(png_buf)
}

View File

@@ -0,0 +1,56 @@
use domain::errors::DomainError;
use domain::ports::VideoRenderConfig;
use tokio::process::Command;
pub async fn stitch_slides(
slides: &[Vec<u8>],
config: &VideoRenderConfig,
) -> Result<Vec<u8>, DomainError> {
let dir =
tempfile::tempdir().map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
// Write slide PNGs
for (i, png) in slides.iter().enumerate() {
let path = dir.path().join(format!("slide_{:04}.png", i));
std::fs::write(&path, png)
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
}
let output_path = dir.path().join("output.mp4");
// -framerate 1/N makes each image last N seconds
let framerate = format!("1/{}", config.slide_duration_secs);
let (w, h) = config.resolution;
let status = Command::new(&config.ffmpeg_path)
.args([
"-y",
"-framerate",
&framerate,
"-i",
&dir.path().join("slide_%04d.png").to_string_lossy(),
"-vf",
&format!("scale={}:{},format=yuv420p", w, h),
"-c:v",
"libx264",
"-preset",
"fast",
"-crf",
"23",
"-movflags",
"+faststart",
&output_path.to_string_lossy(),
])
.output()
.await
.map_err(|e| DomainError::InfrastructureError(format!("ffmpeg failed: {e}")))?;
if !status.status.success() {
let stderr = String::from_utf8_lossy(&status.stderr);
return Err(DomainError::InfrastructureError(format!(
"ffmpeg error: {stderr}"
)));
}
std::fs::read(&output_path).map_err(|e| DomainError::InfrastructureError(e.to_string()))
}

View File

@@ -0,0 +1,49 @@
mod slides;
mod charts;
mod ffmpeg;
use async_trait::async_trait;
use domain::errors::DomainError;
use domain::models::wrapup::WrapUpReport;
use domain::ports::{VideoRenderConfig, WrapUpVideoRenderer};
pub struct FfmpegWrapUpRenderer;
impl FfmpegWrapUpRenderer {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl WrapUpVideoRenderer for FfmpegWrapUpRenderer {
async fn render(
&self,
report: &WrapUpReport,
poster_images: Vec<(String, Vec<u8>)>,
config: &VideoRenderConfig,
) -> Result<Vec<u8>, DomainError> {
let (width, height) = config.resolution;
// 1. Generate slide images
let mut slide_pngs = Vec::new();
slide_pngs.push(slides::render_hero_slide(report, width, height)?);
slide_pngs.push(slides::render_ratings_slide(report, width, height)?);
if !report.top_directors.is_empty() {
slide_pngs.push(slides::render_directors_slide(report, width, height)?);
}
if !report.top_actors.is_empty() {
slide_pngs.push(slides::render_actors_slide(report, width, height)?);
}
if !report.top_genres.is_empty() {
slide_pngs.push(charts::render_genre_chart(report, width, height)?);
}
slide_pngs.push(slides::render_highlights_slide(report, width, height)?);
if !poster_images.is_empty() {
slide_pngs.push(slides::render_mosaic_slide(&poster_images, width, height)?);
}
// 2. Stitch into video
ffmpeg::stitch_slides(&slide_pngs, config).await
}
}

View File

@@ -0,0 +1,97 @@
use domain::errors::DomainError;
use domain::models::wrapup::WrapUpReport;
use image::{Rgba, RgbaImage};
const BG_COLOR: Rgba<u8> = Rgba([26, 26, 36, 255]); // dark blue-gray
const _PRIMARY: Rgba<u8> = Rgba([229, 192, 52, 255]); // gold #e5c034
const _TEXT_COLOR: Rgba<u8> = Rgba([255, 255, 255, 255]); // white
fn to_png(img: &RgbaImage) -> Result<Vec<u8>, DomainError> {
let mut buf = Vec::new();
img.write_to(
&mut std::io::Cursor::new(&mut buf),
image::ImageFormat::Png,
)
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
Ok(buf)
}
fn fill_background(width: u32, height: u32) -> RgbaImage {
RgbaImage::from_pixel(width, height, BG_COLOR)
}
pub fn render_hero_slide(
_report: &WrapUpReport,
width: u32,
height: u32,
) -> Result<Vec<u8>, DomainError> {
let img = fill_background(width, height);
// MVP: solid background. Text overlay added with font rendering later.
to_png(&img)
}
pub fn render_ratings_slide(
_report: &WrapUpReport,
width: u32,
height: u32,
) -> Result<Vec<u8>, DomainError> {
let img = fill_background(width, height);
to_png(&img)
}
pub fn render_directors_slide(
_report: &WrapUpReport,
width: u32,
height: u32,
) -> Result<Vec<u8>, DomainError> {
let img = fill_background(width, height);
to_png(&img)
}
pub fn render_actors_slide(
_report: &WrapUpReport,
width: u32,
height: u32,
) -> Result<Vec<u8>, DomainError> {
let img = fill_background(width, height);
to_png(&img)
}
pub fn render_highlights_slide(
_report: &WrapUpReport,
width: u32,
height: u32,
) -> Result<Vec<u8>, DomainError> {
let img = fill_background(width, height);
to_png(&img)
}
pub fn render_mosaic_slide(
posters: &[(String, Vec<u8>)],
width: u32,
height: u32,
) -> Result<Vec<u8>, DomainError> {
let mut canvas = fill_background(width, height);
let cols = 4u32;
let thumb_w = width / cols;
let thumb_h = (thumb_w * 3) / 2; // 2:3 poster ratio
for (i, (_, bytes)) in posters.iter().enumerate() {
let col = (i as u32) % cols;
let row = (i as u32) / cols;
let x = col * thumb_w;
let y = row * thumb_h;
if y + thumb_h > height {
break;
}
if let Ok(poster) = image::load_from_memory(bytes) {
let thumb =
poster.resize_exact(thumb_w, thumb_h, image::imageops::FilterType::Triangle);
image::imageops::overlay(&mut canvas, &thumb.to_rgba8(), x as i64, y as i64);
}
}
to_png(&canvas)
}

View File

@@ -5,6 +5,7 @@ use uuid::Uuid;
use crate::{
errors::DomainError,
events::{DomainEvent, EventEnvelope},
models::wrapup::WrapUpReport,
models::{
AnnotatedRow, DiaryEntry, DiaryFilter, EntityType, ExportFormat, ExternalPersonId,
FeedEntry, FieldMapping, FileFormat, ImportError, ImportProfile, ImportSession,
@@ -521,3 +522,22 @@ pub trait WrapUpStatsQuery: Send + Sync {
range: &DateRange,
) -> Result<Vec<WrapUpMovieRow>, DomainError>;
}
// ── Video renderer ──────────────────────────────────────────────────────────
pub struct VideoRenderConfig {
pub slide_duration_secs: u32,
pub transition_duration_secs: f32,
pub resolution: (u32, u32),
pub ffmpeg_path: String,
}
#[async_trait]
pub trait WrapUpVideoRenderer: Send + Sync {
async fn render(
&self,
report: &WrapUpReport,
poster_images: Vec<(String, Vec<u8>)>,
config: &VideoRenderConfig,
) -> Result<Vec<u8>, DomainError>;
}