Withhold the empty groups that fill the People rail

`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) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 18:30:16 +02:00
co-authored by Claude Opus 5
parent e8b072c815
commit f26f1ab694
2 changed files with 71 additions and 2 deletions
+2 -2
View File
@@ -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. This is the point of having identified anybody. Without it the screen is a filing cabinet with no drawer handles.
<sub>`ui/dr-ui/ui/identity.slint:552`</sub> <sub>`ui/dr-ui/ui/identity.slint:571`</sub>
### Change how faces are grouped ### 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. 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.
<sub>`ui/dr-ui/ui/identity.slint:589`</sub> <sub>`ui/dr-ui/ui/identity.slint:608`</sub>
## Library grid ## Library grid
+69
View File
@@ -156,6 +156,25 @@ pub struct IdentityView {
} }
/// Read the people rail. /// 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( pub fn load_people(
catalog: &Catalog, catalog: &Catalog,
model_id: &str, model_id: &str,
@@ -163,6 +182,9 @@ pub fn load_people(
let conn = catalog.connection(); let conn = catalog.connection();
let people = faces::people(conn)? let people = faces::people(conn)?
.into_iter() .into_iter()
.filter(|p| {
p.confirmed_faces + p.suggested_faces > 0 || !p.name.trim().is_empty() || p.ignored
})
.map(|p| PersonRow { .map(|p| PersonRow {
id: p.id, id: p.id,
name: p.name, 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] #[test]
fn confirm_all_promotes_every_suggestion_and_nothing_else() { fn confirm_all_promotes_every_suggestion_and_nothing_else() {
let c = catalog(); let c = catalog();