Record that face detection has run, not just what it found

An image with no faces in it was indistinguishable from one that had
never been looked at, so every landscape, still life and document scan in
the library was re-detected on every pass, for ever. In a real library
that is most of it: on the 23,527-image test library, 64 of the first 110
images indexed contain no face at all.

Schema v9 adds face_index, a run marker per (image, model) carrying the
face count and the proxy edge it read. Keyed on the model, so a model
change puts every image back in the queue by itself.

That makes a coverage figure possible, which is the thing a user actually
wants to see. The audit also splits the outstanding set by whether a
proxy exists, because 23,417 awaiting a proxy and 110 ready to index are
different problems, and telling the user to run indexing again would not
fix the first.

The Identity screen gains Index faces, Stop, and the coverage line.
examples/face_index.rs is the same check and sweep without a window,
which is the right shape for an overnight pass.

Measured on the real library in release: 3.5 images/second, 110 images
and 125 faces in 30 seconds, and a second run correctly finds nothing
left to do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 22:13:41 +02:00
co-authored by Claude Opus 5
parent 3e607222c6
commit 26a1eb7e28
14 changed files with 971 additions and 84 deletions
+10
View File
@@ -534,6 +534,8 @@ export component AppWindow inherits Window {
in property <bool> identity-indexing: false;
in property <string> identity-indexing-status;
in property <bool> identity-model-missing: false;
in property <string> identity-coverage;
in property <bool> identity-coverage-complete: false;
in property <int> identity-picked: 0;
callback identity-open();
@@ -546,6 +548,9 @@ export component AppWindow inherits Window {
callback identity-confirm-all();
callback identity-split-picked();
callback identity-recluster();
callback identity-index();
callback identity-stop-indexing();
callback identity-check-coverage();
callback identity-delete-all();
callback library-thumbnail-all();
in-out property <length> library-cell-size: 180px;
@@ -1282,6 +1287,8 @@ in property <bool> panel-visible: true;
indexing-status: root.identity-indexing-status;
model-missing: root.identity-model-missing;
picked-count: root.identity-picked;
coverage: root.identity-coverage;
coverage-complete: root.identity-coverage-complete;
person-picked(id) => { root.identity-person-picked(id); }
rename(name) => { root.identity-rename(name); }
@@ -1291,6 +1298,9 @@ in property <bool> panel-visible: true;
confirm-all() => { root.identity-confirm-all(); }
split-picked() => { root.identity-split-picked(); }
recluster() => { root.identity-recluster(); }
index-faces() => { root.identity-index(); }
stop-indexing() => { root.identity-stop-indexing(); }
check-coverage() => { root.identity-check-coverage(); }
delete-all-face-data() => { root.identity-delete-all(); }
close() => { root.identity-close(); }
}
+37
View File
@@ -157,6 +157,12 @@ export component IdentityScreen inherits Rectangle {
in property <bool> calibrated: false;
in property <bool> indexing: false;
in property <string> indexing-status;
// The batch check's own words: how much of the library face detection has
// actually been over. Text rather than a number, because "4,812 of 5,000
// indexed, 190 awaiting a proxy" is the useful form and a bare percentage
// hides the reason the rest are outstanding.
in property <string> coverage;
in property <bool> coverage-complete: false;
// No model on disk: face indexing cannot run at all (docs/faces.md §2.2).
in property <bool> model-missing: false;
in property <int> picked-count: 0;
@@ -170,6 +176,9 @@ export component IdentityScreen inherits Rectangle {
callback split-picked();
callback merge-into(int);
callback recluster();
callback index-faces();
callback stop-indexing();
callback check-coverage();
callback delete-all-face-data();
callback close();
@@ -320,6 +329,14 @@ export component IdentityScreen inherits Rectangle {
primary: true;
clicked => { root.confirm-all(); }
}
if !root.indexing && !root.model-missing && !root.coverage-complete: Button {
text: "Index faces";
clicked => { root.index-faces(); }
}
if root.indexing: Button {
text: "Stop";
clicked => { root.stop-indexing(); }
}
Button {
text: "Regroup";
clicked => { root.recluster(); }
@@ -337,6 +354,26 @@ export component IdentityScreen inherits Rectangle {
}
}
// The run-marker check, shown rather than buried in a log. Without
// it the screen can say how many faces it has but not how much of
// the library it has actually looked at — and those are the two
// different questions the face_index table exists to separate.
if root.coverage != "": HorizontalLayout {
spacing: Theme.gap-sm;
Text {
text: root.coverage;
color: root.coverage-complete ? Theme.ink-faint : Theme.ink-dim;
font-size: Theme.text-sm;
vertical-alignment: center;
wrap: word-wrap;
}
Rectangle { }
IconButton {
icon: "rotate-cw";
clicked => { root.check-coverage(); }
}
}
// 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.