Index the library's faces, and group them into people

Wires dr-face to dr-catalog: a background sweep that reads the proxy the
grid already built, detects, aligns, embeds and stores, then a clustering
pass that turns those embeddings into suggested people.

Detection runs on the Large thumbnail tier and nowhere else. That is what
makes the feature affordable -- a browsed library has already paid for
its proxies, so face indexing adds no RAW decode that was not already
happening -- and it is why an image whose proxy is missing is skipped
rather than fetched: requesting one here would put face indexing on the
network path FR-CULL-8 keeps it off.

The sweep keeps no cursor. It asks the catalog what is missing, so it
resumes after process death with no repeated work beyond the in-flight
image, and cancelling is dropping the receiver.

recluster writes only the suggested half. Confirmed faces go in as
anchors and come back untouched, and a cluster of one stays nameless --
naming every stray face would fill the People view with noise the user
then has to dismiss.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 20:49:30 +02:00
co-authored by Claude Opus 5
parent 00e78dc2ac
commit 2ac069a6b3
7 changed files with 536 additions and 37 deletions
+5 -5
View File
@@ -209,13 +209,13 @@ impl Detector {
// left/top/right/bottom in units of the stride.
let (cx, cy) = ((c * stride) as f32, (r * stride) as f32);
let b = &boxes[idx * 4..idx * 4 + 4];
let (x0, y0) = lb.to_source(cx - b[0] * s, cy - b[1] * s);
let (x1, y1) = lb.to_source(cx + b[2] * s, cy + b[3] * s);
let (x0, y0) = lb.into_source(cx - b[0] * s, cy - b[1] * s);
let (x1, y1) = lb.into_source(cx + b[2] * s, cy + b[3] * s);
let k = &kps[idx * 10..idx * 10 + 10];
let mut landmarks = [(0.0_f32, 0.0_f32); 5];
for (p, lm) in landmarks.iter_mut().enumerate() {
*lm = lb.to_source(cx + k[p * 2] * s, cy + k[p * 2 + 1] * s);
*lm = lb.into_source(cx + k[p * 2] * s, cy + k[p * 2 + 1] * s);
}
raw.push(Detection {
@@ -340,7 +340,7 @@ impl Letterbox {
}
/// Input-space point back to source pixels.
fn to_source(&self, x: f32, y: f32) -> (f32, f32) {
fn into_source(self, x: f32, y: f32) -> (f32, f32) {
((x - self.pad_x) / self.scale, (y - self.pad_y) / self.scale)
}
}
@@ -353,7 +353,7 @@ mod tests {
fn letterbox_round_trips_a_point() {
let lb = Letterbox::fit(1024.0, 683.0);
for &(x, y) in &[(0.0_f32, 0.0_f32), (512.0, 341.0), (1023.0, 682.0)] {
let (bx, by) = lb.to_source(x * lb.scale + lb.pad_x, y * lb.scale + lb.pad_y);
let (bx, by) = lb.into_source(x * lb.scale + lb.pad_x, y * lb.scale + lb.pad_y);
assert!((bx - x).abs() < 1e-2, "{bx} vs {x}");
assert!((by - y).abs() < 1e-2, "{by} vs {y}");
}