From 86639853d223bfc6d3658c83d88369362642afa6 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 3 Jun 2026 00:19:18 +0200 Subject: [PATCH] fix: render genres via SlideRenderer, enable AVIF decoding, add poster fetch logging --- crates/adapters/wrapup-renderer/Cargo.toml | 3 +- crates/adapters/wrapup-renderer/src/charts.rs | 62 ------------------- crates/adapters/wrapup-renderer/src/lib.rs | 5 +- crates/adapters/wrapup-renderer/src/slides.rs | 52 ++++++++++++++++ .../src/wrapup/handle_requested.rs | 6 +- 5 files changed, 60 insertions(+), 68 deletions(-) delete mode 100644 crates/adapters/wrapup-renderer/src/charts.rs diff --git a/crates/adapters/wrapup-renderer/Cargo.toml b/crates/adapters/wrapup-renderer/Cargo.toml index f746119..278b1bd 100644 --- a/crates/adapters/wrapup-renderer/Cargo.toml +++ b/crates/adapters/wrapup-renderer/Cargo.toml @@ -7,9 +7,8 @@ edition = "2024" domain = { workspace = true } async-trait = { workspace = true } tracing = { workspace = true } -image = "0.25" +image = { version = "0.25", features = ["avif"] } imageproc = "0.25" ab_glyph = "0.2" -plotters = { version = "0.3", default-features = false, features = ["bitmap_backend", "bitmap_encoder", "ab_glyph"] } tokio = { workspace = true, features = ["process"] } tempfile = "3" diff --git a/crates/adapters/wrapup-renderer/src/charts.rs b/crates/adapters/wrapup-renderer/src/charts.rs deleted file mode 100644 index b6a00c5..0000000 --- a/crates/adapters/wrapup-renderer/src/charts.rs +++ /dev/null @@ -1,62 +0,0 @@ -use domain::errors::DomainError; -use domain::models::wrapup::WrapUpReport; -use plotters::prelude::*; - -pub fn render_genre_chart( - report: &WrapUpReport, - width: u32, - height: u32, -) -> Result, 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) - .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() - .disable_x_axis() - .disable_y_axis() - .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()))?; - } - - 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) -} diff --git a/crates/adapters/wrapup-renderer/src/lib.rs b/crates/adapters/wrapup-renderer/src/lib.rs index daadc61..412403c 100644 --- a/crates/adapters/wrapup-renderer/src/lib.rs +++ b/crates/adapters/wrapup-renderer/src/lib.rs @@ -1,4 +1,3 @@ -mod charts; mod ffmpeg; mod slides; @@ -40,11 +39,13 @@ impl WrapUpVideoRenderer for FfmpegWrapUpRenderer { slide_pngs.push(renderer.render_actors(report, width, height)?); } if !report.top_genres.is_empty() { - slide_pngs.push(charts::render_genre_chart(report, width, height)?); + slide_pngs.push(renderer.render_genres(report, width, height)?); } slide_pngs.push(renderer.render_highlights(report, width, height)?); if !poster_images.is_empty() { slide_pngs.push(renderer.render_mosaic(&poster_images, width, height)?); + } else { + tracing::warn!("no poster images resolved, skipping mosaic slide"); } // 2. Stitch into video diff --git a/crates/adapters/wrapup-renderer/src/slides.rs b/crates/adapters/wrapup-renderer/src/slides.rs index e577ee9..64ad281 100644 --- a/crates/adapters/wrapup-renderer/src/slides.rs +++ b/crates/adapters/wrapup-renderer/src/slides.rs @@ -237,6 +237,58 @@ impl SlideRenderer { to_png(&img) } + pub fn render_genres( + &self, + report: &WrapUpReport, + w: u32, + h: u32, + ) -> Result, DomainError> { + let mut img = fill(w, h); + self.draw_centered(&mut img, "Genre Breakdown", (h / 8) as i32, 56.0, GOLD); + + let detail = format!("{} genres explored", report.genre_diversity); + self.draw_centered(&mut img, &detail, (h / 8) as i32 + 64, 28.0, DIM); + + let margin = 80i32; + let bar_area_w = (w as i32 - margin * 2 - 200) as u32; + let start_y = (h / 4) as i32; + let max_count = report.top_genres.first().map(|g| g.count).unwrap_or(1).max(1); + + for (i, g) in report.top_genres.iter().take(8).enumerate() { + let y = start_y + (i as i32) * 80; + self.draw_left(&mut img, &g.genre, margin, y, 30.0, WHITE); + let count_str = format!("{}", g.count); + self.draw_left(&mut img, &count_str, w as i32 - margin - 40, y, 30.0, DIM); + + let bar_y = y + 38; + let bar_w = (g.count as f64 / max_count as f64 * bar_area_w as f64) as u32; + draw_filled_rect_mut( + &mut img, + Rect::at(margin, bar_y).of_size(bar_area_w, 12), + BAR_BG, + ); + if bar_w > 0 { + draw_filled_rect_mut( + &mut img, + Rect::at(margin, bar_y).of_size(bar_w, 12), + GOLD, + ); + } + } + + if let Some(ref best) = report.highest_rated_genre { + let text = format!("Highest rated: {best}"); + self.draw_centered(&mut img, &text, h as i32 - 180, 28.0, WHITE); + } + if let Some(ref worst) = report.lowest_rated_genre { + let text = format!("Lowest rated: {worst}"); + self.draw_centered(&mut img, &text, h as i32 - 140, 28.0, DIM); + } + + self.stamp_logo(&mut img); + to_png(&img) + } + pub fn render_highlights( &self, report: &WrapUpReport, diff --git a/crates/application/src/wrapup/handle_requested.rs b/crates/application/src/wrapup/handle_requested.rs index 8106bee..34e09d3 100644 --- a/crates/application/src/wrapup/handle_requested.rs +++ b/crates/application/src/wrapup/handle_requested.rs @@ -88,9 +88,11 @@ pub async fn execute( async fn resolve_poster_images(ctx: &AppContext, report: &WrapUpReport) -> Vec<(String, Vec)> { let mut images = Vec::new(); for path in report.poster_paths.iter().take(20) { - if let Ok(bytes) = ctx.services.image_storage.get(path).await { - images.push((path.clone(), bytes)); + match ctx.services.image_storage.get(path).await { + Ok(bytes) => images.push((path.clone(), bytes)), + Err(e) => tracing::debug!("poster fetch skipped for {path}: {e}"), } } + tracing::info!("resolved {}/{} poster images for video", images.len(), report.poster_paths.len()); images }