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();