doc(drive.md): face recognition cluster per drive

This commit is contained in:
Edouard Vanbelle
2026-07-01 21:09:56 +02:00
parent e7e7218e9b
commit 473c126291
+82
View File
@@ -1368,6 +1368,88 @@ flip Photos to cross-drive with `forbid_photo_index` as the
opt-out (mirroring Music). That can land later without a schema
change — just a behaviour change.
#### Face indexing — per-drive clustering, scope follows Photos
Face indexing is bound to the same scope as `/api/photos` — the
two surfaces show the same content set, so the face data behind
that content lives in the same scope.
Two layers to keep distinct:
**Storage layer — per blob.** Face fingerprints are keyed on
`blob_hash` (BLAKE3), FK to `storage.blobs.hash`. Fingerprints
are deterministic from content bytes, and OxiCloud dedups
content via blob hash — so a photo uploaded into N drives (or N
times by N users) produces *one* fingerprint set, computed once,
reused forever. Cascade-deletes when the blob is GC'd (ref_count
→ 0). No `user_id`, `created_by`, `file_id`, `drive_id`, or
group key on the fingerprint row: identity is the content.
**Clustering layer — per drive.** Cluster computation runs
*within* a drive: take every fingerprint reachable via a file in
that drive (`storage.files.drive_id = X` JOIN
`face_fingerprints` ON `blob_hash`), cluster them, emit clusters
scoped to drive X. The query repeats per drive the caller can
see (default personal + drives where
`policies.include_in_photo_index = true` AND the caller has
Read). Same-person fingerprints from different drives land in
**separate** clusters by default — even when both drives reach
the exact same blob, because clustering is keyed on drive, not
on fingerprint identity.
**Why per-drive clustering:**
The drive is already the data boundary post-D6 — quota, sharing,
trash, AuthZ all pivot on `drive_id`. The face library is part
of the drive's content, not a cross-drive aggregate. Two
properties fall out cleanly:
- **Family-drive UX works.** Alice and Bob both members of
"Family" with `include_in_photo_index=true`. Alice uploads
Christmas photos; Bob uploads birthday photos. Grandma is in
both. Both see the *same* Grandma cluster in Family — one
merged cluster derived from fingerprints across both uploads.
Labels on the Family cluster are drive-scoped (anyone with
Photos access to Family sees them).
- **Personal-drive isolation is preserved.** Each user's
personal drive is access-isolated by definition (nobody else
has Read on it). So a personal-drive cluster is visible only
to the drive's owner. The privacy guarantee falls out of
drive-access scoping — no separate user-id key needed.
**Cross-drive clusters don't auto-merge.** Bob labelling
"Grandma" in his Personal-drive cluster does NOT propagate to
Family's Grandma cluster. Two separate visual clusters by
default — even if the embedding similarity would otherwise
match them. Rationale: auto-propagating private labels into a
shared drive would silently expose personal classifications.
Future UX can offer explicit per-cluster merging ("these two
clusters are the same person") — user-driven, never silent.
**Shared-drive opt-in is the consent surface.** Enabling
`include_in_photo_index` on a drive is the owner saying "the
photos in this drive are part of the drive's photo library,
including the face data they contain." Doesn't add a new
sharing surface — surfaces what was already visible (anyone
with Read on a photo can see who's in it).
**Implementation:**
- `face_fingerprints(blob_hash, embedding, …)` — FK to
`storage.blobs.hash`, no `user_id` / `file_id` / `drive_id`
column. Cascade-delete via the blob ref-count → 0 GC path.
- Cluster query: `SELECT … FROM storage.files f JOIN
face_fingerprints fp ON fp.blob_hash = f.blob_hash WHERE
f.drive_id = $1 AND NOT f.is_trashed` for each drive in the
caller's Photos-scope set.
- Pre-D7 the legacy `(user_id, blob_hash)` query in
`face_indexing_service.rs::lookup_user` stays in place; D7
drops `user_id` from the column set in lockstep with the
global user_id retirement, leaving the fingerprint row keyed
on `blob_hash` alone. Both the `include_in_photo_index` policy
AND D7's user_id drop must land before face indexing can move
to the per-drive clustering model.
#### Verification sketch
The D0 Hurl suite (`tests/api/drives_foundation.hurl`) covers