Record that face detection has run, not just what it found

An image with no faces in it was indistinguishable from one that had
never been looked at, so every landscape, still life and document scan in
the library was re-detected on every pass, for ever. In a real library
that is most of it: on the 23,527-image test library, 64 of the first 110
images indexed contain no face at all.

Schema v9 adds face_index, a run marker per (image, model) carrying the
face count and the proxy edge it read. Keyed on the model, so a model
change puts every image back in the queue by itself.

That makes a coverage figure possible, which is the thing a user actually
wants to see. The audit also splits the outstanding set by whether a
proxy exists, because 23,417 awaiting a proxy and 110 ready to index are
different problems, and telling the user to run indexing again would not
fix the first.

The Identity screen gains Index faces, Stop, and the coverage line.
examples/face_index.rs is the same check and sweep without a window,
which is the right shape for an overnight pass.

Measured on the real library in release: 3.5 images/second, 110 images
and 125 faces in 30 seconds, and a second run correctly finds nothing
left to do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 22:13:41 +02:00
co-authored by Claude Opus 5
parent 3e607222c6
commit 26a1eb7e28
14 changed files with 971 additions and 84 deletions
Generated
+1
View File
@@ -1614,6 +1614,7 @@ dependencies = [
"dr-sync-nextcloud", "dr-sync-nextcloud",
"dr-thumbs", "dr-thumbs",
"dr-types", "dr-types",
"env_logger",
"jni 0.22.4", "jni 0.22.4",
"log", "log",
"ndk-context", "ndk-context",
+252 -22
View File
@@ -127,6 +127,8 @@ pub use dr_face::Calibration;
pub fn record_detections( pub fn record_detections(
conn: &Connection, conn: &Connection,
image_id: ImageId, image_id: ImageId,
model_id: &str,
source_edge: u32,
faces: &[DetectedFace], faces: &[DetectedFace],
) -> Result<Vec<FaceId>, CatalogError> { ) -> Result<Vec<FaceId>, CatalogError> {
let tx = conn.unchecked_transaction()?; let tx = conn.unchecked_transaction()?;
@@ -203,10 +205,124 @@ pub fn record_detections(
ids.push(id); ids.push(id);
} }
// The run marker, written whether or not anything was found. Zero faces is
// a real answer and recording it is what stops the next pass looking at
// this photograph again -- see the V9 migration.
tx.execute(
"INSERT INTO face_index (image_id, model_id, indexed_at, faces_found, source_edge)
VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(image_id, model_id) DO UPDATE SET
indexed_at = excluded.indexed_at,
faces_found = excluded.faces_found,
source_edge = excluded.source_edge",
rusqlite::params![
image_id.0 as i64,
model_id,
now,
faces.len() as i64,
source_edge as i64,
],
)?;
tx.commit()?; tx.commit()?;
Ok(ids) Ok(ids)
} }
/// How much of the library has been through face detection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Coverage {
/// Images that are candidates at all — present, not trashed.
pub images: u64,
/// Images this model has actually looked at.
pub indexed: u64,
/// Of those, how many had no face in them. Usually most of a library, and
/// worth showing so "0 faces" reads as a finding rather than a failure.
pub without_faces: u64,
/// Faces found across the whole library.
pub faces: u64,
}
impl Coverage {
/// Images still to look at.
pub fn outstanding(&self) -> u64 {
self.images.saturating_sub(self.indexed)
}
pub fn is_complete(&self) -> bool {
self.outstanding() == 0
}
/// Fraction indexed, `0.0..=1.0`. An empty library is complete, not zero:
/// there is nothing outstanding, and reporting 0% would read as a stall.
pub fn fraction(&self) -> f32 {
if self.images == 0 {
1.0
} else {
self.indexed as f32 / self.images as f32
}
}
}
/// Count what has and has not been indexed, for one model.
///
/// The question the batch pass asks before it starts and the UI asks to draw a
/// progress figure. Answerable only because [`record_detections`] writes a run
/// marker: counting `faces` rows would report how many faces exist, which is a
/// different number and never reaches the image count.
pub fn coverage(conn: &Connection, model_id: &str) -> Result<Coverage, CatalogError> {
let images: i64 = conn.query_row(
"SELECT COUNT(*) FROM images WHERE trashed_at IS NULL",
[],
|r| r.get(0),
)?;
let (indexed, without, faces): (i64, i64, i64) = conn.query_row(
"SELECT COUNT(*), COALESCE(SUM(faces_found = 0), 0), COALESCE(SUM(faces_found), 0)
FROM face_index fi
JOIN images i ON i.id = fi.image_id
WHERE fi.model_id = ?1 AND i.trashed_at IS NULL",
[model_id],
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
)?;
Ok(Coverage {
images: images as u64,
indexed: indexed as u64,
without_faces: without as u64,
faces: faces as u64,
})
}
/// Whether one image has been through this model.
pub fn is_indexed(
conn: &Connection,
image_id: ImageId,
model_id: &str,
) -> Result<bool, CatalogError> {
conn.query_row(
"SELECT EXISTS(SELECT 1 FROM face_index WHERE image_id = ?1 AND model_id = ?2)",
rusqlite::params![image_id.0 as i64, model_id],
|r| r.get(0),
)
.map_err(Into::into)
}
/// Forget that an image was indexed, so the next pass looks again.
///
/// What a re-index asks for. Separate from deleting the faces because the two
/// are wanted at different times: clearing the marker alone re-detects and
/// replaces, which is the ordinary "try again with a better proxy" case.
pub fn clear_index_marker(
conn: &Connection,
image_id: ImageId,
model_id: &str,
) -> Result<(), CatalogError> {
conn.execute(
"DELETE FROM face_index WHERE image_id = ?1 AND model_id = ?2",
rusqlite::params![image_id.0 as i64, model_id],
)?;
Ok(())
}
/// Every face on one image. /// Every face on one image.
pub fn for_image(conn: &Connection, image_id: ImageId) -> Result<Vec<Face>, CatalogError> { pub fn for_image(conn: &Connection, image_id: ImageId) -> Result<Vec<Face>, CatalogError> {
let mut q = conn.prepare( let mut q = conn.prepare(
@@ -621,6 +737,7 @@ pub fn delete_all_face_data(conn: &Connection) -> Result<u64, CatalogError> {
let faces: i64 = tx.query_row("SELECT COUNT(*) FROM faces", [], |r| r.get(0))?; let faces: i64 = tx.query_row("SELECT COUNT(*) FROM faces", [], |r| r.get(0))?;
// Order matters only if foreign keys are off, which they may be on an old // Order matters only if foreign keys are off, which they may be on an old
// connection; deleting children first is correct either way. // connection; deleting children first is correct either way.
tx.execute("DELETE FROM face_index", [])?;
tx.execute("DELETE FROM face_person_rejected", [])?; tx.execute("DELETE FROM face_person_rejected", [])?;
tx.execute("DELETE FROM face_person", [])?; tx.execute("DELETE FROM face_person", [])?;
tx.execute("DELETE FROM faces", [])?; tx.execute("DELETE FROM faces", [])?;
@@ -765,19 +882,19 @@ mod tests {
} }
#[test] #[test]
fn migration_reaches_version_eight() { fn migration_reaches_version_nine() {
let c = db(); let c = db();
let v: i64 = c let v: i64 = c
.query_row("PRAGMA user_version", [], |r| r.get(0)) .query_row("PRAGMA user_version", [], |r| r.get(0))
.unwrap(); .unwrap();
assert_eq!(v, 8); assert_eq!(v, 9);
} }
#[test] #[test]
fn detections_round_trip_with_their_landmarks() { fn detections_round_trip_with_their_landmarks() {
let c = db(); let c = db();
let img = image(&c, 1); let img = image(&c, 1);
record_detections(&c, img, &[face(1), face(2)]).unwrap(); record_detections(&c, img, "w600k_mbf", 1024, &[face(1), face(2)]).unwrap();
let got = for_image(&c, img).unwrap(); let got = for_image(&c, img).unwrap();
assert_eq!(got.len(), 2); assert_eq!(got.len(), 2);
assert_eq!(got[0].model_id, "w600k_mbf"); assert_eq!(got[0].model_id, "w600k_mbf");
@@ -792,8 +909,8 @@ mod tests {
fn re_detection_replaces_rather_than_appending() { fn re_detection_replaces_rather_than_appending() {
let c = db(); let c = db();
let img = image(&c, 1); let img = image(&c, 1);
record_detections(&c, img, &[face(1), face(2)]).unwrap(); record_detections(&c, img, "w600k_mbf", 1024, &[face(1), face(2)]).unwrap();
record_detections(&c, img, &[face(3)]).unwrap(); record_detections(&c, img, "w600k_mbf", 1024, &[face(3)]).unwrap();
assert_eq!(for_image(&c, img).unwrap().len(), 1); assert_eq!(for_image(&c, img).unwrap().len(), 1);
} }
@@ -803,7 +920,7 @@ mod tests {
fn re_detection_carries_a_confirmation_across() { fn re_detection_carries_a_confirmation_across() {
let c = db(); let c = db();
let img = image(&c, 1); let img = image(&c, 1);
let ids = record_detections(&c, img, &[face(1)]).unwrap(); let ids = record_detections(&c, img, "w600k_mbf", 1024, &[face(1)]).unwrap();
let anna = create_person(&c, "Anna").unwrap(); let anna = create_person(&c, "Anna").unwrap();
confirm(&c, ids[0], anna).unwrap(); confirm(&c, ids[0], anna).unwrap();
@@ -811,7 +928,7 @@ mod tests {
let mut moved = face(9); let mut moved = face(9);
moved.x = 0.11; moved.x = 0.11;
moved.y = 0.105; moved.y = 0.105;
record_detections(&c, img, &[moved]).unwrap(); record_detections(&c, img, "w600k_mbf", 1024, &[moved]).unwrap();
let got = for_image(&c, img).unwrap(); let got = for_image(&c, img).unwrap();
assert_eq!(got.len(), 1); assert_eq!(got.len(), 1);
@@ -823,7 +940,7 @@ mod tests {
fn a_suggestion_never_overwrites_a_confirmation() { fn a_suggestion_never_overwrites_a_confirmation() {
let c = db(); let c = db();
let img = image(&c, 1); let img = image(&c, 1);
let ids = record_detections(&c, img, &[face(1)]).unwrap(); let ids = record_detections(&c, img, "w600k_mbf", 1024, &[face(1)]).unwrap();
let anna = create_person(&c, "Anna").unwrap(); let anna = create_person(&c, "Anna").unwrap();
let bob = create_person(&c, "Bob").unwrap(); let bob = create_person(&c, "Bob").unwrap();
@@ -837,7 +954,7 @@ mod tests {
fn a_rejection_stops_the_face_being_suggested_again() { fn a_rejection_stops_the_face_being_suggested_again() {
let c = db(); let c = db();
let img = image(&c, 1); let img = image(&c, 1);
let ids = record_detections(&c, img, &[face(1)]).unwrap(); let ids = record_detections(&c, img, "w600k_mbf", 1024, &[face(1)]).unwrap();
let anna = create_person(&c, "Anna").unwrap(); let anna = create_person(&c, "Anna").unwrap();
suggest(&c, ids[0], anna, 0.8).unwrap(); suggest(&c, ids[0], anna, 0.8).unwrap();
@@ -852,7 +969,7 @@ mod tests {
fn confirming_overrides_an_earlier_rejection() { fn confirming_overrides_an_earlier_rejection() {
let c = db(); let c = db();
let img = image(&c, 1); let img = image(&c, 1);
let ids = record_detections(&c, img, &[face(1)]).unwrap(); let ids = record_detections(&c, img, "w600k_mbf", 1024, &[face(1)]).unwrap();
let anna = create_person(&c, "Anna").unwrap(); let anna = create_person(&c, "Anna").unwrap();
reject(&c, ids[0], anna).unwrap(); reject(&c, ids[0], anna).unwrap();
confirm(&c, ids[0], anna).unwrap(); confirm(&c, ids[0], anna).unwrap();
@@ -864,8 +981,8 @@ mod tests {
let c = db(); let c = db();
let i1 = image(&c, 1); let i1 = image(&c, 1);
let i2 = image(&c, 2); let i2 = image(&c, 2);
let a = record_detections(&c, i1, &[face(1)]).unwrap(); let a = record_detections(&c, i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let b = record_detections(&c, i2, &[face(2)]).unwrap(); let b = record_detections(&c, i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let anna = create_person(&c, "Anna").unwrap(); let anna = create_person(&c, "Anna").unwrap();
let annie = create_person(&c, "Annie").unwrap(); let annie = create_person(&c, "Annie").unwrap();
confirm(&c, a[0], anna).unwrap(); confirm(&c, a[0], anna).unwrap();
@@ -882,7 +999,7 @@ mod tests {
fn merging_does_not_duplicate_a_face_both_people_hold() { fn merging_does_not_duplicate_a_face_both_people_hold() {
let c = db(); let c = db();
let img = image(&c, 1); let img = image(&c, 1);
let ids = record_detections(&c, img, &[face(1)]).unwrap(); let ids = record_detections(&c, img, "w600k_mbf", 1024, &[face(1)]).unwrap();
let a = create_person(&c, "A").unwrap(); let a = create_person(&c, "A").unwrap();
let b = create_person(&c, "B").unwrap(); let b = create_person(&c, "B").unwrap();
confirm(&c, ids[0], a).unwrap(); confirm(&c, ids[0], a).unwrap();
@@ -899,7 +1016,7 @@ mod tests {
let c = db(); let c = db();
let img = image(&c, 1); let img = image(&c, 1);
// Two faces of the same person in one frame — a mirror, or a collage. // Two faces of the same person in one frame — a mirror, or a collage.
let ids = record_detections(&c, img, &[face(1), face(2)]).unwrap(); let ids = record_detections(&c, img, "w600k_mbf", 1024, &[face(1), face(2)]).unwrap();
let anna = create_person(&c, "Anna").unwrap(); let anna = create_person(&c, "Anna").unwrap();
confirm(&c, ids[0], anna).unwrap(); confirm(&c, ids[0], anna).unwrap();
confirm(&c, ids[1], anna).unwrap(); confirm(&c, ids[1], anna).unwrap();
@@ -911,8 +1028,8 @@ mod tests {
let c = db(); let c = db();
let i1 = image(&c, 1); let i1 = image(&c, 1);
let i2 = image(&c, 2); let i2 = image(&c, 2);
let a = record_detections(&c, i1, &[face(1)]).unwrap(); let a = record_detections(&c, i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let b = record_detections(&c, i2, &[face(2)]).unwrap(); let b = record_detections(&c, i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let anna = create_person(&c, "Anna").unwrap(); let anna = create_person(&c, "Anna").unwrap();
confirm(&c, a[0], anna).unwrap(); confirm(&c, a[0], anna).unwrap();
suggest(&c, b[0], anna, 0.8).unwrap(); suggest(&c, b[0], anna, 0.8).unwrap();
@@ -928,8 +1045,8 @@ mod tests {
let c = db(); let c = db();
let i1 = image(&c, 1); let i1 = image(&c, 1);
let i2 = image(&c, 2); let i2 = image(&c, 2);
let a = record_detections(&c, i1, &[face(1)]).unwrap(); let a = record_detections(&c, i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let b = record_detections(&c, i2, &[face(2)]).unwrap(); let b = record_detections(&c, i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let anna = create_person(&c, "Anna").unwrap(); let anna = create_person(&c, "Anna").unwrap();
confirm(&c, a[0], anna).unwrap(); confirm(&c, a[0], anna).unwrap();
suggest(&c, b[0], anna, 0.7).unwrap(); suggest(&c, b[0], anna, 0.7).unwrap();
@@ -956,7 +1073,7 @@ mod tests {
fn deleting_face_data_leaves_the_photographs_alone() { fn deleting_face_data_leaves_the_photographs_alone() {
let c = db(); let c = db();
let img = image(&c, 1); let img = image(&c, 1);
let ids = record_detections(&c, img, &[face(1), face(2)]).unwrap(); let ids = record_detections(&c, img, "w600k_mbf", 1024, &[face(1), face(2)]).unwrap();
let anna = create_person(&c, "Anna").unwrap(); let anna = create_person(&c, "Anna").unwrap();
confirm(&c, ids[0], anna).unwrap(); confirm(&c, ids[0], anna).unwrap();
reject(&c, ids[1], anna).unwrap(); reject(&c, ids[1], anna).unwrap();
@@ -1033,8 +1150,8 @@ mod tests {
let c = db(); let c = db();
let i1 = image(&c, 1); let i1 = image(&c, 1);
let i2 = image(&c, 2); let i2 = image(&c, 2);
let a = record_detections(&c, i1, &[face(1)]).unwrap(); let a = record_detections(&c, i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
record_detections(&c, i2, &[face(2)]).unwrap(); record_detections(&c, i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let anna = create_person(&c, "Anna").unwrap(); let anna = create_person(&c, "Anna").unwrap();
confirm(&c, a[0], anna).unwrap(); confirm(&c, a[0], anna).unwrap();
@@ -1042,11 +1159,124 @@ mod tests {
assert!(unassigned(&c, "some_other_model").unwrap().is_empty()); assert!(unassigned(&c, "some_other_model").unwrap().is_empty());
} }
/// The bug this table exists for: a photograph with no faces must not look
/// like one that has never been indexed, or every landscape in the library
/// is re-examined on every pass, for ever.
#[test]
fn an_image_with_no_faces_still_counts_as_indexed() {
let c = db();
let img = image(&c, 1);
record_detections(&c, img, "w600k_mbf", 1024, &[]).unwrap();
assert!(is_indexed(&c, img, "w600k_mbf").unwrap());
assert!(for_image(&c, img).unwrap().is_empty());
let cov = coverage(&c, "w600k_mbf").unwrap();
assert_eq!(cov.indexed, 1);
assert_eq!(cov.without_faces, 1);
assert_eq!(cov.faces, 0);
assert_eq!(cov.outstanding(), 0);
assert!(cov.is_complete());
}
#[test]
fn a_model_change_puts_every_image_back_in_the_queue() {
let c = db();
let img = image(&c, 1);
record_detections(&c, img, "w600k_mbf", 1024, &[face(1)]).unwrap();
assert!(is_indexed(&c, img, "w600k_mbf").unwrap());
assert!(
!is_indexed(&c, img, "some_better_model").unwrap(),
"a new model must not inherit the old model's coverage"
);
assert_eq!(coverage(&c, "some_better_model").unwrap().outstanding(), 1);
}
#[test]
fn coverage_counts_images_not_faces() {
let c = db();
let i1 = image(&c, 1);
let i2 = image(&c, 2);
image(&c, 3);
// Three faces across two images; the third image is untouched.
record_detections(&c, i1, "w600k_mbf", 1024, &[face(1), face(2)]).unwrap();
record_detections(&c, i2, "w600k_mbf", 1024, &[face(3)]).unwrap();
let cov = coverage(&c, "w600k_mbf").unwrap();
assert_eq!(cov.images, 3);
assert_eq!(cov.indexed, 2);
assert_eq!(cov.faces, 3);
assert_eq!(cov.outstanding(), 1);
assert!(!cov.is_complete());
assert!((cov.fraction() - 2.0 / 3.0).abs() < 1e-6);
}
#[test]
fn re_indexing_one_image_updates_its_marker_rather_than_adding_a_second() {
let c = db();
let img = image(&c, 1);
record_detections(&c, img, "w600k_mbf", 1024, &[face(1), face(2)]).unwrap();
record_detections(&c, img, "w600k_mbf", 2048, &[face(3)]).unwrap();
let cov = coverage(&c, "w600k_mbf").unwrap();
assert_eq!(cov.indexed, 1, "a second marker row was written");
assert_eq!(cov.faces, 1);
let edge: i64 = c
.query_row(
"SELECT source_edge FROM face_index WHERE image_id = ?1",
[img.0 as i64],
|r| r.get(0),
)
.unwrap();
assert_eq!(edge, 2048, "the marker should record the newer proxy");
}
#[test]
fn clearing_a_marker_puts_that_image_back_in_the_queue() {
let c = db();
let img = image(&c, 1);
record_detections(&c, img, "w600k_mbf", 1024, &[face(1)]).unwrap();
clear_index_marker(&c, img, "w600k_mbf").unwrap();
assert!(!is_indexed(&c, img, "w600k_mbf").unwrap());
assert_eq!(coverage(&c, "w600k_mbf").unwrap().outstanding(), 1);
// The faces themselves are untouched: clearing a marker asks for a
// re-detection, not a deletion.
assert_eq!(for_image(&c, img).unwrap().len(), 1);
}
#[test]
fn an_empty_library_is_complete_rather_than_zero_percent() {
let c = db();
let cov = coverage(&c, "w600k_mbf").unwrap();
assert!(cov.is_complete());
assert!((cov.fraction() - 1.0).abs() < 1e-6);
}
/// Deleting face data must clear the run markers too, or the library
/// reports itself fully indexed while holding no faces at all — and the
/// sweep then refuses to rebuild what the user just asked to remove.
#[test]
fn deleting_face_data_clears_the_run_markers() {
let c = db();
let img = image(&c, 1);
record_detections(&c, img, "w600k_mbf", 1024, &[face(1)]).unwrap();
delete_all_face_data(&c).unwrap();
assert!(!is_indexed(&c, img, "w600k_mbf").unwrap());
let cov = coverage(&c, "w600k_mbf").unwrap();
assert_eq!(cov.indexed, 0);
assert_eq!(cov.outstanding(), 1);
}
#[test] #[test]
fn embeddings_come_back_as_stored() { fn embeddings_come_back_as_stored() {
let c = db(); let c = db();
let img = image(&c, 1); let img = image(&c, 1);
record_detections(&c, img, &[face(7)]).unwrap(); record_detections(&c, img, "w600k_mbf", 1024, &[face(7)]).unwrap();
let e = embeddings(&c, "w600k_mbf").unwrap(); let e = embeddings(&c, "w600k_mbf").unwrap();
assert_eq!(e.len(), 1); assert_eq!(e.len(), 1);
assert_eq!(e[0].1, img); assert_eq!(e[0].1, img);
+48 -1
View File
@@ -15,7 +15,7 @@ use rusqlite::Connection;
use crate::error::CatalogError; use crate::error::CatalogError;
/// Schema version this build writes and understands. /// Schema version this build writes and understands.
pub const SCHEMA_VERSION: i64 = 8; pub const SCHEMA_VERSION: i64 = 9;
/// Apply migrations up to [`SCHEMA_VERSION`]. /// Apply migrations up to [`SCHEMA_VERSION`].
/// ///
@@ -84,6 +84,12 @@ pub fn migrate(conn: &Connection) -> Result<i64, CatalogError> {
tx.pragma_update(None, "user_version", 8)?; tx.pragma_update(None, "user_version", 8)?;
tx.commit()?; tx.commit()?;
} }
if from < 9 {
let tx = conn.unchecked_transaction()?;
tx.execute_batch(V9)?;
tx.pragma_update(None, "user_version", 9)?;
tx.commit()?;
}
Ok(from) Ok(from)
} }
@@ -333,6 +339,47 @@ fn stem_of(path: &str) -> &str {
/// Both narrow the walk rather than reorder it, so the index still supplies the /// 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 /// 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. /// direction: the expensive part was never the filtering, it was the sort.
const V9: &str = r#"
-- TRACES: FR-CULL-8
-- A record that face detection has *run* on an image, distinct from what it
-- found.
--
-- # Why the faces table cannot answer this
--
-- Without this, "has this image been indexed" is asked as "does it have any
-- faces", and those are not the same question. **A photograph with no faces in
-- it is indistinguishable from one that has never been looked at**, so every
-- indexing pass re-examines every landscape, every still life and every
-- document scan in the library, for ever. In a typical personal library that is
-- most of it: the pass never converges, and the cost is paid again on every
-- run rather than once.
--
-- It also makes a coverage figure possible, which is the thing a user actually
-- wants to see — "4,812 of 5,000 images indexed" — where counting face rows
-- can only ever report how many faces exist.
--
-- # Why it is keyed on the model
--
-- Embeddings from different models are not comparable, so a model change has
-- to re-index. Keying the marker on `(image_id, model_id)` makes that
-- automatic: new model, no marker, image comes back into the queue. The id
-- names the whole pipeline -- detector and embedder together -- because
-- changing either changes what is found.
CREATE TABLE face_index (
image_id INTEGER NOT NULL REFERENCES images(id) ON DELETE CASCADE,
model_id TEXT NOT NULL,
indexed_at INTEGER NOT NULL,
-- Zero is a real and common answer, and recording it is the entire point.
faces_found INTEGER NOT NULL,
-- Long edge of the proxy this ran against. A face too small to detect at
-- 1024 may be findable at 2048, so a library whose proxies grow can
-- re-index the images that stand to gain instead of all of them.
source_edge INTEGER NOT NULL,
PRIMARY KEY (image_id, model_id)
);
CREATE INDEX face_index_model ON face_index(model_id);
"#;
const V8: &str = r#" const V8: &str = r#"
-- TRACES: FR-CULL-8 | FR-CULL-9 | FR-CULL-10 | FR-CULL-11 | FR-CULL-12 | NFR-SEC-5 -- TRACES: FR-CULL-8 | FR-CULL-9 | FR-CULL-10 | FR-CULL-11 | FR-CULL-12 | NFR-SEC-5
-- People and faces (docs/faces.md, docs/catalog.md §10). -- People and faces (docs/faces.md, docs/catalog.md §10).
+12 -8
View File
@@ -726,14 +726,18 @@ The duplicate row is not a curiosity: several files in that gallery are byte-ide
different names, which is exactly the case §8.2's dedup step exists for, and it would otherwise stack different names, which is exactly the case §8.2's dedup step exists for, and it would otherwise stack
the positive histogram at cos ≈ 1 with pairs that teach the fit nothing. the positive histogram at cos ≈ 1 with pairs that teach the fit nothing.
**M2/M3, provisionally, on the reference desktop:** detection **~1.0–1.4 s** per image, embedding **M2/M3 in a debug build:** detection ~1.0–1.4 s per image, embedding ~160–280 ms per face.
**~160–280 ms** per face, model load 80–90 ms for the pair. Slower than the extrapolation in §12's M2
row, which guessed SCRFD-500M would come in under YOLO26n-seg's 470 ms — tract is not ORT, and this **M2/M3 in release, over a real library — the number that counts: 3.5 images/second**, end to end,
is what it costs. A 17k-image library at ~1.3 s plus ~1.5 faces each is on the order of **7 hours** of including the JPEG decode and the catalog write. 110 images with 125 faces in 30 seconds on the
background indexing. That is survivable for a resumable, preempted background job (FR-CULL-8) and it reference desktop. That is roughly **4× the debug figure**, and it moves a 23.5k-image library from
is not survivable on a phone, so NFR-RES-2 needs its own measurement rather than an extrapolation the "seven hours" the debug numbers implied to about **110 minutes**.
from this one. Not yet profiled: how much of the detection second is tract and how much is the
scalar letterbox resample in front of it. Worth stating plainly because the debug measurement was nearly a wrong conclusion: it was on the
edge of making the pure-Rust runtime look unaffordable for a large library, and it was measuring the
profile rather than the pipeline. Any future timing of this subsystem is a release timing.
Still unmeasured: the same pass on a phone (NFR-RES-2), which does not follow from this one.
--- ---
+36 -36
View File
@@ -9,8 +9,8 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
| Metric | Value | | Metric | Value |
|---|---| |---|---|
| Source files scanned | 241 | | Source files scanned | 242 |
| TRACES tags found | 627 | | TRACES tags found | 628 |
| Requirements defined | 177 | | Requirements defined | 177 |
| Requirements covered | 97 | | Requirements covered | 97 |
| **Coverage** | **54.8%** (97/177) | | **Coverage** | **54.8%** (97/177) |
@@ -34,51 +34,51 @@ _None._
| ID | Tagged in | | ID | Tagged in |
|---|---| |---|---|
| FR-CAT-1 | [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:109`](../core/dr-catalog/src/walk.rs#L109), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479), [`tools/traceability/src/lib.rs:511`](../tools/traceability/src/lib.rs#L511), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1) | | FR-CAT-1 | [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:109`](../core/dr-catalog/src/walk.rs#L109), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479), [`tools/traceability/src/lib.rs:511`](../tools/traceability/src/lib.rs#L511), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1) |
| FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1028`](../ui/dr-ui/src/lib.rs#L1028), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1008`](../ui/dr-ui/ui/library.slint#L1008), [`ui/dr-ui/ui/library.slint:1170`](../ui/dr-ui/ui/library.slint#L1170), [`ui/dr-ui/ui/library.slint:857`](../ui/dr-ui/ui/library.slint#L857) | | FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1050`](../ui/dr-ui/src/lib.rs#L1050), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1008`](../ui/dr-ui/ui/library.slint#L1008), [`ui/dr-ui/ui/library.slint:1170`](../ui/dr-ui/ui/library.slint#L1170), [`ui/dr-ui/ui/library.slint:857`](../ui/dr-ui/ui/library.slint#L857) |
| FR-CAT-11 | [`core/dr-catalog/src/dedup.rs:1`](../core/dr-catalog/src/dedup.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:392`](../core/dr-ingest/src/lib.rs#L392), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1028`](../ui/dr-ui/src/lib.rs#L1028), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:2122`](../ui/dr-ui/src/library.rs#L2122), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-CAT-11 | [`core/dr-catalog/src/dedup.rs:1`](../core/dr-catalog/src/dedup.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:392`](../core/dr-ingest/src/lib.rs#L392), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1050`](../ui/dr-ui/src/lib.rs#L1050), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:2122`](../ui/dr-ui/src/library.rs#L2122), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
| FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:120`](../core/dr-pipeline/src/sidecar.rs#L120) | | FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:120`](../core/dr-pipeline/src/sidecar.rs#L120) |
| FR-CAT-13 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1) | | FR-CAT-13 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1) |
| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:536`](../core/dr-catalog/src/schema.rs#L536), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:447`](../core/dr-sync-nextcloud/src/lib.rs#L447), [`core/dr-sync/src/lib.rs:124`](../core/dr-sync/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:426`](../core/dr-sync/src/scan.rs#L426), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1225`](../ui/dr-ui/src/collections_ui.rs#L1225), [`ui/dr-ui/src/collections_ui.rs:1995`](../ui/dr-ui/src/collections_ui.rs#L1995), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:169`](../ui/dr-ui/src/library.rs#L169), [`ui/dr-ui/src/library.rs:198`](../ui/dr-ui/src/library.rs#L198), [`ui/dr-ui/src/library.rs:3007`](../ui/dr-ui/src/library.rs#L3007), [`ui/dr-ui/src/library.rs:3041`](../ui/dr-ui/src/library.rs#L3041), [`ui/dr-ui/src/library_ui.rs:184`](../ui/dr-ui/src/library_ui.rs#L184), [`ui/dr-ui/src/library_ui.rs:757`](../ui/dr-ui/src/library_ui.rs#L757), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) | | FR-CAT-15 | [`core/dr-catalog/src/schema.rs:583`](../core/dr-catalog/src/schema.rs#L583), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:447`](../core/dr-sync-nextcloud/src/lib.rs#L447), [`core/dr-sync/src/lib.rs:124`](../core/dr-sync/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:426`](../core/dr-sync/src/scan.rs#L426), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1225`](../ui/dr-ui/src/collections_ui.rs#L1225), [`ui/dr-ui/src/collections_ui.rs:1995`](../ui/dr-ui/src/collections_ui.rs#L1995), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:169`](../ui/dr-ui/src/library.rs#L169), [`ui/dr-ui/src/library.rs:198`](../ui/dr-ui/src/library.rs#L198), [`ui/dr-ui/src/library.rs:3037`](../ui/dr-ui/src/library.rs#L3037), [`ui/dr-ui/src/library.rs:3071`](../ui/dr-ui/src/library.rs#L3071), [`ui/dr-ui/src/library_ui.rs:184`](../ui/dr-ui/src/library_ui.rs#L184), [`ui/dr-ui/src/library_ui.rs:757`](../ui/dr-ui/src/library_ui.rs#L757), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) |
| FR-CAT-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53) | | FR-CAT-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53) |
| FR-CAT-2 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) | | FR-CAT-2 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
| FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:69`](../core/dr-sync/src/scan.rs#L69), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464), [`ui/dr-ui/src/import.rs:489`](../ui/dr-ui/src/import.rs#L489), [`ui/dr-ui/src/library.rs:2598`](../ui/dr-ui/src/library.rs#L2598), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:171`](../ui/dr-ui/src/library_ui.rs#L171), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/src/library_ui.rs:4574`](../ui/dr-ui/src/library_ui.rs#L4574), [`ui/dr-ui/ui/app.slint:514`](../ui/dr-ui/ui/app.slint#L514), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) | | FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:69`](../core/dr-sync/src/scan.rs#L69), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464), [`ui/dr-ui/src/import.rs:489`](../ui/dr-ui/src/import.rs#L489), [`ui/dr-ui/src/library.rs:2598`](../ui/dr-ui/src/library.rs#L2598), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:171`](../ui/dr-ui/src/library_ui.rs#L171), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/src/library_ui.rs:4574`](../ui/dr-ui/src/library_ui.rs#L4574), [`ui/dr-ui/ui/app.slint:514`](../ui/dr-ui/ui/app.slint#L514), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) |
| FR-CAT-4 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/schema.rs:298`](../core/dr-catalog/src/schema.rs#L298), [`ui/dr-ui/src/library.rs:179`](../ui/dr-ui/src/library.rs#L179), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1) | | FR-CAT-4 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/schema.rs:304`](../core/dr-catalog/src/schema.rs#L304), [`ui/dr-ui/src/library.rs:179`](../ui/dr-ui/src/library.rs#L179), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1) |
| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:451`](../core/dr-catalog/src/schema.rs#L451), [`core/dr-catalog/src/schema.rs:954`](../core/dr-catalog/src/schema.rs#L954), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/collections_ui.rs:1578`](../ui/dr-ui/src/collections_ui.rs#L1578), [`ui/dr-ui/src/collections_ui.rs:1597`](../ui/dr-ui/src/collections_ui.rs#L1597), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3055`](../ui/dr-ui/src/library.rs#L3055), [`ui/dr-ui/src/library_ui.rs:6255`](../ui/dr-ui/src/library_ui.rs#L6255), [`ui/dr-ui/src/library_ui.rs:6266`](../ui/dr-ui/src/library_ui.rs#L6266), [`ui/dr-ui/src/library_ui.rs:6279`](../ui/dr-ui/src/library_ui.rs#L6279), [`ui/dr-ui/src/library_ui.rs:628`](../ui/dr-ui/src/library_ui.rs#L628), [`ui/dr-ui/src/library_ui.rs:6294`](../ui/dr-ui/src/library_ui.rs#L6294), [`ui/dr-ui/src/library_ui.rs:6303`](../ui/dr-ui/src/library_ui.rs#L6303), [`ui/dr-ui/ui/app.slint:459`](../ui/dr-ui/ui/app.slint#L459), [`ui/dr-ui/ui/app.slint:618`](../ui/dr-ui/ui/app.slint#L618), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1222`](../ui/dr-ui/ui/library.slint#L1222), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:850`](../ui/dr-ui/ui/library.slint#L850), [`ui/dr-ui/ui/library.slint:902`](../ui/dr-ui/ui/library.slint#L902) | | FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:1001`](../core/dr-catalog/src/schema.rs#L1001), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/collections_ui.rs:1578`](../ui/dr-ui/src/collections_ui.rs#L1578), [`ui/dr-ui/src/collections_ui.rs:1597`](../ui/dr-ui/src/collections_ui.rs#L1597), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3085`](../ui/dr-ui/src/library.rs#L3085), [`ui/dr-ui/src/library_ui.rs:6255`](../ui/dr-ui/src/library_ui.rs#L6255), [`ui/dr-ui/src/library_ui.rs:6266`](../ui/dr-ui/src/library_ui.rs#L6266), [`ui/dr-ui/src/library_ui.rs:6279`](../ui/dr-ui/src/library_ui.rs#L6279), [`ui/dr-ui/src/library_ui.rs:628`](../ui/dr-ui/src/library_ui.rs#L628), [`ui/dr-ui/src/library_ui.rs:6294`](../ui/dr-ui/src/library_ui.rs#L6294), [`ui/dr-ui/src/library_ui.rs:6303`](../ui/dr-ui/src/library_ui.rs#L6303), [`ui/dr-ui/ui/app.slint:459`](../ui/dr-ui/ui/app.slint#L459), [`ui/dr-ui/ui/app.slint:623`](../ui/dr-ui/ui/app.slint#L623), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1222`](../ui/dr-ui/ui/library.slint#L1222), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:850`](../ui/dr-ui/ui/library.slint#L850), [`ui/dr-ui/ui/library.slint:902`](../ui/dr-ui/ui/library.slint#L902) |
| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:451`](../core/dr-catalog/src/schema.rs#L451), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:3301`](../ui/dr-ui/src/library.rs#L3301), [`ui/dr-ui/src/library_ui.rs:315`](../ui/dr-ui/src/library_ui.rs#L315), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5177`](../ui/dr-ui/src/library_ui.rs#L5177), [`ui/dr-ui/src/library_ui.rs:6395`](../ui/dr-ui/src/library_ui.rs#L6395), [`ui/dr-ui/ui/app.slint:462`](../ui/dr-ui/ui/app.slint#L462), [`ui/dr-ui/ui/app.slint:618`](../ui/dr-ui/ui/app.slint#L618), [`ui/dr-ui/ui/app.slint:857`](../ui/dr-ui/ui/app.slint#L857), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1288`](../ui/dr-ui/ui/library.slint#L1288), [`ui/dr-ui/ui/library.slint:902`](../ui/dr-ui/ui/library.slint#L902), [`ui/dr-ui/ui/settings.slint:115`](../ui/dr-ui/ui/settings.slint#L115) | | FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:3331`](../ui/dr-ui/src/library.rs#L3331), [`ui/dr-ui/src/library_ui.rs:315`](../ui/dr-ui/src/library_ui.rs#L315), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5177`](../ui/dr-ui/src/library_ui.rs#L5177), [`ui/dr-ui/src/library_ui.rs:6395`](../ui/dr-ui/src/library_ui.rs#L6395), [`ui/dr-ui/ui/app.slint:462`](../ui/dr-ui/ui/app.slint#L462), [`ui/dr-ui/ui/app.slint:623`](../ui/dr-ui/ui/app.slint#L623), [`ui/dr-ui/ui/app.slint:862`](../ui/dr-ui/ui/app.slint#L862), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1288`](../ui/dr-ui/ui/library.slint#L1288), [`ui/dr-ui/ui/library.slint:902`](../ui/dr-ui/ui/library.slint#L902), [`ui/dr-ui/ui/settings.slint:115`](../ui/dr-ui/ui/settings.slint#L115) |
| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3466`](../ui/dr-ui/src/library_ui.rs#L3466), [`ui/dr-ui/src/library_ui.rs:4164`](../ui/dr-ui/src/library_ui.rs#L4164), [`ui/dr-ui/ui/app.slint:613`](../ui/dr-ui/ui/app.slint#L613), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2528`](../ui/dr-ui/ui/library.slint#L2528), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) | | FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3466`](../ui/dr-ui/src/library_ui.rs#L3466), [`ui/dr-ui/src/library_ui.rs:4164`](../ui/dr-ui/src/library_ui.rs#L4164), [`ui/dr-ui/ui/app.slint:618`](../ui/dr-ui/ui/app.slint#L618), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2528`](../ui/dr-ui/ui/library.slint#L2528), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) |
| FR-CAT-8 | [`core/dr-pipeline/src/ops/curve.rs:136`](../core/dr-pipeline/src/ops/curve.rs#L136), [`core/dr-pipeline/src/ops/curve.rs:652`](../core/dr-pipeline/src/ops/curve.rs#L652), [`core/dr-pipeline/src/sidecar.rs:1343`](../core/dr-pipeline/src/sidecar.rs#L1343), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`core/dr-pipeline/tests/tone_curve.rs:33`](../core/dr-pipeline/tests/tone_curve.rs#L33), [`ui/dr-ui/src/develop.rs:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:2790`](../ui/dr-ui/src/develop.rs#L2790), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1265`](../ui/dr-ui/src/lib.rs#L1265), [`ui/dr-ui/src/lib.rs:1605`](../ui/dr-ui/src/lib.rs#L1605), [`ui/dr-ui/src/lib.rs:1710`](../ui/dr-ui/src/lib.rs#L1710), [`ui/dr-ui/src/lib.rs:467`](../ui/dr-ui/src/lib.rs#L467), [`ui/dr-ui/src/lib.rs:852`](../ui/dr-ui/src/lib.rs#L852), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library_ui.rs:4866`](../ui/dr-ui/src/library_ui.rs#L4866), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-CAT-8 | [`core/dr-pipeline/src/ops/curve.rs:136`](../core/dr-pipeline/src/ops/curve.rs#L136), [`core/dr-pipeline/src/ops/curve.rs:652`](../core/dr-pipeline/src/ops/curve.rs#L652), [`core/dr-pipeline/src/sidecar.rs:1343`](../core/dr-pipeline/src/sidecar.rs#L1343), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`core/dr-pipeline/tests/tone_curve.rs:33`](../core/dr-pipeline/tests/tone_curve.rs#L33), [`ui/dr-ui/src/develop.rs:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:2790`](../ui/dr-ui/src/develop.rs#L2790), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1287`](../ui/dr-ui/src/lib.rs#L1287), [`ui/dr-ui/src/lib.rs:1627`](../ui/dr-ui/src/lib.rs#L1627), [`ui/dr-ui/src/lib.rs:1732`](../ui/dr-ui/src/lib.rs#L1732), [`ui/dr-ui/src/lib.rs:467`](../ui/dr-ui/src/lib.rs#L467), [`ui/dr-ui/src/lib.rs:852`](../ui/dr-ui/src/lib.rs#L852), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library_ui.rs:4866`](../ui/dr-ui/src/library_ui.rs#L4866), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:508`](../core/dr-catalog/src/schema.rs#L508), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`ui/dr-ui/src/develop.rs:2325`](../ui/dr-ui/src/develop.rs#L2325), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1505`](../ui/dr-ui/src/library.rs#L1505), [`ui/dr-ui/src/library.rs:1582`](../ui/dr-ui/src/library.rs#L1582), [`ui/dr-ui/src/library.rs:220`](../ui/dr-ui/src/library.rs#L220), [`ui/dr-ui/src/library.rs:3408`](../ui/dr-ui/src/library.rs#L3408), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:680`](../ui/dr-ui/src/library.rs#L680), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library_ui.rs:1564`](../ui/dr-ui/src/library_ui.rs#L1564), [`ui/dr-ui/src/library_ui.rs:1590`](../ui/dr-ui/src/library_ui.rs#L1590), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:1700`](../ui/dr-ui/src/library_ui.rs#L1700), [`ui/dr-ui/src/library_ui.rs:226`](../ui/dr-ui/src/library_ui.rs#L226), [`ui/dr-ui/src/library_ui.rs:2316`](../ui/dr-ui/src/library_ui.rs#L2316), [`ui/dr-ui/src/library_ui.rs:259`](../ui/dr-ui/src/library_ui.rs#L259), [`ui/dr-ui/src/library_ui.rs:2754`](../ui/dr-ui/src/library_ui.rs#L2754), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:3257`](../ui/dr-ui/src/library_ui.rs#L3257), [`ui/dr-ui/src/library_ui.rs:3329`](../ui/dr-ui/src/library_ui.rs#L3329), [`ui/dr-ui/src/library_ui.rs:3517`](../ui/dr-ui/src/library_ui.rs#L3517), [`ui/dr-ui/src/library_ui.rs:3635`](../ui/dr-ui/src/library_ui.rs#L3635), [`ui/dr-ui/src/library_ui.rs:440`](../ui/dr-ui/src/library_ui.rs#L440), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/library_ui.rs:5222`](../ui/dr-ui/src/library_ui.rs#L5222), [`ui/dr-ui/src/library_ui.rs:5337`](../ui/dr-ui/src/library_ui.rs#L5337), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:555`](../core/dr-catalog/src/schema.rs#L555), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`ui/dr-ui/src/develop.rs:2325`](../ui/dr-ui/src/develop.rs#L2325), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1505`](../ui/dr-ui/src/library.rs#L1505), [`ui/dr-ui/src/library.rs:1582`](../ui/dr-ui/src/library.rs#L1582), [`ui/dr-ui/src/library.rs:220`](../ui/dr-ui/src/library.rs#L220), [`ui/dr-ui/src/library.rs:3438`](../ui/dr-ui/src/library.rs#L3438), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:680`](../ui/dr-ui/src/library.rs#L680), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library_ui.rs:1564`](../ui/dr-ui/src/library_ui.rs#L1564), [`ui/dr-ui/src/library_ui.rs:1590`](../ui/dr-ui/src/library_ui.rs#L1590), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:1700`](../ui/dr-ui/src/library_ui.rs#L1700), [`ui/dr-ui/src/library_ui.rs:226`](../ui/dr-ui/src/library_ui.rs#L226), [`ui/dr-ui/src/library_ui.rs:2316`](../ui/dr-ui/src/library_ui.rs#L2316), [`ui/dr-ui/src/library_ui.rs:259`](../ui/dr-ui/src/library_ui.rs#L259), [`ui/dr-ui/src/library_ui.rs:2754`](../ui/dr-ui/src/library_ui.rs#L2754), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:3257`](../ui/dr-ui/src/library_ui.rs#L3257), [`ui/dr-ui/src/library_ui.rs:3329`](../ui/dr-ui/src/library_ui.rs#L3329), [`ui/dr-ui/src/library_ui.rs:3517`](../ui/dr-ui/src/library_ui.rs#L3517), [`ui/dr-ui/src/library_ui.rs:3635`](../ui/dr-ui/src/library_ui.rs#L3635), [`ui/dr-ui/src/library_ui.rs:440`](../ui/dr-ui/src/library_ui.rs#L440), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/library_ui.rs:5222`](../ui/dr-ui/src/library_ui.rs#L5222), [`ui/dr-ui/src/library_ui.rs:5337`](../ui/dr-ui/src/library_ui.rs#L5337), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
| FR-CULL-1 | [`core/dr-decode/src/preview.rs:134`](../core/dr-decode/src/preview.rs#L134) | | FR-CULL-1 | [`core/dr-decode/src/preview.rs:134`](../core/dr-decode/src/preview.rs#L134) |
| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
| FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
| FR-CULL-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
| FR-CULL-2 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:161`](../core/dr-decode/src/preview.rs#L161), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464) | | FR-CULL-2 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:161`](../core/dr-decode/src/preview.rs#L161), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464) |
| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364) | | FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364) |
| FR-CULL-8 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) | | FR-CULL-8 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:343`](../core/dr-catalog/src/schema.rs#L343), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) |
| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) | | FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) |
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352) | | FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352) |
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:602`](../core/dr-pipeline/src/framing.rs#L602), [`core/dr-pipeline/src/graph.rs:154`](../core/dr-pipeline/src/graph.rs#L154), [`core/dr-pipeline/src/graph.rs:457`](../core/dr-pipeline/src/graph.rs#L457), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:473`](../core/dr-pipeline/src/operation.rs#L473), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:1343`](../core/dr-pipeline/src/sidecar.rs#L1343), [`core/dr-pipeline/src/sidecar.rs:1399`](../core/dr-pipeline/src/sidecar.rs#L1399), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1111`](../ui/dr-ui/src/develop.rs#L1111), [`ui/dr-ui/src/develop.rs:132`](../ui/dr-ui/src/develop.rs#L132), [`ui/dr-ui/src/develop.rs:1584`](../ui/dr-ui/src/develop.rs#L1584), [`ui/dr-ui/src/develop.rs:1599`](../ui/dr-ui/src/develop.rs#L1599), [`ui/dr-ui/src/develop.rs:1621`](../ui/dr-ui/src/develop.rs#L1621), [`ui/dr-ui/src/develop.rs:1753`](../ui/dr-ui/src/develop.rs#L1753), [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/develop.rs:238`](../ui/dr-ui/src/develop.rs#L238), [`ui/dr-ui/src/develop.rs:270`](../ui/dr-ui/src/develop.rs#L270), [`ui/dr-ui/src/develop.rs:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:3194`](../ui/dr-ui/src/develop.rs#L3194), [`ui/dr-ui/src/develop.rs:3248`](../ui/dr-ui/src/develop.rs#L3248), [`ui/dr-ui/src/develop.rs:3292`](../ui/dr-ui/src/develop.rs#L3292), [`ui/dr-ui/src/develop.rs:3342`](../ui/dr-ui/src/develop.rs#L3342), [`ui/dr-ui/src/develop.rs:506`](../ui/dr-ui/src/develop.rs#L506), [`ui/dr-ui/src/develop.rs:544`](../ui/dr-ui/src/develop.rs#L544), [`ui/dr-ui/src/lib.rs:1301`](../ui/dr-ui/src/lib.rs#L1301), [`ui/dr-ui/src/lib.rs:1960`](../ui/dr-ui/src/lib.rs#L1960), [`ui/dr-ui/src/lib.rs:293`](../ui/dr-ui/src/lib.rs#L293), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:218`](../ui/dr-ui/src/segmentation.rs#L218), [`ui/dr-ui/ui/app.slint:1975`](../ui/dr-ui/ui/app.slint#L1975), [`ui/dr-ui/ui/app.slint:944`](../ui/dr-ui/ui/app.slint#L944) | | FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:602`](../core/dr-pipeline/src/framing.rs#L602), [`core/dr-pipeline/src/graph.rs:154`](../core/dr-pipeline/src/graph.rs#L154), [`core/dr-pipeline/src/graph.rs:457`](../core/dr-pipeline/src/graph.rs#L457), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:473`](../core/dr-pipeline/src/operation.rs#L473), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:1343`](../core/dr-pipeline/src/sidecar.rs#L1343), [`core/dr-pipeline/src/sidecar.rs:1399`](../core/dr-pipeline/src/sidecar.rs#L1399), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1111`](../ui/dr-ui/src/develop.rs#L1111), [`ui/dr-ui/src/develop.rs:132`](../ui/dr-ui/src/develop.rs#L132), [`ui/dr-ui/src/develop.rs:1584`](../ui/dr-ui/src/develop.rs#L1584), [`ui/dr-ui/src/develop.rs:1599`](../ui/dr-ui/src/develop.rs#L1599), [`ui/dr-ui/src/develop.rs:1621`](../ui/dr-ui/src/develop.rs#L1621), [`ui/dr-ui/src/develop.rs:1753`](../ui/dr-ui/src/develop.rs#L1753), [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/develop.rs:238`](../ui/dr-ui/src/develop.rs#L238), [`ui/dr-ui/src/develop.rs:270`](../ui/dr-ui/src/develop.rs#L270), [`ui/dr-ui/src/develop.rs:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:3194`](../ui/dr-ui/src/develop.rs#L3194), [`ui/dr-ui/src/develop.rs:3248`](../ui/dr-ui/src/develop.rs#L3248), [`ui/dr-ui/src/develop.rs:3292`](../ui/dr-ui/src/develop.rs#L3292), [`ui/dr-ui/src/develop.rs:3342`](../ui/dr-ui/src/develop.rs#L3342), [`ui/dr-ui/src/develop.rs:506`](../ui/dr-ui/src/develop.rs#L506), [`ui/dr-ui/src/develop.rs:544`](../ui/dr-ui/src/develop.rs#L544), [`ui/dr-ui/src/lib.rs:1323`](../ui/dr-ui/src/lib.rs#L1323), [`ui/dr-ui/src/lib.rs:1982`](../ui/dr-ui/src/lib.rs#L1982), [`ui/dr-ui/src/lib.rs:293`](../ui/dr-ui/src/lib.rs#L293), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:218`](../ui/dr-ui/src/segmentation.rs#L218), [`ui/dr-ui/ui/app.slint:1985`](../ui/dr-ui/ui/app.slint#L1985), [`ui/dr-ui/ui/app.slint:949`](../ui/dr-ui/ui/app.slint#L949) |
| FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:117`](../core/dr-pipeline/src/descriptor.rs#L117), [`core/dr-pipeline/src/descriptor.rs:157`](../core/dr-pipeline/src/descriptor.rs#L157), [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/descriptor.rs:232`](../core/dr-pipeline/src/descriptor.rs#L232), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:18`](../core/dr-pipeline/src/graph.rs#L18), [`core/dr-pipeline/src/graph.rs:218`](../core/dr-pipeline/src/graph.rs#L218), [`core/dr-pipeline/src/graph.rs:40`](../core/dr-pipeline/src/graph.rs#L40), [`core/dr-pipeline/src/graph.rs:53`](../core/dr-pipeline/src/graph.rs#L53), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1008`](../ui/dr-ui/src/develop.rs#L1008), [`ui/dr-ui/src/lib.rs:582`](../ui/dr-ui/src/lib.rs#L582) | | FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:117`](../core/dr-pipeline/src/descriptor.rs#L117), [`core/dr-pipeline/src/descriptor.rs:157`](../core/dr-pipeline/src/descriptor.rs#L157), [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/descriptor.rs:232`](../core/dr-pipeline/src/descriptor.rs#L232), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:18`](../core/dr-pipeline/src/graph.rs#L18), [`core/dr-pipeline/src/graph.rs:218`](../core/dr-pipeline/src/graph.rs#L218), [`core/dr-pipeline/src/graph.rs:40`](../core/dr-pipeline/src/graph.rs#L40), [`core/dr-pipeline/src/graph.rs:53`](../core/dr-pipeline/src/graph.rs#L53), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1008`](../ui/dr-ui/src/develop.rs#L1008), [`ui/dr-ui/src/lib.rs:582`](../ui/dr-ui/src/lib.rs#L582) |
| FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:53`](../core/dr-pipeline/src/graph.rs#L53), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328) | | FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:53`](../core/dr-pipeline/src/graph.rs#L53), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328) |
| FR-DEV-3c | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:218`](../core/dr-pipeline/src/graph.rs#L218), [`core/dr-pipeline/src/graph.rs:40`](../core/dr-pipeline/src/graph.rs#L40), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:3921`](../ui/dr-ui/src/develop.rs#L3921) | | FR-DEV-3c | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:218`](../core/dr-pipeline/src/graph.rs#L218), [`core/dr-pipeline/src/graph.rs:40`](../core/dr-pipeline/src/graph.rs#L40), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:3921`](../ui/dr-ui/src/develop.rs#L3921) |
| FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`core/dr-gpu/tests/capture_sharpen.rs:433`](../core/dr-gpu/tests/capture_sharpen.rs#L433), [`core/dr-gpu/tests/detail_stage.rs:241`](../core/dr-gpu/tests/detail_stage.rs#L241), [`core/dr-gpu/tests/local_contrast.rs:475`](../core/dr-gpu/tests/local_contrast.rs#L475), [`core/dr-gpu/tests/noise_reduction.rs:555`](../core/dr-gpu/tests/noise_reduction.rs#L555), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/graph.rs:486`](../core/dr-pipeline/src/graph.rs#L486), [`core/dr-pipeline/src/operation.rs:31`](../core/dr-pipeline/src/operation.rs#L31), [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352), [`core/dr-pipeline/src/operation.rs:52`](../core/dr-pipeline/src/operation.rs#L52), [`core/dr-pipeline/src/operation.rs:70`](../core/dr-pipeline/src/operation.rs#L70) | | FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`core/dr-gpu/tests/capture_sharpen.rs:433`](../core/dr-gpu/tests/capture_sharpen.rs#L433), [`core/dr-gpu/tests/detail_stage.rs:241`](../core/dr-gpu/tests/detail_stage.rs#L241), [`core/dr-gpu/tests/local_contrast.rs:475`](../core/dr-gpu/tests/local_contrast.rs#L475), [`core/dr-gpu/tests/noise_reduction.rs:555`](../core/dr-gpu/tests/noise_reduction.rs#L555), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/graph.rs:486`](../core/dr-pipeline/src/graph.rs#L486), [`core/dr-pipeline/src/operation.rs:31`](../core/dr-pipeline/src/operation.rs#L31), [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352), [`core/dr-pipeline/src/operation.rs:52`](../core/dr-pipeline/src/operation.rs#L52), [`core/dr-pipeline/src/operation.rs:70`](../core/dr-pipeline/src/operation.rs#L70) |
| FR-DEV-3e | [`core/dr-decode/src/base_curve.rs:145`](../core/dr-decode/src/base_curve.rs#L145), [`core/dr-decode/src/base_curve.rs:158`](../core/dr-decode/src/base_curve.rs#L158), [`core/dr-decode/src/base_curve.rs:1`](../core/dr-decode/src/base_curve.rs#L1), [`core/dr-decode/src/base_curve.rs:267`](../core/dr-decode/src/base_curve.rs#L267), [`core/dr-decode/src/base_curve.rs:347`](../core/dr-decode/src/base_curve.rs#L347), [`core/dr-decode/src/base_curve.rs:55`](../core/dr-decode/src/base_curve.rs#L55), [`core/dr-decode/src/lib.rs:121`](../core/dr-decode/src/lib.rs#L121), [`core/dr-decode/src/lib.rs:708`](../core/dr-decode/src/lib.rs#L708), [`core/dr-decode/src/lib.rs:748`](../core/dr-decode/src/lib.rs#L748), [`core/dr-decode/src/profile.rs:102`](../core/dr-decode/src/profile.rs#L102), [`core/dr-decode/src/profile.rs:151`](../core/dr-decode/src/profile.rs#L151), [`core/dr-decode/src/profile.rs:1`](../core/dr-decode/src/profile.rs#L1), [`core/dr-decode/src/profile.rs:235`](../core/dr-decode/src/profile.rs#L235), [`core/dr-decode/src/profile.rs:286`](../core/dr-decode/src/profile.rs#L286), [`core/dr-decode/src/profile.rs:343`](../core/dr-decode/src/profile.rs#L343), [`core/dr-decode/src/profile.rs:458`](../core/dr-decode/src/profile.rs#L458), [`core/dr-decode/src/profile.rs:492`](../core/dr-decode/src/profile.rs#L492), [`core/dr-decode/src/profile.rs:630`](../core/dr-decode/src/profile.rs#L630), [`core/dr-gpu/src/adjust.rs:37`](../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/adjust.rs:967`](../core/dr-gpu/src/adjust.rs#L967), [`core/dr-gpu/src/demosaic.rs:121`](../core/dr-gpu/src/demosaic.rs#L121), [`core/dr-gpu/src/demosaic.rs:86`](../core/dr-gpu/src/demosaic.rs#L86), [`core/dr-gpu/tests/base_curve.rs:1`](../core/dr-gpu/tests/base_curve.rs#L1), [`core/dr-pipeline/src/operation.rs:1410`](../core/dr-pipeline/src/operation.rs#L1410), [`core/dr-pipeline/src/operation.rs:1491`](../core/dr-pipeline/src/operation.rs#L1491), [`core/dr-pipeline/src/operation.rs:1516`](../core/dr-pipeline/src/operation.rs#L1516), [`core/dr-pipeline/src/operation.rs:1531`](../core/dr-pipeline/src/operation.rs#L1531), [`core/dr-pipeline/src/operation.rs:1555`](../core/dr-pipeline/src/operation.rs#L1555), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/operation.rs:403`](../core/dr-pipeline/src/operation.rs#L403), [`core/dr-pipeline/src/operation.rs:413`](../core/dr-pipeline/src/operation.rs#L413), [`core/dr-pipeline/src/operation.rs:549`](../core/dr-pipeline/src/operation.rs#L549) | | FR-DEV-3e | [`core/dr-decode/src/base_curve.rs:145`](../core/dr-decode/src/base_curve.rs#L145), [`core/dr-decode/src/base_curve.rs:158`](../core/dr-decode/src/base_curve.rs#L158), [`core/dr-decode/src/base_curve.rs:1`](../core/dr-decode/src/base_curve.rs#L1), [`core/dr-decode/src/base_curve.rs:267`](../core/dr-decode/src/base_curve.rs#L267), [`core/dr-decode/src/base_curve.rs:347`](../core/dr-decode/src/base_curve.rs#L347), [`core/dr-decode/src/base_curve.rs:55`](../core/dr-decode/src/base_curve.rs#L55), [`core/dr-decode/src/lib.rs:121`](../core/dr-decode/src/lib.rs#L121), [`core/dr-decode/src/lib.rs:708`](../core/dr-decode/src/lib.rs#L708), [`core/dr-decode/src/lib.rs:748`](../core/dr-decode/src/lib.rs#L748), [`core/dr-decode/src/profile.rs:102`](../core/dr-decode/src/profile.rs#L102), [`core/dr-decode/src/profile.rs:151`](../core/dr-decode/src/profile.rs#L151), [`core/dr-decode/src/profile.rs:1`](../core/dr-decode/src/profile.rs#L1), [`core/dr-decode/src/profile.rs:235`](../core/dr-decode/src/profile.rs#L235), [`core/dr-decode/src/profile.rs:286`](../core/dr-decode/src/profile.rs#L286), [`core/dr-decode/src/profile.rs:343`](../core/dr-decode/src/profile.rs#L343), [`core/dr-decode/src/profile.rs:458`](../core/dr-decode/src/profile.rs#L458), [`core/dr-decode/src/profile.rs:492`](../core/dr-decode/src/profile.rs#L492), [`core/dr-decode/src/profile.rs:630`](../core/dr-decode/src/profile.rs#L630), [`core/dr-gpu/src/adjust.rs:37`](../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/adjust.rs:967`](../core/dr-gpu/src/adjust.rs#L967), [`core/dr-gpu/src/demosaic.rs:121`](../core/dr-gpu/src/demosaic.rs#L121), [`core/dr-gpu/src/demosaic.rs:86`](../core/dr-gpu/src/demosaic.rs#L86), [`core/dr-gpu/tests/base_curve.rs:1`](../core/dr-gpu/tests/base_curve.rs#L1), [`core/dr-pipeline/src/operation.rs:1410`](../core/dr-pipeline/src/operation.rs#L1410), [`core/dr-pipeline/src/operation.rs:1491`](../core/dr-pipeline/src/operation.rs#L1491), [`core/dr-pipeline/src/operation.rs:1516`](../core/dr-pipeline/src/operation.rs#L1516), [`core/dr-pipeline/src/operation.rs:1531`](../core/dr-pipeline/src/operation.rs#L1531), [`core/dr-pipeline/src/operation.rs:1555`](../core/dr-pipeline/src/operation.rs#L1555), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/operation.rs:403`](../core/dr-pipeline/src/operation.rs#L403), [`core/dr-pipeline/src/operation.rs:413`](../core/dr-pipeline/src/operation.rs#L413), [`core/dr-pipeline/src/operation.rs:549`](../core/dr-pipeline/src/operation.rs#L549) |
| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:88`](../core/dr-film/src/grain.rs#L88), [`core/dr-film/src/lib.rs:158`](../core/dr-film/src/lib.rs#L158), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:110`](../core/dr-pipeline/src/graph.rs#L110), [`core/dr-pipeline/src/graph.rs:292`](../core/dr-pipeline/src/graph.rs#L292), [`core/dr-pipeline/src/graph.rs:96`](../core/dr-pipeline/src/graph.rs#L96), [`core/dr-pipeline/src/operation.rs:1014`](../core/dr-pipeline/src/operation.rs#L1014), [`core/dr-pipeline/src/operation.rs:1043`](../core/dr-pipeline/src/operation.rs#L1043), [`core/dr-pipeline/src/operation.rs:1410`](../core/dr-pipeline/src/operation.rs#L1410), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/ops/film_sim.rs:102`](../core/dr-pipeline/src/ops/film_sim.rs#L102), [`core/dr-pipeline/src/ops/film_sim.rs:126`](../core/dr-pipeline/src/ops/film_sim.rs#L126), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:300`](../core/dr-pipeline/src/ops/film_sim.rs#L300), [`core/dr-pipeline/src/ops/film_sim.rs:68`](../core/dr-pipeline/src/ops/film_sim.rs#L68), [`core/dr-pipeline/src/sidecar.rs:109`](../core/dr-pipeline/src/sidecar.rs#L109), [`core/dr-pipeline/src/sidecar.rs:1647`](../core/dr-pipeline/src/sidecar.rs#L1647), [`core/dr-pipeline/src/sidecar.rs:169`](../core/dr-pipeline/src/sidecar.rs#L169), [`core/dr-pipeline/src/sidecar.rs:1723`](../core/dr-pipeline/src/sidecar.rs#L1723), [`core/dr-pipeline/src/sidecar.rs:415`](../core/dr-pipeline/src/sidecar.rs#L415), [`core/dr-pipeline/src/sidecar.rs:555`](../core/dr-pipeline/src/sidecar.rs#L555), [`core/dr-pipeline/src/sidecar.rs:678`](../core/dr-pipeline/src/sidecar.rs#L678), [`ui/dr-ui/src/develop.rs:2340`](../ui/dr-ui/src/develop.rs#L2340), [`ui/dr-ui/src/develop.rs:2357`](../ui/dr-ui/src/develop.rs#L2357), [`ui/dr-ui/src/develop.rs:2388`](../ui/dr-ui/src/develop.rs#L2388), [`ui/dr-ui/src/develop.rs:2485`](../ui/dr-ui/src/develop.rs#L2485), [`ui/dr-ui/src/develop.rs:2799`](../ui/dr-ui/src/develop.rs#L2799), [`ui/dr-ui/src/lib.rs:1934`](../ui/dr-ui/src/lib.rs#L1934), [`ui/dr-ui/src/lib.rs:515`](../ui/dr-ui/src/lib.rs#L515), [`ui/dr-ui/src/lib.rs:573`](../ui/dr-ui/src/lib.rs#L573), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:651`](../ui/dr-ui/src/library.rs#L651), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:879`](../ui/dr-ui/ui/adjust.slint#L879), [`ui/dr-ui/ui/adjust.slint:949`](../ui/dr-ui/ui/adjust.slint#L949), [`ui/dr-ui/ui/app.slint:2430`](../ui/dr-ui/ui/app.slint#L2430), [`ui/dr-ui/ui/app.slint:738`](../ui/dr-ui/ui/app.slint#L738) | | FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:88`](../core/dr-film/src/grain.rs#L88), [`core/dr-film/src/lib.rs:158`](../core/dr-film/src/lib.rs#L158), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:110`](../core/dr-pipeline/src/graph.rs#L110), [`core/dr-pipeline/src/graph.rs:292`](../core/dr-pipeline/src/graph.rs#L292), [`core/dr-pipeline/src/graph.rs:96`](../core/dr-pipeline/src/graph.rs#L96), [`core/dr-pipeline/src/operation.rs:1014`](../core/dr-pipeline/src/operation.rs#L1014), [`core/dr-pipeline/src/operation.rs:1043`](../core/dr-pipeline/src/operation.rs#L1043), [`core/dr-pipeline/src/operation.rs:1410`](../core/dr-pipeline/src/operation.rs#L1410), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/ops/film_sim.rs:102`](../core/dr-pipeline/src/ops/film_sim.rs#L102), [`core/dr-pipeline/src/ops/film_sim.rs:126`](../core/dr-pipeline/src/ops/film_sim.rs#L126), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:300`](../core/dr-pipeline/src/ops/film_sim.rs#L300), [`core/dr-pipeline/src/ops/film_sim.rs:68`](../core/dr-pipeline/src/ops/film_sim.rs#L68), [`core/dr-pipeline/src/sidecar.rs:109`](../core/dr-pipeline/src/sidecar.rs#L109), [`core/dr-pipeline/src/sidecar.rs:1647`](../core/dr-pipeline/src/sidecar.rs#L1647), [`core/dr-pipeline/src/sidecar.rs:169`](../core/dr-pipeline/src/sidecar.rs#L169), [`core/dr-pipeline/src/sidecar.rs:1723`](../core/dr-pipeline/src/sidecar.rs#L1723), [`core/dr-pipeline/src/sidecar.rs:415`](../core/dr-pipeline/src/sidecar.rs#L415), [`core/dr-pipeline/src/sidecar.rs:555`](../core/dr-pipeline/src/sidecar.rs#L555), [`core/dr-pipeline/src/sidecar.rs:678`](../core/dr-pipeline/src/sidecar.rs#L678), [`ui/dr-ui/src/develop.rs:2340`](../ui/dr-ui/src/develop.rs#L2340), [`ui/dr-ui/src/develop.rs:2357`](../ui/dr-ui/src/develop.rs#L2357), [`ui/dr-ui/src/develop.rs:2388`](../ui/dr-ui/src/develop.rs#L2388), [`ui/dr-ui/src/develop.rs:2485`](../ui/dr-ui/src/develop.rs#L2485), [`ui/dr-ui/src/develop.rs:2799`](../ui/dr-ui/src/develop.rs#L2799), [`ui/dr-ui/src/lib.rs:1956`](../ui/dr-ui/src/lib.rs#L1956), [`ui/dr-ui/src/lib.rs:515`](../ui/dr-ui/src/lib.rs#L515), [`ui/dr-ui/src/lib.rs:573`](../ui/dr-ui/src/lib.rs#L573), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:651`](../ui/dr-ui/src/library.rs#L651), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:879`](../ui/dr-ui/ui/adjust.slint#L879), [`ui/dr-ui/ui/adjust.slint:949`](../ui/dr-ui/ui/adjust.slint#L949), [`ui/dr-ui/ui/app.slint:2440`](../ui/dr-ui/ui/app.slint#L2440), [`ui/dr-ui/ui/app.slint:743`](../ui/dr-ui/ui/app.slint#L743) |
| FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:202`](../core/dr-pipeline/src/framing.rs#L202), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336) | | FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:202`](../core/dr-pipeline/src/framing.rs#L202), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336) |
| FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) | | FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) |
| FR-DEV-5 | [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`core/dr-pipeline/src/history.rs:71`](../core/dr-pipeline/src/history.rs#L71), [`core/dr-pipeline/src/history.rs:79`](../core/dr-pipeline/src/history.rs#L79), [`ui/dr-ui/src/develop.rs:2817`](../ui/dr-ui/src/develop.rs#L2817), [`ui/dr-ui/src/develop.rs:2827`](../ui/dr-ui/src/develop.rs#L2827), [`ui/dr-ui/src/develop.rs:488`](../ui/dr-ui/src/develop.rs#L488), [`ui/dr-ui/src/lib.rs:1293`](../ui/dr-ui/src/lib.rs#L1293) | | FR-DEV-5 | [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`core/dr-pipeline/src/history.rs:71`](../core/dr-pipeline/src/history.rs#L71), [`core/dr-pipeline/src/history.rs:79`](../core/dr-pipeline/src/history.rs#L79), [`ui/dr-ui/src/develop.rs:2817`](../ui/dr-ui/src/develop.rs#L2817), [`ui/dr-ui/src/develop.rs:2827`](../ui/dr-ui/src/develop.rs#L2827), [`ui/dr-ui/src/develop.rs:488`](../ui/dr-ui/src/develop.rs#L488), [`ui/dr-ui/src/lib.rs:1315`](../ui/dr-ui/src/lib.rs#L1315) |
| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:2756`](../ui/dr-ui/src/develop.rs#L2756), [`ui/dr-ui/src/develop.rs:2777`](../ui/dr-ui/src/develop.rs#L2777), [`ui/dr-ui/src/lib.rs:1265`](../ui/dr-ui/src/lib.rs#L1265), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:535`](../ui/dr-ui/src/settings_ui.rs#L535), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1315`](../ui/dr-ui/ui/library.slint#L1315), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:913`](../ui/dr-ui/ui/library.slint#L913), [`ui/dr-ui/ui/settings.slint:87`](../ui/dr-ui/ui/settings.slint#L87) | | FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:2756`](../ui/dr-ui/src/develop.rs#L2756), [`ui/dr-ui/src/develop.rs:2777`](../ui/dr-ui/src/develop.rs#L2777), [`ui/dr-ui/src/lib.rs:1287`](../ui/dr-ui/src/lib.rs#L1287), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:535`](../ui/dr-ui/src/settings_ui.rs#L535), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1315`](../ui/dr-ui/ui/library.slint#L1315), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:913`](../ui/dr-ui/ui/library.slint#L913), [`ui/dr-ui/ui/settings.slint:87`](../ui/dr-ui/ui/settings.slint#L87) |
| FR-DEV-8 | [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299) | | FR-DEV-8 | [`core/dr-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299) |
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2090`](../core/dr-gpu/src/adjust.rs#L2090), [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:2252`](../core/dr-gpu/src/adjust.rs#L2252), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs:199`](../core/dr-gpu/tests/capture_sharpen.rs#L199), [`core/dr-gpu/tests/detail_stage.rs:327`](../core/dr-gpu/tests/detail_stage.rs#L327), [`core/dr-gpu/tests/local_contrast.rs:262`](../core/dr-gpu/tests/local_contrast.rs#L262), [`core/dr-gpu/tests/noise_reduction.rs:377`](../core/dr-gpu/tests/noise_reduction.rs#L377), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/graph.rs:427`](../core/dr-pipeline/src/graph.rs#L427), [`core/dr-pipeline/src/graph.rs:457`](../core/dr-pipeline/src/graph.rs#L457), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:647`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L647), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:658`](../core/dr-pipeline/src/ops/local_contrast.rs#L658), [`core/dr-pipeline/src/ops/noise_reduction.rs:691`](../core/dr-pipeline/src/ops/noise_reduction.rs#L691), [`ui/dr-ui/src/develop.rs:2161`](../ui/dr-ui/src/develop.rs#L2161), [`ui/dr-ui/src/develop.rs:2983`](../ui/dr-ui/src/develop.rs#L2983), [`ui/dr-ui/src/develop.rs:3466`](../ui/dr-ui/src/develop.rs#L3466), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:62`](../ui/dr-ui/src/lib.rs#L62), [`ui/dr-ui/src/lib.rs:723`](../ui/dr-ui/src/lib.rs#L723) | | FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2090`](../core/dr-gpu/src/adjust.rs#L2090), [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:2252`](../core/dr-gpu/src/adjust.rs#L2252), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs:199`](../core/dr-gpu/tests/capture_sharpen.rs#L199), [`core/dr-gpu/tests/detail_stage.rs:327`](../core/dr-gpu/tests/detail_stage.rs#L327), [`core/dr-gpu/tests/local_contrast.rs:262`](../core/dr-gpu/tests/local_contrast.rs#L262), [`core/dr-gpu/tests/noise_reduction.rs:377`](../core/dr-gpu/tests/noise_reduction.rs#L377), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/graph.rs:427`](../core/dr-pipeline/src/graph.rs#L427), [`core/dr-pipeline/src/graph.rs:457`](../core/dr-pipeline/src/graph.rs#L457), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:647`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L647), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:658`](../core/dr-pipeline/src/ops/local_contrast.rs#L658), [`core/dr-pipeline/src/ops/noise_reduction.rs:691`](../core/dr-pipeline/src/ops/noise_reduction.rs#L691), [`ui/dr-ui/src/develop.rs:2161`](../ui/dr-ui/src/develop.rs#L2161), [`ui/dr-ui/src/develop.rs:2983`](../ui/dr-ui/src/develop.rs#L2983), [`ui/dr-ui/src/develop.rs:3466`](../ui/dr-ui/src/develop.rs#L3466), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:62`](../ui/dr-ui/src/lib.rs#L62), [`ui/dr-ui/src/lib.rs:723`](../ui/dr-ui/src/lib.rs#L723) |
| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1) | | FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1) |
| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:2206`](../ui/dr-ui/src/develop.rs#L2206), [`ui/dr-ui/src/develop.rs:4568`](../ui/dr-ui/src/develop.rs#L4568), [`ui/dr-ui/src/develop.rs:4600`](../ui/dr-ui/src/develop.rs#L4600), [`ui/dr-ui/src/develop.rs:499`](../ui/dr-ui/src/develop.rs#L499), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1352`](../ui/dr-ui/src/lib.rs#L1352), [`ui/dr-ui/src/lib.rs:288`](../ui/dr-ui/src/lib.rs#L288), [`ui/dr-ui/ui/app.slint:277`](../ui/dr-ui/ui/app.slint#L277), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | | FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:2206`](../ui/dr-ui/src/develop.rs#L2206), [`ui/dr-ui/src/develop.rs:4568`](../ui/dr-ui/src/develop.rs#L4568), [`ui/dr-ui/src/develop.rs:4600`](../ui/dr-ui/src/develop.rs#L4600), [`ui/dr-ui/src/develop.rs:499`](../ui/dr-ui/src/develop.rs#L499), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1374`](../ui/dr-ui/src/lib.rs#L1374), [`ui/dr-ui/src/lib.rs:288`](../ui/dr-ui/src/lib.rs#L288), [`ui/dr-ui/ui/app.slint:277`](../ui/dr-ui/ui/app.slint#L277), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) |
| FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2400`](../core/dr-gpu/src/adjust.rs#L2400), [`core/dr-pipeline/src/graph.rs:417`](../core/dr-pipeline/src/graph.rs#L417), [`core/dr-pipeline/src/graph.rs:470`](../core/dr-pipeline/src/graph.rs#L470), [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:595`](../core/dr-types/src/settings.rs#L595), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2400`](../core/dr-gpu/src/adjust.rs#L2400), [`core/dr-pipeline/src/graph.rs:417`](../core/dr-pipeline/src/graph.rs#L417), [`core/dr-pipeline/src/graph.rs:470`](../core/dr-pipeline/src/graph.rs#L470), [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:595`](../core/dr-types/src/settings.rs#L595), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
| FR-EXP-3 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`core/dr-export/src/size.rs:25`](../core/dr-export/src/size.rs#L25), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-3 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`core/dr-export/src/size.rs:25`](../core/dr-export/src/size.rs#L25), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
| FR-EXP-4 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/sharpen.rs:1`](../core/dr-export/src/sharpen.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-4 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/sharpen.rs:1`](../core/dr-export/src/sharpen.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
| FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
| FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:331`](../ui/dr-ui/src/lib.rs#L331), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:48`](../ui/dr-ui/src/settings_ui.rs#L48), [`ui/dr-ui/src/settings_ui.rs:590`](../ui/dr-ui/src/settings_ui.rs#L590) | | FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:331`](../ui/dr-ui/src/lib.rs#L331), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:48`](../ui/dr-ui/src/settings_ui.rs#L48), [`ui/dr-ui/src/settings_ui.rs:590`](../ui/dr-ui/src/settings_ui.rs#L590) |
| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:944`](../ui/dr-ui/src/export.rs#L944), [`ui/dr-ui/src/lib.rs:178`](../ui/dr-ui/src/lib.rs#L178), [`ui/dr-ui/src/lib.rs:1876`](../ui/dr-ui/src/lib.rs#L1876), [`ui/dr-ui/src/lib.rs:331`](../ui/dr-ui/src/lib.rs#L331), [`ui/dr-ui/src/lib.rs:366`](../ui/dr-ui/src/lib.rs#L366), [`ui/dr-ui/src/lib.rs:393`](../ui/dr-ui/src/lib.rs#L393), [`ui/dr-ui/src/library_ui.rs:3346`](../ui/dr-ui/src/library_ui.rs#L3346), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`ui/dr-ui/src/library_ui.rs:6237`](../ui/dr-ui/src/library_ui.rs#L6237), [`ui/dr-ui/src/library_ui.rs:6314`](../ui/dr-ui/src/library_ui.rs#L6314), [`ui/dr-ui/src/library_ui.rs:6326`](../ui/dr-ui/src/library_ui.rs#L6326), [`ui/dr-ui/src/library_ui.rs:660`](../ui/dr-ui/src/library_ui.rs#L660), [`ui/dr-ui/src/library_ui.rs:717`](../ui/dr-ui/src/library_ui.rs#L717), [`ui/dr-ui/ui/app.slint:1035`](../ui/dr-ui/ui/app.slint#L1035), [`ui/dr-ui/ui/app.slint:1443`](../ui/dr-ui/ui/app.slint#L1443), [`ui/dr-ui/ui/library.slint:1323`](../ui/dr-ui/ui/library.slint#L1323), [`ui/dr-ui/ui/library.slint:835`](../ui/dr-ui/ui/library.slint#L835), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) | | FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:944`](../ui/dr-ui/src/export.rs#L944), [`ui/dr-ui/src/lib.rs:178`](../ui/dr-ui/src/lib.rs#L178), [`ui/dr-ui/src/lib.rs:1898`](../ui/dr-ui/src/lib.rs#L1898), [`ui/dr-ui/src/lib.rs:331`](../ui/dr-ui/src/lib.rs#L331), [`ui/dr-ui/src/lib.rs:366`](../ui/dr-ui/src/lib.rs#L366), [`ui/dr-ui/src/lib.rs:393`](../ui/dr-ui/src/lib.rs#L393), [`ui/dr-ui/src/library_ui.rs:3346`](../ui/dr-ui/src/library_ui.rs#L3346), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`ui/dr-ui/src/library_ui.rs:6237`](../ui/dr-ui/src/library_ui.rs#L6237), [`ui/dr-ui/src/library_ui.rs:6314`](../ui/dr-ui/src/library_ui.rs#L6314), [`ui/dr-ui/src/library_ui.rs:6326`](../ui/dr-ui/src/library_ui.rs#L6326), [`ui/dr-ui/src/library_ui.rs:660`](../ui/dr-ui/src/library_ui.rs#L660), [`ui/dr-ui/src/library_ui.rs:717`](../ui/dr-ui/src/library_ui.rs#L717), [`ui/dr-ui/ui/app.slint:1040`](../ui/dr-ui/ui/app.slint#L1040), [`ui/dr-ui/ui/app.slint:1453`](../ui/dr-ui/ui/app.slint#L1453), [`ui/dr-ui/ui/library.slint:1323`](../ui/dr-ui/ui/library.slint#L1323), [`ui/dr-ui/ui/library.slint:835`](../ui/dr-ui/ui/library.slint#L835), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) |
| FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/settings.rs:313`](../core/dr-types/src/settings.rs#L313), [`ui/dr-ui/src/export.rs:620`](../ui/dr-ui/src/export.rs#L620), [`ui/dr-ui/src/export.rs:648`](../ui/dr-ui/src/export.rs#L648), [`ui/dr-ui/src/export.rs:779`](../ui/dr-ui/src/export.rs#L779), [`ui/dr-ui/src/export.rs:796`](../ui/dr-ui/src/export.rs#L796), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/settings.rs:313`](../core/dr-types/src/settings.rs#L313), [`ui/dr-ui/src/export.rs:620`](../ui/dr-ui/src/export.rs#L620), [`ui/dr-ui/src/export.rs:648`](../ui/dr-ui/src/export.rs#L648), [`ui/dr-ui/src/export.rs:779`](../ui/dr-ui/src/export.rs#L779), [`ui/dr-ui/src/export.rs:796`](../ui/dr-ui/src/export.rs#L796), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
| FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2284`](../ui/dr-ui/src/develop.rs#L2284), [`ui/dr-ui/src/lib.rs:331`](../ui/dr-ui/src/lib.rs#L331) | | FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2284`](../ui/dr-ui/src/develop.rs#L2284), [`ui/dr-ui/src/lib.rs:331`](../ui/dr-ui/src/lib.rs#L331) |
| FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`ui/dr-ui/src/launch.rs:256`](../ui/dr-ui/src/launch.rs#L256), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49), [`ui/dr-ui/src/launch_ui.rs:344`](../ui/dr-ui/src/launch_ui.rs#L344) | | FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`ui/dr-ui/src/launch.rs:256`](../ui/dr-ui/src/launch.rs#L256), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49), [`ui/dr-ui/src/launch_ui.rs:344`](../ui/dr-ui/src/launch_ui.rs#L344) |
@@ -89,14 +89,14 @@ _None._
| FR-NC-4 | [`core/dr-sync-nextcloud/src/propfind.rs:100`](../core/dr-sync-nextcloud/src/propfind.rs#L100), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49) | | FR-NC-4 | [`core/dr-sync-nextcloud/src/propfind.rs:100`](../core/dr-sync-nextcloud/src/propfind.rs#L100), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49) |
| FR-NC-5 | [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`ui/dr-ui/src/import.rs:489`](../ui/dr-ui/src/import.rs#L489) | | FR-NC-5 | [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`ui/dr-ui/src/import.rs:489`](../ui/dr-ui/src/import.rs#L489) |
| FR-NC-6 | [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1) | | FR-NC-6 | [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1) |
| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:508`](../core/dr-catalog/src/schema.rs#L508), [`core/dr-catalog/src/schema.rs:909`](../core/dr-catalog/src/schema.rs#L909), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1636`](../ui/dr-ui/src/lib.rs#L1636), [`ui/dr-ui/src/lib.rs:2433`](../ui/dr-ui/src/lib.rs#L2433), [`ui/dr-ui/src/library.rs:1324`](../ui/dr-ui/src/library.rs#L1324), [`ui/dr-ui/src/library.rs:1347`](../ui/dr-ui/src/library.rs#L1347), [`ui/dr-ui/src/library.rs:1505`](../ui/dr-ui/src/library.rs#L1505), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1412`](../ui/dr-ui/src/library_ui.rs#L1412), [`ui/dr-ui/src/library_ui.rs:1531`](../ui/dr-ui/src/library_ui.rs#L1531), [`ui/dr-ui/src/library_ui.rs:2058`](../ui/dr-ui/src/library_ui.rs#L2058), [`ui/dr-ui/src/library_ui.rs:267`](../ui/dr-ui/src/library_ui.rs#L267), [`ui/dr-ui/src/library_ui.rs:278`](../ui/dr-ui/src/library_ui.rs#L278), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:298`](../ui/dr-ui/src/library_ui.rs#L298), [`ui/dr-ui/src/library_ui.rs:307`](../ui/dr-ui/src/library_ui.rs#L307), [`ui/dr-ui/src/library_ui.rs:399`](../ui/dr-ui/src/library_ui.rs#L399), [`ui/dr-ui/src/library_ui.rs:409`](../ui/dr-ui/src/library_ui.rs#L409), [`ui/dr-ui/src/library_ui.rs:451`](../ui/dr-ui/src/library_ui.rs#L451), [`ui/dr-ui/src/library_ui.rs:514`](../ui/dr-ui/src/library_ui.rs#L514), [`ui/dr-ui/src/library_ui.rs:5239`](../ui/dr-ui/src/library_ui.rs#L5239), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/src/library_ui.rs:545`](../ui/dr-ui/src/library_ui.rs#L545), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:2480`](../ui/dr-ui/ui/app.slint#L2480), [`ui/dr-ui/ui/app.slint:607`](../ui/dr-ui/ui/app.slint#L607), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:974`](../ui/dr-ui/ui/library.slint#L974) | | FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:555`](../core/dr-catalog/src/schema.rs#L555), [`core/dr-catalog/src/schema.rs:956`](../core/dr-catalog/src/schema.rs#L956), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1658`](../ui/dr-ui/src/lib.rs#L1658), [`ui/dr-ui/src/lib.rs:2455`](../ui/dr-ui/src/lib.rs#L2455), [`ui/dr-ui/src/library.rs:1324`](../ui/dr-ui/src/library.rs#L1324), [`ui/dr-ui/src/library.rs:1347`](../ui/dr-ui/src/library.rs#L1347), [`ui/dr-ui/src/library.rs:1505`](../ui/dr-ui/src/library.rs#L1505), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1412`](../ui/dr-ui/src/library_ui.rs#L1412), [`ui/dr-ui/src/library_ui.rs:1531`](../ui/dr-ui/src/library_ui.rs#L1531), [`ui/dr-ui/src/library_ui.rs:2058`](../ui/dr-ui/src/library_ui.rs#L2058), [`ui/dr-ui/src/library_ui.rs:267`](../ui/dr-ui/src/library_ui.rs#L267), [`ui/dr-ui/src/library_ui.rs:278`](../ui/dr-ui/src/library_ui.rs#L278), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:298`](../ui/dr-ui/src/library_ui.rs#L298), [`ui/dr-ui/src/library_ui.rs:307`](../ui/dr-ui/src/library_ui.rs#L307), [`ui/dr-ui/src/library_ui.rs:399`](../ui/dr-ui/src/library_ui.rs#L399), [`ui/dr-ui/src/library_ui.rs:409`](../ui/dr-ui/src/library_ui.rs#L409), [`ui/dr-ui/src/library_ui.rs:451`](../ui/dr-ui/src/library_ui.rs#L451), [`ui/dr-ui/src/library_ui.rs:514`](../ui/dr-ui/src/library_ui.rs#L514), [`ui/dr-ui/src/library_ui.rs:5239`](../ui/dr-ui/src/library_ui.rs#L5239), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/src/library_ui.rs:545`](../ui/dr-ui/src/library_ui.rs#L545), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:2490`](../ui/dr-ui/ui/app.slint#L2490), [`ui/dr-ui/ui/app.slint:612`](../ui/dr-ui/ui/app.slint#L612), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:974`](../ui/dr-ui/ui/library.slint#L974) |
| FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294) | | FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294) |
| FR-NC-6c | [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) | | FR-NC-6c | [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) |
| FR-NC-7 | [`core/dr-sync-nextcloud/src/lib.rs:95`](../core/dr-sync-nextcloud/src/lib.rs#L95), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340) | | FR-NC-7 | [`core/dr-sync-nextcloud/src/lib.rs:95`](../core/dr-sync-nextcloud/src/lib.rs#L95), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340) |
| FR-NC-7a | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1028`](../ui/dr-ui/src/lib.rs#L1028), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-NC-7a | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1050`](../ui/dr-ui/src/lib.rs#L1050), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
| FR-NC-7b | [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`ui/dr-ui/src/import.rs:123`](../ui/dr-ui/src/import.rs#L123), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import.rs:585`](../ui/dr-ui/src/import.rs#L585), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1028`](../ui/dr-ui/src/lib.rs#L1028) | | FR-NC-7b | [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`ui/dr-ui/src/import.rs:123`](../ui/dr-ui/src/import.rs#L123), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import.rs:585`](../ui/dr-ui/src/import.rs#L585), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1050`](../ui/dr-ui/src/lib.rs#L1050) |
| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:120`](../core/dr-pipeline/src/sidecar.rs#L120), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`ui/dr-ui/src/lib.rs:1605`](../ui/dr-ui/src/lib.rs#L1605), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466) | | FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:120`](../core/dr-pipeline/src/sidecar.rs#L120), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`ui/dr-ui/src/lib.rs:1627`](../ui/dr-ui/src/lib.rs#L1627), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466) |
| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:451`](../core/dr-catalog/src/schema.rs#L451), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/src/sidecar.rs:1723`](../core/dr-pipeline/src/sidecar.rs#L1723), [`core/dr-pipeline/src/sidecar.rs:332`](../core/dr-pipeline/src/sidecar.rs#L332), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:880`](../ui/dr-ui/src/library.rs#L880) | | FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/src/sidecar.rs:1723`](../core/dr-pipeline/src/sidecar.rs#L1723), [`core/dr-pipeline/src/sidecar.rs:332`](../core/dr-pipeline/src/sidecar.rs#L332), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:880`](../ui/dr-ui/src/library.rs#L880) |
| FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53) | | FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53) |
| FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) |
| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/lib.rs:792`](../ui/dr-ui/src/lib.rs#L792), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/lib.rs:792`](../ui/dr-ui/src/lib.rs#L792), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
@@ -104,19 +104,19 @@ _None._
| FR-RAW-3 | [`core/dr-decode/src/lib.rs:139`](../core/dr-decode/src/lib.rs#L139), [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-decode/src/locate.rs:1366`](../core/dr-decode/src/locate.rs#L1366) | | FR-RAW-3 | [`core/dr-decode/src/lib.rs:139`](../core/dr-decode/src/lib.rs#L139), [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-decode/src/locate.rs:1366`](../core/dr-decode/src/locate.rs#L1366) |
| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:178`](../ui/dr-ui/src/lib.rs#L178) | | FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:178`](../ui/dr-ui/src/lib.rs#L178) |
| FR-RAW-5 | [`core/dr-decode/src/lib.rs:167`](../core/dr-decode/src/lib.rs#L167), [`core/dr-gpu/src/demosaic.rs:34`](../core/dr-gpu/src/demosaic.rs#L34), [`core/dr-gpu/src/demosaic.rs:602`](../core/dr-gpu/src/demosaic.rs#L602), [`core/dr-gpu/src/demosaic.rs:681`](../core/dr-gpu/src/demosaic.rs#L681), [`core/dr-gpu/src/demosaic.rs:805`](../core/dr-gpu/src/demosaic.rs#L805) | | FR-RAW-5 | [`core/dr-decode/src/lib.rs:167`](../core/dr-decode/src/lib.rs#L167), [`core/dr-gpu/src/demosaic.rs:34`](../core/dr-gpu/src/demosaic.rs#L34), [`core/dr-gpu/src/demosaic.rs:602`](../core/dr-gpu/src/demosaic.rs#L602), [`core/dr-gpu/src/demosaic.rs:681`](../core/dr-gpu/src/demosaic.rs#L681), [`core/dr-gpu/src/demosaic.rs:805`](../core/dr-gpu/src/demosaic.rs#L805) |
| FR-UI-1 | [`ui/dr-ui/src/lib.rs:2515`](../ui/dr-ui/src/lib.rs#L2515), [`ui/dr-ui/src/lib.rs:70`](../ui/dr-ui/src/lib.rs#L70), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/library.slint:1040`](../ui/dr-ui/ui/library.slint#L1040) | | FR-UI-1 | [`ui/dr-ui/src/lib.rs:2537`](../ui/dr-ui/src/lib.rs#L2537), [`ui/dr-ui/src/lib.rs:70`](../ui/dr-ui/src/lib.rs#L70), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/library.slint:1040`](../ui/dr-ui/ui/library.slint#L1040) |
| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/lib.rs:70`](../ui/dr-ui/src/lib.rs#L70), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/src/library_ui.rs:6345`](../ui/dr-ui/src/library_ui.rs#L6345), [`ui/dr-ui/ui/app.slint:2202`](../ui/dr-ui/ui/app.slint#L2202), [`ui/dr-ui/ui/app.slint:263`](../ui/dr-ui/ui/app.slint#L263), [`ui/dr-ui/ui/app.slint:637`](../ui/dr-ui/ui/app.slint#L637), [`ui/dr-ui/ui/app.slint:644`](../ui/dr-ui/ui/app.slint#L644), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/settings.slint:93`](../ui/dr-ui/ui/settings.slint#L93) | | FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/lib.rs:70`](../ui/dr-ui/src/lib.rs#L70), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/src/library_ui.rs:6345`](../ui/dr-ui/src/library_ui.rs#L6345), [`ui/dr-ui/ui/app.slint:2212`](../ui/dr-ui/ui/app.slint#L2212), [`ui/dr-ui/ui/app.slint:263`](../ui/dr-ui/ui/app.slint#L263), [`ui/dr-ui/ui/app.slint:642`](../ui/dr-ui/ui/app.slint#L642), [`ui/dr-ui/ui/app.slint:649`](../ui/dr-ui/ui/app.slint#L649), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/settings.slint:93`](../ui/dr-ui/ui/settings.slint#L93) |
| FR-UI-3 | [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/library_ui.rs:4369`](../ui/dr-ui/src/library_ui.rs#L4369), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:908`](../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/ui/app.slint:1975`](../ui/dr-ui/ui/app.slint#L1975), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682) | | FR-UI-3 | [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/library_ui.rs:4369`](../ui/dr-ui/src/library_ui.rs#L4369), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:908`](../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/ui/app.slint:1985`](../ui/dr-ui/ui/app.slint#L1985), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682) |
| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4369`](../ui/dr-ui/src/library_ui.rs#L4369), [`ui/dr-ui/src/library_ui.rs:4414`](../ui/dr-ui/src/library_ui.rs#L4414), [`ui/dr-ui/src/library_ui.rs:4517`](../ui/dr-ui/src/library_ui.rs#L4517), [`ui/dr-ui/src/library_ui.rs:4545`](../ui/dr-ui/src/library_ui.rs#L4545), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/ui/app.slint:1671`](../ui/dr-ui/ui/app.slint#L1671), [`ui/dr-ui/ui/app.slint:613`](../ui/dr-ui/ui/app.slint#L613), [`ui/dr-ui/ui/app.slint:644`](../ui/dr-ui/ui/app.slint#L644), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) | | FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4369`](../ui/dr-ui/src/library_ui.rs#L4369), [`ui/dr-ui/src/library_ui.rs:4414`](../ui/dr-ui/src/library_ui.rs#L4414), [`ui/dr-ui/src/library_ui.rs:4517`](../ui/dr-ui/src/library_ui.rs#L4517), [`ui/dr-ui/src/library_ui.rs:4545`](../ui/dr-ui/src/library_ui.rs#L4545), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/ui/app.slint:1681`](../ui/dr-ui/ui/app.slint#L1681), [`ui/dr-ui/ui/app.slint:618`](../ui/dr-ui/ui/app.slint#L618), [`ui/dr-ui/ui/app.slint:649`](../ui/dr-ui/ui/app.slint#L649), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) |
| FR-UI-5 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2160`](../ui/dr-ui/src/lib.rs#L2160), [`ui/dr-ui/src/lib.rs:2549`](../ui/dr-ui/src/lib.rs#L2549), [`ui/dr-ui/src/lib.rs:2734`](../ui/dr-ui/src/lib.rs#L2734), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:298`](../ui/dr-ui/ui/app.slint#L298), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | | FR-UI-5 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2182`](../ui/dr-ui/src/lib.rs#L2182), [`ui/dr-ui/src/lib.rs:2571`](../ui/dr-ui/src/lib.rs#L2571), [`ui/dr-ui/src/lib.rs:2756`](../ui/dr-ui/src/lib.rs#L2756), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:298`](../ui/dr-ui/ui/app.slint#L298), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) |
| FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:100`](../core/dr-pipeline/src/descriptor.rs#L100), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259) | | FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:100`](../core/dr-pipeline/src/descriptor.rs#L100), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259) |
| NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) | | NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) |
| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1555`](../ui/dr-ui/src/export.rs#L1555), [`ui/dr-ui/src/export.rs:1581`](../ui/dr-ui/src/export.rs#L1581), [`ui/dr-ui/src/export.rs:410`](../ui/dr-ui/src/export.rs#L410), [`ui/dr-ui/src/export.rs:436`](../ui/dr-ui/src/export.rs#L436), [`ui/dr-ui/src/lib.rs:1910`](../ui/dr-ui/src/lib.rs#L1910), [`ui/dr-ui/ui/app.slint:1046`](../ui/dr-ui/ui/app.slint#L1046), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) | | NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1555`](../ui/dr-ui/src/export.rs#L1555), [`ui/dr-ui/src/export.rs:1581`](../ui/dr-ui/src/export.rs#L1581), [`ui/dr-ui/src/export.rs:410`](../ui/dr-ui/src/export.rs#L410), [`ui/dr-ui/src/export.rs:436`](../ui/dr-ui/src/export.rs#L436), [`ui/dr-ui/src/lib.rs:1932`](../ui/dr-ui/src/lib.rs#L1932), [`ui/dr-ui/ui/app.slint:1051`](../ui/dr-ui/ui/app.slint#L1051), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) |
| NFR-ARCH-4 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-export/src/error.rs:1`](../core/dr-export/src/error.rs#L1), [`core/dr-thumbs/src/error.rs:1`](../core/dr-thumbs/src/error.rs#L1), [`ui/dr-ui/src/export.rs:500`](../ui/dr-ui/src/export.rs#L500) | | NFR-ARCH-4 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-export/src/error.rs:1`](../core/dr-export/src/error.rs#L1), [`core/dr-thumbs/src/error.rs:1`](../core/dr-thumbs/src/error.rs#L1), [`ui/dr-ui/src/export.rs:500`](../ui/dr-ui/src/export.rs#L500) |
| NFR-OPS-1 | [`tools/traceability/src/lib.rs:266`](../tools/traceability/src/lib.rs#L266) | | NFR-OPS-1 | [`tools/traceability/src/lib.rs:266`](../tools/traceability/src/lib.rs#L266) |
| NFR-P1 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) | | NFR-P1 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
| NFR-P13 | [`core/dr-decode/src/preview.rs:134`](../core/dr-decode/src/preview.rs#L134) | | NFR-P13 | [`core/dr-decode/src/preview.rs:134`](../core/dr-decode/src/preview.rs#L134) |
| NFR-P5 | [`core/dr-catalog/src/schema.rs:298`](../core/dr-catalog/src/schema.rs#L298), [`ui/dr-ui/src/library.rs:3141`](../ui/dr-ui/src/library.rs#L3141), [`ui/dr-ui/src/library.rs:3914`](../ui/dr-ui/src/library.rs#L3914), [`ui/dr-ui/src/library_ui.rs:4054`](../ui/dr-ui/src/library_ui.rs#L4054), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) | | NFR-P5 | [`core/dr-catalog/src/schema.rs:304`](../core/dr-catalog/src/schema.rs#L304), [`ui/dr-ui/src/library.rs:3171`](../ui/dr-ui/src/library.rs#L3171), [`ui/dr-ui/src/library.rs:3944`](../ui/dr-ui/src/library.rs#L3944), [`ui/dr-ui/src/library_ui.rs:4054`](../ui/dr-ui/src/library_ui.rs#L4054), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) |
| NFR-P9 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:944`](../ui/dr-ui/src/export.rs#L944), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1) | | NFR-P9 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:944`](../ui/dr-ui/src/export.rs#L944), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1) |
| NFR-PORT-1 | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302) | | NFR-PORT-1 | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302) |
| NFR-R1 | [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`ui/dr-ui/src/library.rs:947`](../ui/dr-ui/src/library.rs#L947) | | NFR-R1 | [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`ui/dr-ui/src/library.rs:947`](../ui/dr-ui/src/library.rs#L947) |
@@ -125,9 +125,9 @@ _None._
| NFR-R7 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) | | NFR-R7 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) |
| NFR-R8 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) | | NFR-R8 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) |
| NFR-RES-1 | [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`ui/dr-ui/src/lib.rs:62`](../ui/dr-ui/src/lib.rs#L62) | | NFR-RES-1 | [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`ui/dr-ui/src/lib.rs:62`](../ui/dr-ui/src/lib.rs#L62) |
| NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:508`](../core/dr-catalog/src/schema.rs#L508), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/library.rs:2598`](../ui/dr-ui/src/library.rs#L2598) | | NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:555`](../core/dr-catalog/src/schema.rs#L555), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/library.rs:2598`](../ui/dr-ui/src/library.rs#L2598) |
| NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) | | NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) |
| NFR-SEC-5 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | NFR-SEC-5 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
| R1 | [`tools/traceability/src/lib.rs:495`](../tools/traceability/src/lib.rs#L495), [`tools/traceability/src/lib.rs:499`](../tools/traceability/src/lib.rs#L499) | | R1 | [`tools/traceability/src/lib.rs:495`](../tools/traceability/src/lib.rs#L495), [`tools/traceability/src/lib.rs:499`](../tools/traceability/src/lib.rs#L499) |
| R4 | [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) | | R4 | [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) |
+5
View File
@@ -107,3 +107,8 @@ default = []
# without rebuilding. Costs the constant-folding of every token, so it stays # without rebuilding. Costs the constant-folding of every token, so it stays
# off by default and has no business in a release build. # off by default and has no business in a release build.
live-style = ["dep:serde_norway"] live-style = ["dep:serde_norway"]
[dev-dependencies]
# The face_index batch job wants a log level from the environment; the library
# itself only ever calls `log`, and picks up whatever the application installs.
env_logger.workspace = true
+157
View File
@@ -0,0 +1,157 @@
//! The face-indexing batch job, off the GUI.
//!
//! Checks every library image for a face-detection run marker, and optionally
//! indexes whatever is missing one.
//!
//! cargo run -p dr-ui --example face_index -- CATALOG.db THUMBS_DIR [--run DET.onnx EMB.onnx]
//!
//! # Why this exists beside the button in the Identity screen
//!
//! Indexing a real library is hours of work (docs/faces.md §12.2), and the
//! cases where that is worth starting — an overnight pass, a fresh import, a
//! machine left running — are exactly the ones where holding a window open is
//! the wrong shape. The check half is useful on its own: it is cheap, it
//! answers "has face recognition been over all of this", and it distinguishes
//! *not yet indexed* from *waiting on a proxy*, which are different problems
//! with different fixes.
//!
//! The models must have had their input dims frozen first; see
//! `tools/fix-face-model-shapes.sh`.
use std::path::PathBuf;
use dr_catalog::Catalog;
use dr_thumbs::ThumbStore;
use dr_ui::faces::{self, FaceSweepMessage};
const MODEL_ID: &str = "w600k_mbf";
fn main() {
env_logger::init();
let args: Vec<String> = std::env::args().skip(1).collect();
if args.len() < 2 {
eprintln!(
"usage: face_index CATALOG.db THUMBS_DIR [--run DETECTOR.onnx EMBEDDER.onnx]\n\
\n\
With no --run this only reports; nothing is written."
);
std::process::exit(2);
}
let catalog_path = PathBuf::from(&args[0]);
let store_dir = PathBuf::from(&args[1]);
let catalog = match Catalog::open(&catalog_path) {
Ok(c) => c,
Err(e) => {
eprintln!("cannot open catalog {}: {e}", catalog_path.display());
std::process::exit(1);
}
};
let store = match ThumbStore::open(&store_dir) {
Ok(s) => s,
Err(e) => {
eprintln!("cannot open thumbnail store {}: {e}", store_dir.display());
std::process::exit(1);
}
};
let audit = match faces::audit(&catalog, &store, MODEL_ID) {
Ok(a) => a,
Err(e) => {
eprintln!("coverage check failed: {e}");
std::process::exit(1);
}
};
println!("model {MODEL_ID}");
println!("images {}", audit.coverage.images);
println!(
"indexed {} ({:.1}%)",
audit.coverage.indexed,
audit.coverage.fraction() * 100.0
);
println!("faces {}", audit.coverage.faces);
println!(
"no faces {} (indexed, nothing found — the common case)",
audit.coverage.without_faces
);
println!("outstanding {}", audit.coverage.outstanding());
println!(" ready to index {}", audit.ready);
println!(" awaiting proxy {}", audit.awaiting_proxy);
let run = args.iter().position(|a| a == "--run");
let Some(i) = run else {
if audit.coverage.is_complete() {
println!("\nnothing outstanding.");
} else {
println!("\npass --run DETECTOR.onnx EMBEDDER.onnx to index the outstanding images.");
}
return;
};
let (Some(detector), Some(embedder)) = (args.get(i + 1), args.get(i + 2)) else {
eprintln!("--run needs both a detector and an embedder");
std::process::exit(2);
};
if audit.ready == 0 {
println!("\nnothing ready to index.");
if audit.awaiting_proxy > 0 {
// Worth saying plainly: running this again will not help, because
// the blocker is in the thumbnail store rather than here.
println!(
"{} image(s) are waiting on a proxy — run the thumbnail sweep first.",
audit.awaiting_proxy
);
}
return;
}
println!("\nindexing {} image(s)…", audit.ready);
let rx = faces::spawn_face_sweep(
catalog_path,
store_dir,
PathBuf::from(detector),
PathBuf::from(embedder),
MODEL_ID.to_string(),
dr_face::DetectOptions::default(),
);
let mut seen = 0usize;
let mut total = 0usize;
let start = std::time::Instant::now();
for msg in rx {
match msg {
FaceSweepMessage::Total(n) => total = n,
FaceSweepMessage::Indexed { faces, .. } => {
seen += 1;
// One line per image would be thousands of lines; one per
// twenty-five is enough to see it moving and to estimate.
if seen % 25 == 0 || faces > 0 {
let rate = seen as f64 / start.elapsed().as_secs_f64().max(1e-6);
println!(
" {seen}/{total} {:.2} img/s ~{:.0} min left",
rate,
(total.saturating_sub(seen)) as f64 / rate.max(1e-6) / 60.0
);
}
}
FaceSweepMessage::Finished {
images,
faces,
failed,
} => {
println!(
"\ndone: {images} image(s), {faces} face(s), {failed} failed, in {:.0}s",
start.elapsed().as_secs_f64()
);
}
}
}
if let Ok(a) = faces::audit(&catalog, &store, MODEL_ID) {
println!("{}", a.summary());
}
}
+147 -6
View File
@@ -59,11 +59,17 @@ pub struct FaceRequest {
pub file_id: u64, pub file_id: u64,
} }
/// Images that have a usable proxy and no faces recorded for this model. /// Images that have a usable proxy and have not been through this model.
///
/// Asks `face_index` — the *run* marker — rather than asking whether the image
/// has any faces. Those are different questions, and confusing them is the
/// difference between a pass that converges and one that does not: a
/// photograph with no face in it would otherwise look identical to one never
/// examined, so every landscape in the library would be re-detected on every
/// run, for ever. See the V9 migration.
/// ///
/// Keyed on the model, so a model upgrade re-indexes rather than leaving the /// Keyed on the model, so a model upgrade re-indexes rather than leaving the
/// library half-described by weights that are no longer comparable — the /// library half-described by weights that are no longer comparable.
/// failure `faces.model_id` exists to make detectable.
pub fn faces_outstanding( pub fn faces_outstanding(
catalog: &Catalog, catalog: &Catalog,
store: &ThumbStore, store: &ThumbStore,
@@ -76,8 +82,8 @@ pub fn faces_outstanding(
WHERE r.file_id IS NOT NULL WHERE r.file_id IS NOT NULL
AND i.trashed_at IS NULL AND i.trashed_at IS NULL
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM faces f SELECT 1 FROM face_index fi
WHERE f.image_id = i.id AND f.model_id = ?1 WHERE fi.image_id = i.id AND fi.model_id = ?1
) )
ORDER BY i.id", ORDER BY i.id",
)?; )?;
@@ -98,6 +104,93 @@ pub fn faces_outstanding(
Ok(rows) Ok(rows)
} }
/// What a coverage check found.
///
/// The catalog can say how many images have been through the model; only this
/// layer can say *why* the rest have not, because the reason usually lives in
/// the thumbnail store rather than the catalog.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
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.
pub awaiting_proxy: u64,
}
impl IndexAudit {
/// One line, for a log or a status strip.
pub fn summary(&self) -> String {
let c = &self.coverage;
if c.images == 0 {
return "no images in the library".into();
}
let mut s = format!(
"{}/{} images indexed ({:.0}%), {} face(s), {} image(s) with none",
c.indexed,
c.images,
c.fraction() * 100.0,
c.faces,
c.without_faces,
);
if self.ready > 0 {
s.push_str(&format!("; {} ready to index", self.ready));
}
if self.awaiting_proxy > 0 {
s.push_str(&format!("; {} awaiting a proxy", self.awaiting_proxy));
}
s
}
}
/// Check every library image for a face-detection run marker.
///
/// The batch pass that answers "has face recognition been over all of this",
/// and the one to run before deciding whether to start a sweep. Cheap: two
/// counts and one indexed scan, no decoding and no inference.
pub fn audit(
catalog: &Catalog,
store: &ThumbStore,
model_id: &str,
) -> Result<IndexAudit, dr_catalog::CatalogError> {
let conn = catalog.connection();
let coverage = faces::coverage(conn, model_id)?;
// Split the outstanding set by whether a proxy exists. This is the query
// `faces_outstanding` runs without the store filter, so the two cannot
// disagree about what is outstanding.
let mut stmt = conn.prepare(
"SELECT r.file_id
FROM images i
JOIN remote r ON r.image_id = i.id
WHERE r.file_id IS NOT NULL
AND i.trashed_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM face_index fi
WHERE fi.image_id = i.id AND fi.model_id = ?1
)",
)?;
let (mut ready, mut awaiting) = (0u64, 0u64);
for file_id in stmt
.query_map([model_id], |r| r.get::<_, i64>(0))?
.filter_map(Result::ok)
{
if store.contains(file_id as u64, FACE_TIER) {
ready += 1;
} else {
awaiting += 1;
}
}
Ok(IndexAudit {
coverage,
ready,
awaiting_proxy: awaiting,
})
}
/// Detect and embed every face in one decoded proxy. /// Detect and embed every face in one decoded proxy.
/// ///
/// Coordinates come back **normalised to the long edge**, which is what the /// Coordinates come back **normalised to the long edge**, which is what the
@@ -284,7 +377,13 @@ pub fn spawn_face_sweep(
} }
}; };
if let Err(e) = faces::record_detections(catalog.connection(), req.image_id, &faces) { if let Err(e) = faces::record_detections(
catalog.connection(),
req.image_id,
&model_id,
w.max(h),
&faces,
) {
log::warn!("face sweep: storing faces for {:?}: {e}", req.image_id); log::warn!("face sweep: storing faces for {:?}: {e}", req.image_id);
failed += 1; failed += 1;
continue; continue;
@@ -537,6 +636,48 @@ fn rgba_to_rgb_f32(rgba: &[u8]) -> Vec<f32> {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn an_audit_summary_names_both_kinds_of_outstanding() {
let a = IndexAudit {
coverage: faces::Coverage {
images: 100,
indexed: 60,
without_faces: 40,
faces: 35,
},
ready: 30,
awaiting_proxy: 10,
};
let s = a.summary();
assert!(s.contains("60/100"), "{s}");
assert!(s.contains("60%"), "{s}");
assert!(s.contains("30 ready"), "{s}");
assert!(s.contains("10 awaiting"), "{s}");
}
#[test]
fn a_complete_audit_mentions_neither() {
let a = IndexAudit {
coverage: faces::Coverage {
images: 10,
indexed: 10,
without_faces: 7,
faces: 4,
},
ready: 0,
awaiting_proxy: 0,
};
let s = a.summary();
assert!(!s.contains("ready"), "{s}");
assert!(!s.contains("awaiting"), "{s}");
assert!(s.contains("10/10"), "{s}");
}
#[test]
fn an_empty_library_says_so_rather_than_reporting_zero_of_zero() {
assert_eq!(IndexAudit::default().summary(), "no images in the library");
}
#[test] #[test]
fn landmarks_normalise_against_the_long_edge() { 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)];
+10 -10
View File
@@ -474,8 +474,8 @@ mod tests {
let c = catalog(); let c = catalog();
let i1 = image(&c, 1); let i1 = image(&c, 1);
let i2 = image(&c, 2); let i2 = image(&c, 2);
let a = faces::record_detections(c.connection(), i1, &[face(1)]).unwrap(); let a = faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
faces::record_detections(c.connection(), i2, &[face(2)]).unwrap(); faces::record_detections(c.connection(), i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let anna = faces::create_person(c.connection(), "Anna").unwrap(); let anna = faces::create_person(c.connection(), "Anna").unwrap();
faces::confirm(c.connection(), a[0], anna).unwrap(); faces::confirm(c.connection(), a[0], anna).unwrap();
@@ -493,9 +493,9 @@ mod tests {
let i1 = image(&c, 1); let i1 = image(&c, 1);
let i2 = image(&c, 2); let i2 = image(&c, 2);
let i3 = image(&c, 3); let i3 = image(&c, 3);
let a = faces::record_detections(c.connection(), i1, &[face(1)]).unwrap(); let a = faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let b = faces::record_detections(c.connection(), i2, &[face(2)]).unwrap(); let b = faces::record_detections(c.connection(), i2, "w600k_mbf", 1024, &[face(2)]).unwrap();
let d = faces::record_detections(c.connection(), i3, &[face(3)]).unwrap(); let d = faces::record_detections(c.connection(), i3, "w600k_mbf", 1024, &[face(3)]).unwrap();
let anna = faces::create_person(c.connection(), "Anna").unwrap(); let anna = faces::create_person(c.connection(), "Anna").unwrap();
faces::confirm(c.connection(), a[0], anna).unwrap(); faces::confirm(c.connection(), a[0], anna).unwrap();
@@ -514,8 +514,8 @@ mod tests {
let c = catalog(); let c = catalog();
let i1 = image(&c, 1); let i1 = image(&c, 1);
let i2 = image(&c, 2); let i2 = image(&c, 2);
let a = faces::record_detections(c.connection(), i1, &[face(1)]).unwrap(); let a = faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let b = faces::record_detections(c.connection(), i2, &[face(2)]).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 anna = faces::create_person(c.connection(), "Anna").unwrap();
faces::confirm(c.connection(), a[0], anna).unwrap(); faces::confirm(c.connection(), a[0], anna).unwrap();
@@ -547,8 +547,8 @@ mod tests {
let c = catalog(); let c = catalog();
let i1 = image(&c, 1); let i1 = image(&c, 1);
let i2 = image(&c, 2); let i2 = image(&c, 2);
let a = faces::record_detections(c.connection(), i1, &[face(1)]).unwrap(); let a = faces::record_detections(c.connection(), i1, "w600k_mbf", 1024, &[face(1)]).unwrap();
let b = faces::record_detections(c.connection(), i2, &[face(2)]).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 anna = faces::create_person(c.connection(), "Anna").unwrap();
let annie = faces::create_person(c.connection(), "Annie").unwrap(); let annie = faces::create_person(c.connection(), "Annie").unwrap();
faces::confirm(c.connection(), a[0], anna).unwrap(); faces::confirm(c.connection(), a[0], anna).unwrap();
@@ -562,7 +562,7 @@ mod tests {
fn deleting_everything_empties_the_screen() { fn deleting_everything_empties_the_screen() {
let c = catalog(); let c = catalog();
let i1 = image(&c, 1); let i1 = image(&c, 1);
let a = faces::record_detections(c.connection(), i1, &[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(); let anna = faces::create_person(c.connection(), "Anna").unwrap();
faces::confirm(c.connection(), a[0], anna).unwrap(); faces::confirm(c.connection(), a[0], anna).unwrap();
+204 -1
View File
@@ -7,12 +7,15 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use std::sync::mpsc::Receiver;
use std::time::Duration;
use dr_catalog::faces::{FaceId, PersonId}; use dr_catalog::faces::{FaceId, PersonId};
use dr_catalog::Catalog; use dr_catalog::Catalog;
use dr_thumbs::ThumbStore; use dr_thumbs::ThumbStore;
use slint::{ComponentHandle, ModelRc, VecModel}; use slint::{ComponentHandle, ModelRc, VecModel};
use crate::faces::FaceSweepMessage;
use crate::identity::{self, FaceCell, PersonRow}; use crate::identity::{self, FaceCell, PersonRow};
use crate::{AppWindow, IdentityFace, IdentityPerson}; use crate::{AppWindow, IdentityFace, IdentityPerson};
@@ -31,6 +34,15 @@ pub struct IdentityController {
selected: std::cell::Cell<Option<PersonId>>, selected: std::cell::Cell<Option<PersonId>>,
/// Faces the user has ticked, which is what a split would carry. /// Faces the user has ticked, which is what a split would carry.
picked: RefCell<Vec<FaceId>>, picked: RefCell<Vec<FaceId>>,
/// The running indexing sweep, if any.
///
/// Holding the receiver *is* the cancellation handle: the worker stops when
/// its send fails, so dropping this is how "Stop" works. No flag to get out
/// of step with the thread, and everything already written stays written.
sweep: RefCell<Option<Receiver<FaceSweepMessage>>>,
/// Images visited so far in the current sweep, and the total it announced.
progress: std::cell::Cell<(usize, usize)>,
faces_found: std::cell::Cell<usize>,
} }
impl IdentityController { impl IdentityController {
@@ -126,6 +138,36 @@ pub fn refresh(
} }
window.set_identity_picked(ctl.picked.borrow().len() as i32); window.set_identity_picked(ctl.picked.borrow().len() as i32);
drop(borrow);
refresh_coverage(window, catalog, store);
}
/// Run the batch check and put its answer on screen.
///
/// Cheap enough to call on every open and after every sweep: two counts and one
/// indexed scan, no decoding and no inference.
pub fn refresh_coverage(
window: &AppWindow,
catalog: &Rc<RefCell<Option<Catalog>>>,
store: Option<&ThumbStore>,
) {
let borrow = catalog.borrow();
let (Some(cat), Some(store)) = (borrow.as_ref(), store) else {
window.set_identity_coverage(Default::default());
return;
};
match crate::faces::audit(cat, store, MODEL_ID) {
Ok(a) => {
window.set_identity_coverage(a.summary().into());
// Complete means nothing left to index, not "every image has a
// face": most of a library has none, and that is a finding.
window.set_identity_coverage_complete(a.coverage.is_complete());
}
Err(e) => {
log::warn!("identity: coverage check: {e}");
window.set_identity_coverage("coverage unknown".into());
}
}
} }
fn push_faces(window: &AppWindow, ctl: &IdentityController, cells: &[FaceCell]) { fn push_faces(window: &AppWindow, ctl: &IdentityController, cells: &[FaceCell]) {
@@ -155,16 +197,34 @@ fn to_slint_image(width: u32, height: u32, rgba: &[u8]) -> slint::Image {
slint::Image::from_rgba8(buf) slint::Image::from_rgba8(buf)
} }
/// Where a background sweep reads from: the catalog file and the thumbnail
/// store directory.
///
/// Paths rather than the live handles this screen holds, because the sweep runs
/// on its own thread and opens its own connection. Two connections to one
/// SQLite file is the normal arrangement here; sharing a handle across the
/// boundary would not be.
pub type SweepPaths = (std::path::PathBuf, std::path::PathBuf);
/// The detector and embedder files, when both are present.
pub type ModelPaths = (std::path::PathBuf, std::path::PathBuf);
/// Attach every Identity callback. /// Attach every Identity callback.
pub fn wire<S>( pub fn wire<S, M, P>(
window: &AppWindow, window: &AppWindow,
ctl: Rc<IdentityController>, ctl: Rc<IdentityController>,
catalog: Rc<RefCell<Option<Catalog>>>, catalog: Rc<RefCell<Option<Catalog>>>,
store: S, store: S,
models: M,
paths: P,
) where ) where
S: Fn() -> Option<Rc<ThumbStore>> + 'static, S: Fn() -> Option<Rc<ThumbStore>> + 'static,
M: Fn() -> Option<ModelPaths> + 'static,
P: Fn() -> Option<SweepPaths> + 'static,
{ {
let store: Rc<dyn Fn() -> Option<Rc<ThumbStore>>> = Rc::new(store); let store: Rc<dyn Fn() -> Option<Rc<ThumbStore>>> = Rc::new(store);
let models: Rc<dyn Fn() -> Option<ModelPaths>> = Rc::new(models);
let paths: Rc<dyn Fn() -> Option<SweepPaths>> = Rc::new(paths);
// Re-read everything and redraw. Every mutating callback ends in this // Re-read everything and redraw. Every mutating callback ends in this
// rather than patching the model in place: the operations here have // rather than patching the model in place: the operations here have
@@ -182,9 +242,13 @@ pub fn wire<S>(
let ctl = ctl.clone(); let ctl = ctl.clone();
let catalog = catalog.clone(); let catalog = catalog.clone();
let store = store.clone(); let store = store.clone();
let models_present = models.clone();
window.on_identity_open(move || { window.on_identity_open(move || {
let Some(w) = weak.upgrade() else { return }; let Some(w) = weak.upgrade() else { return };
w.set_show_identity(true); w.set_show_identity(true);
// A fact about the filesystem, so it is re-checked on every open
// rather than cached: the user may have just put the models there.
w.set_identity_model_missing(models_present().is_none());
reload!(w, ctl, catalog, store); reload!(w, ctl, catalog, store);
}); });
} }
@@ -348,6 +412,128 @@ pub fn wire<S>(
}); });
} }
{
let weak = window.as_weak();
let ctl = ctl.clone();
let catalog = catalog.clone();
let store = store.clone();
let models = models.clone();
let paths = paths.clone();
window.on_identity_index(move || {
let Some(w) = weak.upgrade() else { return };
if ctl.sweep.borrow().is_some() {
return;
}
let Some((detector, embedder)) = models() else {
w.set_identity_model_missing(true);
return;
};
let Some((catalog_path, store_dir)) = paths() else {
return;
};
ctl.progress.set((0, 0));
ctl.faces_found.set(0);
*ctl.sweep.borrow_mut() = Some(crate::faces::spawn_face_sweep(
catalog_path,
store_dir,
detector,
embedder,
MODEL_ID.to_string(),
dr_face::DetectOptions::default(),
));
w.set_identity_indexing(true);
w.set_identity_indexing_status("looking for images to index…".into());
// Polled rather than pushed: the worker is a plain thread with an
// mpsc channel, and a timer on the UI thread keeps every Slint
// property write where Slint requires it. 250 ms is far shorter
// than one image takes, so the timer never becomes the bottleneck
// and never spins on an empty channel for long.
let timer = slint::Timer::default();
let weak_tick = w.as_weak();
let ctl_tick = ctl.clone();
let catalog_tick = catalog.clone();
let store_tick = store.clone();
timer.start(
slint::TimerMode::Repeated,
Duration::from_millis(250),
move || {
let Some(w) = weak_tick.upgrade() else { return };
let mut done = false;
{
let borrow = ctl_tick.sweep.borrow();
let Some(rx) = borrow.as_ref() else { return };
// Drained rather than one per tick: several images can
// land between ticks, and showing the oldest would make
// the count visibly lag the work.
while let Ok(msg) = rx.try_recv() {
match msg {
FaceSweepMessage::Total(n) => ctl_tick.progress.set((0, n)),
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);
}
FaceSweepMessage::Finished { .. } => done = true,
}
}
}
let (seen, total) = ctl_tick.progress.get();
w.set_identity_indexing_status(
format!(
"indexing {seen}/{total} — {} face(s) found",
ctl_tick.faces_found.get()
)
.into(),
);
if done {
*ctl_tick.sweep.borrow_mut() = None;
w.set_identity_indexing(false);
// Newly indexed faces belong to nobody until they are
// grouped, so a sweep ending with an unchanged people
// rail is expected. The coverage line is what shows it
// worked, and Regroup is the next step.
refresh(&w, &ctl_tick, &catalog_tick, store_tick().as_deref());
}
},
);
// A Slint timer stops when it drops, so it has to outlive this
// callback.
park_timer(timer);
});
}
{
let weak = window.as_weak();
let ctl = ctl.clone();
let catalog = catalog.clone();
let store = store.clone();
window.on_identity_stop_indexing(move || {
let Some(w) = weak.upgrade() else { return };
// Dropping the receiver *is* the cancellation: the worker's next
// send fails and it returns, leaving everything already written
// written. No flag to fall out of step with the thread.
*ctl.sweep.borrow_mut() = None;
w.set_identity_indexing(false);
reload!(w, ctl, catalog, store);
});
}
{
let weak = window.as_weak();
let catalog = catalog.clone();
let store = store.clone();
window.on_identity_check_coverage(move || {
let Some(w) = weak.upgrade() else { return };
refresh_coverage(&w, &catalog, store().as_deref());
});
}
{ {
let weak = window.as_weak(); let weak = window.as_weak();
let ctl = ctl.clone(); let ctl = ctl.clone();
@@ -368,6 +554,22 @@ pub fn wire<S>(
} }
} }
/// Keep the running sweep's poll timer alive.
///
/// A `slint::Timer` stops when it drops, so the one driving a sweep has to
/// outlive the callback that started it. Parking it here rather than in the
/// controller keeps `IdentityController` free of Slint types, which is what
/// lets it be constructed in a test with no event loop.
///
/// One slot: starting a second sweep replaces the first's timer, and by then
/// the first sweep has already finished or been stopped.
fn park_timer(timer: slint::Timer) {
thread_local! {
static SWEEP_TIMER: RefCell<Option<slint::Timer>> = const { RefCell::new(None) };
}
SWEEP_TIMER.with(|slot| *slot.borrow_mut() = Some(timer));
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -398,3 +600,4 @@ mod tests {
assert!(ctl.picked.borrow().is_empty()); assert!(ctl.picked.borrow().is_empty());
} }
} }
+22
View File
@@ -972,6 +972,28 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
.ok() .ok()
.map(std::rc::Rc::new) .map(std::rc::Rc::new)
}, },
// The weights are not shipped and are not a build input
// (docs/faces.md §2): the user puts them beside the catalog,
// and their absence is the ordinary state of a fresh install.
{
let lib = library.clone();
move || {
let (_, session) = lib.session()?;
library::face_models(&session.server, &session.user_id)
}
},
// The sweep opens its own connection on its own thread, so it
// takes paths rather than the handles this screen holds.
{
let lib = library.clone();
move || {
let (_, session) = lib.session()?;
Some((
library::catalog_path(&session.server, &session.user_id),
library::thumbs_dir(&session.server, &session.user_id),
))
}
},
); );
} }
+30
View File
@@ -2892,6 +2892,36 @@ pub fn thumbs_dir(server: &str, user_id: &str) -> PathBuf {
.unwrap_or_else(|| std::env::temp_dir().join("darkroom-thumbs")) .unwrap_or_else(|| std::env::temp_dir().join("darkroom-thumbs"))
} }
/// Where the face models live, beside the catalog and the thumbnails.
///
/// **Not shipped with the application** and not a build input: the InsightFace
/// weights carry a non-commercial research grant incompatible with this
/// project's licence, so the user obtains them and the app loads them from here
/// (docs/faces.md §2). An absent directory is the ordinary state of a fresh
/// install, not an error.
pub fn face_models_dir(server: &str, user_id: &str) -> PathBuf {
catalog_path(server, user_id)
.parent()
.map(|p| p.join("models"))
.unwrap_or_else(|| std::env::temp_dir().join("darkroom-models"))
}
/// The detector and embedder files, if both are present.
///
/// Both or neither: an embedder with no detector has nothing to embed, and a
/// detector with no embedder finds faces it cannot tell apart. Reporting the
/// pair missing is more useful than half-starting.
///
/// The names are the **shape-fixed** exports, not what InsightFace ships:
/// `tools/fix-face-model-shapes.sh` has to run over the originals first,
/// because tract cannot parse either graph with a dynamic input.
pub fn face_models(server: &str, user_id: &str) -> Option<(PathBuf, PathBuf)> {
let dir = face_models_dir(server, user_id);
let detector = dir.join("scrfd_500m_640.onnx");
let embedder = dir.join("arcface_mbf_b1.onnx");
(detector.is_file() && embedder.is_file()).then_some((detector, embedder))
}
/// One grid cell's data, read from the catalog. /// One grid cell's data, read from the catalog.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct LibraryCell { pub struct LibraryCell {
+10
View File
@@ -534,6 +534,8 @@ export component AppWindow inherits Window {
in property <bool> identity-indexing: false; in property <bool> identity-indexing: false;
in property <string> identity-indexing-status; in property <string> identity-indexing-status;
in property <bool> identity-model-missing: false; in property <bool> identity-model-missing: false;
in property <string> identity-coverage;
in property <bool> identity-coverage-complete: false;
in property <int> identity-picked: 0; in property <int> identity-picked: 0;
callback identity-open(); callback identity-open();
@@ -546,6 +548,9 @@ export component AppWindow inherits Window {
callback identity-confirm-all(); callback identity-confirm-all();
callback identity-split-picked(); callback identity-split-picked();
callback identity-recluster(); callback identity-recluster();
callback identity-index();
callback identity-stop-indexing();
callback identity-check-coverage();
callback identity-delete-all(); callback identity-delete-all();
callback library-thumbnail-all(); callback library-thumbnail-all();
in-out property <length> library-cell-size: 180px; in-out property <length> library-cell-size: 180px;
@@ -1282,6 +1287,8 @@ in property <bool> panel-visible: true;
indexing-status: root.identity-indexing-status; indexing-status: root.identity-indexing-status;
model-missing: root.identity-model-missing; model-missing: root.identity-model-missing;
picked-count: root.identity-picked; picked-count: root.identity-picked;
coverage: root.identity-coverage;
coverage-complete: root.identity-coverage-complete;
person-picked(id) => { root.identity-person-picked(id); } person-picked(id) => { root.identity-person-picked(id); }
rename(name) => { root.identity-rename(name); } rename(name) => { root.identity-rename(name); }
@@ -1291,6 +1298,9 @@ in property <bool> panel-visible: true;
confirm-all() => { root.identity-confirm-all(); } confirm-all() => { root.identity-confirm-all(); }
split-picked() => { root.identity-split-picked(); } split-picked() => { root.identity-split-picked(); }
recluster() => { root.identity-recluster(); } recluster() => { root.identity-recluster(); }
index-faces() => { root.identity-index(); }
stop-indexing() => { root.identity-stop-indexing(); }
check-coverage() => { root.identity-check-coverage(); }
delete-all-face-data() => { root.identity-delete-all(); } delete-all-face-data() => { root.identity-delete-all(); }
close() => { root.identity-close(); } close() => { root.identity-close(); }
} }
+37
View File
@@ -157,6 +157,12 @@ export component IdentityScreen inherits Rectangle {
in property <bool> calibrated: false; in property <bool> calibrated: false;
in property <bool> indexing: false; in property <bool> indexing: false;
in property <string> indexing-status; in property <string> indexing-status;
// The batch check's own words: how much of the library face detection has
// actually been over. Text rather than a number, because "4,812 of 5,000
// indexed, 190 awaiting a proxy" is the useful form and a bare percentage
// hides the reason the rest are outstanding.
in property <string> coverage;
in property <bool> coverage-complete: false;
// No model on disk: face indexing cannot run at all (docs/faces.md §2.2). // No model on disk: face indexing cannot run at all (docs/faces.md §2.2).
in property <bool> model-missing: false; in property <bool> model-missing: false;
in property <int> picked-count: 0; in property <int> picked-count: 0;
@@ -170,6 +176,9 @@ export component IdentityScreen inherits Rectangle {
callback split-picked(); callback split-picked();
callback merge-into(int); callback merge-into(int);
callback recluster(); callback recluster();
callback index-faces();
callback stop-indexing();
callback check-coverage();
callback delete-all-face-data(); callback delete-all-face-data();
callback close(); callback close();
@@ -320,6 +329,14 @@ export component IdentityScreen inherits Rectangle {
primary: true; primary: true;
clicked => { root.confirm-all(); } clicked => { root.confirm-all(); }
} }
if !root.indexing && !root.model-missing && !root.coverage-complete: Button {
text: "Index faces";
clicked => { root.index-faces(); }
}
if root.indexing: Button {
text: "Stop";
clicked => { root.stop-indexing(); }
}
Button { Button {
text: "Regroup"; text: "Regroup";
clicked => { root.recluster(); } clicked => { root.recluster(); }
@@ -337,6 +354,26 @@ export component IdentityScreen inherits Rectangle {
} }
} }
// The run-marker check, shown rather than buried in a log. Without
// it the screen can say how many faces it has but not how much of
// the library it has actually looked at — and those are the two
// different questions the face_index table exists to separate.
if root.coverage != "": HorizontalLayout {
spacing: Theme.gap-sm;
Text {
text: root.coverage;
color: root.coverage-complete ? Theme.ink-faint : Theme.ink-dim;
font-size: Theme.text-sm;
vertical-alignment: center;
wrap: word-wrap;
}
Rectangle { }
IconButton {
icon: "rotate-cw";
clicked => { root.check-coverage(); }
}
}
// FR-CULL-9, made visible: where the calibration is not fitted the // FR-CULL-9, made visible: where the calibration is not fitted the
// screen says the confidences are unavailable rather than letting // screen says the confidences are unavailable rather than letting
// per-face percentages imply a measurement that was never made. // per-face percentages imply a measurement that was never made.