From 0d2c77c258bc4a74289c0004f4a847ff9ff5edf5 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:38:33 +0200 Subject: [PATCH] Fix Assisted Culling bursts that cross midnight at month end (#2095) Found while testing crates/engine/src/cmd/cull.rs:17-20 (secs, used by the burst grouping at cull.rs:45) Co-authored-by: Claude Haiku 5.5 --- crates/engine/src/cmd/cull.rs | 2 +- crates/engine/src/tests_import.rs | 44 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/cull.rs b/crates/engine/src/cmd/cull.rs index 676519d29..030cb6e93 100644 --- a/crates/engine/src/cmd/cull.rs +++ b/crates/engine/src/cmd/cull.rs @@ -16,7 +16,7 @@ const SAME: f32 = 0.93; fn secs(iso: &str) -> Option { let d = lightcraft_meta::DateTime::parse_iso(iso)?; - Some(((d.year as i64 * 372 + d.month as i64 * 31 + d.day as i64) * 24 + d.hour as i64) * 3600 + d.minute as i64 * 60 + d.second as i64) + Some(d.unix_seconds()) } fn analyze(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/tests_import.rs b/crates/engine/src/tests_import.rs index 3d6469deb..13f02ce77 100644 --- a/crates/engine/src/tests_import.rs +++ b/crates/engine/src/tests_import.rs @@ -778,6 +778,50 @@ fn assisted_culling_groups_bursts_and_scores_focus() { let _ = std::fs::remove_dir_all(&dir); } +/// Assisted culling: two look-alike shots one second apart across midnight form a burst at the end of +/// every month, including those with fewer than 31 days. +#[test] +fn assisted_culling_groups_bursts_across_month_end() { + let dir = temp_dir("cull-month-end"); + for (name, (w, h)) in [("a.png", (480, 320)), ("b.png", (482, 321))] { + let img = lightcraft_scenes::demo_library()[0].render(w, h); + let data: Vec<[u8; 4]> = img + .data + .iter() + .map(|c| { + let e = |v: f32| (lightcraft_color::transfer::linear_to_srgb(v.clamp(0.0, 1.0)) * 255.0).round() as u8; + [e(c[0]), e(c[1]), e(c[2]), 255] + }) + .collect(); + let png = lightcraft_codecs::encode_png( + &lightcraft_codecs::EncodeImage::rgba8(&lightcraft_raster::Rgba8 { width: img.width, height: img.height, data }), + &Default::default(), + ) + .unwrap(); + std::fs::write(dir.join(name), png).unwrap(); + } + let mut s = Session::new().with_fs(); + let r = s.execute("library.import", &json!({"paths": [dir.to_string_lossy()]})).unwrap(); + let ids: Vec = r["imported"].as_array().unwrap().iter().filter_map(Value::as_u64).collect(); + let by_name = |s: &Session, n: &str| s.catalog.photos().find(|p| p.file_name == n).unwrap().id; + let (a, b) = (by_name(&s, "a.png"), by_name(&s, "b.png")); + for (before, after) in [ + ("2026-01-31T23:59:59", "2026-02-01T00:00:00"), + ("2026-02-28T23:59:59", "2026-03-01T00:00:00"), + ("2026-04-30T23:59:59", "2026-05-01T00:00:00"), + ("2026-06-30T23:59:59", "2026-07-01T00:00:00"), + ("2026-09-30T23:59:59", "2026-10-01T00:00:00"), + ("2026-11-30T23:59:59", "2026-12-01T00:00:00"), + ("2026-12-31T23:59:59", "2027-01-01T00:00:00"), + ] { + s.commit("t", lightcraft_catalog::Op::SetCaptured { id: a, captured: Some(before.into()) }).unwrap(); + s.commit("t", lightcraft_catalog::Op::SetCaptured { id: b, captured: Some(after.into()) }).unwrap(); + let r = s.execute("photo.analyze", &json!({"ids": ids})).unwrap(); + assert_eq!(r["groups"], 1, "shots 1 s apart ({before} / {after}) must form a burst: {r}"); + } + let _ = std::fs::remove_dir_all(&dir); +} + /// Find Dust Spots: soft dark spots on a smooth sky are found and healed (one undo step). #[test] fn dust_spots_are_found_and_healed() {