Show the confidence, and say which curve it came from

FR-CULL-9 was read as "no fit, no number", so every library without 200
confirmed positive pairs showed "Confidence unavailable" on every
suggestion — which is every library, until enough confirmations exist to
fit one. The confirmations are made on this screen, ranked by the number
it was withholding, so the degraded state was also the permanent one.

There has always been a curve: Calibration::default is the reference
implementation's fitted MBF sigmoid, which is what clustering already
operates at. It is a published operating point, not an invention, and
what the requirement forbids is presenting it *as though it were
measured on this library*. So the percentage is shown, and the screen
says once, above the grid, where the curve came from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-29 09:57:38 +02:00
co-authored by Claude Opus 5
parent 2fb3eb5d2d
commit c8e05831f4
7 changed files with 76 additions and 60 deletions
+24 -26
View File
@@ -88,25 +88,29 @@ pub struct FaceCell {
/// The user asserted this, as against the system guessing it.
pub confirmed: bool,
/// Calibrated P(this face is this person).
///
/// Always meaningful, and always shown. Where the library has no fit of
/// its own the number comes from the reference curve — a published
/// operating point, not an invention — and the screen says so once, at the
/// top, rather than blanking every face (FR-CULL-9).
pub probability: f32,
/// Whether that probability means anything — false when the library's
/// calibration has not been fitted (FR-CULL-9).
pub probability_known: bool,
/// Source pixels across the aligned crop. Small faces embed worse, and the
/// user deserves to know which of a bad suggestion's causes is in play.
pub crop_px: f32,
}
impl FaceCell {
/// Confidence as text, or why there is none.
/// Confidence as text.
///
/// The FR-CULL-9 rule made concrete: where the calibration is not fitted,
/// this says so rather than printing an untuned number that looks measured.
/// FR-CULL-9's rule is about *provenance*, not about silence: the number
/// must not be passed off as measured when it is not. Withholding it
/// entirely was the wrong reading — it left the user ranking forty
/// suggestions with nothing to rank them by, on every library that has not
/// yet earned a fit, which is most of them. The number is shown; where the
/// curve is the built-in one the screen says so above the grid.
pub fn confidence_label(&self) -> String {
if self.confirmed {
"Confirmed".into()
} else if !self.probability_known {
"Confidence unavailable".into()
} else {
format!("{:.0}% likely", self.probability * 100.0)
}
@@ -122,7 +126,9 @@ pub struct IdentityView {
/// Faces belonging to nobody — the pool the user can pull a new person out
/// of, and the honest answer to "why is this photo not under anyone".
pub unassigned: usize,
/// Whether the library's similarity calibration has been fitted.
/// Whether the library's similarity calibration has been fitted from its
/// own faces, as against the built-in reference curve. Not a gate on
/// showing confidences — only on how the screen describes them.
pub calibrated: bool,
}
@@ -166,7 +172,6 @@ pub fn load_faces(
catalog: &Catalog,
store: &ThumbStore,
person: PersonId,
calibrated: bool,
) -> Result<Vec<FaceCell>, dr_catalog::CatalogError> {
let conn = catalog.connection();
let rows = faces::for_person(conn, person, true)?;
@@ -203,7 +208,6 @@ pub fn load_faces(
crop,
confirmed: f.confirmed,
probability: f.probability,
probability_known: calibrated,
crop_px: f.crop_px,
});
}
@@ -557,9 +561,8 @@ pub fn preview_split(
let cal = faces::calibration(conn, model_id)?
.map(|(c, _)| c)
.unwrap_or_default();
let calibrated = cal.valid;
let cells = load_faces(catalog, store, person, calibrated)?;
let cells = load_faces(catalog, store, person)?;
if cells.len() < 2 {
return Ok(vec![cells]);
}
@@ -715,30 +718,25 @@ mod tests {
assert!(!row.is_unconfirmed());
}
/// FR-CULL-9's rule at the point it becomes visible: an unfitted
/// calibration must not print a number that looks measured.
/// FR-CULL-9 at the point it becomes visible. The rule is that an unfitted
/// curve must not be *described* as measured — not that the number is
/// withheld, which left the user with forty unrankable suggestions on every
/// library too young to have earned a fit. The percentage is always shown;
/// the provenance is said once, at the screen level.
#[test]
fn an_uncalibrated_library_says_so_instead_of_showing_a_percentage() {
fn every_suggestion_shows_a_percentage_whatever_the_curve_was_fitted_from() {
let cell = FaceCell {
face: FaceId(1),
image: ImageId(1),
crop: None,
confirmed: false,
probability: 0.87,
probability_known: false,
crop_px: 120.0,
};
assert_eq!(cell.confidence_label(), "Confidence unavailable");
let calibrated = FaceCell {
probability_known: true,
..cell.clone()
};
assert_eq!(calibrated.confidence_label(), "87% likely");
assert_eq!(cell.confidence_label(), "87% likely");
let confirmed = FaceCell {
confirmed: true,
probability_known: false,
..cell
};
assert_eq!(confirmed.confidence_label(), "Confirmed");
@@ -762,7 +760,7 @@ mod tests {
assert_eq!(view.unassigned, 1);
assert!(
!view.calibrated,
"a fresh library has no fitted calibration"
"a fresh library has no fit of its own, and says so — it still shows confidences"
);
}
+4 -5
View File
@@ -189,11 +189,10 @@ pub fn refresh(
match (ctl.selected.get(), store) {
(Some(person), Some(store)) => {
let cells =
identity::load_faces(cat, store, person, view.calibrated).unwrap_or_else(|e| {
log::warn!("identity: reading faces: {e}");
Vec::new()
});
let cells = identity::load_faces(cat, store, person).unwrap_or_else(|e| {
log::warn!("identity: reading faces: {e}");
Vec::new()
});
push_faces(window, ctl, &cells);
*ctl.faces.borrow_mut() = cells;