Keep the face, not just a way to find it again
A face was drawn by decoding the 1024px proxy it was found on and cutting the box out again, every time the People screen opened. That made the screen a derivative of the thumbnail cache: evict a proxy — which the cache may do at any moment — and the cell goes blank, with no way back short of re-fetching the original over the network and re-detecting it. It also cost a full JPEG decode per image, per visit, to show a 96px cell. So the crop is cut once, when the pixels are already in hand at detection time, and kept. A 160px JPEG is a few KB against the ~250 KB proxy it replaces reading. Where it lives is the interesting part. The catalog snapshot is uploaded *whole* on every sync and downloaded by every device, so a crop column there would put tens of MB on every round trip — the exact cost `face_shard`'s 25 MB cap exists to bound, and the reason bulk per-face data lives in shards already. Crops therefore travel in the face shards, beside the embeddings, and `snapshot_for_upload` strips them from the copy it writes. Nothing reads a crop out of a merged remote catalog — the merge touches collections and keywords only — so a receiving device loses nothing. A shard carrying crops holds around 3,500 faces rather than 22,000, which is the price of a second device showing People immediately instead of re-fetching every proxy. The column is nullable and the reader falls back to the proxy, so a face indexed before this still works and the next indexing pass fills it in. V10 also adds `people.ignored`, for a person the user has looked at and does not want to identify. Most clusters in a real library are strangers — passers-by, other people's guests, a face on a poster — and there is no way to tell "not yet looked at" from "looked at, don't care" without recording the second. It is a column rather than a deletion because a deleted cluster comes straight back on the next Regroup: the faces are still there and still similar, and nothing short of remembering the judgement survives re-clustering. Same argument `face_person_rejected` makes one level down. And `prune_empty_unnamed`, for what clustering leaves behind. Regroup creates a person per unanchored group and never removed the previous run's now-empty ones, so pressing it twice added a rail entry per group it no longer believed in. Named people are never touched however empty — a name is user data — nor is a merge tombstone, which must outlive its faces to keep redirecting. 298 tests pass, including that the snapshot carries no crops while the live catalog keeps them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ use rusqlite::Connection;
|
||||
use crate::error::CatalogError;
|
||||
|
||||
/// Schema version this build writes and understands.
|
||||
pub const SCHEMA_VERSION: i64 = 9;
|
||||
pub const SCHEMA_VERSION: i64 = 10;
|
||||
|
||||
/// Apply migrations up to [`SCHEMA_VERSION`].
|
||||
///
|
||||
@@ -91,6 +91,13 @@ pub fn migrate(conn: &Connection) -> Result<i64, CatalogError> {
|
||||
tx.commit()?;
|
||||
}
|
||||
|
||||
if from < 10 {
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
tx.execute_batch(V10)?;
|
||||
tx.pragma_update(None, "user_version", 10)?;
|
||||
tx.commit()?;
|
||||
}
|
||||
|
||||
Ok(from)
|
||||
}
|
||||
|
||||
@@ -339,6 +346,50 @@ fn stem_of(path: &str) -> &str {
|
||||
/// Both narrow the walk rather than reorder it, so the index still supplies the
|
||||
/// ordering and SQLite tests the extra predicate per row. That is the cheap
|
||||
/// direction: the expensive part was never the filtering, it was the sort.
|
||||
const V10: &str = r#"
|
||||
-- TRACES: FR-CULL-10 | FR-CULL-12
|
||||
-- Two columns the People screen turned out to need, and neither is derivable.
|
||||
|
||||
-- A person the user does not want to identify.
|
||||
--
|
||||
-- Most of a real library's clusters are strangers: people in the background of
|
||||
-- a street, guests at somebody else's party, a face on a poster. They are
|
||||
-- correctly detected and correctly grouped, and the user will never name any of
|
||||
-- them -- but they crowd out the handful of groups that matter, and there is no
|
||||
-- way to tell "not yet looked at" from "looked at, don't care" without
|
||||
-- recording the second.
|
||||
--
|
||||
-- **User data**, and the reason this is a column rather than a deletion: a
|
||||
-- deleted cluster comes straight back on the next Regroup, because the faces
|
||||
-- are still there and still similar. Nothing short of remembering the judgement
|
||||
-- survives re-clustering, which is the same argument `face_person_rejected`
|
||||
-- makes one level down (FR-CULL-12).
|
||||
ALTER TABLE people ADD COLUMN ignored INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- The face, cut out and kept.
|
||||
--
|
||||
-- A face used to be drawn by decoding the 1024px proxy it was found on and
|
||||
-- cutting the box out again, every time the screen opened. That made the People
|
||||
-- screen a *derivative of the thumbnail cache*: evict a proxy -- which the
|
||||
-- cache is entitled to do at any moment -- and the cell goes blank, with no way
|
||||
-- back short of re-fetching the original over the network and re-detecting it.
|
||||
-- It also cost one full JPEG decode per image per visit to show a 96px cell.
|
||||
--
|
||||
-- So the crop is cut once, when the pixels are already in hand at detection
|
||||
-- time, and kept. Small: a 160px JPEG is a few KB, against ~250 KB for the
|
||||
-- proxy it replaces reading.
|
||||
--
|
||||
-- Nullable, because a face indexed before this column existed has no crop and
|
||||
-- must still work -- the reader falls back to the old proxy path, and the next
|
||||
-- indexing pass fills it in.
|
||||
--
|
||||
-- **Stripped from the sync snapshot.** The catalog is uploaded whole, so this
|
||||
-- would otherwise put tens of MB of JPEG on every sync; crops travel in the
|
||||
-- face shards instead, which is where the bulk per-face data already goes
|
||||
-- (`face_shard`). See `sync::snapshot_for_upload`.
|
||||
ALTER TABLE faces ADD COLUMN crop BLOB;
|
||||
"#;
|
||||
|
||||
const V9: &str = r#"
|
||||
-- TRACES: FR-CULL-8
|
||||
-- A record that face detection has *run* on an image, distinct from what it
|
||||
|
||||
Reference in New Issue
Block a user