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
🐳 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:
@@ -247,7 +247,8 @@ impl FaceShardStore {
|
||||
}
|
||||
|
||||
pub fn shard_path(&self, shard: u32) -> PathBuf {
|
||||
self.dir.join(format!("shard-{}-{shard:04}.sqlite", self.client))
|
||||
self.dir
|
||||
.join(format!("shard-{}-{shard:04}.sqlite", self.client))
|
||||
}
|
||||
|
||||
/// Every shard, with whether it is sealed.
|
||||
@@ -300,7 +301,8 @@ impl FaceShardStore {
|
||||
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||
)?;
|
||||
|
||||
let mut q = src.prepare("SELECT file_id, model_id, faces_found, source_edge FROM indexed")?;
|
||||
let mut q =
|
||||
src.prepare("SELECT file_id, model_id, faces_found, source_edge FROM indexed")?;
|
||||
let images: Vec<(i64, String, i64, i64)> = q
|
||||
.query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)))?
|
||||
.collect::<Result<_, _>>()?;
|
||||
@@ -357,7 +359,10 @@ impl FaceShardStore {
|
||||
FROM faces WHERE file_id = ?1 AND model_id = ?2",
|
||||
)?;
|
||||
let faces: Vec<SharedFace> = q
|
||||
.query_map(rusqlite::params![file_id as i64, model_id], read_shared_face)?
|
||||
.query_map(
|
||||
rusqlite::params![file_id as i64, model_id],
|
||||
read_shared_face,
|
||||
)?
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(Some((faces, edge as u32)))
|
||||
}
|
||||
@@ -699,7 +704,11 @@ mod tests {
|
||||
}
|
||||
|
||||
let shards = s.shards().unwrap();
|
||||
assert!(shards.len() >= 2, "expected a seal, got {} shard(s)", shards.len());
|
||||
assert!(
|
||||
shards.len() >= 2,
|
||||
"expected a seal, got {} shard(s)",
|
||||
shards.len()
|
||||
);
|
||||
assert!(shards[0].sealed, "the first shard should be sealed");
|
||||
assert!(
|
||||
shards[0].bytes <= SHARD_MAX_BYTES,
|
||||
|
||||
@@ -364,10 +364,7 @@ pub type StoredEmbedding = (FaceId, ImageId, Vec<u8>, f32);
|
||||
/// Returned as raw f16 blobs rather than decoded vectors: the caller is
|
||||
/// `dr-face`, which owns the decoding, and a catalog that widened them here
|
||||
/// would double the memory of the one operation that holds them all at once.
|
||||
pub fn embeddings(
|
||||
conn: &Connection,
|
||||
model_id: &str,
|
||||
) -> Result<Vec<StoredEmbedding>, CatalogError> {
|
||||
pub fn embeddings(conn: &Connection, model_id: &str) -> Result<Vec<StoredEmbedding>, CatalogError> {
|
||||
let mut q = conn.prepare(
|
||||
"SELECT id, image_id, embedding, crop_px FROM faces
|
||||
WHERE model_id = ?1 ORDER BY id",
|
||||
@@ -401,11 +398,7 @@ pub fn create_person(conn: &Connection, name: &str) -> Result<PersonId, CatalogE
|
||||
}
|
||||
|
||||
/// Rename a person. The identity is untouched.
|
||||
pub fn rename_person(
|
||||
conn: &Connection,
|
||||
person: PersonId,
|
||||
name: &str,
|
||||
) -> Result<(), CatalogError> {
|
||||
pub fn rename_person(conn: &Connection, person: PersonId, name: &str) -> Result<(), CatalogError> {
|
||||
conn.execute(
|
||||
"UPDATE people SET name = ?2, revision = revision + 1, modified = ?3
|
||||
WHERE id = ?1",
|
||||
@@ -483,10 +476,7 @@ pub fn merge_people(
|
||||
}
|
||||
|
||||
/// Follow a merge redirect to the person that outlived it.
|
||||
pub fn resolve_person(
|
||||
conn: &Connection,
|
||||
person: PersonId,
|
||||
) -> Result<PersonId, CatalogError> {
|
||||
pub fn resolve_person(conn: &Connection, person: PersonId) -> Result<PersonId, CatalogError> {
|
||||
let mut at = person;
|
||||
// Bounded rather than `loop`: a redirect cycle would otherwise hang the UI
|
||||
// thread, and a corrupt index is exactly the case this has to survive.
|
||||
@@ -546,11 +536,7 @@ pub fn suggest(
|
||||
}
|
||||
|
||||
/// The user says this face is this person.
|
||||
pub fn confirm(
|
||||
conn: &Connection,
|
||||
face: FaceId,
|
||||
person: PersonId,
|
||||
) -> Result<(), CatalogError> {
|
||||
pub fn confirm(conn: &Connection, face: FaceId, person: PersonId) -> Result<(), CatalogError> {
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
// Confirming overrides an earlier rejection of the same pair: the user has
|
||||
// changed their mind, and the newer judgement is the one that counts.
|
||||
@@ -576,11 +562,7 @@ pub fn confirm(
|
||||
/// Stored rather than implied by removal, so the next clustering pass does not
|
||||
/// re-suggest it. This is user data in the same sense a confirmation is
|
||||
/// (FR-CULL-12) — a judgement, just a negative one.
|
||||
pub fn reject(
|
||||
conn: &Connection,
|
||||
face: FaceId,
|
||||
person: PersonId,
|
||||
) -> Result<(), CatalogError> {
|
||||
pub fn reject(conn: &Connection, face: FaceId, person: PersonId) -> Result<(), CatalogError> {
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO face_person_rejected (face_id, person_id)
|
||||
@@ -601,7 +583,10 @@ pub fn reject(
|
||||
/// the pool for the next clustering pass to place. Rejection is "not this
|
||||
/// person", and is remembered.
|
||||
pub fn unassign(conn: &Connection, face: FaceId) -> Result<(), CatalogError> {
|
||||
conn.execute("DELETE FROM face_person WHERE face_id = ?1", [face.0 as i64])?;
|
||||
conn.execute(
|
||||
"DELETE FROM face_person WHERE face_id = ?1",
|
||||
[face.0 as i64],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -873,7 +858,13 @@ mod tests {
|
||||
y: 0.1,
|
||||
w: 0.2,
|
||||
h: 0.3,
|
||||
landmarks: [(0.1, 0.1), (0.2, 0.1), (0.15, 0.2), (0.12, 0.25), (0.18, 0.25)],
|
||||
landmarks: [
|
||||
(0.1, 0.1),
|
||||
(0.2, 0.1),
|
||||
(0.15, 0.2),
|
||||
(0.12, 0.25),
|
||||
(0.18, 0.25),
|
||||
],
|
||||
confidence: 0.9,
|
||||
embedding: vec![seed; 1024],
|
||||
crop_px: 180.0,
|
||||
@@ -932,7 +923,11 @@ mod tests {
|
||||
|
||||
let got = for_image(&c, img).unwrap();
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].person, Some(anna), "confirmation was lost on re-index");
|
||||
assert_eq!(
|
||||
got[0].person,
|
||||
Some(anna),
|
||||
"confirmation was lost on re-index"
|
||||
);
|
||||
assert!(got[0].confirmed);
|
||||
}
|
||||
|
||||
@@ -962,7 +957,10 @@ mod tests {
|
||||
assert_eq!(for_image(&c, img).unwrap()[0].person, None);
|
||||
|
||||
let again = suggest(&c, ids[0], anna, 0.95).unwrap();
|
||||
assert!(!again, "clustering re-suggested a face the user pushed away");
|
||||
assert!(
|
||||
!again,
|
||||
"clustering re-suggested a face the user pushed away"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -48,7 +48,10 @@ fn main() {
|
||||
let dets = detector.detect(&rgb, w, h, &opts).expect("detect");
|
||||
let detect_ms = t.elapsed().as_secs_f64() * 1e3;
|
||||
|
||||
println!("\n{path} ({w}×{h}) {} face(s) in {detect_ms:.0} ms", dets.len());
|
||||
println!(
|
||||
"\n{path} ({w}×{h}) {} face(s) in {detect_ms:.0} ms",
|
||||
dets.len()
|
||||
);
|
||||
|
||||
for (i, d) in dets.iter().enumerate() {
|
||||
let Some(aligned) = align::warp(&rgb, w, h, &d.landmarks) else {
|
||||
|
||||
@@ -31,8 +31,8 @@ fn main() {
|
||||
|
||||
dr_face::install_backend_for_probe();
|
||||
|
||||
let session = ort::session::Session::builder()
|
||||
.and_then(|mut b| b.commit_from_memory(&bytes));
|
||||
let session =
|
||||
ort::session::Session::builder().and_then(|mut b| b.commit_from_memory(&bytes));
|
||||
|
||||
match session {
|
||||
Err(e) => {
|
||||
@@ -42,18 +42,10 @@ fn main() {
|
||||
Ok(s) => {
|
||||
println!(" LOADED");
|
||||
for i in s.inputs() {
|
||||
println!(
|
||||
" in {:<24} {:?}",
|
||||
i.name(),
|
||||
i.dtype().tensor_shape()
|
||||
);
|
||||
println!(" in {:<24} {:?}", i.name(), i.dtype().tensor_shape());
|
||||
}
|
||||
for o in s.outputs() {
|
||||
println!(
|
||||
" out {:<24} {:?}",
|
||||
o.name(),
|
||||
o.dtype().tensor_shape()
|
||||
);
|
||||
println!(" out {:<24} {:?}", o.name(), o.dtype().tensor_shape());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,10 @@ impl Similarity {
|
||||
let det = self.a * self.a + self.b * self.b;
|
||||
let du = u - self.tx;
|
||||
let dv = v - self.ty;
|
||||
((self.a * du + self.b * dv) / det, (-self.b * du + self.a * dv) / det)
|
||||
(
|
||||
(self.a * du + self.b * dv) / det,
|
||||
(-self.b * du + self.a * dv) / det,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,10 +256,7 @@ mod tests {
|
||||
let (s, c) = (rot.sin(), rot.cos());
|
||||
let mut out = [(0.0, 0.0); 5];
|
||||
for (i, &(x, y)) in ARCFACE_TEMPLATE.iter().enumerate() {
|
||||
out[i] = (
|
||||
scale * (c * x - s * y) + dx,
|
||||
scale * (s * x + c * y) + dy,
|
||||
);
|
||||
out[i] = (scale * (c * x - s * y) + dx, scale * (s * x + c * y) + dy);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -108,10 +108,7 @@ impl Calibration {
|
||||
/// What turns "merge above 0.9" into one comparison against a stored
|
||||
/// similarity, rather than a sigmoid evaluated per candidate edge.
|
||||
pub fn boundary_at(&self, p: f32, min_crop_px: f32, log_prior_odds: f32) -> f32 {
|
||||
((p / (1.0 - p)).ln()
|
||||
- self.b
|
||||
- self.w_size * min_crop_px.max(1.0).log2()
|
||||
- log_prior_odds)
|
||||
((p / (1.0 - p)).ln() - self.b - self.w_size * min_crop_px.max(1.0).log2() - log_prior_odds)
|
||||
/ self.a
|
||||
}
|
||||
}
|
||||
@@ -416,7 +413,10 @@ mod tests {
|
||||
b.count
|
||||
);
|
||||
}
|
||||
assert!(checked >= 2, "only {checked} bands had enough pairs to check");
|
||||
assert!(
|
||||
checked >= 2,
|
||||
"only {checked} bands had enough pairs to check"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -75,11 +75,7 @@ pub struct Cluster {
|
||||
/// probability between two groups. Deterministic: the same input yields the
|
||||
/// same clusters, because the merge order is by score with the index pair as
|
||||
/// the tiebreak.
|
||||
pub fn cluster(
|
||||
faces: &[Candidate],
|
||||
cal: &Calibration,
|
||||
min_probability: f32,
|
||||
) -> Vec<Cluster> {
|
||||
pub fn cluster(faces: &[Candidate], cal: &Calibration, min_probability: f32) -> Vec<Cluster> {
|
||||
if faces.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -150,7 +146,12 @@ pub fn cluster(
|
||||
})
|
||||
.collect();
|
||||
// Largest first: the People view shows the best-evidenced groups at the top.
|
||||
out.sort_by(|x, y| y.members.len().cmp(&x.members.len()).then(x.members[0].cmp(&y.members[0])));
|
||||
out.sort_by(|x, y| {
|
||||
y.members
|
||||
.len()
|
||||
.cmp(&x.members.len())
|
||||
.then(x.members[0].cmp(&y.members[0]))
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
@@ -123,8 +123,7 @@ impl Detector {
|
||||
for s in 0..fmc {
|
||||
let idx = group * fmc + s;
|
||||
let out = &session.outputs()[idx];
|
||||
let last: Option<i64> =
|
||||
out.dtype().tensor_shape().and_then(|d| d.last().copied());
|
||||
let last: Option<i64> = out.dtype().tensor_shape().and_then(|d| d.last().copied());
|
||||
if last != Some(expected_last) {
|
||||
return Err(FaceError::WrongModel {
|
||||
expected: "InsightFace SCRFD",
|
||||
|
||||
@@ -23,10 +23,7 @@ pub struct Embedder {
|
||||
}
|
||||
|
||||
impl Embedder {
|
||||
pub fn from_path(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
model: ModelId,
|
||||
) -> Result<Self, FaceError> {
|
||||
pub fn from_path(path: impl AsRef<std::path::Path>, model: ModelId) -> Result<Self, FaceError> {
|
||||
let bytes = std::fs::read(path).map_err(FaceError::ModelRead)?;
|
||||
Self::from_bytes(&bytes, model)
|
||||
}
|
||||
@@ -50,7 +47,11 @@ impl Embedder {
|
||||
if last != Some(EMBEDDING_DIM as i64) {
|
||||
return Err(FaceError::WrongModel {
|
||||
expected: "ArcFace",
|
||||
detail: format!("output '{}' is {:?}-wide, expected {EMBEDDING_DIM}", out.name(), last),
|
||||
detail: format!(
|
||||
"output '{}' is {:?}-wide, expected {EMBEDDING_DIM}",
|
||||
out.name(),
|
||||
last
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,13 @@ fn f32_to_f16_bits(x: f32) -> u16 {
|
||||
// Overflow, inf, or NaN. Embeddings are unit-norm so this is the
|
||||
// broken-model path; infinity is the honest answer, not a clamp that
|
||||
// hides it.
|
||||
return sign | 0x7c00 | if mant != 0 && exp == 0x1f + 112 { 0x200 } else { 0 };
|
||||
return sign
|
||||
| 0x7c00
|
||||
| if mant != 0 && exp == 0x1f + 112 {
|
||||
0x200
|
||||
} else {
|
||||
0
|
||||
};
|
||||
}
|
||||
if exp <= 0 {
|
||||
// Subnormal or underflow. A component of a unit 512-vector is ~0.04,
|
||||
@@ -195,7 +201,11 @@ mod tests {
|
||||
let a = unit(1);
|
||||
let mut b = unit(1);
|
||||
b.model = ModelId::new("other");
|
||||
assert_eq!(a.cosine(&b), None, "a cross-model cosine must not be a number");
|
||||
assert_eq!(
|
||||
a.cosine(&b),
|
||||
None,
|
||||
"a cross-model cosine must not be a number"
|
||||
);
|
||||
}
|
||||
|
||||
/// The claim docs/faces.md §6 makes about the storage format: the f16
|
||||
|
||||
@@ -33,22 +33,22 @@
|
||||
pub mod align;
|
||||
pub mod calibrate;
|
||||
pub mod cluster;
|
||||
pub mod embedding;
|
||||
pub mod naming;
|
||||
#[cfg(feature = "inference")]
|
||||
pub mod detect;
|
||||
#[cfg(feature = "inference")]
|
||||
pub mod embed;
|
||||
pub mod embedding;
|
||||
pub mod naming;
|
||||
|
||||
pub use align::{warp, Aligned112, Similarity, ALIGNED_EDGE, ARCFACE_TEMPLATE};
|
||||
pub use calibrate::{Calibration, Pairs, ReliabilityBand};
|
||||
pub use cluster::{cluster, split, Candidate, Cluster, DEFAULT_MERGE_PROBABILITY};
|
||||
pub use embedding::{Embedding, ModelId, EMBEDDING_DIM};
|
||||
pub use naming::{name_for_instance, name_instances, NamedFace};
|
||||
#[cfg(feature = "inference")]
|
||||
pub use detect::{DetectOptions, Detection, Detector};
|
||||
#[cfg(feature = "inference")]
|
||||
pub use embed::Embedder;
|
||||
pub use embedding::{Embedding, ModelId, EMBEDDING_DIM};
|
||||
pub use naming::{name_for_instance, name_instances, NamedFace};
|
||||
|
||||
/// What can go wrong between an image and a face.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
@@ -152,14 +152,20 @@ mod tests {
|
||||
fn a_tiny_face_in_a_large_person_still_matches() {
|
||||
let tall = (0.0, 0.0, 500.0, 2000.0);
|
||||
let small = (240.0, 40.0, 280.0, 100.0);
|
||||
assert_eq!(name_for_instance(tall, &[named(small, "Anna")]), Some("Anna"));
|
||||
assert_eq!(
|
||||
name_for_instance(tall, &[named(small, "Anna")]),
|
||||
Some("Anna")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_face_mostly_outside_the_person_is_not_theirs() {
|
||||
// Overlapping the person's edge, but only just.
|
||||
let straddling = (60.0, 80.0, 140.0, 220.0);
|
||||
assert_eq!(name_for_instance(PERSON, &[named(straddling, "Anna")]), None);
|
||||
assert_eq!(
|
||||
name_for_instance(PERSON, &[named(straddling, "Anna")]),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// A tight portrait, where the segmenter's "person" is head and shoulders
|
||||
@@ -175,7 +181,10 @@ mod tests {
|
||||
fn a_tight_portrait_is_named_rather_than_treated_as_suspicious() {
|
||||
let head = (100.0, 50.0, 400.0, 400.0);
|
||||
let face = (110.0, 60.0, 390.0, 390.0);
|
||||
assert_eq!(name_for_instance(head, &[named(face, "Anna")]), Some("Anna"));
|
||||
assert_eq!(
|
||||
name_for_instance(head, &[named(face, "Anna")]),
|
||||
Some("Anna")
|
||||
);
|
||||
}
|
||||
|
||||
/// A face box *larger* than the instance is a genuine disagreement, and
|
||||
@@ -185,7 +194,10 @@ mod tests {
|
||||
fn a_face_larger_than_the_instance_does_not_name_it() {
|
||||
let small_instance = (200.0, 200.0, 260.0, 260.0);
|
||||
let huge_face = (100.0, 100.0, 500.0, 500.0);
|
||||
assert_eq!(name_for_instance(small_instance, &[named(huge_face, "Anna")]), None);
|
||||
assert_eq!(
|
||||
name_for_instance(small_instance, &[named(huge_face, "Anna")]),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Two people merged into one blob: naming either would be a coin toss,
|
||||
@@ -226,8 +238,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn degenerate_boxes_name_nothing_rather_than_panicking() {
|
||||
assert_eq!(name_for_instance((0.0, 0.0, 0.0, 0.0), &[named(FACE, "A")]), None);
|
||||
assert_eq!(name_for_instance(PERSON, &[named((5.0, 5.0, 5.0, 5.0), "A")]), None);
|
||||
assert_eq!(
|
||||
name_for_instance((0.0, 0.0, 0.0, 0.0), &[named(FACE, "A")]),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
name_for_instance(PERSON, &[named((5.0, 5.0, 5.0, 5.0), "A")]),
|
||||
None
|
||||
);
|
||||
assert_eq!(name_for_instance(PERSON, &[]), None);
|
||||
}
|
||||
|
||||
@@ -282,4 +300,3 @@ mod tests {
|
||||
assert_eq!(instances[0].class, "person");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user