From f26f1ab694b888869d5be4a405deeac3594de39c Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 30 Aug 2026 17:58:32 +0200 Subject: [PATCH] Withhold the empty groups that fill the People rail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `load_people` returned every live person, and on the reference library that was 14,268 rows of which 11,739 held no faces at all — 85 per cent of the rail naming nobody and able to do nothing. They are not a mystery. A regrouping pass creates a person per cluster; the next pass moves those faces elsewhere and leaves the person it emptied behind. `faces::prune_empty_unnamed` exists for exactly this and runs only at the end of a pass, so nothing clears what accumulates between them, and the Identity screen never prunes at all. Each one cost a `for_person` query and a built row on every reload. This withholds precisely the set the prune already treats as disposable — empty, unnamed, not set aside — and no more. Filtered rather than deleted: a screen is being drawn, not a catalog repaired. Nothing is lost, a sync cannot resurrect what was never removed, and the prune stays the one place that decides these can go. An empty group with a *name* still shows. That one is not debris but the symptom of a real failure — a named person whose faces were regrouped out from under them — and hiding it would take away the only way to merge them back. Co-Authored-By: Claude Opus 5 (1M context) --- docs/gestures.md | 4 +-- ui/dr-ui/src/identity.rs | 69 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/docs/gestures.md b/docs/gestures.md index 92df466..db05d25 100644 --- a/docs/gestures.md +++ b/docs/gestures.md @@ -34,7 +34,7 @@ A face is either the system's guess or the user's judgement, and the two are nev This is the point of having identified anybody. Without it the screen is a filing cabinet with no drawer handles. -`ui/dr-ui/ui/identity.slint:552` +`ui/dr-ui/ui/identity.slint:571` ### Change how faces are grouped @@ -43,7 +43,7 @@ This is the point of having identified anybody. Without it the screen is a filin The right match confidence is a property of your library, not of the model. "What would this do?" answers for this library without writing anything; names, confirmations and the groups you have set aside are kept whatever the dials say. -`ui/dr-ui/ui/identity.slint:589` +`ui/dr-ui/ui/identity.slint:608` ## Library grid diff --git a/ui/dr-ui/src/identity.rs b/ui/dr-ui/src/identity.rs index 29c4c25..a23107a 100644 --- a/ui/dr-ui/src/identity.rs +++ b/ui/dr-ui/src/identity.rs @@ -156,6 +156,25 @@ pub struct IdentityView { } /// Read the people rail. +/// +/// **A group holding no faces is not a group, and does not appear.** Only the +/// unnamed ones, which is exactly the set `faces::prune_empty_unnamed` treats +/// as debris: a pass creates a person per cluster, the next pass regroups those +/// faces elsewhere, and the person it emptied survives until something prunes +/// it. Nothing prunes on this path, so they accumulate — the reference library +/// reached 11,739 of them against 2,525 real people, and the rail was 85 per +/// cent rows that named nobody and could do nothing. +/// +/// Filtered rather than deleted, because this is a screen being drawn and not a +/// catalog being repaired. A row is withheld; nothing is lost, a sync cannot +/// resurrect what was never removed, and the prune stays the one place that +/// decides these are disposable. +/// +/// An empty group with a *name* still shows. That one is not debris, it is the +/// symptom of a real failure — a named person whose faces were regrouped out +/// from under them (see `Population::read` on why naming has to anchor) — and +/// the user cannot merge it back into the group that took them if the rail has +/// hidden it. pub fn load_people( catalog: &Catalog, model_id: &str, @@ -163,6 +182,9 @@ pub fn load_people( let conn = catalog.connection(); let people = faces::people(conn)? .into_iter() + .filter(|p| { + p.confirmed_faces + p.suggested_faces > 0 || !p.name.trim().is_empty() || p.ignored + }) .map(|p| PersonRow { id: p.id, name: p.name, @@ -812,6 +834,53 @@ mod tests { ); } + /// The rail withholds the debris a regrouping pass leaves behind, and only + /// that: the same rows `prune_empty_unnamed` treats as disposable. + /// + /// Not a nicety. Every pass creates a person per cluster and the next pass + /// may empty it, nothing on this path prunes, and the reference library + /// reached 11,739 of them against 2,525 real people — a rail that was 85 + /// per cent rows naming nobody, each one costing a query and a built row. + #[test] + fn the_rail_withholds_empty_unnamed_groups_and_nothing_else() { + let c = catalog(); + let i1 = image(&c, 1); + let held = + faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap(); + + let with_faces = faces::create_person(c.connection(), "Anna").unwrap(); + faces::confirm(c.connection(), held[0], with_faces).unwrap(); + + // The three that hold nothing. Only the first is debris. + faces::create_person(c.connection(), "").unwrap(); + let named = faces::create_person(c.connection(), "Bob").unwrap(); + let set_aside = faces::create_person(c.connection(), "").unwrap(); + faces::set_ignored(c.connection(), set_aside, true).unwrap(); + + let shown: Vec<_> = load_people(&c, "w600k_mbf") + .unwrap() + .people + .into_iter() + .map(|p| p.id) + .collect(); + + assert!(shown.contains(&with_faces), "a group with faces is a group"); + assert!( + shown.contains(&named), + "an empty group with a name is the visible symptom of a regroup that \ + emptied it, and hiding it takes away the only way to merge it back" + ); + assert!( + shown.contains(&set_aside), + "set aside is a judgement the user made, and the prune spares it too" + ); + assert_eq!( + shown.len(), + 3, + "the empty unnamed group is the one that goes" + ); + } + #[test] fn confirm_all_promotes_every_suggestion_and_nothing_else() { let c = catalog();