Run the formatter over the face branch before it reaches CI
🐳 Android image / Build and push (push) Successful in 2s
Build and test / android-image (push) Successful in 2s
Build and test / Desktop (Linux) (push) Successful in 1h21m32s
Build and test / Layer separation (push) Successful in 37s
Traceability / Requirement traces (push) Successful in 25s
Build and test / Android (aarch64) (push) Failing after 33m58s

The merge of the SCRFD/MobileFaceNet work brought 69 rustfmt diffs across
dr-catalog, dr-face and dr-ui with it, so `cargo fmt --all -- --check` fails
on master and the Desktop job stops at its Format step — before clippy, the
tests or the release build have run at all. That makes the whole desktop
half of CI blind: a real compile error behind this would look exactly the
same from the outside. There was nothing behind it, as it turns out — with
the formatting fixed, clippy, the test suite and the release build all pass.

Every .rs hunk is `cargo fmt --all` on the pinned 1.92.0 toolchain, not a
hand edit, but it is worth being precise about what that moved, because it
is more than whitespace. Besides reflowing signatures and call chains,
rustfmt reordered the `pub mod` and `pub use` items in dr-face/src/lib.rs so
the `#[cfg(feature = "inference")]` entries sort in place, added the trailing
semicolon inside `let ... else { return }` bodies in identity_ui.rs, wrapped
a bare closure body in braces in cluster.rs, adjusted trailing commas, and
dropped a stray blank line at the end of identity_ui.rs. All of it is
semantically inert; none of it changes behaviour.

