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:
+24
-26
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -42,10 +42,9 @@ export struct IdentityFace {
|
||||
crop: image,
|
||||
has-crop: bool,
|
||||
confirmed: bool,
|
||||
// "Confirmed", "83% likely", or "Confidence unavailable" — composed in
|
||||
// Rust, because FR-CULL-9's rule about not showing an untuned number as
|
||||
// though it were measured is a rule about *content*, and it should not be
|
||||
// re-derived from a float in a second place.
|
||||
// "Confirmed" or "83% likely" — composed in Rust, because FR-CULL-9's rule
|
||||
// about how a confidence is described is a rule about *content*, and it
|
||||
// should not be re-derived from a float in a second place.
|
||||
confidence: string,
|
||||
// Source pixels across the aligned crop. A suggestion the user disagrees
|
||||
// with has causes, and "the face was 41 pixels across" is one the user can
|
||||
@@ -177,7 +176,9 @@ export component IdentityScreen inherits Rectangle {
|
||||
// Faces belonging to nobody. Shown as a count rather than hidden, because
|
||||
// "why is this photograph not under anyone" deserves an answer.
|
||||
in property <int> unassigned: 0;
|
||||
// Whether the library's similarity calibration is fitted (FR-CULL-9).
|
||||
// Whether the similarity curve was fitted from this library's own faces,
|
||||
// as against the built-in one (FR-CULL-9). Changes what the screen says
|
||||
// about the confidences, never whether they are shown.
|
||||
in property <bool> calibrated: false;
|
||||
in property <bool> indexing: false;
|
||||
in property <string> indexing-status;
|
||||
@@ -591,11 +592,12 @@ export component IdentityScreen inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// FR-CULL-9, made visible: where the calibration is not fitted the
|
||||
// screen says the confidences are unavailable rather than letting
|
||||
// per-face percentages imply a measurement that was never made.
|
||||
// FR-CULL-9, made visible: the percentages are real numbers off a
|
||||
// real curve, but on a young library that curve is the built-in one
|
||||
// rather than one measured here. Said once, above the grid, so no
|
||||
// per-face percentage implies a measurement that was never made.
|
||||
if !root.calibrated && !root.model-missing: Text {
|
||||
text: "Similarity is not calibrated for this library yet, so no confidence is shown.";
|
||||
text: "Confidences use the built-in similarity curve; they will sharpen once this library has enough confirmed faces to fit its own.";
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
wrap: word-wrap;
|
||||
|
||||
Reference in New Issue
Block a user