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
+11
View File
@@ -35,6 +35,16 @@ pub enum JobKind {
FetchPreview = 6,
/// Fetch a full original: pinned by rule, or explicitly asked for.
FetchOriginal = 7,
/// Detect and embed the faces in one image (FR-CULL-8).
///
/// One job does both, rather than splitting them: the proxy is already
/// decoded and in memory, and the natural unit of resumable work is one
/// photograph. Splitting would double the queue's row count for nothing.
///
/// Runs against the proxy tier, never a full decode — a library that has
/// been browsed has already paid for its proxies, so face indexing adds no
/// RAW decodes that were not already happening.
DetectFaces = 8,
}
impl JobKind {
@@ -48,6 +58,7 @@ impl JobKind {
5 => JobKind::ContentHash,
6 => JobKind::FetchPreview,
7 => JobKind::FetchOriginal,
8 => JobKind::DetectFaces,
_ => return None,
})
}