docs/traceability.md rides along because it has to. The matrix records each
TRACES tag by line number, and reflowing develop.rs, lib.rs, faces.rs,
identity.rs and identity_ui.rs moved them — FR-CAT-8, FR-CAT-9, FR-CULL-10,
FR-DEV-3, FR-DEV-3a and FR-DEV-3c all shift by a line or two. The matrix was
verified up to date on d777f7f before this commit, so this is drift these
formatting changes introduced, not pre-existing staleness being swept up.
Leaving it for a follow-up commit would hand traceability-check.yml a
failure caused entirely by a whitespace change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 13:12:49 +02:00
co-authored by Claude Opus 5
parent d777f7f44d
commit b846b312b8
18 changed files with 285 additions and 207 deletions
+1 -2
View File
@@ -189,8 +189,7 @@ impl SegmentationJob {
return Ok(None);
}
let mut seg =
segmentation::compute(&self.ctx, &rgb, rw, rh, self.orientation, options)?;
let mut seg = segmentation::compute(&self.ctx, &rgb, rw, rh, self.orientation, options)?;
// TRACES: FR-CULL-10
// Put names on the people the segmenter found.
+16 -10
View File
@@ -314,15 +314,15 @@ pub fn spawn_face_sweep(
return;
}
};
let mut embedder = match Embedder::from_path(&embedder_model, ModelId::new(model_id.clone()))
{
Ok(e) => e,
Err(e) => {
log::warn!("face sweep: cannot load the embedder: {e}");
finish_empty(&tx);
return;
}
};
let mut embedder =
match Embedder::from_path(&embedder_model, ModelId::new(model_id.clone())) {
Ok(e) => e,
Err(e) => {
log::warn!("face sweep: cannot load the embedder: {e}");
finish_empty(&tx);
return;
}
};
let wanted = match faces_outstanding(&catalog, &store, &model_id) {
Ok(w) => w,
@@ -723,7 +723,13 @@ mod tests {
#[test]
fn landmarks_normalise_against_the_long_edge() {
let lm = [(512.0, 256.0), (0.0, 0.0), (1024.0, 512.0), (10.0, 20.0), (5.0, 5.0)];
let lm = [
(512.0, 256.0),
(0.0, 0.0),
(1024.0, 512.0),
(10.0, 20.0),
(5.0, 5.0),
];
let n = normalise_landmarks(&lm, 1024.0);
assert!((n[0].0 - 0.5).abs() < 1e-6);
assert!((n[0].1 - 0.25).abs() < 1e-6);
+62 -28
View File
@@ -61,7 +61,10 @@ impl PersonRow {
/// — the exact conflation the confirmed/suggested split exists to prevent.
pub fn display_name(&self) -> String {
if self.name.is_empty() {
format!("Unnamed ({} faces)", self.confirmed_faces + self.suggested_faces)
format!(
"Unnamed ({} faces)",
self.confirmed_faces + self.suggested_faces
)
} else {
self.name.clone()
}
@@ -122,7 +125,10 @@ pub struct IdentityView {
}
/// Read the people rail.
pub fn load_people(catalog: &Catalog, model_id: &str) -> Result<IdentityView, dr_catalog::CatalogError> {
pub fn load_people(
catalog: &Catalog,
model_id: &str,
) -> Result<IdentityView, dr_catalog::CatalogError> {
let conn = catalog.connection();
let people = faces::people(conn)?
.into_iter()
@@ -410,10 +416,7 @@ pub fn rename(
/// The operation the screen exists to make cheap. A cluster that is simply
/// right — the common case for a well-photographed person — should cost one
/// click, not forty.
pub fn confirm_all(
catalog: &Catalog,
person: PersonId,
) -> Result<usize, dr_catalog::CatalogError> {
pub fn confirm_all(catalog: &Catalog, person: PersonId) -> Result<usize, dr_catalog::CatalogError> {
let conn = catalog.connection();
let mut n = 0;
for f in faces::for_person(conn, person, true)? {
@@ -662,7 +665,8 @@ mod tests {
let c = catalog();
let i1 = image(&c, 1);
let i2 = image(&c, 2);
let a = faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let a =
faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
faces::record_detections(c.connection(), i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let anna = faces::create_person(c.connection(), "Anna").unwrap();
@@ -672,7 +676,10 @@ mod tests {
assert_eq!(view.people.len(), 1);
assert_eq!(view.people[0].name, "Anna");
assert_eq!(view.unassigned, 1);
assert!(!view.calibrated, "a fresh library has no fitted calibration");
assert!(
!view.calibrated,
"a fresh library has no fitted calibration"
);
}
#[test]
@@ -681,9 +688,12 @@ mod tests {
let i1 = image(&c, 1);
let i2 = image(&c, 2);
let i3 = image(&c, 3);
let a = faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let b = faces::record_detections(c.connection(), i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let d = faces::record_detections(c.connection(), i3, "w600k_mbf", 1024, &[face(3)]).unwrap();
let a =
faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let b =
faces::record_detections(c.connection(), i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let d =
faces::record_detections(c.connection(), i3, "w600k_mbf", 1024, &[face(3)]).unwrap();
let anna = faces::create_person(c.connection(), "Anna").unwrap();
faces::confirm(c.connection(), a[0], anna).unwrap();
@@ -692,7 +702,12 @@ mod tests {
assert_eq!(confirm_all(&c, anna).unwrap(), 2);
assert_eq!(confirm_all(&c, anna).unwrap(), 0, "should be idempotent");
assert_eq!(faces::for_person(c.connection(), anna, false).unwrap().len(), 3);
assert_eq!(
faces::for_person(c.connection(), anna, false)
.unwrap()
.len(),
3
);
}
/// The correction must stick. Splitting a face off and then re-running
@@ -702,16 +717,24 @@ mod tests {
let c = catalog();
let i1 = image(&c, 1);
let i2 = image(&c, 2);
let a = faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let b = faces::record_detections(c.connection(), i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let a =
faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let b =
faces::record_detections(c.connection(), i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let anna = faces::create_person(c.connection(), "Anna").unwrap();
faces::confirm(c.connection(), a[0], anna).unwrap();
faces::suggest(c.connection(), b[0], anna, 0.95).unwrap();
let bob = split_off(&c, anna, &[b[0]], "Bob").unwrap();
assert_eq!(faces::for_person(c.connection(), bob, false).unwrap().len(), 1);
assert_eq!(faces::for_person(c.connection(), anna, true).unwrap().len(), 1);
assert_eq!(
faces::for_person(c.connection(), bob, false).unwrap().len(),
1
);
assert_eq!(
faces::for_person(c.connection(), anna, true).unwrap().len(),
1
);
// The next clustering pass tries again and is refused.
assert!(
@@ -735,8 +758,10 @@ mod tests {
let c = catalog();
let i1 = image(&c, 1);
let i2 = image(&c, 2);
let a = faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let b = faces::record_detections(c.connection(), i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let a =
faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let b =
faces::record_detections(c.connection(), i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let anna = faces::create_person(c.connection(), "Anna").unwrap();
let annie = faces::create_person(c.connection(), "Annie").unwrap();
faces::confirm(c.connection(), a[0], anna).unwrap();
@@ -752,8 +777,8 @@ mod tests {
fn named_boxes_come_back_in_proxy_pixels() {
let c = catalog();
let img = image(&c, 1);
let ids = faces::record_detections(c.connection(), img, "w600k_mbf", 1024, &[face(1)])
.unwrap();
let ids =
faces::record_detections(c.connection(), img, "w600k_mbf", 1024, &[face(1)]).unwrap();
let anna = faces::create_person(c.connection(), "Anna").unwrap();
faces::confirm(c.connection(), ids[0], anna).unwrap();
@@ -777,12 +802,14 @@ mod tests {
fn a_suggested_person_does_not_name_a_region() {
let c = catalog();
let img = image(&c, 1);
let ids = faces::record_detections(c.connection(), img, "w600k_mbf", 1024, &[face(1)])
.unwrap();
let ids =
faces::record_detections(c.connection(), img, "w600k_mbf", 1024, &[face(1)]).unwrap();
let anna = faces::create_person(c.connection(), "Anna").unwrap();
faces::suggest(c.connection(), ids[0], anna, 0.95).unwrap();
assert!(named_boxes_for_image(&c, img, 1024, 683).unwrap().is_empty());
assert!(named_boxes_for_image(&c, img, 1024, 683)
.unwrap()
.is_empty());
// Confirming it makes the name appear.
faces::confirm(c.connection(), ids[0], anna).unwrap();
@@ -794,12 +821,14 @@ mod tests {
fn an_unnamed_group_does_not_name_a_region() {
let c = catalog();
let img = image(&c, 1);
let ids = faces::record_detections(c.connection(), img, "w600k_mbf", 1024, &[face(1)])
.unwrap();
let ids =
faces::record_detections(c.connection(), img, "w600k_mbf", 1024, &[face(1)]).unwrap();
let nameless = faces::create_person(c.connection(), "").unwrap();
faces::confirm(c.connection(), ids[0], nameless).unwrap();
assert!(named_boxes_for_image(&c, img, 1024, 683).unwrap().is_empty());
assert!(named_boxes_for_image(&c, img, 1024, 683)
.unwrap()
.is_empty());
}
#[test]
@@ -817,7 +846,11 @@ mod tests {
height: 4,
rgba,
};
assert!((mean_luma(&crop) - 1.0).abs() < 1e-3, "{}", mean_luma(&crop));
assert!(
(mean_luma(&crop) - 1.0).abs() < 1e-3,
"{}",
mean_luma(&crop)
);
}
#[test]
@@ -835,7 +868,8 @@ mod tests {
fn deleting_everything_empties_the_screen() {
let c = catalog();
let i1 = image(&c, 1);
let a = faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let a =
faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let anna = faces::create_person(c.connection(), "Anna").unwrap();
faces::confirm(c.connection(), a[0], anna).unwrap();
+21 -17
View File
@@ -109,8 +109,7 @@ pub fn refresh(
// Drop portraits for people who no longer exist, so a long session that
// merges and splits repeatedly does not accumulate them.
{
let live: std::collections::HashSet<PersonId> =
view.people.iter().map(|p| p.id).collect();
let live: std::collections::HashSet<PersonId> = view.people.iter().map(|p| p.id).collect();
ctl.covers.borrow_mut().retain(|id, _| live.contains(id));
}
@@ -152,8 +151,8 @@ pub fn refresh(
match (ctl.selected.get(), store) {
(Some(person), Some(store)) => {
let cells = identity::load_faces(cat, store, person, view.calibrated)
.unwrap_or_else(|e| {
let cells =
identity::load_faces(cat, store, person, view.calibrated).unwrap_or_else(|e| {
log::warn!("identity: reading faces: {e}");
Vec::new()
});
@@ -335,7 +334,9 @@ pub fn wire<S, M, P>(
let store = store.clone();
window.on_identity_rename(move |name| {
let Some(w) = weak.upgrade() else { return };
let Some(person) = ctl.selected.get() else { return };
let Some(person) = ctl.selected.get() else {
return;
};
if let Some(cat) = catalog.borrow().as_ref() {
if let Err(e) = identity::rename(cat, person, &name) {
log::warn!("identity: rename: {e}");
@@ -352,7 +353,9 @@ pub fn wire<S, M, P>(
let store = store.clone();
window.on_identity_confirm_face(move |id| {
let Some(w) = weak.upgrade() else { return };
let Some(person) = ctl.selected.get() else { return };
let Some(person) = ctl.selected.get() else {
return;
};
if let Some(cat) = catalog.borrow().as_ref() {
if let Err(e) = identity::confirm(cat, FaceId(id as u64), person) {
log::warn!("identity: confirm: {e}");
@@ -369,7 +372,9 @@ pub fn wire<S, M, P>(
let store = store.clone();
window.on_identity_reject_face(move |id| {
let Some(w) = weak.upgrade() else { return };
let Some(person) = ctl.selected.get() else { return };
let Some(person) = ctl.selected.get() else {
return;
};
if let Some(cat) = catalog.borrow().as_ref() {
if let Err(e) = identity::reject(cat, FaceId(id as u64), person) {
log::warn!("identity: reject: {e}");
@@ -408,7 +413,9 @@ pub fn wire<S, M, P>(
let store = store.clone();
window.on_identity_confirm_all(move || {
let Some(w) = weak.upgrade() else { return };
let Some(person) = ctl.selected.get() else { return };
let Some(person) = ctl.selected.get() else {
return;
};
if let Some(cat) = catalog.borrow().as_ref() {
match identity::confirm_all(cat, person) {
Ok(n) => log::info!("identity: confirmed {n} suggestion(s)"),
@@ -426,7 +433,9 @@ pub fn wire<S, M, P>(
let store = store.clone();
window.on_identity_split_picked(move || {
let Some(w) = weak.upgrade() else { return };
let Some(person) = ctl.selected.get() else { return };
let Some(person) = ctl.selected.get() else {
return;
};
let members: Vec<FaceId> = ctl.picked.borrow().clone();
if members.is_empty() {
return;
@@ -488,10 +497,8 @@ pub fn wire<S, M, P>(
ctl.progress.set((0, 0));
ctl.faces_found.set(0);
*ctl.activity.borrow_mut() = Some(activity.begin(
crate::activity::Kind::Index,
"Indexing faces",
));
*ctl.activity.borrow_mut() =
Some(activity.begin(crate::activity::Kind::Index, "Indexing faces"));
*ctl.sweep.borrow_mut() = Some(crate::faces::spawn_face_sweep(
catalog_path,
store_dir,
@@ -531,9 +538,7 @@ pub fn wire<S, M, P>(
FaceSweepMessage::Indexed { faces, .. } => {
let (seen, total) = ctl_tick.progress.get();
ctl_tick.progress.set((seen + 1, total));
ctl_tick
.faces_found
.set(ctl_tick.faces_found.get() + faces);
ctl_tick.faces_found.set(ctl_tick.faces_found.get() + faces);
}
FaceSweepMessage::Finished { .. } => done = true,
}
@@ -671,4 +676,3 @@ mod tests {
assert!(ctl.picked.borrow().is_empty());
}
}
+53 -48
View File
@@ -22,13 +22,13 @@
mod activity;
mod collections_ui;
mod derived_sync;
pub mod faces;
pub mod identity;
mod identity_ui;
mod develop;
mod export;
pub mod faces;
mod gradient;
mod histogram;
pub mod identity;
mod identity_ui;
mod import;
mod import_ui;
mod labels;
@@ -1295,54 +1295,59 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
let lib = library.clone();
let ctl = settings.clone();
let weak = window.as_weak();
settings_ui::wire(&window, settings.clone(), move |s| {
// A ceiling that moved has to be applied to what is already on
// disk, or lowering it would only affect future downloads and the
// cache would sit over budget indefinitely.
lib.set_cache_budget(s.cache.original_budget_bytes);
lib.set_keep_opened_originals(s.cache.keep_opened_originals);
if let Some(w) = weak.upgrade() {
refresh_export_label(&w, &ctl);
}
// The axis is cut into bars when the window is loaded, so a new
// count only shows once something reloads it. Done here, and only
// when the figure actually moved: a setting that appears to do
// nothing until the user scrolls is one they will change twice and
// then leave wrong.
if lib.set_timeline_bars(s.library.timeline_bars) {
settings_ui::wire(
&window,
settings.clone(),
move |s| {
// A ceiling that moved has to be applied to what is already on
// disk, or lowering it would only affect future downloads and the
// cache would sit over budget indefinitely.
lib.set_cache_budget(s.cache.original_budget_bytes);
lib.set_keep_opened_originals(s.cache.keep_opened_originals);
if let Some(w) = weak.upgrade() {
library_ui::reload(&w, &lib);
refresh_export_label(&w, &ctl);
}
}
// Lowering the ceiling evicts, so the figure beside it has just
// changed — leaving the old one would show the cache still over a
// limit that was enforced a moment ago.
ctl.set_usage_label(describe_cache_usage(&lib));
if let Some(w) = weak.upgrade() {
settings_ui::render(&w, &ctl);
}
},
{
// Face coverage, read when the page opens. The figures live in the
// catalog and the settings page holds no session, so they arrive
// through here rather than being kept up to date continuously —
// they are only ever looked at while this page is on screen.
let lib = library.clone();
let catalog = library.catalog();
move |w: &AppWindow| {
let store = lib.session().and_then(|(_, s)| {
dr_thumbs::ThumbStore::open(&library::thumbs_dir(&s.server, &s.user_id)).ok()
});
identity_ui::refresh_coverage(w, &catalog, store.as_ref());
w.set_identity_model_missing(
lib.session()
.and_then(|(_, s)| library::face_models(&s.server, &s.user_id))
.is_none(),
);
}
});
// The axis is cut into bars when the window is loaded, so a new
// count only shows once something reloads it. Done here, and only
// when the figure actually moved: a setting that appears to do
// nothing until the user scrolls is one they will change twice and
// then leave wrong.
if lib.set_timeline_bars(s.library.timeline_bars) {
if let Some(w) = weak.upgrade() {
library_ui::reload(&w, &lib);
}
}
// Lowering the ceiling evicts, so the figure beside it has just
// changed — leaving the old one would show the cache still over a
// limit that was enforced a moment ago.
ctl.set_usage_label(describe_cache_usage(&lib));
if let Some(w) = weak.upgrade() {
settings_ui::render(&w, &ctl);
}
},
{
// Face coverage, read when the page opens. The figures live in the
// catalog and the settings page holds no session, so they arrive
// through here rather than being kept up to date continuously —
// they are only ever looked at while this page is on screen.
let lib = library.clone();
let catalog = library.catalog();
move |w: &AppWindow| {
let store = lib.session().and_then(|(_, s)| {
dr_thumbs::ThumbStore::open(&library::thumbs_dir(&s.server, &s.user_id))
.ok()
});
identity_ui::refresh_coverage(w, &catalog, store.as_ref());
w.set_identity_model_missing(
lib.session()
.and_then(|(_, s)| library::face_models(&s.server, &s.user_id))
.is_none(),
);
}
},
);
}
window.set_total(entries.borrow().len() as i32);