Index the whole library by fetching what it has not seen
"Index faces in the whole library" could not. Its work list was intersected
with the thumbnail store at `ThumbSize::Large`, and nothing fills that class for
a whole library — `SWEEP_THUMB_SIZE` is deliberately `Grid`, because the large
class is ~860 MB of shards against ~200 MB and every syncing device pays it. So
the only images with a large proxy were the ones the user had personally zoomed
into or opened in the loupe. On this library that was 220 of 23,529.
The comment defending it misread the requirement:
// Requesting one here would put face indexing on the network path,
// which FR-CULL-8 explicitly keeps it off.
FR-CULL-8 keeps indexing off the **full decode**, not the network, and then says
the opposite in the same paragraph: "where no proxy exists, the job requests one
at background priority rather than decoding inline". faces.md §7 repeats it.
Neither was implemented.
So the pass fetches. Same two-stage route the thumbnail sweep uses — the header,
then the located preview's own byte range (FR-NC-3) — so no whole file is pulled
and no RAW is decoded, because an embedded preview is a JPEG. The work list is
now every visible image with no `face_index` row for the model: 23,308 here,
against nearly none before.
**It indexes at the resolution the preview actually has**, not the 1024 the old
tier would have given. `locate_preview` already picks the largest embedded
preview, and the thumbnail sweep was decoding it and throwing the detail away at
`downscale_to(256)`. A face 2% across the frame is 5 px on a grid thumbnail and
~61 px at the cap here — and 112 is what the embedder samples, so this is the
difference between an upsampled crop and a real one. `crop_px` records which,
per face, as §7 intended.
Capped at 3072 rather than truly full: `index_proxy` needs packed `f32` RGB at
12 bytes a pixel, so a 24 MP frame is ~288 MB and the fetch lanes hold one each.
The constant is named and sits next to the reason.
Orientation is applied **before** detection, not after downscaling. That costs a
permutation of a larger buffer — ~15 ms against a ~150 ms decode — and buys the
entire class of bug this codebase keeps having: detection then runs on the
photograph rather than the sensor, so every box and landmark is already in the
space the catalog stores and the overlay draws, with no second mapping to get
backwards.
One detector and one embedder serve every lane. The lanes are concurrent futures
on a single thread, not threads, and inference contains no await, so a `RefCell`
borrow never overlaps another — a pair per lane would duplicate ~16 MB of
weights for no parallelism.
Images with no face in them are recorded too. `face_index` records that
detection *ran*, and zero is its most valuable value: without the row every
landscape and document scan returns on every pass, for ever, and in a personal
library that is most of it (§7a).
The old store-only pass survives as `spawn_store_face_sweep` for
`examples/face_index.rs`, which indexes a local store with no network. The
settings copy no longer claims indexing reads "the photographs already
thumbnailed above", and the audit line says "to fetch" rather than "awaiting a
proxy", which had become a blocker that no longer blocks.
Verified against the real catalog: the new work list returns 23,308 where the
old one returned effectively nothing. 469 tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+58
-11
@@ -95,10 +95,16 @@ pub fn faces_outstanding(
|
||||
})
|
||||
})?
|
||||
.filter_map(Result::ok)
|
||||
// A proxy that is not in the store yet is not this pass's problem: the
|
||||
// thumbnail sweep builds it, and the next face pass picks the image up.
|
||||
// Requesting one here would put face indexing on the network path,
|
||||
// which FR-CULL-8 explicitly keeps it off.
|
||||
// This pass reads the store and only the store, so an image without a
|
||||
// proxy is not work it can do.
|
||||
//
|
||||
// **Not because FR-CULL-8 forbids it.** That requirement keeps indexing
|
||||
// off the *full decode* and says the opposite about proxies — "where no
|
||||
// proxy exists, the job requests one at background priority". An
|
||||
// earlier comment here read it the other way round, and the result was
|
||||
// a whole-library button that could only reach photographs the user had
|
||||
// personally zoomed into. `library::spawn_face_sweep` is that
|
||||
// requirement implemented; this one is the local-only variant.
|
||||
.filter(|req| store.contains(req.file_id, FACE_TIER))
|
||||
.collect();
|
||||
Ok(rows)
|
||||
@@ -114,9 +120,9 @@ pub struct IndexAudit {
|
||||
pub coverage: faces::Coverage,
|
||||
/// Outstanding and ready: the proxy exists, so a sweep would do these now.
|
||||
pub ready: u64,
|
||||
/// Outstanding and blocked: no proxy yet, so the thumbnail sweep has to run
|
||||
/// first. Reported separately because it is not a face-indexing problem and
|
||||
/// telling the user to "run indexing again" would not fix it.
|
||||
/// Outstanding with no proxy on disk, so the whole-library pass will fetch
|
||||
/// one. Still reported separately because it is the expensive half — these
|
||||
/// cost a range request each, and the ones above cost nothing.
|
||||
pub awaiting_proxy: u64,
|
||||
}
|
||||
|
||||
@@ -147,7 +153,9 @@ impl IndexAudit {
|
||||
s.push_str(&format!("; {} ready to index", self.ready));
|
||||
}
|
||||
if self.awaiting_proxy > 0 {
|
||||
s.push_str(&format!("; {} awaiting a proxy", self.awaiting_proxy));
|
||||
// Not a blocker any more, and it must not read like one: the
|
||||
// whole-library pass fetches these rather than skipping them.
|
||||
s.push_str(&format!("; {} to fetch", self.awaiting_proxy));
|
||||
}
|
||||
s
|
||||
}
|
||||
@@ -256,7 +264,13 @@ fn normalise_landmarks(lm: &[(f32, f32); 5], long_edge: f32) -> [(f32, f32); 5]
|
||||
out
|
||||
}
|
||||
|
||||
/// Index every image that needs it, in the background.
|
||||
/// Index every image whose proxy is **already on this disk**, in the background.
|
||||
///
|
||||
/// Not the whole-library pass — that is `library::spawn_face_sweep`, which
|
||||
/// fetches what it has not got. This one never touches the network, which makes
|
||||
/// it the right shape for a tool run against a local store (see
|
||||
/// `examples/face_index.rs`) and the wrong shape for a user pressing "index my
|
||||
/// library", because for an unbrowsed library the work list is nearly empty.
|
||||
///
|
||||
/// Strictly background work: it competes with thumbnailing, not with rendering
|
||||
/// (NFR-ARCH-2), and it is interruptible simply by dropping the receiver — the
|
||||
@@ -265,7 +279,7 @@ fn normalise_landmarks(lm: &[(f32, f32); 5], long_edge: f32) -> [(f32, f32); 5]
|
||||
/// cursor. That is what makes it survive process death (FR-PLAT-AND-3) with no
|
||||
/// repeated work beyond the in-flight image.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spawn_face_sweep(
|
||||
pub fn spawn_store_face_sweep(
|
||||
catalog_path: PathBuf,
|
||||
store_dir: PathBuf,
|
||||
detector_model: PathBuf,
|
||||
@@ -629,6 +643,36 @@ pub fn crop_face(
|
||||
})
|
||||
}
|
||||
|
||||
/// Detect and embed every face in a decoded preview.
|
||||
///
|
||||
/// The counterpart to [`index_proxy`] for the fetching sweep, which holds a
|
||||
/// `dr_decode::Preview` rather than a thumbnail out of the store. The preview
|
||||
/// arrives **already turned the right way up** — `fetch_preview` applies the
|
||||
/// orientation before returning — so the coordinates this produces are in the
|
||||
/// photograph's space, which is the space the catalog stores and the develop
|
||||
/// overlay draws in. Nothing here has to know about the sensor.
|
||||
///
|
||||
/// Returns the faces and the long edge they were normalised against, which is
|
||||
/// what `record_detections` stores so a proxy regenerated at another size does
|
||||
/// not move them.
|
||||
pub fn index_preview(
|
||||
detector: &mut Detector,
|
||||
embedder: &mut Embedder,
|
||||
preview: &dr_decode::Preview,
|
||||
options: &DetectOptions,
|
||||
) -> Result<(Vec<DetectedFace>, u32), dr_face::FaceError> {
|
||||
let rgb = rgba_to_rgb_f32(&preview.rgba);
|
||||
let faces = index_proxy(
|
||||
detector,
|
||||
embedder,
|
||||
&rgb,
|
||||
preview.width as usize,
|
||||
preview.height as usize,
|
||||
options,
|
||||
)?;
|
||||
Ok((faces, preview.width.max(preview.height)))
|
||||
}
|
||||
|
||||
/// `dr-thumbs` decodes to RGBA; `dr-face` reads packed `f32` RGB.
|
||||
fn rgba_to_rgb_f32(rgba: &[u8]) -> Vec<f32> {
|
||||
let mut out = Vec::with_capacity(rgba.len() / 4 * 3);
|
||||
@@ -661,7 +705,10 @@ mod tests {
|
||||
assert!(s.contains("60%"), "{s}");
|
||||
assert!(!s.contains("60.0%"), "whole numbers above ten percent: {s}");
|
||||
assert!(s.contains("30 ready"), "{s}");
|
||||
assert!(s.contains("10 awaiting"), "{s}");
|
||||
// "to fetch", not "awaiting a proxy": the whole-library pass fetches
|
||||
// these rather than being blocked by them, and the line must not send
|
||||
// the user off to run the thumbnail sweep first.
|
||||
assert!(s.contains("10 to fetch"), "{s}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user