From e26f71d15d61af40469ff58149ba3cb8d9cf5efe Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 30 Aug 2026 10:05:04 +0200 Subject: [PATCH 1/6] Gather every model under one tree at the repository root The weights were in two places: face detection and recognition in `models/face/`, segmentation in `core/dr-segment/models/`. Nothing was wrong with either path, but between them there was nowhere to look to answer "how much model does this application carry", and that number is about to start growing. So the crate-local copy moves up beside the other. `models/` now holds `face/` and `segment/`, and a `du -sh` of one directory is the whole answer. No content changes: the .onnx and its vocabulary are byte-identical, and `LICENCE.md` moves up a level to cover the tree rather than one crate. The LFS pattern in `.gitattributes` is `*.onnx` and already matched both locations, so only its comment needed the new path. `include_bytes!` is relative to the source file and `build.rs` runs with the crate root as its working directory, which is why the two paths climb a different number of levels. Co-Authored-By: Claude Opus 5 (1M context) --- .gitattributes | 2 +- .gitea/workflows/build-and-test.yml | 8 ++++---- core/dr-segment/build.rs | 4 ++-- core/dr-segment/src/lib.rs | 2 +- core/dr-segment/src/semantic.rs | 8 ++++---- {core/dr-segment/models => models}/LICENCE.md | 0 .../models => models/segment}/yolo26n-seg.classes.json | 0 .../dr-segment/models => models/segment}/yolo26n-seg.onnx | 0 tools/export-seg-model.sh | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) rename {core/dr-segment/models => models}/LICENCE.md (100%) rename {core/dr-segment/models => models/segment}/yolo26n-seg.classes.json (100%) rename {core/dr-segment/models => models/segment}/yolo26n-seg.onnx (100%) diff --git a/.gitattributes b/.gitattributes index 4d03e0a..17249bf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,6 @@ # Model weights live in LFS. # -# `core/dr-segment/models/*.onnx` is ~11 MB of binary that changes wholesale +# `models/**/*.onnx` is tens of MB of binary that changes wholesale # when it changes at all. In ordinary git objects every future revision of it # would be stored in full, in every clone, forever — and the one thing nobody # can do with it is a useful diff. diff --git a/.gitea/workflows/build-and-test.yml b/.gitea/workflows/build-and-test.yml index 0945317..db31c3a 100644 --- a/.gitea/workflows/build-and-test.yml +++ b/.gitea/workflows/build-and-test.yml @@ -57,7 +57,7 @@ jobs: # The model, which is in LFS and is not optional. # - # `core/dr-segment/models/*.onnx` is tracked in LFS (.gitattributes), so a + # `models/**/*.onnx` is tracked in LFS (.gitattributes), so a # plain checkout writes a ~130-byte pointer where 11 MB should be, and # `dr-segment`'s build script panics by design rather than embedding a # pointer and failing at inference. That failure reads like a broken build @@ -97,7 +97,7 @@ jobs: git config --local lfs.url \ "https://x-access-token:${LFS_TOKEN}@gitea.tourolle.paris/dtourolle/DarkRoom.git/info/lfs" git lfs pull - ls -l core/dr-segment/models/ + ls -lR models/ - name: Cache cargo uses: actions/cache@v4 @@ -174,7 +174,7 @@ jobs: # The model, which is in LFS and is not optional. # - # `core/dr-segment/models/*.onnx` is tracked in LFS (.gitattributes), so a + # `models/**/*.onnx` is tracked in LFS (.gitattributes), so a # plain checkout writes a ~130-byte pointer where 11 MB should be, and # `dr-segment`'s build script panics by design rather than embedding a # pointer and failing at inference. That failure reads like a broken build @@ -214,7 +214,7 @@ jobs: git config --local lfs.url \ "https://x-access-token:${LFS_TOKEN}@gitea.tourolle.paris/dtourolle/DarkRoom.git/info/lfs" git lfs pull - ls -l core/dr-segment/models/ + ls -lR models/ - name: Cache cargo uses: actions/cache@v4 diff --git a/core/dr-segment/build.rs b/core/dr-segment/build.rs index dcd300e..276ff85 100644 --- a/core/dr-segment/build.rs +++ b/core/dr-segment/build.rs @@ -1,6 +1,6 @@ //! Check the model is a model and not an LFS pointer. //! -//! `models/*.onnx` is stored in Git LFS (see `.gitattributes`). A clone made +//! `models/segment/*.onnx` is stored in Git LFS (see `.gitattributes`). A clone made //! without git-lfs installed, or with `GIT_LFS_SKIP_SMUDGE` set, leaves a //! ~130-byte text pointer at that path instead of the weights. //! @@ -12,7 +12,7 @@ use std::path::Path; -const MODEL: &str = "models/yolo26n-seg.onnx"; +const MODEL: &str = "../../models/segment/yolo26n-seg.onnx"; fn main() { println!("cargo:rerun-if-changed={MODEL}"); diff --git a/core/dr-segment/src/lib.rs b/core/dr-segment/src/lib.rs index 4909f5b..e82eb75 100644 --- a/core/dr-segment/src/lib.rs +++ b/core/dr-segment/src/lib.rs @@ -26,7 +26,7 @@ //! //! That combination is also what repairs the vocabulary problem. The shipped //! model is COCO-trained, so it recognises subjects and has no class for sky, -//! foliage or wall (`models/LICENCE.md`). Selecting those falls to arm A, +//! foliage or wall (`models/LICENCE.md` at the repository root). Selecting those falls to arm A, //! which never needed a vocabulary to begin with. pub mod distance; diff --git a/core/dr-segment/src/semantic.rs b/core/dr-segment/src/semantic.rs index d01865a..fe530df 100644 --- a/core/dr-segment/src/semantic.rs +++ b/core/dr-segment/src/semantic.rs @@ -20,7 +20,7 @@ //! behaviour, and it is worth being glad of rather than working around. //! //! So arm B here contributes *subjects*, and the watershed contributes -//! everything else. See `models/LICENCE.md` for why no ADE20K variant is +//! everything else. See `models/LICENCE.md` at the repository root for why no ADE20K variant is //! shipped instead. //! //! # Cost, and where it may run @@ -198,15 +198,15 @@ pub struct SemanticModel { classes: Vec>, } -/// The weights that ship with this crate (`models/`, AGPL — see LICENCE.md). +/// The weights, from the repository-root `models/segment/` (AGPL — see `models/LICENCE.md`). /// /// Embedded rather than read from a path because Android hands the app no /// filesystem location to read from (ARCH §6.9) — the same reasoning that has /// the Lensfun database shipping inside its crate. #[cfg(feature = "embedded-model")] -const EMBEDDED_MODEL: &[u8] = include_bytes!("../models/yolo26n-seg.onnx"); +const EMBEDDED_MODEL: &[u8] = include_bytes!("../../../models/segment/yolo26n-seg.onnx"); #[cfg(feature = "embedded-model")] -const EMBEDDED_CLASSES: &str = include_str!("../models/yolo26n-seg.classes.json"); +const EMBEDDED_CLASSES: &str = include_str!("../../../models/segment/yolo26n-seg.classes.json"); impl SemanticModel { /// Load the model that ships with this crate. diff --git a/core/dr-segment/models/LICENCE.md b/models/LICENCE.md similarity index 100% rename from core/dr-segment/models/LICENCE.md rename to models/LICENCE.md diff --git a/core/dr-segment/models/yolo26n-seg.classes.json b/models/segment/yolo26n-seg.classes.json similarity index 100% rename from core/dr-segment/models/yolo26n-seg.classes.json rename to models/segment/yolo26n-seg.classes.json diff --git a/core/dr-segment/models/yolo26n-seg.onnx b/models/segment/yolo26n-seg.onnx similarity index 100% rename from core/dr-segment/models/yolo26n-seg.onnx rename to models/segment/yolo26n-seg.onnx diff --git a/tools/export-seg-model.sh b/tools/export-seg-model.sh index 5468390..911effa 100755 --- a/tools/export-seg-model.sh +++ b/tools/export-seg-model.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Re-export the segmentation model that ships in core/dr-segment/models/. +# Re-export the segmentation model that ships in models/ at the repository root. # # The .onnx is committed (D14), so this is not part of any build — it exists so # the committed artefact is reproducible rather than a binary someone once @@ -24,7 +24,7 @@ set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO="$(cd "${HERE}/.." && pwd)" -OUT="${REPO}/core/dr-segment/models" +OUT="${REPO}/models/segment" MODEL="${1:-yolo26n-seg}" IMGSZ="${2:-640}" From 2e09906a0877d3c9265dee204e0c21aa3df2cebe Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 30 Aug 2026 10:05:19 +0200 Subject: [PATCH 2/6] Keep the export venv off tmpfs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `mktemp -d` lands in `/tmp`, which on this and most current Linux distributions is a tmpfs — memory, not disk, sized at half of RAM. The venv this script builds installs torch into it, several gigabytes, and the failure mode is not subtle: error: Failed to install: torch-2.13.0-...whl Caused by: No space left on device (os error 28) on a machine with 102 GB free on the filesystem holding `/var/tmp`. The quieter version of the same bug is worse: when it does fit, it evicts whatever the user had in page cache to make room. `${TMPDIR:-/var/tmp}` respects an explicit TMPDIR and otherwise picks the disk-backed directory, which is what a multi-gigabyte throwaway wants. Co-Authored-By: Claude Opus 5 (1M context) --- tools/export-seg-model.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/export-seg-model.sh b/tools/export-seg-model.sh index 911effa..261fad5 100755 --- a/tools/export-seg-model.sh +++ b/tools/export-seg-model.sh @@ -28,7 +28,12 @@ OUT="${REPO}/models/segment" MODEL="${1:-yolo26n-seg}" IMGSZ="${2:-640}" -WORK="$(mktemp -d)" +# Not `mktemp -d`: the default TMPDIR is `/tmp`, which on most current Linux +# distributions is a tmpfs — RAM, sized at half of physical memory. The venv +# below pulls torch, several gigabytes of it, and installing that into RAM +# either evicts the user's page cache or fails outright with ENOSPC on a +# machine that has hundreds of gigabytes of actual disk free. +WORK="$(mktemp -d -p "${TMPDIR:-/var/tmp}")" trap 'rm -rf "${WORK}"' EXIT echo "==> exporting ${MODEL} at imgsz=${IMGSZ} in ${WORK}" From 9a2b39b8e5c29ab53da3de229e18742580eb46e3 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 30 Aug 2026 10:05:44 +0200 Subject: [PATCH 3/6] Add the ADE20K scene model beside the instance one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `models/LICENCE.md` recorded, on 2026-08-21, that no YOLO model trained on ADE20K existed in usable form — the stuff classes photography cares about, sky and vegetation and water, had no model to come from. Re-checked 2026-08-30: Ultralytics now ships a `semantic` task with ADE20K checkpoints, so `models/scene/` holds `yolo26s-sem-ade20k`. This is an addition, not a replacement. A semantic model labels every pixel but merges same-class pixels into one region, so it cannot tell three people apart — which is exactly what clicking a subject needs, and exactly what `segment/`'s COCO instance model already does. The scene tab grades per category and does not care that instances are merged. Keeping both is the point. ## The export is truncated, deliberately Ultralytics ends the graph with `Resize -> ArgMax -> Cast` and hands back a `[1, 640, 640]` u8 label map. The script cuts that tail and exposes the classifier's `[1, 150, 80, 80]` f32 logits instead, for two reasons. Cost: the Resize materialises 150 x 640 x 640 x f32, 246 MB, and ArgMax then reduces across the channel axis, striding 409,600 elements per comparison. On one loaded machine the full graph ran ~1160 ms against ~500 ms truncated — roughly four fifths of the time spent on work the application discards. Those numbers were measured under contention and are upper bounds, but the ratio is structural. Softness: ArgMax destroys the per-class scores, and the scene tab needs them. Softmax over the 150 channels, summed within each photographic category, yields per-category weights summing to 1 at every pixel. Feathering a partition of unity cannot double-grade a boundary, whereas feathering hard labels outward from two adjacent categories paints both grades into the overlap and haloes every horizon. The discarded upsample was never information: the graph's true spatial resolution is the 80x80 logit grid, and the application can resample from that itself. The tail is matched by op type and asserted before cutting, so an upstream graph change fails loudly in the exporter rather than quietly shipping a differently-shaped model. Nothing reads these weights yet — the decode path, the category descriptor grouping 150 classes into ~8 photographic ones, and the scene tab are still to come. At 24 MB this model also wants the runtime-asset treatment `models/face/` already gets on Android rather than `include_bytes!`; embedding it would put ~35 MB of weights in the binary. Co-Authored-By: Claude Opus 5 (1M context) --- models/LICENCE.md | 49 ++++-- models/scene/yolo26s-sem-ade20k.classes.json | 152 +++++++++++++++++++ models/scene/yolo26s-sem-ade20k.onnx | 3 + tools/export-seg-model.sh | 71 ++++++++- 4 files changed, 251 insertions(+), 24 deletions(-) create mode 100644 models/scene/yolo26s-sem-ade20k.classes.json create mode 100644 models/scene/yolo26s-sem-ade20k.onnx diff --git a/models/LICENCE.md b/models/LICENCE.md index 83235e8..803bf1a 100644 --- a/models/LICENCE.md +++ b/models/LICENCE.md @@ -1,9 +1,16 @@ # Model weights — licensing -`yolo26n-seg.onnx` is exported from Ultralytics YOLO26n-seg -(`https://huggingface.co/Ultralytics/YOLO26`, `yolo26n-seg.pt`) by -`tools/export-seg-model.sh`. `yolo26n-seg.classes.json` is that checkpoint's -class vocabulary, written out by the same script. +Two Ultralytics checkpoints ship here, both exported by +`tools/export-seg-model.sh`, each with its class vocabulary written out by the +same script: + +| File | Checkpoint | Trained on | Used by | +|---|---|---|---| +| `segment/yolo26n-seg.onnx` | `yolo26n-seg.pt` | COCO, 80 *thing* classes | local adjustments, subject selection | +| `scene/yolo26s-sem-ade20k.onnx` | `yolo26s-sem-ade20k.pt` | ADE20K, 150 classes | the scene tab's per-category grades | + +Both come from `https://huggingface.co/Ultralytics/YOLO26`. The face weights in +`face/` are a separate matter with a separate grant — see `face/README.md`. ## The grant @@ -38,17 +45,27 @@ This was decided deliberately (D14), not arrived at by accident, and classes include the *stuff* categories that matter most in photography — sky, vegetation, water, wall, mountain. -**No such model exists in usable form.** Checked 2026-08-21: Ultralytics ships -YOLO26-seg trained on **COCO**, whose 80 classes are all *things* — person, -dog, car, bird, potted plant — and the one HuggingFace repository claiming a -YOLO/ADE20K combination (`laxmacl/yolov8-ade20k`) is empty. ADE20K semantic -models do exist, but as SegFormer/OneFormer/MaskFormer transformers, not YOLO. +**This was true when written and is not any more.** Checked 2026-08-21, no +YOLO/ADE20K combination existed: Ultralytics shipped YOLO26-seg on **COCO** +only, and the one HuggingFace repository claiming otherwise +(`laxmacl/yolov8-ade20k`) was empty. Re-checked 2026-08-30: Ultralytics now +ships a `semantic` task with ADE20K checkpoints +(`https://docs.ultralytics.com/tasks/semantic`), and `yolo26s-sem-ade20k` is +what `scene/` holds. -So the shipped vocabulary selects **subjects**, not **stuff**. "Select the -person" works; "select the sky" does not come from the model and must come from -the watershed hierarchy instead. That is a narrower arm B than §4 assumed, and -it raises rather than lowers the importance of arm C. +So the two vocabularies divide the work rather than compete: -The loader treats the vocabulary as model metadata rather than compiled-in -knowledge, so adding a stuff-class model later is a file plus a descriptor, not -a code change. +- **`segment/`, COCO, 80 things.** Separates *instances* — clicking one of + three people selects that person. This is what local adjustments need, and a + semantic model cannot do it: it would return one "person" region covering all + three. +- **`scene/`, ADE20K, 150 classes.** Labels every pixel, including the *stuff* + COCO has no word for — sky, vegetation, water, mountain, wall. This is what + the scene tab's per-category grades need, and it does not care that instances + are merged, because a per-category grade applies to the whole category. + +Neither replaces the other. Keeping both is the deliberate choice. + +The loader treats each vocabulary as model metadata rather than compiled-in +knowledge, which is what made adding the second model a file plus a descriptor +rather than a code change — as this document predicted it would be. diff --git a/models/scene/yolo26s-sem-ade20k.classes.json b/models/scene/yolo26s-sem-ade20k.classes.json new file mode 100644 index 0000000..e5357c4 --- /dev/null +++ b/models/scene/yolo26s-sem-ade20k.classes.json @@ -0,0 +1,152 @@ +[ + "wall", + "building", + "sky", + "floor", + "tree", + "ceiling", + "road", + "bed", + "windowpane", + "grass", + "cabinet", + "sidewalk", + "person", + "earth", + "door", + "table", + "mountain", + "plant", + "curtain", + "chair", + "car", + "water", + "painting", + "sofa", + "shelf", + "house", + "sea", + "mirror", + "rug", + "field", + "armchair", + "seat", + "fence", + "desk", + "rock", + "wardrobe", + "lamp", + "bathtub", + "railing", + "cushion", + "base", + "box", + "column", + "signboard", + "chest of drawers", + "counter", + "sand", + "sink", + "skyscraper", + "fireplace", + "refrigerator", + "grandstand", + "path", + "stairs", + "runway", + "case", + "pool table", + "pillow", + "screen door", + "stairway", + "river", + "bridge", + "bookcase", + "blind", + "coffee table", + "toilet", + "flower", + "book", + "hill", + "bench", + "countertop", + "stove", + "palm", + "kitchen island", + "computer", + "swivel chair", + "boat", + "bar", + "arcade machine", + "hovel", + "bus", + "towel", + "light", + "truck", + "tower", + "chandelier", + "awning", + "streetlight", + "booth", + "television receiver", + "airplane", + "dirt track", + "apparel", + "pole", + "land", + "bannister", + "escalator", + "ottoman", + "bottle", + "buffet", + "poster", + "stage", + "van", + "ship", + "fountain", + "conveyor belt", + "canopy", + "washer", + "plaything", + "swimming pool", + "stool", + "barrel", + "basket", + "waterfall", + "tent", + "bag", + "minibike", + "cradle", + "oven", + "ball", + "food", + "step", + "tank", + "trade name", + "microwave", + "pot", + "animal", + "bicycle", + "lake", + "dishwasher", + "screen", + "blanket", + "sculpture", + "hood", + "sconce", + "vase", + "traffic light", + "tray", + "ashcan", + "fan", + "pier", + "crt screen", + "plate", + "monitor", + "bulletin board", + "shower", + "radiator", + "glass", + "clock", + "flag" +] \ No newline at end of file diff --git a/models/scene/yolo26s-sem-ade20k.onnx b/models/scene/yolo26s-sem-ade20k.onnx new file mode 100644 index 0000000..b5d4946 --- /dev/null +++ b/models/scene/yolo26s-sem-ade20k.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:296cbbf983a7dbd53eb551f4bcb2ce28d6503bb5ddd30a60c4a13b44087e01d7 +size 24921749 diff --git a/tools/export-seg-model.sh b/tools/export-seg-model.sh index 261fad5..de12bce 100755 --- a/tools/export-seg-model.sh +++ b/tools/export-seg-model.sh @@ -1,14 +1,25 @@ #!/usr/bin/env bash -# Re-export the segmentation model that ships in models/ at the repository root. +# Re-export the segmentation models that ship in models/ at the repository root. # # The .onnx is committed (D14), so this is not part of any build — it exists so # the committed artefact is reproducible rather than a binary someone once -# produced and nobody can regenerate. Run it when bumping the model. +# produced and nobody can regenerate. Run it when bumping a model. # -# ./tools/export-seg-model.sh +# ./tools/export-seg-model.sh # instance -> models/segment/ +# ./tools/export-seg-model.sh yolo26s-sem-ade20k # semantic -> models/scene/ # # Requires `uv`. Everything else is fetched into a throwaway venv. # +# ## The two models, and why they are both here +# +# `yolo26n-seg` is COCO instance segmentation: it separates *things*, so +# clicking one of three people selects that person. `yolo26s-sem-ade20k` is +# ADE20K semantic segmentation: it labels every pixel with one of 150 classes +# including the *stuff* — sky, vegetation, water — that COCO has no word for, +# but it merges same-class pixels into one region and so cannot tell those +# three people apart. Neither substitutes for the other; the scene tab wants +# the second and local adjustments want the first. +# # ## Why these export flags # # `dynamic=False` is not a default we failed to change: **tract cannot parse @@ -20,14 +31,38 @@ # `imgsz` square rather than a rectangle matched to 3:2: one graph has to # serve portrait, landscape, square crops and panoramas. A landscape-shaped # graph trades letterbox waste on 3:2 for worse waste on everything else. +# +# ## Why the semantic export is truncated +# +# Ultralytics ends the `-sem-` graph with `Resize -> ArgMax -> Cast`, handing +# back a `[1, 640, 640]` u8 label map. Two reasons that tail is cut here: +# +# 1. **Cost.** The Resize materialises 150 x 640 x 640 x f32 — *246 MB* — and +# ArgMax then reduces across the channel axis, which in NCHW strides +# 409,600 elements per comparison. Measured on one machine it was roughly +# four fifths of total runtime, for work the app throws away. +# 2. **Softness.** ArgMax destroys the per-class scores. The scene tab needs +# them: softmax over the 150 channels, summed within each photographic +# category, gives per-category weights that sum to 1 at every pixel — a +# partition of unity. Feathering those cannot double-grade a boundary, +# where feathering hard labels outward from two adjacent categories does. +# +# The upsample is not information: the graph's true spatial resolution is the +# logit grid (80x80 at imgsz=640), and the app can resample from that itself. set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO="$(cd "${HERE}/.." && pwd)" -OUT="${REPO}/models/segment" MODEL="${1:-yolo26n-seg}" IMGSZ="${2:-640}" + +# Semantic models are the scene tab's; instance models are the selection path's. +case "${MODEL}" in + *-sem-*|*-sem) OUT="${REPO}/models/scene"; SEMANTIC=1 ;; + *) OUT="${REPO}/models/segment"; SEMANTIC=0 ;; +esac + # Not `mktemp -d`: the default TMPDIR is `/tmp`, which on most current Linux # distributions is a tmpfs — RAM, sized at half of physical memory. The venv # below pulls torch, several gigabytes of it, and installing that into RAM @@ -41,12 +76,13 @@ cd "${WORK}" uv venv --python 3.12 venv VIRTUAL_ENV="${WORK}/venv" uv pip install ultralytics onnx onnxslim -VIRTUAL_ENV="${WORK}/venv" "${WORK}/venv/bin/python" - "${MODEL}" "${IMGSZ}" <<'PY' +VIRTUAL_ENV="${WORK}/venv" "${WORK}/venv/bin/python" - "${MODEL}" "${IMGSZ}" "${SEMANTIC}" <<'PY' import sys, json +import onnx +from onnx import helper from ultralytics import YOLO -name = sys.argv[1] -imgsz = int(sys.argv[2]) +name, imgsz, semantic = sys.argv[1], int(sys.argv[2]), sys.argv[3] == "1" m = YOLO(f"{name}.pt") path = m.export(format="onnx", opset=17, simplify=True, imgsz=imgsz, dynamic=False) print("ONNX:", path) @@ -57,6 +93,25 @@ print("ONNX:", path) with open("classes.json", "w") as f: json.dump([m.names[i] for i in range(len(m.names))], f, indent=1) print("classes:", len(m.names)) + +if semantic: + # Drop `Resize -> ArgMax -> Cast` and expose the classifier's logits. See + # the header for why. Matched by op type rather than by node name so a + # re-export under a different naming scheme still works, and asserted + # rather than assumed so an upstream graph change fails loudly here + # instead of silently shipping a differently-shaped model. + g = onnx.load(path).graph + tail = [n.op_type for n in g.node[-3:]] + assert tail == ["Resize", "ArgMax", "Cast"], f"unexpected graph tail: {tail}" + logits = g.node[-3].input[0] + del g.node[-3:] + del g.output[:] + g.output.extend([helper.make_tensor_value_info(logits, onnx.TensorProto.FLOAT, None)]) + model = onnx.shape_inference.infer_shapes(helper.make_model(g, opset_imports=[helper.make_opsetid("", 17)])) + onnx.checker.check_model(model) + onnx.save(model, path) + shape = [d.dim_value for d in model.graph.output[0].type.tensor_type.shape.dim] + print("truncated to logits:", logits, shape) PY mkdir -p "${OUT}" @@ -66,4 +121,4 @@ cp "${WORK}/classes.json" "${OUT}/${MODEL}.classes.json" echo "==> wrote:" ls -la "${OUT}" echo -echo "Remember: these weights are AGPL-3.0 (see ${OUT}/LICENCE.md)." +echo "Remember: these weights are AGPL-3.0 (see ${REPO}/models/LICENCE.md)." From 79b7f54e0421d18e6cd54e159455f5d8d35a6eed Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 30 Aug 2026 10:07:19 +0200 Subject: [PATCH 4/6] Tell javac the Java sources are UTF-8 The APK stopped building the moment the tree gained Java with prose in it: 55 errors, every one "unmappable character (0x94)", every one from a comment. The code was fine. javac reads sources in the *platform* encoding, and the build container sets no locale, so that is US-ASCII. Every curly quote and em dash in a doc comment is then unrepresentable. The repository is UTF-8 throughout, so this says so rather than asking one file's prose to be typed in ASCII to suit a default nobody chose. Co-Authored-By: Claude Opus 5 (1M context) --- docker/android/assemble-apk.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docker/android/assemble-apk.sh b/docker/android/assemble-apk.sh index dfa1f0a..6e11e09 100755 --- a/docker/android/assemble-apk.sh +++ b/docker/android/assemble-apk.sh @@ -140,8 +140,16 @@ if [[ ${#JAVA_FILES[@]} -gt 0 ]]; then # `-Xlint:-options` silences one note, "source value 8 is obsolete", which # is advice about a future JDK rather than about this code. The JDK is # pinned in the Dockerfile, so the day it matters is a deliberate bump. + # + # `-encoding UTF-8` because javac otherwise reads sources in the *platform* + # encoding, and the container sets no locale, so that is US-ASCII. Every + # curly quote and em dash in a comment then becomes "unmappable character + # (0x94)" — 55 errors from prose, in a file whose code is fine. The sources + # are UTF-8 like everything else in the repo; this says so rather than + # asking the prose to be typed in ASCII. javac \ -source 8 -target 8 \ + -encoding UTF-8 \ -bootclasspath "${ANDROID_JAR}" \ -classpath "${ANDROID_JAR}" \ -Xlint:-options \ From 8df6000e4bd33cb4e607688686018ac51bd7defc Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 30 Aug 2026 10:48:37 +0200 Subject: [PATCH 5/6] Decode the scene model into per-category weights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The weights landed last commit with nothing to read them. This is the decoder, and the shape of it follows from one property worth stating before the code: the categories must partition the image. ## Why a partition, and not a mask per category The scene tab applies one grade to every pixel of a category — lift the sky, desaturate foliage — and both grades meet at the horizon. If each category carried an independent mask, feathering them outward would make the boundary band belong to both, so both grades would land there and every horizon would acquire a visible seam. Feathering has to *blend* there, not accumulate. So `marginalise` takes one softmax over all 150 channels and sums within each category. Grouping cannot change a total of one, so the listed categories plus the unlisted remainder sum to one at every pixel, by construction rather than by normalising afterwards. `parse_categories` refuses a descriptor that claims a class twice, because that is the one input that would quietly make the property untrue. ## The descriptor is data, and hand-written `models/scene/categories.txt` groups ADE20K's 150 classes into the eight a photographer would recognise. It is a file rather than a table in Rust for the reason `models/LICENCE.md` predicted — a vocabulary is model metadata — and it is line-oriented with comments rather than JSON like the `.classes.json` beside it, because that file is generated and this one is argued. Why `swimming pool` is water and not architecture belongs next to the line that says so. Classes are named, not indexed. An index is silently wrong after a re-export; a name is loudly wrong, and the loader refuses one the model does not have. ## Resolution, kept visible `Scene` holds the native 80×80 logit grid and resamples on demand rather than upsampling once at load. The coarseness is real — it is what the graph produces — and a type that hides it behind an early resize invites callers to expect detail that was never there. `rasterise` is where the letterbox inverse lives, once. `Letterbox` and `Window` become `pub(crate)` and `to_proto` generalises to `to_grid`, because both dense outputs this crate reads are an even fraction of the same letterboxed square and differ only in the divisor. ## Verified by looking, which is the only way this gets verified `examples/scene.rs` writes the photograph dimmed outside each category. A transposed axis or an off-by-one in the inverse produces perfectly plausible weights over slightly the wrong pixels, and no unit test catches that. On an indoor frame the person mask lands on the person, including the outstretched arm, and sky reads ~5% against a bright ceiling. It doubles as the benchmark, because every timing quoted while this model was chosen came off a laptop compiling other things and none of them belong in a document. Co-Authored-By: Claude Opus 5 (1M context) --- core/dr-segment/Cargo.toml | 10 + core/dr-segment/examples/scene.rs | 174 ++++++++++ core/dr-segment/src/lib.rs | 21 +- core/dr-segment/src/scene.rs | 525 ++++++++++++++++++++++++++++++ core/dr-segment/src/semantic.rs | 43 ++- docs/segmentation.md | 66 ++++ docs/traceability.md | 84 ++--- models/scene/categories.txt | 55 ++++ 8 files changed, 922 insertions(+), 56 deletions(-) create mode 100644 core/dr-segment/examples/scene.rs create mode 100644 core/dr-segment/src/scene.rs create mode 100644 models/scene/categories.txt diff --git a/core/dr-segment/Cargo.toml b/core/dr-segment/Cargo.toml index ecb0457..0733375 100644 --- a/core/dr-segment/Cargo.toml +++ b/core/dr-segment/Cargo.toml @@ -44,3 +44,13 @@ semantic = ["dep:ort", "dep:ort-tract", "dep:ndarray"] # it must be embedded; a desktop packager pointing at a system model directory, # or a test that only needs the decoder, wants the runtime without the 11 MB. embedded-model = ["semantic"] + +# Compile the *scene* model in too, and off by default where `embedded-model` +# is on. +# +# The asymmetry is its size. At 24 MB it is more than twice the instance model, +# and Android reaches it the way it reaches the face weights — unpacked from +# APK assets at first launch — rather than by carrying it in the binary. This +# feature is for a desktop build with nowhere else to read it from, and for +# tests that want the real graph. +embedded-scene-model = ["semantic"] diff --git a/core/dr-segment/examples/scene.rs b/core/dr-segment/examples/scene.rs new file mode 100644 index 0000000..d350fd0 --- /dev/null +++ b/core/dr-segment/examples/scene.rs @@ -0,0 +1,174 @@ +//! Run the scene model over a JPEG, time it, and write what it saw. +//! +//! Two jobs in one example because they need the same setup and answering +//! either one alone leaves the other open. +//! +//! **Looking.** Same argument as `detect`: no unit test settles whether the +//! letterbox inverse in `Scene::rasterise` is right, because an off-by-one +//! produces perfectly plausible weights over slightly the wrong pixels. A sky +//! mask laid over the photograph settles it in one glance. +//! +//! **Timing.** Every number quoted while this model was being chosen came off a +//! laptop that was compiling other things at the time, which makes them upper +//! bounds and nothing better. This exists so the figure that ends up in a +//! document came from a quiet machine and can be reproduced on another one. +//! +//! ```sh +//! cargo run -p dr-segment --example scene --release --features embedded-scene-model -- photo.jpg +//! cargo run -p dr-segment --example scene --release -- photo.jpg out 20 \ +//! models/scene/yolo26s-sem-ade20k.onnx +//! ``` +//! +//! Writes `-.ppm` per category — the photograph darkened +//! where the category is absent, so the mask is legible *against the picture it +//! came from* rather than as an abstract grey field. PPM for the same reason +//! the other examples use it: no encoder dependency, and every viewer reads it. +//! +//! Timings are reported as a median over the requested run count, with the +//! first run excluded. That first pass pays for tract's lazy allocation and is +//! not representative of the second image a session decodes. + +use std::time::Instant; + +use dr_segment::scene::SceneModel; + +fn main() { + env_logger::init(); + + let mut args = std::env::args().skip(1); + let Some(path) = args.next() else { + eprintln!( + "usage: scene [out-prefix] [runs] [model.onnx classes.json categories.txt]" + ); + eprintln!(" with --features embedded-scene-model the model arguments may be omitted"); + std::process::exit(2); + }; + let prefix = args.next().unwrap_or_else(|| "scene".into()); + let runs: usize = args + .next() + .and_then(|r| r.parse().ok()) + .unwrap_or(10) + .max(1); + + let (rgb, width, height) = read_jpeg(&path); + println!("{path}: {width}×{height}"); + + let mut model = match (args.next(), args.next(), args.next()) { + (Some(m), Some(c), Some(g)) => { + SceneModel::from_path(m, c, g).expect("could not load the scene model") + } + _ => embedded(), + }; + + // Excluded from the statistics deliberately — see the header. + let warm = Instant::now(); + let scene = model + .analyse(&rgb, width, height) + .expect("inference failed"); + println!("first run: {:?} (allocation included)", warm.elapsed()); + + let mut times: Vec = Vec::with_capacity(runs); + for _ in 0..runs { + let start = Instant::now(); + let _ = model + .analyse(&rgb, width, height) + .expect("inference failed"); + times.push(start.elapsed().as_secs_f64() * 1000.0); + } + times.sort_by(f64::total_cmp); + println!( + "{runs} runs: median {:.0} ms (min {:.0}, max {:.0})", + times[times.len() / 2], + times[0], + times[times.len() - 1], + ); + + let (gw, gh) = scene.grid_size(); + println!("logit grid: {gw}×{gh}"); + println!(); + + // Coverage first and sorted, because on any given photograph most + // categories are absent and the two or three that are not are the whole + // story. + let mut ranked: Vec<(usize, f32)> = (0..scene.categories().len()) + .map(|k| (k, scene.coverage(k))) + .collect(); + ranked.sort_by(|a, b| b.1.total_cmp(&a.1)); + + for (k, coverage) in ranked { + let name = &scene.categories()[k]; + println!("{name:>14} {:5.1}%", coverage * 100.0); + // A category covering essentially nothing produces a black image and a + // file nobody wants; the threshold is what the scene tab would use to + // decide whether to offer a slider at all. + if coverage < 0.005 { + continue; + } + let mask = scene + .rasterise(k, width, height) + .expect("category index came from the same Scene"); + write_overlay(&format!("{prefix}-{name}.ppm"), &rgb, &mask, width, height); + } +} + +#[cfg(feature = "embedded-scene-model")] +fn embedded() -> SceneModel { + SceneModel::embedded().expect("could not load the embedded scene model") +} + +#[cfg(not(feature = "embedded-scene-model"))] +fn embedded() -> SceneModel { + eprintln!( + "no model given, and this build has no embedded one.\n\ + Either pass the three paths, or rebuild with --features embedded-scene-model." + ); + std::process::exit(2); +} + +/// The photograph, dimmed where the category is not. +/// +/// Not a bare greyscale mask: the question being asked is "does this weight +/// land on the sky", and a mask on its own cannot answer it — you have to see +/// the sky underneath. A floor rather than a multiply, so that a region the +/// model gave up on is still visible enough to recognise. +fn write_overlay(path: &str, rgb: &[f32], mask: &[f32], width: usize, height: usize) { + let mut out = String::with_capacity(64); + out.push_str(&format!("P3\n{width} {height}\n255\n")); + let mut bytes = out.into_bytes(); + + for i in 0..width * height { + let w = mask[i].clamp(0.0, 1.0); + let gain = 0.15 + 0.85 * w; + for c in 0..3 { + let v = (rgb[i * 3 + c] * gain * 255.0).clamp(0.0, 255.0) as u8; + bytes.extend_from_slice(v.to_string().as_bytes()); + bytes.push(if c == 2 { b'\n' } else { b' ' }); + } + } + + match std::fs::write(path, bytes) { + Ok(()) => println!(" wrote {path}"), + Err(e) => eprintln!(" could not write {path}: {e}"), + } +} + +/// Decode to the tightly packed `f32` RGB the model wants. +fn read_jpeg(path: &str) -> (Vec, usize, usize) { + let bytes = std::fs::read(path).expect("could not read the photograph"); + let mut decoder = zune_jpeg::JpegDecoder::new(&bytes); + let pixels = decoder.decode().expect("could not decode the photograph"); + let info = decoder.info().expect("decoded image has no dimensions"); + let (width, height) = (info.width as usize, info.height as usize); + + // zune hands back whatever the file had. Three channels is the ordinary + // case; one is a greyscale scan, which is worth handling because a + // black-and-white frame is exactly the kind of thing someone reaches for + // when a colour one looks wrong. + let components = pixels.len() / (width * height); + let rgb = match components { + 3 => pixels.iter().map(|&p| p as f32 / 255.0).collect(), + 1 => pixels.iter().flat_map(|&p| [p as f32 / 255.0; 3]).collect(), + n => panic!("unsupported component count: {n}"), + }; + (rgb, width, height) +} diff --git a/core/dr-segment/src/lib.rs b/core/dr-segment/src/lib.rs index e82eb75..e9a44c1 100644 --- a/core/dr-segment/src/lib.rs +++ b/core/dr-segment/src/lib.rs @@ -26,19 +26,32 @@ //! //! That combination is also what repairs the vocabulary problem. The shipped //! model is COCO-trained, so it recognises subjects and has no class for sky, -//! foliage or wall (`models/LICENCE.md` at the repository root). Selecting those falls to arm A, +//! foliage or wall (`models/LICENCE.md`). Selecting those falls to arm A, //! which never needed a vocabulary to begin with. +//! +//! # And [`scene`], which is not one of the arms +//! +//! The three arms all serve *local* adjustment: they exist so a mask can be +//! snapped to one region of the picture. [`scene`] serves the opposite move — +//! one grade applied to every pixel of a category at once, sky or foliage or +//! water — and reads a second, ADE20K-trained model to do it. It shares this +//! crate because it shares the runtime and the letterbox, not because it is +//! another way of doing the same thing. pub mod distance; pub mod hierarchy; pub mod prior; #[cfg(feature = "semantic")] +pub mod scene; +#[cfg(feature = "semantic")] pub mod semantic; pub use distance::{signed_distance, Falloff, Morphology, Shaped}; pub use hierarchy::{Edge, Merge, MergeTree, RegionField}; pub use prior::{Membership, PriorOptions}; #[cfg(feature = "semantic")] +pub use scene::{Category, Scene, SceneModel}; +#[cfg(feature = "semantic")] pub use semantic::{Instance, SemanticModel, SemanticOptions, Tiling}; /// What can go wrong between an image and a region map. @@ -58,4 +71,10 @@ pub enum SegmentError { /// different model, or a different export of the same one. #[error("model output '{0}' did not have the expected shape")] OutputShape(&'static str), + + /// `models/scene/categories.txt` and the model disagree, or the descriptor + /// is malformed. Its own variant rather than a parse error because every + /// case carries a specific sentence about what to fix. + #[error("category descriptor: {0}")] + CategoryDescriptor(String), } diff --git a/core/dr-segment/src/scene.rs b/core/dr-segment/src/scene.rs new file mode 100644 index 0000000..b5ae215 --- /dev/null +++ b/core/dr-segment/src/scene.rs @@ -0,0 +1,525 @@ +//! Per-category weights over the whole frame — what the scene tab grades. +//! +//! [`semantic`](crate::semantic) answers "what objects are in this picture, and +//! which pixels are each one". This module answers a different question: "how +//! much of each pixel is sky". They are not the same question and they do not +//! want the same model. +//! +//! # Why a second model rather than a second reading of the first +//! +//! The instance model is COCO-trained, and COCO is eighty classes of *things*. +//! There is no class for sky, none for foliage, none for water — the categories +//! a landscape is mostly made of. That gap is recorded in `models/LICENCE.md` +//! and it is why the scene model exists: ADE20K's 150 classes are a scene +//! parse, *stuff* included. +//! +//! Going the other way is just as impossible. A semantic model merges every +//! pixel of a class into one region, so it cannot tell three people apart, and +//! telling three people apart is exactly what clicking a subject needs. Neither +//! model substitutes for the other, which is why both ship. +//! +//! # The partition of unity, and why it is the point +//! +//! [`Scene::weight`] is not a mask per category that each independently says +//! yes or no. It is a *partition*: at every pixel the listed categories plus +//! the unlisted remainder sum to one, because they come from one softmax over +//! all 150 channels, summed within each category. +//! +//! That property is what makes feathering safe. Feather a hard label map +//! outward from sky and outward from vegetation and the boundary band belongs +//! to both, so a `+20` on sky and a `−10` on vegetation both land there and +//! every horizon acquires a visible seam. Feather a partition of unity and the +//! weights still sum to one — the band gets a blend of the two grades, which is +//! what a photographer drawing that boundary by hand would have painted. +//! +//! # Resolution, stated plainly +//! +//! The graph's logits are `[1, 150, 80, 80]`: an eighth of the input edge, and +//! that is the real spatial resolution of everything here. The stock export +//! ends with a `Resize` to 640×640 and an `ArgMax`, and +//! `tools/export-seg-model.sh` cuts both — the upsample adds no information and +//! the argmax destroys the per-class scores this module needs. [`Scene`] keeps +//! the native grid and resamples on demand ([`Scene::rasterise`]) so that the +//! coarseness is visible in the type rather than hidden behind an early +//! upsample. +//! +//! Practically: a graduated grade over sky or water is unbothered by 80×80. A +//! hard edge — a rooftop against sky at 100% zoom — will show it, and no +//! feather setting invents detail the model never had. +//! +//! # Cost +//! +//! One inference per image, on the same background precompute as the instance +//! pass and never on the frame path (ARCH §6.1). The scene tab's sliders read +//! [`Scene`] and re-run nothing. + +use std::sync::Arc; + +use ndarray::ArrayView3; + +use crate::semantic::{install_backend, Letterbox, Window}; +use crate::SegmentError; + +/// Classes in the ADE20K vocabulary the scene model was trained on. +/// +/// Checked against the graph's output rather than trusted: a re-export against +/// a different dataset would otherwise be decoded as though its channels meant +/// what these ones mean, which produces plausible weights for the wrong thing. +pub const CLASSES: usize = 150; + +/// Logit grid stride — the graph's output is this many times coarser than its +/// input edge, giving the 80×80 grid at [`crate::semantic::INPUT_EDGE`] 640. +const GRID_STRIDE: usize = 8; + +/// One photographic category and the ADE20K classes it marginalises over. +#[derive(Debug, Clone)] +pub struct Category { + pub name: Arc, + /// Indices into the model's vocabulary. Resolved from names at load, so a + /// descriptor cannot silently drift out of step with a re-exported model. + pub classes: Vec, +} + +/// The scene model, and the categories it has been told to report. +pub struct SceneModel { + session: ort::session::Session, + categories: Vec, +} + +/// The weights that ship in `models/scene/` (AGPL — see `models/LICENCE.md`). +/// +/// Behind its own feature and **off by default**: this graph is 24 MB, where +/// the instance model is 11, and Android carries it as an unpacked asset +/// rather than inside the binary (`install_bundled_models`). A desktop build +/// or a test that wants it compiled in opts in. +#[cfg(feature = "embedded-scene-model")] +const EMBEDDED_MODEL: &[u8] = include_bytes!("../../../models/scene/yolo26s-sem-ade20k.onnx"); +#[cfg(feature = "embedded-scene-model")] +const EMBEDDED_CLASSES: &str = + include_str!("../../../models/scene/yolo26s-sem-ade20k.classes.json"); +#[cfg(feature = "embedded-scene-model")] +const EMBEDDED_CATEGORIES: &str = include_str!("../../../models/scene/categories.txt"); + +impl SceneModel { + /// Load the scene model compiled into the binary. + #[cfg(feature = "embedded-scene-model")] + pub fn embedded() -> Result { + let classes = crate::semantic::parse_classes(EMBEDDED_CLASSES); + let categories = parse_categories(EMBEDDED_CATEGORIES, &classes)?; + Self::from_bytes(EMBEDDED_MODEL, categories) + } + + /// Load from files on disk: the graph, its vocabulary, and the category + /// descriptor that groups the vocabulary into what the scene tab shows. + /// + /// Three paths rather than one directory because a packager may put the + /// weights somewhere the descriptor is not, and because a caller + /// experimenting with a different grouping should not have to move a 24 MB + /// file to try it. + pub fn from_path( + model: impl AsRef, + classes: impl AsRef, + categories: impl AsRef, + ) -> Result { + let bytes = std::fs::read(model).map_err(SegmentError::ModelRead)?; + let classes = std::fs::read_to_string(classes).map_err(SegmentError::ModelRead)?; + let categories = std::fs::read_to_string(categories).map_err(SegmentError::ModelRead)?; + let classes = crate::semantic::parse_classes(&classes); + let categories = parse_categories(&categories, &classes)?; + Self::from_bytes(&bytes, categories) + } + + pub fn from_bytes(bytes: &[u8], categories: Vec) -> Result { + install_backend(); + + let session = ort::session::Session::builder() + .map_err(SegmentError::Inference)? + .commit_from_memory(bytes) + .map_err(SegmentError::Inference)?; + + Ok(Self { + session, + categories, + }) + } + + pub fn categories(&self) -> &[Category] { + &self.categories + } + + /// Weigh every category over one image. + /// + /// `rgb` is tightly packed `f32` RGB in `0.0..=1.0`, row-major — the same + /// proxy buffer the instance pass reads, so the two describe one picture. + /// + /// One inference over the whole frame. There is no tiling counterpart to + /// [`crate::semantic::Tiling`] here on purpose: tiling buys resolution on a + /// small subject, and no category in the descriptor is a small subject. + pub fn analyse( + &mut self, + rgb: &[f32], + width: usize, + height: usize, + ) -> Result { + if rgb.len() != width * height * 3 { + return Err(SegmentError::ImageShape { + expected: width * height * 3, + got: rgb.len(), + }); + } + + // Split the borrow: `run` needs the session mutably while + // `marginalise` needs the categories, and going through `self` for + // both at once is what the borrow checker objects to. + let Self { + session, + categories, + } = self; + + let window = Window { + x: 0.0, + y: 0.0, + w: width as f32, + h: height as f32, + }; + let letterbox = Letterbox::fit(window.w, window.h); + let input = letterbox.sample(rgb, width, height, &window); + + let outputs = session + .run(ort::inputs![ + ort::value::Tensor::from_array(input).map_err(SegmentError::Inference)? + ]) + .map_err(SegmentError::Inference)?; + + let (shape, logits) = outputs[0] + .try_extract_tensor::() + .map_err(|_| SegmentError::OutputShape("logits"))?; + + // `[1, 150, gh, gw]`. Checked rather than assumed: the stock export + // ends in an ArgMax and returns `[1, 640, 640]` u8 instead, and that + // mistake should read as "wrong model" rather than as garbled output. + if shape.len() != 4 || shape[0] != 1 || shape[1] as usize != CLASSES { + return Err(SegmentError::OutputShape("logits")); + } + let (gh, gw) = (shape[2] as usize, shape[3] as usize); + let logits = ArrayView3::from_shape((CLASSES, gh, gw), &logits[..CLASSES * gh * gw]) + .map_err(|_| SegmentError::OutputShape("logits"))?; + + Ok(marginalise(categories, logits, gw, gh, letterbox, window)) + } +} + +/// Softmax over the vocabulary, then sum within each category. +/// +/// The summation is what makes the result a partition: softmax gives 150 +/// numbers summing to one, and grouping them cannot change that total. The +/// remainder — every class no category claims — is simply not reported, which +/// is why the listed weights sum to *at most* one rather than to one. +/// +/// Free rather than a method so it can be called while the session is borrowed +/// mutably, and so the tests can reach it without a graph. +fn marginalise( + categories: &[Category], + logits: ArrayView3, + gw: usize, + gh: usize, + letterbox: Letterbox, + window: Window, +) -> Scene { + let cells = gw * gh; + let mut weight = vec![0.0f32; categories.len() * cells]; + let mut probability = vec![0.0f32; CLASSES]; + + for cell in 0..cells { + let (y, x) = (cell / gw, cell % gw); + + // Shift by the maximum before exponentiating. The logits here are + // small enough that the naive form would not actually overflow, + // but a re-export with a hotter head would, and the cost is one + // pass over 150 floats. + let mut peak = f32::NEG_INFINITY; + for c in 0..CLASSES { + peak = peak.max(logits[[c, y, x]]); + } + let mut total = 0.0f32; + for c in 0..CLASSES { + let p = (logits[[c, y, x]] - peak).exp(); + probability[c] = p; + total += p; + } + let norm = if total > 0.0 { 1.0 / total } else { 0.0 }; + + for (k, category) in categories.iter().enumerate() { + let mut sum = 0.0f32; + for &class in &category.classes { + sum += probability[class as usize]; + } + weight[k * cells + cell] = sum * norm; + } + } + + Scene { + names: categories.iter().map(|c| c.name.clone()).collect(), + weight, + grid_width: gw, + grid_height: gh, + letterbox, + window, + } +} + +/// One image's category weights, at the model's own resolution. +#[derive(Debug, Clone)] +pub struct Scene { + names: Vec>, + /// `[category][y * grid_width + x]`, each in `0.0..=1.0`, and across + /// categories summing to at most one at every cell. + weight: Vec, + grid_width: usize, + grid_height: usize, + letterbox: Letterbox, + window: Window, +} + +impl Scene { + pub fn categories(&self) -> &[Arc] { + &self.names + } + + pub fn grid_size(&self) -> (usize, usize) { + (self.grid_width, self.grid_height) + } + + /// One category's weights over the logit grid. + pub fn weight(&self, category: usize) -> Option<&[f32]> { + let cells = self.grid_width * self.grid_height; + self.weight.get(category * cells..(category + 1) * cells) + } + + pub fn index_of(&self, name: &str) -> Option { + self.names.iter().position(|n| &**n == name) + } + + /// How much of the frame this category covers, `0.0..=1.0`. + /// + /// Cheap, and the scene tab needs it: a category weighing essentially + /// nothing should not be offered a slider, because a control that does + /// nothing when moved is worse than an absent one. + pub fn coverage(&self, category: usize) -> f32 { + match self.weight(category) { + Some(w) if !w.is_empty() => w.iter().sum::() / w.len() as f32, + _ => 0.0, + } + } + + /// Resample one category to source-image resolution. + /// + /// Bilinear over the logit grid. This does not add detail and is not meant + /// to — see the module header on resolution — it exists because a mask has + /// to be the size of the picture before it can weight an adjustment, and + /// doing the resample here keeps the one correct letterbox inverse in one + /// place. + pub fn rasterise(&self, category: usize, width: usize, height: usize) -> Option> { + let grid = self.weight(category)?; + let mut out = vec![0.0f32; width * height]; + + for y in 0..height { + for x in 0..width { + let (gx, gy) = self.letterbox.to_grid( + x as f32 + 0.5, + y as f32 + 0.5, + &self.window, + GRID_STRIDE as f32, + ); + // Half-cell shift: `to_grid` lands on the grid's coordinate + // space, where a cell's *centre* is at its index plus a half. + let (gx, gy) = (gx - 0.5, gy - 0.5); + let x0 = gx.floor(); + let y0 = gy.floor(); + let (fx, fy) = (gx - x0, gy - y0); + let x0 = (x0 as isize).clamp(0, self.grid_width as isize - 1) as usize; + let y0 = (y0 as isize).clamp(0, self.grid_height as isize - 1) as usize; + let x1 = (x0 + 1).min(self.grid_width - 1); + let y1 = (y0 + 1).min(self.grid_height - 1); + + let at = |gx: usize, gy: usize| grid[gy * self.grid_width + gx]; + let top = at(x0, y0) * (1.0 - fx) + at(x1, y0) * fx; + let bot = at(x0, y1) * (1.0 - fx) + at(x1, y1) * fx; + out[y * width + x] = top * (1.0 - fy) + bot * fy; + } + } + + Some(out) + } +} + +/// Read `models/scene/categories.txt`, resolving class names to indices. +/// +/// Hand-written rather than generated, unlike the `.classes.json` beside it, +/// which is why the format is line-oriented with comments: the *reasoning* for +/// a grouping belongs next to the grouping, and JSON has nowhere to put it. +pub fn parse_categories(text: &str, classes: &[Arc]) -> Result, SegmentError> { + let mut out: Vec = Vec::new(); + let mut claimed: Vec>> = vec![None; classes.len()]; + + for line in text.lines() { + let line = line.split('#').next().unwrap_or("").trim(); + if line.is_empty() { + continue; + } + let Some((name, members)) = line.split_once('=') else { + return Err(SegmentError::CategoryDescriptor(format!( + "line is not `name = class, class, ...`: {line}" + ))); + }; + let name: Arc = name.trim().into(); + + let mut indices = Vec::new(); + for member in members.split(',') { + let member = member.trim(); + if member.is_empty() { + continue; + } + let Some(index) = classes.iter().position(|c| &**c == member) else { + return Err(SegmentError::CategoryDescriptor(format!( + "category '{name}' names class '{member}', which this model does not have" + ))); + }; + // Two categories sharing a class would each count its probability, + // so the weights would exceed one where it appears and the + // partition — the whole reason for summing after a softmax — would + // be quietly untrue. + if let Some(owner) = &claimed[index] { + return Err(SegmentError::CategoryDescriptor(format!( + "class '{member}' is claimed by both '{owner}' and '{name}'" + ))); + } + claimed[index] = Some(name.clone()); + indices.push(index as u16); + } + + if indices.is_empty() { + return Err(SegmentError::CategoryDescriptor(format!( + "category '{name}' lists no classes" + ))); + } + out.push(Category { + name, + classes: indices, + }); + } + + if out.is_empty() { + return Err(SegmentError::CategoryDescriptor( + "descriptor defines no categories".into(), + )); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn vocabulary() -> Vec> { + ["sky", "tree", "grass", "person", "wall"] + .iter() + .map(|s| Arc::from(*s)) + .collect() + } + + #[test] + fn descriptor_resolves_names_to_indices() { + let v = vocabulary(); + let cats = parse_categories("sky = sky\nvegetation = tree, grass\n", &v).unwrap(); + assert_eq!(cats.len(), 2); + assert_eq!(&*cats[0].name, "sky"); + assert_eq!(cats[0].classes, vec![0]); + assert_eq!(cats[1].classes, vec![1, 2]); + } + + #[test] + fn comments_and_blank_lines_are_ignored() { + let v = vocabulary(); + let cats = parse_categories("# a note\n\nsky = sky # trailing\n", &v).unwrap(); + assert_eq!(cats.len(), 1); + assert_eq!(cats[0].classes, vec![0]); + } + + #[test] + fn an_unknown_class_is_refused() { + let v = vocabulary(); + let e = parse_categories("sky = cloud\n", &v).unwrap_err(); + assert!(format!("{e}").contains("cloud"), "{e}"); + } + + /// The partition is the module's one load-bearing property, so the + /// descriptor is not allowed to break it before inference even runs. + #[test] + fn a_class_in_two_categories_is_refused() { + let v = vocabulary(); + let e = parse_categories("a = tree\nb = grass, tree\n", &v).unwrap_err(); + assert!(format!("{e}").contains("claimed by both"), "{e}"); + } + + #[test] + fn the_shipped_descriptor_matches_the_shipped_vocabulary() { + let classes = crate::semantic::parse_classes(include_str!( + "../../../models/scene/yolo26s-sem-ade20k.classes.json" + )); + assert_eq!(classes.len(), CLASSES); + let cats = parse_categories( + include_str!("../../../models/scene/categories.txt"), + &classes, + ) + .expect("shipped descriptor must load against the shipped vocabulary"); + assert!(cats.iter().any(|c| &*c.name == "sky")); + assert!(cats.iter().any(|c| &*c.name == "vegetation")); + } + + /// Softmax then group: the reported weights must never exceed one, and + /// must equal one exactly when the categories name every class. + #[test] + fn marginalising_preserves_the_partition() { + let classes: Vec> = vocabulary(); + let cats = parse_categories( + "sky = sky\nvegetation = tree, grass\nrest = person, wall\n", + &classes, + ) + .unwrap(); + + // Hand-rolled rather than run through a graph: this test is about the + // arithmetic, and a model would only make it slower and less certain. + let (gw, gh) = (2usize, 2usize); + let mut logits = vec![0.0f32; classes.len() * gw * gh]; + for (i, v) in logits.iter_mut().enumerate() { + *v = (i % 7) as f32 * 0.3; + } + let view = ArrayView3::from_shape((classes.len(), gh, gw), &logits).unwrap(); + + // `marginalise` is a method for access to `self.categories`; build the + // smallest thing that owns them rather than a session. + let cells = gw * gh; + let mut weight = vec![0.0f32; cats.len() * cells]; + for cell in 0..cells { + let (y, x) = (cell / gw, cell % gw); + let peak = (0..classes.len()).fold(f32::NEG_INFINITY, |m, c| m.max(view[[c, y, x]])); + let p: Vec = (0..classes.len()) + .map(|c| (view[[c, y, x]] - peak).exp()) + .collect(); + let total: f32 = p.iter().sum(); + for (k, category) in cats.iter().enumerate() { + let s: f32 = category.classes.iter().map(|&c| p[c as usize]).sum(); + weight[k * cells + cell] = s / total; + } + } + + for cell in 0..cells { + let sum: f32 = (0..cats.len()).map(|k| weight[k * cells + cell]).sum(); + assert!( + (sum - 1.0).abs() < 1e-5, + "categories covering every class must sum to 1, got {sum}" + ); + } + } +} diff --git a/core/dr-segment/src/semantic.rs b/core/dr-segment/src/semantic.rs index fe530df..c47179c 100644 --- a/core/dr-segment/src/semantic.rs +++ b/core/dr-segment/src/semantic.rs @@ -446,16 +446,16 @@ fn decode( /// A source-space rectangle fed through one inference. #[derive(Debug, Clone, Copy)] -struct Window { - x: f32, - y: f32, - w: f32, - h: f32, +pub(crate) struct Window { + pub(crate) x: f32, + pub(crate) y: f32, + pub(crate) w: f32, + pub(crate) h: f32, } /// The scale-and-pad that fits an arbitrary rectangle into the square input. #[derive(Debug, Clone, Copy)] -struct Letterbox { +pub(crate) struct Letterbox { /// Input pixels per source pixel. scale: f32, pad_x: f32, @@ -463,7 +463,7 @@ struct Letterbox { } impl Letterbox { - fn fit(w: f32, h: f32) -> Self { + pub(crate) fn fit(w: f32, h: f32) -> Self { let scale = (INPUT_EDGE as f32 / w).min(INPUT_EDGE as f32 / h); Self { scale, @@ -477,7 +477,13 @@ impl Letterbox { /// Bilinear, and grey (`0.5`) in the padding — the value the network sees /// least as an edge, where black would draw a hard border across the frame /// and invite a detection along it. - fn sample(&self, rgb: &[f32], width: usize, height: usize, window: &Window) -> Array4 { + pub(crate) fn sample( + &self, + rgb: &[f32], + width: usize, + height: usize, + window: &Window, + ) -> Array4 { let mut input = Array4::::from_elem((1, 3, INPUT_EDGE, INPUT_EDGE), 0.5); for iy in 0..INPUT_EDGE { @@ -519,12 +525,23 @@ impl Letterbox { ) } + /// Source pixel to the coordinates of an output grid `stride` times + /// coarser than the graph's input. + /// + /// Every dense output this crate reads is some even fraction of the input + /// edge — YOLO's mask prototypes at a quarter, the scene model's logits at + /// an eighth — and they all sit inside the same letterboxed square, so the + /// mapping differs only in that divisor. + pub(crate) fn to_grid(self, sx: f32, sy: f32, w: &Window, stride: f32) -> (f32, f32) { + ( + ((sx - w.x) * self.scale + self.pad_x) / stride, + ((sy - w.y) * self.scale + self.pad_y) / stride, + ) + } + /// Source pixel to prototype-grid coordinates. fn to_proto(self, sx: f32, sy: f32, w: &Window) -> (f32, f32) { - ( - ((sx - w.x) * self.scale + self.pad_x) / PROTO_STRIDE as f32, - ((sy - w.y) * self.scale + self.pad_y) / PROTO_STRIDE as f32, - ) + self.to_grid(sx, sy, w, PROTO_STRIDE as f32) } } @@ -648,7 +665,7 @@ fn steps(extent: f32, edge: f32, stride: f32) -> usize { } /// Point `ort` at tract, exactly once per process. -fn install_backend() { +pub(crate) fn install_backend() { use std::sync::Once; static ONCE: Once = Once::new(); ONCE.call_once(|| { diff --git a/docs/segmentation.md b/docs/segmentation.md index 8d440b2..6381e84 100644 --- a/docs/segmentation.md +++ b/docs/segmentation.md @@ -435,3 +435,69 @@ dependency detail (`core/dr-segment/models/LICENCE.md`). generalises: `ort` + `ort-tract` gives ONNX inference in pure Rust, so the face pipeline of §3.9.1 needs no C dependency either. The *model licensing* half of D13 is untouched — the InsightFace weights are still non-commercial and still unusable here. + +--- + +## 16. The scene model — per-category grades + +Added 2026-08-30, after §4's premise stopped being true. + +### What changed + +§4 specified a semantic model pretrained on ADE20K, whose 150 classes include the *stuff* categories +photography cares about. §13 recorded that no such model existed in usable form and that arm B would +therefore contribute subjects only, which made "select the sky" arm A's problem. Re-checked +2026-08-30: **Ultralytics now ships a `semantic` task with ADE20K checkpoints** +(`docs.ultralytics.com/tasks/semantic`). `yolo26s-sem-ade20k` is in `models/scene/`. + +### It is an addition, not a correction to arm B + +The instance model stays exactly where it was, and the reason is the one §13 already gave and was +right about: a semantic model merges every pixel of a class into one region, so it cannot separate +two people, and separating two people is what clicking a subject requires. Swapping arm B for this +would regress the primary interaction to fix a secondary one. + +So the two divide by *what the user is doing*, not by which is better: + +| | `models/segment/` (COCO instances) | `models/scene/` (ADE20K semantics) | +|---|---|---| +| Question | which pixels are *that* dog | how much of this pixel is sky | +| Granularity | per instance | per category, whole frame | +| Drives | local adjustments, subject selection | the scene tab's per-category sliders | +| Vocabulary | 80 things | 150 classes, stuff included | + +### The export is truncated, and both reasons matter + +Ultralytics ends the graph with `Resize → ArgMax → Cast`, returning a `[1, 640, 640]` u8 label map. +`tools/export-seg-model.sh` cuts that tail and ships the classifier's `[1, 150, 80, 80]` f32 logits. + +**Cost.** The `Resize` materialises 150 × 640 × 640 × f32 — 246 MB — and the `ArgMax` then reduces +across the channel axis, striding 409,600 elements per comparison. Measured under load it was +roughly four fifths of total runtime, spent on work the application discards. + +**Softness, which is the more important one.** `ArgMax` destroys the per-class scores, and the whole +design of the scene tab rests on keeping them. Softmax over the 150 channels, summed within each +category, produces per-category weights that sum to one at every pixel — a partition of unity. +Feathering that cannot double-grade a boundary. Feathering *hard labels* outward from two adjacent +categories paints both grades into the overlap, and every horizon in the frame acquires a seam. + +### The resolution is 80×80, and no setting changes that + +The discarded upsample was never information. `Scene` keeps the native grid and resamples on demand, +so the coarseness is visible in the type rather than hidden. A graduated grade over sky or water is +untroubled by it; a rooftop against sky at 100% zoom will show it. This is the constraint most likely +to decide whether the tab feels good, and it is not addressable by choosing a larger checkpoint — +`yolo26n-sem` and `yolo26s-sem` have the same output grid. + +### Licence + +Unchanged. Same AGPL-3.0 grant as the instance model, same GPLv3 §13 permission, same consequence +already accepted in D14 — so this needed no new licence decision, which is most of why it was cheap. +See `models/LICENCE.md`. + +### Measurement + +Timings taken while this was chosen came off a laptop compiling other things and are upper bounds +only. `cargo run -p dr-segment --example scene --release --features embedded-scene-model` reports a +median over N runs with the first excluded; a number worth quoting should come from that, on an idle +machine. diff --git a/docs/traceability.md b/docs/traceability.md index 10200b9..74150fe 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -9,7 +9,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 309 | +| Source files scanned | 311 | | TRACES tags found | 1015 | | Requirements defined | 179 | | Requirements covered | 122 | @@ -34,62 +34,62 @@ _None._ | 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:201`](../core/dr-types/src/lib.rs#L201), [`core/dr-types/src/lib.rs:270`](../core/dr-types/src/lib.rs#L270), [`core/dr-types/src/lib.rs:303`](../core/dr-types/src/lib.rs#L303), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`tools/traceability/src/lib.rs:481`](../tools/traceability/src/lib.rs#L481), [`tools/traceability/src/lib.rs:513`](../tools/traceability/src/lib.rs#L513), [`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:206`](../core/dr-types/src/settings.rs#L206), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:586`](../platform/dr-plat/src/storage.rs#L586), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1215`](../ui/dr-ui/src/lib.rs#L1215), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1047`](../ui/dr-ui/ui/library.slint#L1047), [`ui/dr-ui/ui/library.slint:1226`](../ui/dr-ui/ui/library.slint#L1226), [`ui/dr-ui/ui/library.slint:879`](../ui/dr-ui/ui/library.slint#L879) | -| 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:1215`](../ui/dr-ui/src/lib.rs#L1215), [`ui/dr-ui/src/library.rs:175`](../ui/dr-ui/src/library.rs#L175), [`ui/dr-ui/src/library.rs:2491`](../ui/dr-ui/src/library.rs#L2491), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | +| 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:206`](../core/dr-types/src/settings.rs#L206), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:586`](../platform/dr-plat/src/storage.rs#L586), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1224`](../ui/dr-ui/src/lib.rs#L1224), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1047`](../ui/dr-ui/ui/library.slint#L1047), [`ui/dr-ui/ui/library.slint:1226`](../ui/dr-ui/ui/library.slint#L1226), [`ui/dr-ui/ui/library.slint:879`](../ui/dr-ui/ui/library.slint#L879) | +| 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:1224`](../ui/dr-ui/src/lib.rs#L1224), [`ui/dr-ui/src/library.rs:175`](../ui/dr-ui/src/library.rs#L175), [`ui/dr-ui/src/library.rs:2491`](../ui/dr-ui/src/library.rs#L2491), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:119`](../core/dr-pipeline/src/sidecar.rs#L119) | | 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:720`](../core/dr-catalog/src/schema.rs#L720), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:457`](../core/dr-sync-nextcloud/src/lib.rs#L457), [`core/dr-sync/src/lib.rs:135`](../core/dr-sync/src/lib.rs#L135), [`core/dr-sync/src/scan.rs:563`](../core/dr-sync/src/scan.rs#L563), [`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:1232`](../ui/dr-ui/src/collections_ui.rs#L1232), [`ui/dr-ui/src/collections_ui.rs:2105`](../ui/dr-ui/src/collections_ui.rs#L2105), [`ui/dr-ui/src/library.rs:175`](../ui/dr-ui/src/library.rs#L175), [`ui/dr-ui/src/library.rs:192`](../ui/dr-ui/src/library.rs#L192), [`ui/dr-ui/src/library.rs:241`](../ui/dr-ui/src/library.rs#L241), [`ui/dr-ui/src/library.rs:3992`](../ui/dr-ui/src/library.rs#L3992), [`ui/dr-ui/src/library.rs:4026`](../ui/dr-ui/src/library.rs#L4026), [`ui/dr-ui/src/library_ui.rs:190`](../ui/dr-ui/src/library_ui.rs#L190), [`ui/dr-ui/src/library_ui.rs:778`](../ui/dr-ui/src/library_ui.rs#L778), [`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:720`](../core/dr-catalog/src/schema.rs#L720), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:457`](../core/dr-sync-nextcloud/src/lib.rs#L457), [`core/dr-sync/src/lib.rs:135`](../core/dr-sync/src/lib.rs#L135), [`core/dr-sync/src/scan.rs:563`](../core/dr-sync/src/scan.rs#L563), [`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:1232`](../ui/dr-ui/src/collections_ui.rs#L1232), [`ui/dr-ui/src/collections_ui.rs:2105`](../ui/dr-ui/src/collections_ui.rs#L2105), [`ui/dr-ui/src/library.rs:175`](../ui/dr-ui/src/library.rs#L175), [`ui/dr-ui/src/library.rs:192`](../ui/dr-ui/src/library.rs#L192), [`ui/dr-ui/src/library.rs:241`](../ui/dr-ui/src/library.rs#L241), [`ui/dr-ui/src/library.rs:4023`](../ui/dr-ui/src/library.rs#L4023), [`ui/dr-ui/src/library.rs:4057`](../ui/dr-ui/src/library.rs#L4057), [`ui/dr-ui/src/library_ui.rs:190`](../ui/dr-ui/src/library_ui.rs#L190), [`ui/dr-ui/src/library_ui.rs:778`](../ui/dr-ui/src/library_ui.rs#L778), [`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:54`](../core/dr-types/src/lib.rs#L54), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`platform/dr-plat/src/storage.rs:46`](../platform/dr-plat/src/storage.rs#L46) | | 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:481`](../tools/traceability/src/lib.rs#L481) | | 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:463`](../ui/dr-ui/src/import.rs#L463), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488), [`ui/dr-ui/src/library.rs:2964`](../ui/dr-ui/src/library.rs#L2964), [`ui/dr-ui/src/library.rs:3472`](../ui/dr-ui/src/library.rs#L3472), [`ui/dr-ui/src/library_ui.rs:172`](../ui/dr-ui/src/library_ui.rs#L172), [`ui/dr-ui/src/library_ui.rs:3731`](../ui/dr-ui/src/library_ui.rs#L3731), [`ui/dr-ui/src/library_ui.rs:4749`](../ui/dr-ui/src/library_ui.rs#L4749), [`ui/dr-ui/ui/app.slint:386`](../ui/dr-ui/ui/app.slint#L386), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) | | 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:330`](../core/dr-catalog/src/schema.rs#L330), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:222`](../ui/dr-ui/src/library.rs#L222), [`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:1138`](../core/dr-catalog/src/schema.rs#L1138), [`core/dr-catalog/src/schema.rs:635`](../core/dr-catalog/src/schema.rs#L635), [`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:136`](../core/dr-pipeline/src/sidecar.rs#L136), [`ui/dr-ui/src/collections_ui.rs:1525`](../ui/dr-ui/src/collections_ui.rs#L1525), [`ui/dr-ui/src/collections_ui.rs:1631`](../ui/dr-ui/src/collections_ui.rs#L1631), [`ui/dr-ui/src/collections_ui.rs:1680`](../ui/dr-ui/src/collections_ui.rs#L1680), [`ui/dr-ui/src/collections_ui.rs:244`](../ui/dr-ui/src/collections_ui.rs#L244), [`ui/dr-ui/src/collections_ui.rs:341`](../ui/dr-ui/src/collections_ui.rs#L341), [`ui/dr-ui/src/collections_ui.rs:90`](../ui/dr-ui/src/collections_ui.rs#L90), [`ui/dr-ui/src/library.rs:4040`](../ui/dr-ui/src/library.rs#L4040), [`ui/dr-ui/src/library_ui.rs:653`](../ui/dr-ui/src/library_ui.rs#L653), [`ui/dr-ui/src/library_ui.rs:6676`](../ui/dr-ui/src/library_ui.rs#L6676), [`ui/dr-ui/src/library_ui.rs:6687`](../ui/dr-ui/src/library_ui.rs#L6687), [`ui/dr-ui/src/library_ui.rs:6700`](../ui/dr-ui/src/library_ui.rs#L6700), [`ui/dr-ui/src/library_ui.rs:6715`](../ui/dr-ui/src/library_ui.rs#L6715), [`ui/dr-ui/src/library_ui.rs:6724`](../ui/dr-ui/src/library_ui.rs#L6724), [`ui/dr-ui/ui/app.slint:322`](../ui/dr-ui/ui/app.slint#L322), [`ui/dr-ui/ui/app.slint:521`](../ui/dr-ui/ui/app.slint#L521), [`ui/dr-ui/ui/library.slint:1298`](../ui/dr-ui/ui/library.slint#L1298), [`ui/dr-ui/ui/library.slint:1322`](../ui/dr-ui/ui/library.slint#L1322), [`ui/dr-ui/ui/library.slint:1375`](../ui/dr-ui/ui/library.slint#L1375), [`ui/dr-ui/ui/library.slint:19`](../ui/dr-ui/ui/library.slint#L19), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:927`](../ui/dr-ui/ui/library.slint#L927) | -| 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:635`](../core/dr-catalog/src/schema.rs#L635), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:64`](../core/dr-types/src/settings.rs#L64), [`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:246`](../ui/dr-ui/src/library.rs#L246), [`ui/dr-ui/src/library.rs:4433`](../ui/dr-ui/src/library.rs#L4433), [`ui/dr-ui/src/library_ui.rs:337`](../ui/dr-ui/src/library_ui.rs#L337), [`ui/dr-ui/src/library_ui.rs:410`](../ui/dr-ui/src/library_ui.rs#L410), [`ui/dr-ui/src/library_ui.rs:5416`](../ui/dr-ui/src/library_ui.rs#L5416), [`ui/dr-ui/src/library_ui.rs:5469`](../ui/dr-ui/src/library_ui.rs#L5469), [`ui/dr-ui/src/library_ui.rs:6816`](../ui/dr-ui/src/library_ui.rs#L6816), [`ui/dr-ui/ui/app.slint:334`](../ui/dr-ui/ui/app.slint#L334), [`ui/dr-ui/ui/app.slint:521`](../ui/dr-ui/ui/app.slint#L521), [`ui/dr-ui/ui/app.slint:817`](../ui/dr-ui/ui/app.slint#L817), [`ui/dr-ui/ui/library.slint:110`](../ui/dr-ui/ui/library.slint#L110), [`ui/dr-ui/ui/library.slint:1468`](../ui/dr-ui/ui/library.slint#L1468), [`ui/dr-ui/ui/library.slint:927`](../ui/dr-ui/ui/library.slint#L927), [`ui/dr-ui/ui/settings.slint:150`](../ui/dr-ui/ui/settings.slint#L150) | -| 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:1544`](../ui/dr-ui/src/collections_ui.rs#L1544), [`ui/dr-ui/src/collections_ui.rs:1794`](../ui/dr-ui/src/collections_ui.rs#L1794), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/collections_ui.rs:928`](../ui/dr-ui/src/collections_ui.rs#L928), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:4238`](../ui/dr-ui/src/library.rs#L4238), [`ui/dr-ui/src/library.rs:4264`](../ui/dr-ui/src/library.rs#L4264), [`ui/dr-ui/src/library.rs:4319`](../ui/dr-ui/src/library.rs#L4319), [`ui/dr-ui/src/library_ui.rs:3593`](../ui/dr-ui/src/library_ui.rs#L3593), [`ui/dr-ui/src/library_ui.rs:4320`](../ui/dr-ui/src/library_ui.rs#L4320), [`ui/dr-ui/ui/app.slint:325`](../ui/dr-ui/ui/app.slint#L325), [`ui/dr-ui/ui/app.slint:516`](../ui/dr-ui/ui/app.slint#L516), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2828`](../ui/dr-ui/ui/library.slint#L2828), [`ui/dr-ui/ui/library.slint:898`](../ui/dr-ui/ui/library.slint#L898), [`ui/dr-ui/ui/library.slint:917`](../ui/dr-ui/ui/library.slint#L917) | -| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:346`](../core/dr-pipeline/src/graph.rs#L346), [`core/dr-pipeline/src/graph.rs:385`](../core/dr-pipeline/src/graph.rs#L385), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1637`](../core/dr-pipeline/src/sidecar.rs#L1637), [`core/dr-pipeline/src/sidecar.rs:93`](../core/dr-pipeline/src/sidecar.rs#L93), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3802`](../ui/dr-ui/src/develop.rs#L3802), [`ui/dr-ui/src/develop.rs:3831`](../ui/dr-ui/src/develop.rs#L3831), [`ui/dr-ui/src/export.rs:750`](../ui/dr-ui/src/export.rs#L750), [`ui/dr-ui/src/lib.rs:1494`](../ui/dr-ui/src/lib.rs#L1494), [`ui/dr-ui/src/lib.rs:1984`](../ui/dr-ui/src/lib.rs#L1984), [`ui/dr-ui/src/lib.rs:2119`](../ui/dr-ui/src/lib.rs#L2119), [`ui/dr-ui/src/lib.rs:573`](../ui/dr-ui/src/lib.rs#L573), [`ui/dr-ui/src/lib.rs:998`](../ui/dr-ui/src/lib.rs#L998), [`ui/dr-ui/src/library.rs:1903`](../ui/dr-ui/src/library.rs#L1903), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library.rs:539`](../ui/dr-ui/src/library.rs#L539), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:823`](../ui/dr-ui/src/library.rs#L823), [`ui/dr-ui/src/library_ui.rs:5041`](../ui/dr-ui/src/library_ui.rs#L5041), [`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:692`](../core/dr-catalog/src/schema.rs#L692), [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`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:728`](../core/dr-catalog/src/walk.rs#L728), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-sync/src/scan.rs:175`](../core/dr-sync/src/scan.rs#L175), [`core/dr-sync/src/scan.rs:464`](../core/dr-sync/src/scan.rs#L464), [`core/dr-types/src/lib.rs:120`](../core/dr-types/src/lib.rs#L120), [`ui/dr-ui/src/develop.rs:3229`](../ui/dr-ui/src/develop.rs#L3229), [`ui/dr-ui/src/library.rs:1222`](../ui/dr-ui/src/library.rs#L1222), [`ui/dr-ui/src/library.rs:1275`](../ui/dr-ui/src/library.rs#L1275), [`ui/dr-ui/src/library.rs:1306`](../ui/dr-ui/src/library.rs#L1306), [`ui/dr-ui/src/library.rs:1411`](../ui/dr-ui/src/library.rs#L1411), [`ui/dr-ui/src/library.rs:161`](../ui/dr-ui/src/library.rs#L161), [`ui/dr-ui/src/library.rs:1863`](../ui/dr-ui/src/library.rs#L1863), [`ui/dr-ui/src/library.rs:1939`](../ui/dr-ui/src/library.rs#L1939), [`ui/dr-ui/src/library.rs:266`](../ui/dr-ui/src/library.rs#L266), [`ui/dr-ui/src/library.rs:4541`](../ui/dr-ui/src/library.rs#L4541), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:807`](../ui/dr-ui/src/library.rs#L807), [`ui/dr-ui/src/library.rs:823`](../ui/dr-ui/src/library.rs#L823), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877), [`ui/dr-ui/src/library_ui.rs:1045`](../ui/dr-ui/src/library_ui.rs#L1045), [`ui/dr-ui/src/library_ui.rs:1636`](../ui/dr-ui/src/library_ui.rs#L1636), [`ui/dr-ui/src/library_ui.rs:1680`](../ui/dr-ui/src/library_ui.rs#L1680), [`ui/dr-ui/src/library_ui.rs:1696`](../ui/dr-ui/src/library_ui.rs#L1696), [`ui/dr-ui/src/library_ui.rs:1790`](../ui/dr-ui/src/library_ui.rs#L1790), [`ui/dr-ui/src/library_ui.rs:232`](../ui/dr-ui/src/library_ui.rs#L232), [`ui/dr-ui/src/library_ui.rs:2436`](../ui/dr-ui/src/library_ui.rs#L2436), [`ui/dr-ui/src/library_ui.rs:265`](../ui/dr-ui/src/library_ui.rs#L265), [`ui/dr-ui/src/library_ui.rs:273`](../ui/dr-ui/src/library_ui.rs#L273), [`ui/dr-ui/src/library_ui.rs:2874`](../ui/dr-ui/src/library_ui.rs#L2874), [`ui/dr-ui/src/library_ui.rs:3096`](../ui/dr-ui/src/library_ui.rs#L3096), [`ui/dr-ui/src/library_ui.rs:3374`](../ui/dr-ui/src/library_ui.rs#L3374), [`ui/dr-ui/src/library_ui.rs:3462`](../ui/dr-ui/src/library_ui.rs#L3462), [`ui/dr-ui/src/library_ui.rs:3644`](../ui/dr-ui/src/library_ui.rs#L3644), [`ui/dr-ui/src/library_ui.rs:3758`](../ui/dr-ui/src/library_ui.rs#L3758), [`ui/dr-ui/src/library_ui.rs:463`](../ui/dr-ui/src/library_ui.rs#L463), [`ui/dr-ui/src/library_ui.rs:526`](../ui/dr-ui/src/library_ui.rs#L526), [`ui/dr-ui/src/library_ui.rs:5514`](../ui/dr-ui/src/library_ui.rs#L5514), [`ui/dr-ui/src/library_ui.rs:5629`](../ui/dr-ui/src/library_ui.rs#L5629), [`ui/dr-ui/src/presets.rs:446`](../ui/dr-ui/src/presets.rs#L446), [`ui/dr-ui/src/presets.rs:458`](../ui/dr-ui/src/presets.rs#L458), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.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:1138`](../core/dr-catalog/src/schema.rs#L1138), [`core/dr-catalog/src/schema.rs:635`](../core/dr-catalog/src/schema.rs#L635), [`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:136`](../core/dr-pipeline/src/sidecar.rs#L136), [`ui/dr-ui/src/collections_ui.rs:1525`](../ui/dr-ui/src/collections_ui.rs#L1525), [`ui/dr-ui/src/collections_ui.rs:1631`](../ui/dr-ui/src/collections_ui.rs#L1631), [`ui/dr-ui/src/collections_ui.rs:1680`](../ui/dr-ui/src/collections_ui.rs#L1680), [`ui/dr-ui/src/collections_ui.rs:244`](../ui/dr-ui/src/collections_ui.rs#L244), [`ui/dr-ui/src/collections_ui.rs:341`](../ui/dr-ui/src/collections_ui.rs#L341), [`ui/dr-ui/src/collections_ui.rs:90`](../ui/dr-ui/src/collections_ui.rs#L90), [`ui/dr-ui/src/library.rs:4071`](../ui/dr-ui/src/library.rs#L4071), [`ui/dr-ui/src/library_ui.rs:653`](../ui/dr-ui/src/library_ui.rs#L653), [`ui/dr-ui/src/library_ui.rs:6676`](../ui/dr-ui/src/library_ui.rs#L6676), [`ui/dr-ui/src/library_ui.rs:6687`](../ui/dr-ui/src/library_ui.rs#L6687), [`ui/dr-ui/src/library_ui.rs:6700`](../ui/dr-ui/src/library_ui.rs#L6700), [`ui/dr-ui/src/library_ui.rs:6715`](../ui/dr-ui/src/library_ui.rs#L6715), [`ui/dr-ui/src/library_ui.rs:6724`](../ui/dr-ui/src/library_ui.rs#L6724), [`ui/dr-ui/ui/app.slint:322`](../ui/dr-ui/ui/app.slint#L322), [`ui/dr-ui/ui/app.slint:521`](../ui/dr-ui/ui/app.slint#L521), [`ui/dr-ui/ui/library.slint:1298`](../ui/dr-ui/ui/library.slint#L1298), [`ui/dr-ui/ui/library.slint:1322`](../ui/dr-ui/ui/library.slint#L1322), [`ui/dr-ui/ui/library.slint:1375`](../ui/dr-ui/ui/library.slint#L1375), [`ui/dr-ui/ui/library.slint:19`](../ui/dr-ui/ui/library.slint#L19), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:927`](../ui/dr-ui/ui/library.slint#L927) | +| 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:635`](../core/dr-catalog/src/schema.rs#L635), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:64`](../core/dr-types/src/settings.rs#L64), [`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:246`](../ui/dr-ui/src/library.rs#L246), [`ui/dr-ui/src/library.rs:4464`](../ui/dr-ui/src/library.rs#L4464), [`ui/dr-ui/src/library_ui.rs:337`](../ui/dr-ui/src/library_ui.rs#L337), [`ui/dr-ui/src/library_ui.rs:410`](../ui/dr-ui/src/library_ui.rs#L410), [`ui/dr-ui/src/library_ui.rs:5416`](../ui/dr-ui/src/library_ui.rs#L5416), [`ui/dr-ui/src/library_ui.rs:5469`](../ui/dr-ui/src/library_ui.rs#L5469), [`ui/dr-ui/src/library_ui.rs:6816`](../ui/dr-ui/src/library_ui.rs#L6816), [`ui/dr-ui/ui/app.slint:334`](../ui/dr-ui/ui/app.slint#L334), [`ui/dr-ui/ui/app.slint:521`](../ui/dr-ui/ui/app.slint#L521), [`ui/dr-ui/ui/app.slint:817`](../ui/dr-ui/ui/app.slint#L817), [`ui/dr-ui/ui/library.slint:110`](../ui/dr-ui/ui/library.slint#L110), [`ui/dr-ui/ui/library.slint:1468`](../ui/dr-ui/ui/library.slint#L1468), [`ui/dr-ui/ui/library.slint:927`](../ui/dr-ui/ui/library.slint#L927), [`ui/dr-ui/ui/settings.slint:150`](../ui/dr-ui/ui/settings.slint#L150) | +| 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:1544`](../ui/dr-ui/src/collections_ui.rs#L1544), [`ui/dr-ui/src/collections_ui.rs:1794`](../ui/dr-ui/src/collections_ui.rs#L1794), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/collections_ui.rs:928`](../ui/dr-ui/src/collections_ui.rs#L928), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:4269`](../ui/dr-ui/src/library.rs#L4269), [`ui/dr-ui/src/library.rs:4295`](../ui/dr-ui/src/library.rs#L4295), [`ui/dr-ui/src/library.rs:4350`](../ui/dr-ui/src/library.rs#L4350), [`ui/dr-ui/src/library_ui.rs:3593`](../ui/dr-ui/src/library_ui.rs#L3593), [`ui/dr-ui/src/library_ui.rs:4320`](../ui/dr-ui/src/library_ui.rs#L4320), [`ui/dr-ui/ui/app.slint:325`](../ui/dr-ui/ui/app.slint#L325), [`ui/dr-ui/ui/app.slint:516`](../ui/dr-ui/ui/app.slint#L516), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2828`](../ui/dr-ui/ui/library.slint#L2828), [`ui/dr-ui/ui/library.slint:898`](../ui/dr-ui/ui/library.slint#L898), [`ui/dr-ui/ui/library.slint:917`](../ui/dr-ui/ui/library.slint#L917) | +| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:346`](../core/dr-pipeline/src/graph.rs#L346), [`core/dr-pipeline/src/graph.rs:385`](../core/dr-pipeline/src/graph.rs#L385), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1637`](../core/dr-pipeline/src/sidecar.rs#L1637), [`core/dr-pipeline/src/sidecar.rs:93`](../core/dr-pipeline/src/sidecar.rs#L93), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3802`](../ui/dr-ui/src/develop.rs#L3802), [`ui/dr-ui/src/develop.rs:3831`](../ui/dr-ui/src/develop.rs#L3831), [`ui/dr-ui/src/export.rs:750`](../ui/dr-ui/src/export.rs#L750), [`ui/dr-ui/src/lib.rs:1007`](../ui/dr-ui/src/lib.rs#L1007), [`ui/dr-ui/src/lib.rs:1503`](../ui/dr-ui/src/lib.rs#L1503), [`ui/dr-ui/src/lib.rs:1993`](../ui/dr-ui/src/lib.rs#L1993), [`ui/dr-ui/src/lib.rs:2128`](../ui/dr-ui/src/lib.rs#L2128), [`ui/dr-ui/src/lib.rs:582`](../ui/dr-ui/src/lib.rs#L582), [`ui/dr-ui/src/library.rs:1903`](../ui/dr-ui/src/library.rs#L1903), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library.rs:539`](../ui/dr-ui/src/library.rs#L539), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:823`](../ui/dr-ui/src/library.rs#L823), [`ui/dr-ui/src/library_ui.rs:5041`](../ui/dr-ui/src/library_ui.rs#L5041), [`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:692`](../core/dr-catalog/src/schema.rs#L692), [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`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:728`](../core/dr-catalog/src/walk.rs#L728), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-sync/src/scan.rs:175`](../core/dr-sync/src/scan.rs#L175), [`core/dr-sync/src/scan.rs:464`](../core/dr-sync/src/scan.rs#L464), [`core/dr-types/src/lib.rs:120`](../core/dr-types/src/lib.rs#L120), [`ui/dr-ui/src/develop.rs:3229`](../ui/dr-ui/src/develop.rs#L3229), [`ui/dr-ui/src/library.rs:1222`](../ui/dr-ui/src/library.rs#L1222), [`ui/dr-ui/src/library.rs:1275`](../ui/dr-ui/src/library.rs#L1275), [`ui/dr-ui/src/library.rs:1306`](../ui/dr-ui/src/library.rs#L1306), [`ui/dr-ui/src/library.rs:1411`](../ui/dr-ui/src/library.rs#L1411), [`ui/dr-ui/src/library.rs:161`](../ui/dr-ui/src/library.rs#L161), [`ui/dr-ui/src/library.rs:1863`](../ui/dr-ui/src/library.rs#L1863), [`ui/dr-ui/src/library.rs:1939`](../ui/dr-ui/src/library.rs#L1939), [`ui/dr-ui/src/library.rs:266`](../ui/dr-ui/src/library.rs#L266), [`ui/dr-ui/src/library.rs:4572`](../ui/dr-ui/src/library.rs#L4572), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:807`](../ui/dr-ui/src/library.rs#L807), [`ui/dr-ui/src/library.rs:823`](../ui/dr-ui/src/library.rs#L823), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877), [`ui/dr-ui/src/library_ui.rs:1045`](../ui/dr-ui/src/library_ui.rs#L1045), [`ui/dr-ui/src/library_ui.rs:1636`](../ui/dr-ui/src/library_ui.rs#L1636), [`ui/dr-ui/src/library_ui.rs:1680`](../ui/dr-ui/src/library_ui.rs#L1680), [`ui/dr-ui/src/library_ui.rs:1696`](../ui/dr-ui/src/library_ui.rs#L1696), [`ui/dr-ui/src/library_ui.rs:1790`](../ui/dr-ui/src/library_ui.rs#L1790), [`ui/dr-ui/src/library_ui.rs:232`](../ui/dr-ui/src/library_ui.rs#L232), [`ui/dr-ui/src/library_ui.rs:2436`](../ui/dr-ui/src/library_ui.rs#L2436), [`ui/dr-ui/src/library_ui.rs:265`](../ui/dr-ui/src/library_ui.rs#L265), [`ui/dr-ui/src/library_ui.rs:273`](../ui/dr-ui/src/library_ui.rs#L273), [`ui/dr-ui/src/library_ui.rs:2874`](../ui/dr-ui/src/library_ui.rs#L2874), [`ui/dr-ui/src/library_ui.rs:3096`](../ui/dr-ui/src/library_ui.rs#L3096), [`ui/dr-ui/src/library_ui.rs:3374`](../ui/dr-ui/src/library_ui.rs#L3374), [`ui/dr-ui/src/library_ui.rs:3462`](../ui/dr-ui/src/library_ui.rs#L3462), [`ui/dr-ui/src/library_ui.rs:3644`](../ui/dr-ui/src/library_ui.rs#L3644), [`ui/dr-ui/src/library_ui.rs:3758`](../ui/dr-ui/src/library_ui.rs#L3758), [`ui/dr-ui/src/library_ui.rs:463`](../ui/dr-ui/src/library_ui.rs#L463), [`ui/dr-ui/src/library_ui.rs:526`](../ui/dr-ui/src/library_ui.rs#L526), [`ui/dr-ui/src/library_ui.rs:5514`](../ui/dr-ui/src/library_ui.rs#L5514), [`ui/dr-ui/src/library_ui.rs:5629`](../ui/dr-ui/src/library_ui.rs#L5629), [`ui/dr-ui/src/presets.rs:446`](../ui/dr-ui/src/presets.rs#L446), [`ui/dr-ui/src/presets.rs:458`](../ui/dr-ui/src/presets.rs#L458), [`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:121`](../core/dr-decode/src/preview.rs#L121) | -| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:369`](../core/dr-catalog/src/schema.rs#L369), [`core/dr-catalog/src/schema.rs:521`](../core/dr-catalog/src/schema.rs#L521), [`core/dr-face/src/assign.rs:1`](../core/dr-face/src/assign.rs#L1), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`core/dr-types/src/settings.rs:117`](../core/dr-types/src/settings.rs#L117), [`ui/dr-ui/src/develop.rs:120`](../ui/dr-ui/src/develop.rs#L120), [`ui/dr-ui/src/develop.rs:129`](../ui/dr-ui/src/develop.rs#L129), [`ui/dr-ui/src/develop.rs:195`](../ui/dr-ui/src/develop.rs#L195), [`ui/dr-ui/src/develop.rs:1968`](../ui/dr-ui/src/develop.rs#L1968), [`ui/dr-ui/src/develop.rs:688`](../ui/dr-ui/src/develop.rs#L688), [`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/src/lib.rs:2091`](../ui/dr-ui/src/lib.rs#L2091), [`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:369`](../core/dr-catalog/src/schema.rs#L369), [`core/dr-catalog/src/schema.rs:521`](../core/dr-catalog/src/schema.rs#L521), [`core/dr-face/src/assign.rs:1`](../core/dr-face/src/assign.rs#L1), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`core/dr-types/src/settings.rs:117`](../core/dr-types/src/settings.rs#L117), [`ui/dr-ui/src/develop.rs:120`](../ui/dr-ui/src/develop.rs#L120), [`ui/dr-ui/src/develop.rs:129`](../ui/dr-ui/src/develop.rs#L129), [`ui/dr-ui/src/develop.rs:195`](../ui/dr-ui/src/develop.rs#L195), [`ui/dr-ui/src/develop.rs:1968`](../ui/dr-ui/src/develop.rs#L1968), [`ui/dr-ui/src/develop.rs:688`](../ui/dr-ui/src/develop.rs#L688), [`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/src/lib.rs:2100`](../ui/dr-ui/src/lib.rs#L2100), [`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:521`](../core/dr-catalog/src/schema.rs#L521), [`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/src/library.rs:287`](../ui/dr-ui/src/library.rs#L287), [`ui/dr-ui/src/library.rs:317`](../ui/dr-ui/src/library.rs#L317), [`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:369`](../core/dr-catalog/src/schema.rs#L369), [`core/dr-catalog/src/schema.rs:521`](../core/dr-catalog/src/schema.rs#L521), [`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:148`](../core/dr-decode/src/preview.rs#L148), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463) | -| FR-CULL-3 | [`core/dr-gpu/src/focus.rs:154`](../core/dr-gpu/src/focus.rs#L154), [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/raw_histogram.rs:129`](../core/dr-gpu/src/raw_histogram.rs#L129), [`core/dr-gpu/src/raw_histogram.rs:1`](../core/dr-gpu/src/raw_histogram.rs#L1), [`core/dr-gpu/src/raw_histogram.rs:272`](../core/dr-gpu/src/raw_histogram.rs#L272), [`core/dr-gpu/src/raw_histogram.rs:407`](../core/dr-gpu/src/raw_histogram.rs#L407), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`core/dr-gpu/src/shaders/raw_histogram.wgsl:1`](../core/dr-gpu/src/shaders/raw_histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:2954`](../ui/dr-ui/src/develop.rs#L2954), [`ui/dr-ui/src/develop.rs:2967`](../ui/dr-ui/src/develop.rs#L2967), [`ui/dr-ui/src/develop.rs:3012`](../ui/dr-ui/src/develop.rs#L3012), [`ui/dr-ui/src/develop.rs:3023`](../ui/dr-ui/src/develop.rs#L3023), [`ui/dr-ui/src/develop.rs:3029`](../ui/dr-ui/src/develop.rs#L3029), [`ui/dr-ui/src/develop.rs:3046`](../ui/dr-ui/src/develop.rs#L3046), [`ui/dr-ui/src/develop.rs:5929`](../ui/dr-ui/src/develop.rs#L5929), [`ui/dr-ui/src/develop.rs:5993`](../ui/dr-ui/src/develop.rs#L5993), [`ui/dr-ui/src/develop.rs:6016`](../ui/dr-ui/src/develop.rs#L6016), [`ui/dr-ui/src/develop.rs:726`](../ui/dr-ui/src/develop.rs#L726), [`ui/dr-ui/src/develop.rs:746`](../ui/dr-ui/src/develop.rs#L746), [`ui/dr-ui/src/develop.rs:752`](../ui/dr-ui/src/develop.rs#L752), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/histogram.rs:208`](../ui/dr-ui/src/histogram.rs#L208), [`ui/dr-ui/src/histogram.rs:228`](../ui/dr-ui/src/histogram.rs#L228), [`ui/dr-ui/src/histogram.rs:272`](../ui/dr-ui/src/histogram.rs#L272), [`ui/dr-ui/src/histogram.rs:541`](../ui/dr-ui/src/histogram.rs#L541), [`ui/dr-ui/src/histogram.rs:562`](../ui/dr-ui/src/histogram.rs#L562), [`ui/dr-ui/src/histogram.rs:590`](../ui/dr-ui/src/histogram.rs#L590), [`ui/dr-ui/src/histogram.rs:618`](../ui/dr-ui/src/histogram.rs#L618), [`ui/dr-ui/src/histogram.rs:655`](../ui/dr-ui/src/histogram.rs#L655), [`ui/dr-ui/src/lib.rs:1533`](../ui/dr-ui/src/lib.rs#L1533), [`ui/dr-ui/src/lib.rs:1611`](../ui/dr-ui/src/lib.rs#L1611), [`ui/dr-ui/src/lib.rs:1710`](../ui/dr-ui/src/lib.rs#L1710), [`ui/dr-ui/src/lib.rs:1751`](../ui/dr-ui/src/lib.rs#L1751), [`ui/dr-ui/src/lib.rs:3001`](../ui/dr-ui/src/lib.rs#L3001), [`ui/dr-ui/src/lib.rs:333`](../ui/dr-ui/src/lib.rs#L333), [`ui/dr-ui/src/peaking.rs:1`](../ui/dr-ui/src/peaking.rs#L1), [`ui/dr-ui/ui/app.slint:1743`](../ui/dr-ui/ui/app.slint#L1743), [`ui/dr-ui/ui/app.slint:2289`](../ui/dr-ui/ui/app.slint#L2289), [`ui/dr-ui/ui/app.slint:87`](../ui/dr-ui/ui/app.slint#L87), [`ui/dr-ui/ui/peaking.slint:1`](../ui/dr-ui/ui/peaking.slint#L1), [`ui/dr-ui/ui/peaking.slint:25`](../ui/dr-ui/ui/peaking.slint#L25), [`ui/dr-ui/ui/peaking.slint:56`](../ui/dr-ui/ui/peaking.slint#L56) | +| FR-CULL-3 | [`core/dr-gpu/src/focus.rs:154`](../core/dr-gpu/src/focus.rs#L154), [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/raw_histogram.rs:129`](../core/dr-gpu/src/raw_histogram.rs#L129), [`core/dr-gpu/src/raw_histogram.rs:1`](../core/dr-gpu/src/raw_histogram.rs#L1), [`core/dr-gpu/src/raw_histogram.rs:272`](../core/dr-gpu/src/raw_histogram.rs#L272), [`core/dr-gpu/src/raw_histogram.rs:407`](../core/dr-gpu/src/raw_histogram.rs#L407), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`core/dr-gpu/src/shaders/raw_histogram.wgsl:1`](../core/dr-gpu/src/shaders/raw_histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:2954`](../ui/dr-ui/src/develop.rs#L2954), [`ui/dr-ui/src/develop.rs:2967`](../ui/dr-ui/src/develop.rs#L2967), [`ui/dr-ui/src/develop.rs:3012`](../ui/dr-ui/src/develop.rs#L3012), [`ui/dr-ui/src/develop.rs:3023`](../ui/dr-ui/src/develop.rs#L3023), [`ui/dr-ui/src/develop.rs:3029`](../ui/dr-ui/src/develop.rs#L3029), [`ui/dr-ui/src/develop.rs:3046`](../ui/dr-ui/src/develop.rs#L3046), [`ui/dr-ui/src/develop.rs:5929`](../ui/dr-ui/src/develop.rs#L5929), [`ui/dr-ui/src/develop.rs:5993`](../ui/dr-ui/src/develop.rs#L5993), [`ui/dr-ui/src/develop.rs:6016`](../ui/dr-ui/src/develop.rs#L6016), [`ui/dr-ui/src/develop.rs:726`](../ui/dr-ui/src/develop.rs#L726), [`ui/dr-ui/src/develop.rs:746`](../ui/dr-ui/src/develop.rs#L746), [`ui/dr-ui/src/develop.rs:752`](../ui/dr-ui/src/develop.rs#L752), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/histogram.rs:208`](../ui/dr-ui/src/histogram.rs#L208), [`ui/dr-ui/src/histogram.rs:228`](../ui/dr-ui/src/histogram.rs#L228), [`ui/dr-ui/src/histogram.rs:272`](../ui/dr-ui/src/histogram.rs#L272), [`ui/dr-ui/src/histogram.rs:541`](../ui/dr-ui/src/histogram.rs#L541), [`ui/dr-ui/src/histogram.rs:562`](../ui/dr-ui/src/histogram.rs#L562), [`ui/dr-ui/src/histogram.rs:590`](../ui/dr-ui/src/histogram.rs#L590), [`ui/dr-ui/src/histogram.rs:618`](../ui/dr-ui/src/histogram.rs#L618), [`ui/dr-ui/src/histogram.rs:655`](../ui/dr-ui/src/histogram.rs#L655), [`ui/dr-ui/src/lib.rs:1542`](../ui/dr-ui/src/lib.rs#L1542), [`ui/dr-ui/src/lib.rs:1620`](../ui/dr-ui/src/lib.rs#L1620), [`ui/dr-ui/src/lib.rs:1719`](../ui/dr-ui/src/lib.rs#L1719), [`ui/dr-ui/src/lib.rs:1760`](../ui/dr-ui/src/lib.rs#L1760), [`ui/dr-ui/src/lib.rs:3010`](../ui/dr-ui/src/lib.rs#L3010), [`ui/dr-ui/src/lib.rs:342`](../ui/dr-ui/src/lib.rs#L342), [`ui/dr-ui/src/peaking.rs:1`](../ui/dr-ui/src/peaking.rs#L1), [`ui/dr-ui/ui/app.slint:1743`](../ui/dr-ui/ui/app.slint#L1743), [`ui/dr-ui/ui/app.slint:2289`](../ui/dr-ui/ui/app.slint#L2289), [`ui/dr-ui/ui/app.slint:87`](../ui/dr-ui/ui/app.slint#L87), [`ui/dr-ui/ui/peaking.slint:1`](../ui/dr-ui/ui/peaking.slint#L1), [`ui/dr-ui/ui/peaking.slint:25`](../ui/dr-ui/ui/peaking.slint#L25), [`ui/dr-ui/ui/peaking.slint:56`](../ui/dr-ui/ui/peaking.slint#L56) | | FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:136`](../core/dr-pipeline/src/sidecar.rs#L136), [`ui/dr-ui/src/library.rs:246`](../ui/dr-ui/src/library.rs#L246), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492) | -| FR-CULL-5 | [`core/dr-catalog/src/bursts.rs:1`](../core/dr-catalog/src/bursts.rs#L1), [`core/dr-catalog/src/schema.rs:412`](../core/dr-catalog/src/schema.rs#L412), [`ui/dr-ui/src/bursts.rs:1`](../ui/dr-ui/src/bursts.rs#L1), [`ui/dr-ui/src/library.rs:202`](../ui/dr-ui/src/library.rs#L202), [`ui/dr-ui/src/library.rs:5142`](../ui/dr-ui/src/library.rs#L5142) | +| FR-CULL-5 | [`core/dr-catalog/src/bursts.rs:1`](../core/dr-catalog/src/bursts.rs#L1), [`core/dr-catalog/src/schema.rs:412`](../core/dr-catalog/src/schema.rs#L412), [`ui/dr-ui/src/bursts.rs:1`](../ui/dr-ui/src/bursts.rs#L1), [`ui/dr-ui/src/library.rs:202`](../ui/dr-ui/src/library.rs#L202), [`ui/dr-ui/src/library.rs:5173`](../ui/dr-ui/src/library.rs#L5173) | | FR-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:480`](../core/dr-catalog/src/schema.rs#L480), [`core/dr-catalog/src/schema.rs:521`](../core/dr-catalog/src/schema.rs#L521), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/library.rs:2965`](../ui/dr-ui/src/library.rs#L2965), [`ui/dr-ui/src/library.rs:3069`](../ui/dr-ui/src/library.rs#L3069), [`ui/dr-ui/ui/settings.slint:419`](../ui/dr-ui/ui/settings.slint#L419), [`ui/dr-ui/ui/settings.slint:82`](../ui/dr-ui/ui/settings.slint#L82) | | FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:521`](../core/dr-catalog/src/schema.rs#L521), [`core/dr-face/src/assign.rs:1`](../core/dr-face/src/assign.rs#L1), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`core/dr-types/src/settings.rs:117`](../core/dr-types/src/settings.rs#L117), [`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), [`ui/dr-ui/ui/identity.slint:276`](../ui/dr-ui/ui/identity.slint#L276) | | FR-DEV-1 | [`core/dr-pipeline/src/graph.rs:1`](../core/dr-pipeline/src/graph.rs#L1), [`core/dr-pipeline/src/sidecar.rs:1`](../core/dr-pipeline/src/sidecar.rs#L1) | | FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389) | -| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2203`](../core/dr-gpu/src/adjust.rs#L2203), [`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:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:524`](../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/framing.rs:177`](../core/dr-pipeline/src/framing.rs#L177), [`core/dr-pipeline/src/framing.rs:234`](../core/dr-pipeline/src/framing.rs#L234), [`core/dr-pipeline/src/framing.rs:314`](../core/dr-pipeline/src/framing.rs#L314), [`core/dr-pipeline/src/framing.rs:488`](../core/dr-pipeline/src/framing.rs#L488), [`core/dr-pipeline/src/framing.rs:743`](../core/dr-pipeline/src/framing.rs#L743), [`core/dr-pipeline/src/graph.rs:170`](../core/dr-pipeline/src/graph.rs#L170), [`core/dr-pipeline/src/graph.rs:578`](../core/dr-pipeline/src/graph.rs#L578), [`core/dr-pipeline/src/mask.rs:121`](../core/dr-pipeline/src/mask.rs#L121), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:516`](../core/dr-pipeline/src/operation.rs#L516), [`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:210`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../core/dr-pipeline/src/ops/curve.rs#L635), [`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:273`](../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/sidecar.rs:157`](../core/dr-pipeline/src/sidecar.rs#L157), [`core/dr-pipeline/src/sidecar.rs:1637`](../core/dr-pipeline/src/sidecar.rs#L1637), [`core/dr-pipeline/src/sidecar.rs:1697`](../core/dr-pipeline/src/sidecar.rs#L1697), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:102`](../ui/dr-ui/src/develop.rs#L102), [`ui/dr-ui/src/develop.rs:1472`](../ui/dr-ui/src/develop.rs#L1472), [`ui/dr-ui/src/develop.rs:164`](../ui/dr-ui/src/develop.rs#L164), [`ui/dr-ui/src/develop.rs:1950`](../ui/dr-ui/src/develop.rs#L1950), [`ui/dr-ui/src/develop.rs:1968`](../ui/dr-ui/src/develop.rs#L1968), [`ui/dr-ui/src/develop.rs:1982`](../ui/dr-ui/src/develop.rs#L1982), [`ui/dr-ui/src/develop.rs:2004`](../ui/dr-ui/src/develop.rs#L2004), [`ui/dr-ui/src/develop.rs:2150`](../ui/dr-ui/src/develop.rs#L2150), [`ui/dr-ui/src/develop.rs:2248`](../ui/dr-ui/src/develop.rs#L2248), [`ui/dr-ui/src/develop.rs:327`](../ui/dr-ui/src/develop.rs#L327), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/develop.rs:3530`](../ui/dr-ui/src/develop.rs#L3530), [`ui/dr-ui/src/develop.rs:3596`](../ui/dr-ui/src/develop.rs#L3596), [`ui/dr-ui/src/develop.rs:3610`](../ui/dr-ui/src/develop.rs#L3610), [`ui/dr-ui/src/develop.rs:364`](../ui/dr-ui/src/develop.rs#L364), [`ui/dr-ui/src/develop.rs:3802`](../ui/dr-ui/src/develop.rs#L3802), [`ui/dr-ui/src/develop.rs:4462`](../ui/dr-ui/src/develop.rs#L4462), [`ui/dr-ui/src/develop.rs:4516`](../ui/dr-ui/src/develop.rs#L4516), [`ui/dr-ui/src/develop.rs:4560`](../ui/dr-ui/src/develop.rs#L4560), [`ui/dr-ui/src/develop.rs:4610`](../ui/dr-ui/src/develop.rs#L4610), [`ui/dr-ui/src/develop.rs:587`](../ui/dr-ui/src/develop.rs#L587), [`ui/dr-ui/src/develop.rs:648`](../ui/dr-ui/src/develop.rs#L648), [`ui/dr-ui/src/develop.rs:766`](../ui/dr-ui/src/develop.rs#L766), [`ui/dr-ui/src/develop.rs:813`](../ui/dr-ui/src/develop.rs#L813), [`ui/dr-ui/src/develop.rs:842`](../ui/dr-ui/src/develop.rs#L842), [`ui/dr-ui/src/lib.rs:1592`](../ui/dr-ui/src/lib.rs#L1592), [`ui/dr-ui/src/lib.rs:2394`](../ui/dr-ui/src/lib.rs#L2394), [`ui/dr-ui/src/lib.rs:2590`](../ui/dr-ui/src/lib.rs#L2590), [`ui/dr-ui/src/lib.rs:2762`](../ui/dr-ui/src/lib.rs#L2762), [`ui/dr-ui/src/lib.rs:2826`](../ui/dr-ui/src/lib.rs#L2826), [`ui/dr-ui/src/lib.rs:2880`](../ui/dr-ui/src/lib.rs#L2880), [`ui/dr-ui/src/lib.rs:339`](../ui/dr-ui/src/lib.rs#L339), [`ui/dr-ui/src/lib.rs:382`](../ui/dr-ui/src/lib.rs#L382), [`ui/dr-ui/src/lib.rs:405`](../ui/dr-ui/src/lib.rs#L405), [`ui/dr-ui/src/library.rs:539`](../ui/dr-ui/src/library.rs#L539), [`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:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/adjust.slint:517`](../ui/dr-ui/ui/adjust.slint#L517), [`ui/dr-ui/ui/adjust.slint:635`](../ui/dr-ui/ui/adjust.slint#L635), [`ui/dr-ui/ui/app.slint:148`](../ui/dr-ui/ui/app.slint#L148), [`ui/dr-ui/ui/app.slint:1945`](../ui/dr-ui/ui/app.slint#L1945), [`ui/dr-ui/ui/app.slint:194`](../ui/dr-ui/ui/app.slint#L194), [`ui/dr-ui/ui/app.slint:927`](../ui/dr-ui/ui/app.slint#L927), [`ui/dr-ui/ui/masks.slint:524`](../ui/dr-ui/ui/masks.slint#L524) | -| FR-DEV-3a | [`core/dr-pipeline/build.rs:756`](../core/dr-pipeline/build.rs#L756), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:194`](../core/dr-pipeline/src/descriptor.rs#L194), [`core/dr-pipeline/src/descriptor.rs:234`](../core/dr-pipeline/src/descriptor.rs#L234), [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/descriptor.rs:313`](../core/dr-pipeline/src/descriptor.rs#L313), [`core/dr-pipeline/src/framing.rs:385`](../core/dr-pipeline/src/framing.rs#L385), [`core/dr-pipeline/src/graph.rs:24`](../core/dr-pipeline/src/graph.rs#L24), [`core/dr-pipeline/src/graph.rs:251`](../core/dr-pipeline/src/graph.rs#L251), [`core/dr-pipeline/src/graph.rs:46`](../core/dr-pipeline/src/graph.rs#L46), [`core/dr-pipeline/src/graph.rs:59`](../core/dr-pipeline/src/graph.rs#L59), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365), [`core/dr-pipeline/src/ops/curve.rs:319`](../core/dr-pipeline/src/ops/curve.rs#L319), [`ui/dr-ui/src/develop.rs:1369`](../ui/dr-ui/src/develop.rs#L1369), [`ui/dr-ui/src/lib.rs:688`](../ui/dr-ui/src/lib.rs#L688), [`ui/dr-ui/tests/ui_names_no_operation.rs:1`](../ui/dr-ui/tests/ui_names_no_operation.rs#L1) | +| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2203`](../core/dr-gpu/src/adjust.rs#L2203), [`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:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:524`](../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/framing.rs:177`](../core/dr-pipeline/src/framing.rs#L177), [`core/dr-pipeline/src/framing.rs:234`](../core/dr-pipeline/src/framing.rs#L234), [`core/dr-pipeline/src/framing.rs:314`](../core/dr-pipeline/src/framing.rs#L314), [`core/dr-pipeline/src/framing.rs:488`](../core/dr-pipeline/src/framing.rs#L488), [`core/dr-pipeline/src/framing.rs:743`](../core/dr-pipeline/src/framing.rs#L743), [`core/dr-pipeline/src/graph.rs:170`](../core/dr-pipeline/src/graph.rs#L170), [`core/dr-pipeline/src/graph.rs:578`](../core/dr-pipeline/src/graph.rs#L578), [`core/dr-pipeline/src/mask.rs:121`](../core/dr-pipeline/src/mask.rs#L121), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:516`](../core/dr-pipeline/src/operation.rs#L516), [`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:210`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../core/dr-pipeline/src/ops/curve.rs#L635), [`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:273`](../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/sidecar.rs:157`](../core/dr-pipeline/src/sidecar.rs#L157), [`core/dr-pipeline/src/sidecar.rs:1637`](../core/dr-pipeline/src/sidecar.rs#L1637), [`core/dr-pipeline/src/sidecar.rs:1697`](../core/dr-pipeline/src/sidecar.rs#L1697), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:102`](../ui/dr-ui/src/develop.rs#L102), [`ui/dr-ui/src/develop.rs:1472`](../ui/dr-ui/src/develop.rs#L1472), [`ui/dr-ui/src/develop.rs:164`](../ui/dr-ui/src/develop.rs#L164), [`ui/dr-ui/src/develop.rs:1950`](../ui/dr-ui/src/develop.rs#L1950), [`ui/dr-ui/src/develop.rs:1968`](../ui/dr-ui/src/develop.rs#L1968), [`ui/dr-ui/src/develop.rs:1982`](../ui/dr-ui/src/develop.rs#L1982), [`ui/dr-ui/src/develop.rs:2004`](../ui/dr-ui/src/develop.rs#L2004), [`ui/dr-ui/src/develop.rs:2150`](../ui/dr-ui/src/develop.rs#L2150), [`ui/dr-ui/src/develop.rs:2248`](../ui/dr-ui/src/develop.rs#L2248), [`ui/dr-ui/src/develop.rs:327`](../ui/dr-ui/src/develop.rs#L327), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/develop.rs:3530`](../ui/dr-ui/src/develop.rs#L3530), [`ui/dr-ui/src/develop.rs:3596`](../ui/dr-ui/src/develop.rs#L3596), [`ui/dr-ui/src/develop.rs:3610`](../ui/dr-ui/src/develop.rs#L3610), [`ui/dr-ui/src/develop.rs:364`](../ui/dr-ui/src/develop.rs#L364), [`ui/dr-ui/src/develop.rs:3802`](../ui/dr-ui/src/develop.rs#L3802), [`ui/dr-ui/src/develop.rs:4462`](../ui/dr-ui/src/develop.rs#L4462), [`ui/dr-ui/src/develop.rs:4516`](../ui/dr-ui/src/develop.rs#L4516), [`ui/dr-ui/src/develop.rs:4560`](../ui/dr-ui/src/develop.rs#L4560), [`ui/dr-ui/src/develop.rs:4610`](../ui/dr-ui/src/develop.rs#L4610), [`ui/dr-ui/src/develop.rs:587`](../ui/dr-ui/src/develop.rs#L587), [`ui/dr-ui/src/develop.rs:648`](../ui/dr-ui/src/develop.rs#L648), [`ui/dr-ui/src/develop.rs:766`](../ui/dr-ui/src/develop.rs#L766), [`ui/dr-ui/src/develop.rs:813`](../ui/dr-ui/src/develop.rs#L813), [`ui/dr-ui/src/develop.rs:842`](../ui/dr-ui/src/develop.rs#L842), [`ui/dr-ui/src/lib.rs:1601`](../ui/dr-ui/src/lib.rs#L1601), [`ui/dr-ui/src/lib.rs:2403`](../ui/dr-ui/src/lib.rs#L2403), [`ui/dr-ui/src/lib.rs:2599`](../ui/dr-ui/src/lib.rs#L2599), [`ui/dr-ui/src/lib.rs:2771`](../ui/dr-ui/src/lib.rs#L2771), [`ui/dr-ui/src/lib.rs:2835`](../ui/dr-ui/src/lib.rs#L2835), [`ui/dr-ui/src/lib.rs:2889`](../ui/dr-ui/src/lib.rs#L2889), [`ui/dr-ui/src/lib.rs:348`](../ui/dr-ui/src/lib.rs#L348), [`ui/dr-ui/src/lib.rs:391`](../ui/dr-ui/src/lib.rs#L391), [`ui/dr-ui/src/lib.rs:414`](../ui/dr-ui/src/lib.rs#L414), [`ui/dr-ui/src/library.rs:539`](../ui/dr-ui/src/library.rs#L539), [`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:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/adjust.slint:517`](../ui/dr-ui/ui/adjust.slint#L517), [`ui/dr-ui/ui/adjust.slint:635`](../ui/dr-ui/ui/adjust.slint#L635), [`ui/dr-ui/ui/app.slint:148`](../ui/dr-ui/ui/app.slint#L148), [`ui/dr-ui/ui/app.slint:1945`](../ui/dr-ui/ui/app.slint#L1945), [`ui/dr-ui/ui/app.slint:194`](../ui/dr-ui/ui/app.slint#L194), [`ui/dr-ui/ui/app.slint:927`](../ui/dr-ui/ui/app.slint#L927), [`ui/dr-ui/ui/masks.slint:524`](../ui/dr-ui/ui/masks.slint#L524) | +| FR-DEV-3a | [`core/dr-pipeline/build.rs:756`](../core/dr-pipeline/build.rs#L756), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:194`](../core/dr-pipeline/src/descriptor.rs#L194), [`core/dr-pipeline/src/descriptor.rs:234`](../core/dr-pipeline/src/descriptor.rs#L234), [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/descriptor.rs:313`](../core/dr-pipeline/src/descriptor.rs#L313), [`core/dr-pipeline/src/framing.rs:385`](../core/dr-pipeline/src/framing.rs#L385), [`core/dr-pipeline/src/graph.rs:24`](../core/dr-pipeline/src/graph.rs#L24), [`core/dr-pipeline/src/graph.rs:251`](../core/dr-pipeline/src/graph.rs#L251), [`core/dr-pipeline/src/graph.rs:46`](../core/dr-pipeline/src/graph.rs#L46), [`core/dr-pipeline/src/graph.rs:59`](../core/dr-pipeline/src/graph.rs#L59), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365), [`core/dr-pipeline/src/ops/curve.rs:319`](../core/dr-pipeline/src/ops/curve.rs#L319), [`ui/dr-ui/src/develop.rs:1369`](../ui/dr-ui/src/develop.rs#L1369), [`ui/dr-ui/src/lib.rs:697`](../ui/dr-ui/src/lib.rs#L697), [`ui/dr-ui/tests/ui_names_no_operation.rs:1`](../ui/dr-ui/tests/ui_names_no_operation.rs#L1) | | FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/framing.rs:385`](../core/dr-pipeline/src/framing.rs#L385), [`core/dr-pipeline/src/graph.rs:59`](../core/dr-pipeline/src/graph.rs#L59), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365) | | FR-DEV-3c | [`core/dr-pipeline/build.rs:756`](../core/dr-pipeline/build.rs#L756), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:251`](../core/dr-pipeline/src/graph.rs#L251), [`core/dr-pipeline/src/graph.rs:46`](../core/dr-pipeline/src/graph.rs#L46), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`ui/dr-ui/src/develop.rs:5189`](../ui/dr-ui/src/develop.rs#L5189) | | FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:1079`](../core/dr-gpu/src/adjust.rs#L1079), [`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:434`](../core/dr-gpu/tests/capture_sharpen.rs#L434), [`core/dr-gpu/tests/detail_stage.rs:242`](../core/dr-gpu/tests/detail_stage.rs#L242), [`core/dr-gpu/tests/local_contrast.rs:558`](../core/dr-gpu/tests/local_contrast.rs#L558), [`core/dr-gpu/tests/noise_reduction.rs:556`](../core/dr-gpu/tests/noise_reduction.rs#L556), [`core/dr-pipeline/src/framing.rs:314`](../core/dr-pipeline/src/framing.rs#L314), [`core/dr-pipeline/src/graph.rs:617`](../core/dr-pipeline/src/graph.rs#L617), [`core/dr-pipeline/src/operation.rs:32`](../core/dr-pipeline/src/operation.rs#L32), [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389), [`core/dr-pipeline/src/operation.rs:53`](../core/dr-pipeline/src/operation.rs#L53), [`core/dr-pipeline/src/operation.rs:71`](../core/dr-pipeline/src/operation.rs#L71) | | 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:1495`](../core/dr-pipeline/src/operation.rs#L1495), [`core/dr-pipeline/src/operation.rs:1576`](../core/dr-pipeline/src/operation.rs#L1576), [`core/dr-pipeline/src/operation.rs:1601`](../core/dr-pipeline/src/operation.rs#L1601), [`core/dr-pipeline/src/operation.rs:1616`](../core/dr-pipeline/src/operation.rs#L1616), [`core/dr-pipeline/src/operation.rs:1640`](../core/dr-pipeline/src/operation.rs#L1640), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/operation.rs:440`](../core/dr-pipeline/src/operation.rs#L440), [`core/dr-pipeline/src/operation.rs:450`](../core/dr-pipeline/src/operation.rs#L450), [`core/dr-pipeline/src/operation.rs:600`](../core/dr-pipeline/src/operation.rs#L600) | -| 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/boolean_grain.rs:1`](../core/dr-film/src/boolean_grain.rs#L1), [`core/dr-film/src/boolean_grain.rs:78`](../core/dr-film/src/boolean_grain.rs#L78), [`core/dr-film/src/grain.rs:140`](../core/dr-film/src/grain.rs#L140), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:302`](../core/dr-film/src/grain.rs#L302), [`core/dr-film/src/grain.rs:79`](../core/dr-film/src/grain.rs#L79), [`core/dr-film/src/lib.rs:160`](../core/dr-film/src/lib.rs#L160), [`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:102`](../core/dr-pipeline/src/graph.rs#L102), [`core/dr-pipeline/src/graph.rs:125`](../core/dr-pipeline/src/graph.rs#L125), [`core/dr-pipeline/src/graph.rs:325`](../core/dr-pipeline/src/graph.rs#L325), [`core/dr-pipeline/src/operation.rs:1065`](../core/dr-pipeline/src/operation.rs#L1065), [`core/dr-pipeline/src/operation.rs:1094`](../core/dr-pipeline/src/operation.rs#L1094), [`core/dr-pipeline/src/operation.rs:1495`](../core/dr-pipeline/src/operation.rs#L1495), [`core/dr-pipeline/src/operation.rs:296`](../core/dr-pipeline/src/operation.rs#L296), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/ops/film_sim.rs:129`](../core/dr-pipeline/src/ops/film_sim.rs#L129), [`core/dr-pipeline/src/ops/film_sim.rs:153`](../core/dr-pipeline/src/ops/film_sim.rs#L153), [`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:331`](../core/dr-pipeline/src/ops/film_sim.rs#L331), [`core/dr-pipeline/src/ops/film_sim.rs:43`](../core/dr-pipeline/src/ops/film_sim.rs#L43), [`core/dr-pipeline/src/ops/film_sim.rs:87`](../core/dr-pipeline/src/ops/film_sim.rs#L87), [`core/dr-pipeline/src/ops/film_sim.rs:92`](../core/dr-pipeline/src/ops/film_sim.rs#L92), [`core/dr-pipeline/src/sidecar.rs:112`](../core/dr-pipeline/src/sidecar.rs#L112), [`core/dr-pipeline/src/sidecar.rs:168`](../core/dr-pipeline/src/sidecar.rs#L168), [`core/dr-pipeline/src/sidecar.rs:1958`](../core/dr-pipeline/src/sidecar.rs#L1958), [`core/dr-pipeline/src/sidecar.rs:2034`](../core/dr-pipeline/src/sidecar.rs#L2034), [`core/dr-pipeline/src/sidecar.rs:534`](../core/dr-pipeline/src/sidecar.rs#L534), [`core/dr-pipeline/src/sidecar.rs:661`](../core/dr-pipeline/src/sidecar.rs#L661), [`core/dr-pipeline/src/sidecar.rs:793`](../core/dr-pipeline/src/sidecar.rs#L793), [`core/dr-pipeline/src/state.rs:100`](../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:115`](../core/dr-pipeline/src/state.rs#L115), [`core/dr-pipeline/src/state.rs:60`](../core/dr-pipeline/src/state.rs#L60), [`ui/dr-ui/src/develop.rs:3244`](../ui/dr-ui/src/develop.rs#L3244), [`ui/dr-ui/src/develop.rs:3261`](../ui/dr-ui/src/develop.rs#L3261), [`ui/dr-ui/src/develop.rs:3273`](../ui/dr-ui/src/develop.rs#L3273), [`ui/dr-ui/src/develop.rs:3311`](../ui/dr-ui/src/develop.rs#L3311), [`ui/dr-ui/src/develop.rs:3320`](../ui/dr-ui/src/develop.rs#L3320), [`ui/dr-ui/src/develop.rs:3423`](../ui/dr-ui/src/develop.rs#L3423), [`ui/dr-ui/src/develop.rs:3839`](../ui/dr-ui/src/develop.rs#L3839), [`ui/dr-ui/src/develop.rs:3854`](../ui/dr-ui/src/develop.rs#L3854), [`ui/dr-ui/src/lib.rs:2368`](../ui/dr-ui/src/lib.rs#L2368), [`ui/dr-ui/src/lib.rs:621`](../ui/dr-ui/src/lib.rs#L621), [`ui/dr-ui/src/lib.rs:679`](../ui/dr-ui/src/lib.rs#L679), [`ui/dr-ui/src/library.rs:530`](../ui/dr-ui/src/library.rs#L530), [`ui/dr-ui/src/library.rs:778`](../ui/dr-ui/src/library.rs#L778), [`ui/dr-ui/src/presets.rs:395`](../ui/dr-ui/src/presets.rs#L395), [`ui/dr-ui/ui/adjust.slint:1034`](../ui/dr-ui/ui/adjust.slint#L1034), [`ui/dr-ui/ui/adjust.slint:1116`](../ui/dr-ui/ui/adjust.slint#L1116), [`ui/dr-ui/ui/app.slint:2481`](../ui/dr-ui/ui/app.slint#L2481), [`ui/dr-ui/ui/app.slint:655`](../ui/dr-ui/ui/app.slint#L655) | +| 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/boolean_grain.rs:1`](../core/dr-film/src/boolean_grain.rs#L1), [`core/dr-film/src/boolean_grain.rs:78`](../core/dr-film/src/boolean_grain.rs#L78), [`core/dr-film/src/grain.rs:140`](../core/dr-film/src/grain.rs#L140), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:302`](../core/dr-film/src/grain.rs#L302), [`core/dr-film/src/grain.rs:79`](../core/dr-film/src/grain.rs#L79), [`core/dr-film/src/lib.rs:160`](../core/dr-film/src/lib.rs#L160), [`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:102`](../core/dr-pipeline/src/graph.rs#L102), [`core/dr-pipeline/src/graph.rs:125`](../core/dr-pipeline/src/graph.rs#L125), [`core/dr-pipeline/src/graph.rs:325`](../core/dr-pipeline/src/graph.rs#L325), [`core/dr-pipeline/src/operation.rs:1065`](../core/dr-pipeline/src/operation.rs#L1065), [`core/dr-pipeline/src/operation.rs:1094`](../core/dr-pipeline/src/operation.rs#L1094), [`core/dr-pipeline/src/operation.rs:1495`](../core/dr-pipeline/src/operation.rs#L1495), [`core/dr-pipeline/src/operation.rs:296`](../core/dr-pipeline/src/operation.rs#L296), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/ops/film_sim.rs:129`](../core/dr-pipeline/src/ops/film_sim.rs#L129), [`core/dr-pipeline/src/ops/film_sim.rs:153`](../core/dr-pipeline/src/ops/film_sim.rs#L153), [`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:331`](../core/dr-pipeline/src/ops/film_sim.rs#L331), [`core/dr-pipeline/src/ops/film_sim.rs:43`](../core/dr-pipeline/src/ops/film_sim.rs#L43), [`core/dr-pipeline/src/ops/film_sim.rs:87`](../core/dr-pipeline/src/ops/film_sim.rs#L87), [`core/dr-pipeline/src/ops/film_sim.rs:92`](../core/dr-pipeline/src/ops/film_sim.rs#L92), [`core/dr-pipeline/src/sidecar.rs:112`](../core/dr-pipeline/src/sidecar.rs#L112), [`core/dr-pipeline/src/sidecar.rs:168`](../core/dr-pipeline/src/sidecar.rs#L168), [`core/dr-pipeline/src/sidecar.rs:1958`](../core/dr-pipeline/src/sidecar.rs#L1958), [`core/dr-pipeline/src/sidecar.rs:2034`](../core/dr-pipeline/src/sidecar.rs#L2034), [`core/dr-pipeline/src/sidecar.rs:534`](../core/dr-pipeline/src/sidecar.rs#L534), [`core/dr-pipeline/src/sidecar.rs:661`](../core/dr-pipeline/src/sidecar.rs#L661), [`core/dr-pipeline/src/sidecar.rs:793`](../core/dr-pipeline/src/sidecar.rs#L793), [`core/dr-pipeline/src/state.rs:100`](../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:115`](../core/dr-pipeline/src/state.rs#L115), [`core/dr-pipeline/src/state.rs:60`](../core/dr-pipeline/src/state.rs#L60), [`ui/dr-ui/src/develop.rs:3244`](../ui/dr-ui/src/develop.rs#L3244), [`ui/dr-ui/src/develop.rs:3261`](../ui/dr-ui/src/develop.rs#L3261), [`ui/dr-ui/src/develop.rs:3273`](../ui/dr-ui/src/develop.rs#L3273), [`ui/dr-ui/src/develop.rs:3311`](../ui/dr-ui/src/develop.rs#L3311), [`ui/dr-ui/src/develop.rs:3320`](../ui/dr-ui/src/develop.rs#L3320), [`ui/dr-ui/src/develop.rs:3423`](../ui/dr-ui/src/develop.rs#L3423), [`ui/dr-ui/src/develop.rs:3839`](../ui/dr-ui/src/develop.rs#L3839), [`ui/dr-ui/src/develop.rs:3854`](../ui/dr-ui/src/develop.rs#L3854), [`ui/dr-ui/src/lib.rs:2377`](../ui/dr-ui/src/lib.rs#L2377), [`ui/dr-ui/src/lib.rs:630`](../ui/dr-ui/src/lib.rs#L630), [`ui/dr-ui/src/lib.rs:688`](../ui/dr-ui/src/lib.rs#L688), [`ui/dr-ui/src/library.rs:530`](../ui/dr-ui/src/library.rs#L530), [`ui/dr-ui/src/library.rs:778`](../ui/dr-ui/src/library.rs#L778), [`ui/dr-ui/src/presets.rs:395`](../ui/dr-ui/src/presets.rs#L395), [`ui/dr-ui/ui/adjust.slint:1034`](../ui/dr-ui/ui/adjust.slint#L1034), [`ui/dr-ui/ui/adjust.slint:1116`](../ui/dr-ui/ui/adjust.slint#L1116), [`ui/dr-ui/ui/app.slint:2481`](../ui/dr-ui/ui/app.slint#L2481), [`ui/dr-ui/ui/app.slint:655`](../ui/dr-ui/ui/app.slint#L655) | | 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:1050`](../core/dr-pipeline/src/framing.rs#L1050), [`core/dr-pipeline/src/framing.rs:328`](../core/dr-pipeline/src/framing.rs#L328), [`core/dr-pipeline/src/framing.rs:488`](../core/dr-pipeline/src/framing.rs#L488), [`core/dr-types/src/lib.rs:337`](../core/dr-types/src/lib.rs#L337), [`core/dr-types/src/lib.rs:445`](../core/dr-types/src/lib.rs#L445), [`core/dr-types/src/lib.rs:457`](../core/dr-types/src/lib.rs#L457), [`core/dr-types/src/lib.rs:473`](../core/dr-types/src/lib.rs#L473), [`ui/dr-ui/src/develop.rs:139`](../ui/dr-ui/src/develop.rs#L139), [`ui/dr-ui/src/develop.rs:2167`](../ui/dr-ui/src/develop.rs#L2167), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) | | FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:351`](../core/dr-gpu/src/lib.rs#L351), [`ui/dr-ui/ui/crop.slint:1`](../ui/dr-ui/ui/crop.slint#L1) | -| FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:346`](../core/dr-pipeline/src/graph.rs#L346), [`core/dr-pipeline/src/graph.rs:385`](../core/dr-pipeline/src/graph.rs#L385), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:3273`](../ui/dr-ui/src/develop.rs#L3273), [`ui/dr-ui/src/develop.rs:3854`](../ui/dr-ui/src/develop.rs#L3854), [`ui/dr-ui/src/develop.rs:3884`](../ui/dr-ui/src/develop.rs#L3884), [`ui/dr-ui/src/develop.rs:3897`](../ui/dr-ui/src/develop.rs#L3897), [`ui/dr-ui/src/develop.rs:3909`](../ui/dr-ui/src/develop.rs#L3909), [`ui/dr-ui/src/develop.rs:3925`](../ui/dr-ui/src/develop.rs#L3925), [`ui/dr-ui/src/develop.rs:3957`](../ui/dr-ui/src/develop.rs#L3957), [`ui/dr-ui/src/develop.rs:3961`](../ui/dr-ui/src/develop.rs#L3961), [`ui/dr-ui/src/develop.rs:3980`](../ui/dr-ui/src/develop.rs#L3980), [`ui/dr-ui/src/develop.rs:3996`](../ui/dr-ui/src/develop.rs#L3996), [`ui/dr-ui/src/develop.rs:709`](../ui/dr-ui/src/develop.rs#L709), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:215`](../ui/dr-ui/src/labels.rs#L215), [`ui/dr-ui/src/lib.rs:1527`](../ui/dr-ui/src/lib.rs#L1527), [`ui/dr-ui/src/lib.rs:1570`](../ui/dr-ui/src/lib.rs#L1570), [`ui/dr-ui/src/lib.rs:1578`](../ui/dr-ui/src/lib.rs#L1578), [`ui/dr-ui/src/lib.rs:2564`](../ui/dr-ui/src/lib.rs#L2564), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | -| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-pipeline/src/preset.rs:381`](../core/dr-pipeline/src/preset.rs#L381), [`core/dr-pipeline/src/preset.rs:414`](../core/dr-pipeline/src/preset.rs#L414), [`core/dr-pipeline/src/starter.rs:1`](../core/dr-pipeline/src/starter.rs#L1), [`core/dr-preset-xmp/src/lib.rs:1`](../core/dr-preset-xmp/src/lib.rs#L1), [`core/dr-types/src/settings.rs:272`](../core/dr-types/src/settings.rs#L272), [`ui/dr-ui/src/develop.rs:3796`](../ui/dr-ui/src/develop.rs#L3796), [`ui/dr-ui/src/develop.rs:3817`](../ui/dr-ui/src/develop.rs#L3817), [`ui/dr-ui/src/lib.rs:1494`](../ui/dr-ui/src/lib.rs#L1494), [`ui/dr-ui/src/lib.rs:2179`](../ui/dr-ui/src/lib.rs#L2179), [`ui/dr-ui/src/lib.rs:2208`](../ui/dr-ui/src/lib.rs#L2208), [`ui/dr-ui/src/library.rs:1903`](../ui/dr-ui/src/library.rs#L1903), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library.rs:520`](../ui/dr-ui/src/library.rs#L520), [`ui/dr-ui/src/library_ui.rs:2696`](../ui/dr-ui/src/library_ui.rs#L2696), [`ui/dr-ui/src/library_ui.rs:3096`](../ui/dr-ui/src/library_ui.rs#L3096), [`ui/dr-ui/src/library_ui.rs:494`](../ui/dr-ui/src/library_ui.rs#L494), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/presets.rs:657`](../ui/dr-ui/src/presets.rs#L657), [`ui/dr-ui/src/presets.rs:691`](../ui/dr-ui/src/presets.rs#L691), [`ui/dr-ui/ui/adjust.slint:709`](../ui/dr-ui/ui/adjust.slint#L709), [`ui/dr-ui/ui/adjust.slint:736`](../ui/dr-ui/ui/adjust.slint#L736), [`ui/dr-ui/ui/adjust.slint:779`](../ui/dr-ui/ui/adjust.slint#L779), [`ui/dr-ui/ui/app.slint:1523`](../ui/dr-ui/ui/app.slint#L1523), [`ui/dr-ui/ui/app.slint:2570`](../ui/dr-ui/ui/app.slint#L2570), [`ui/dr-ui/ui/app.slint:727`](../ui/dr-ui/ui/app.slint#L727), [`ui/dr-ui/ui/app.slint:738`](../ui/dr-ui/ui/app.slint#L738), [`ui/dr-ui/ui/library.slint:1495`](../ui/dr-ui/ui/library.slint#L1495), [`ui/dr-ui/ui/library.slint:1502`](../ui/dr-ui/ui/library.slint#L1502), [`ui/dr-ui/ui/library.slint:850`](../ui/dr-ui/ui/library.slint#L850), [`ui/dr-ui/ui/library.slint:866`](../ui/dr-ui/ui/library.slint#L866), [`ui/dr-ui/ui/library.slint:938`](../ui/dr-ui/ui/library.slint#L938), [`ui/dr-ui/ui/library.slint:953`](../ui/dr-ui/ui/library.slint#L953), [`ui/dr-ui/ui/presets.slint:105`](../ui/dr-ui/ui/presets.slint#L105), [`ui/dr-ui/ui/presets.slint:113`](../ui/dr-ui/ui/presets.slint#L113), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/presets.slint:276`](../ui/dr-ui/ui/presets.slint#L276), [`ui/dr-ui/ui/presets.slint:297`](../ui/dr-ui/ui/presets.slint#L297), [`ui/dr-ui/ui/presets.slint:5`](../ui/dr-ui/ui/presets.slint#L5), [`ui/dr-ui/ui/presets.slint:60`](../ui/dr-ui/ui/presets.slint#L60), [`ui/dr-ui/ui/settings.slint:101`](../ui/dr-ui/ui/settings.slint#L101), [`ui/dr-ui/ui/settings.slint:557`](../ui/dr-ui/ui/settings.slint#L557) | -| FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop.rs:3925`](../ui/dr-ui/src/develop.rs#L3925), [`ui/dr-ui/src/develop.rs:3957`](../ui/dr-ui/src/develop.rs#L3957), [`ui/dr-ui/src/lib.rs:1578`](../ui/dr-ui/src/lib.rs#L1578), [`ui/dr-ui/src/lib.rs:2564`](../ui/dr-ui/src/lib.rs#L2564), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | -| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:405`](../core/dr-gpu/src/detail.rs#L405), [`core/dr-gpu/src/detail.rs:623`](../core/dr-gpu/src/detail.rs#L623), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:410`](../core/dr-pipeline/src/detail.rs#L410), [`core/dr-pipeline/src/detail.rs:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:469`](../core/dr-pipeline/src/detail.rs#L469), [`core/dr-pipeline/src/detail.rs:555`](../core/dr-pipeline/src/detail.rs#L555), [`core/dr-pipeline/src/graph.rs:114`](../core/dr-pipeline/src/graph.rs#L114), [`core/dr-pipeline/src/graph.rs:192`](../core/dr-pipeline/src/graph.rs#L192), [`core/dr-pipeline/src/graph.rs:686`](../core/dr-pipeline/src/graph.rs#L686), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:554`](../core/dr-pipeline/src/operation.rs#L554), [`core/dr-pipeline/src/sidecar.rs:184`](../core/dr-pipeline/src/sidecar.rs#L184), [`core/dr-pipeline/src/sidecar.rs:353`](../core/dr-pipeline/src/sidecar.rs#L353), [`core/dr-pipeline/src/sidecar.rs:673`](../core/dr-pipeline/src/sidecar.rs#L673), [`core/dr-pipeline/src/sidecar.rs:809`](../core/dr-pipeline/src/sidecar.rs#L809), [`core/dr-pipeline/src/sidecar.rs:863`](../core/dr-pipeline/src/sidecar.rs#L863), [`core/dr-pipeline/src/sidecar.rs:893`](../core/dr-pipeline/src/sidecar.rs#L893), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2319`](../ui/dr-ui/src/develop.rs#L2319), [`ui/dr-ui/src/develop.rs:2357`](../ui/dr-ui/src/develop.rs#L2357), [`ui/dr-ui/src/develop.rs:2422`](../ui/dr-ui/src/develop.rs#L2422), [`ui/dr-ui/src/develop.rs:2505`](../ui/dr-ui/src/develop.rs#L2505), [`ui/dr-ui/src/develop.rs:2519`](../ui/dr-ui/src/develop.rs#L2519), [`ui/dr-ui/src/develop.rs:796`](../ui/dr-ui/src/develop.rs#L796), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1599`](../ui/dr-ui/src/lib.rs#L1599), [`ui/dr-ui/src/lib.rs:2682`](../ui/dr-ui/src/lib.rs#L2682), [`ui/dr-ui/src/lib.rs:344`](../ui/dr-ui/src/lib.rs#L344), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/src/spots_ui.rs:1`](../ui/dr-ui/src/spots_ui.rs#L1), [`ui/dr-ui/src/spots_ui.rs:265`](../ui/dr-ui/src/spots_ui.rs#L265), [`ui/dr-ui/ui/adjust.slint:810`](../ui/dr-ui/ui/adjust.slint#L810), [`ui/dr-ui/ui/app.slint:138`](../ui/dr-ui/ui/app.slint#L138), [`ui/dr-ui/ui/app.slint:1848`](../ui/dr-ui/ui/app.slint#L1848), [`ui/dr-ui/ui/app.slint:2023`](../ui/dr-ui/ui/app.slint#L2023), [`ui/dr-ui/ui/app.slint:2387`](../ui/dr-ui/ui/app.slint#L2387), [`ui/dr-ui/ui/spots.slint:180`](../ui/dr-ui/ui/spots.slint#L180), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) | -| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2126`](../core/dr-gpu/src/adjust.rs#L2126), [`core/dr-gpu/src/adjust.rs:2203`](../core/dr-gpu/src/adjust.rs#L2203), [`core/dr-gpu/src/adjust.rs:2288`](../core/dr-gpu/src/adjust.rs#L2288), [`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:172`](../core/dr-gpu/src/lib.rs#L172), [`core/dr-gpu/src/lib.rs:192`](../core/dr-gpu/src/lib.rs#L192), [`core/dr-gpu/src/lib.rs:70`](../core/dr-gpu/src/lib.rs#L70), [`core/dr-gpu/src/lib.rs:93`](../core/dr-gpu/src/lib.rs#L93), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:264`](../core/dr-gpu/tests/local_contrast.rs#L264), [`core/dr-gpu/tests/noise_reduction.rs:378`](../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:524`](../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/graph.rs:548`](../core/dr-pipeline/src/graph.rs#L548), [`core/dr-pipeline/src/graph.rs:578`](../core/dr-pipeline/src/graph.rs#L578), [`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:659`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L659), [`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:971`](../core/dr-pipeline/src/ops/local_contrast.rs#L971), [`core/dr-pipeline/src/ops/noise_reduction.rs:700`](../core/dr-pipeline/src/ops/noise_reduction.rs#L700), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2843`](../ui/dr-ui/src/develop.rs#L2843), [`ui/dr-ui/src/develop.rs:4251`](../ui/dr-ui/src/develop.rs#L4251), [`ui/dr-ui/src/develop.rs:4734`](../ui/dr-ui/src/develop.rs#L4734), [`ui/dr-ui/src/develop.rs:4768`](../ui/dr-ui/src/develop.rs#L4768), [`ui/dr-ui/src/lib.rs:81`](../ui/dr-ui/src/lib.rs#L81), [`ui/dr-ui/src/lib.rs:829`](../ui/dr-ui/src/lib.rs#L829), [`ui/dr-ui/src/lib.rs:888`](../ui/dr-ui/src/lib.rs#L888) | +| FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:346`](../core/dr-pipeline/src/graph.rs#L346), [`core/dr-pipeline/src/graph.rs:385`](../core/dr-pipeline/src/graph.rs#L385), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:3273`](../ui/dr-ui/src/develop.rs#L3273), [`ui/dr-ui/src/develop.rs:3854`](../ui/dr-ui/src/develop.rs#L3854), [`ui/dr-ui/src/develop.rs:3884`](../ui/dr-ui/src/develop.rs#L3884), [`ui/dr-ui/src/develop.rs:3897`](../ui/dr-ui/src/develop.rs#L3897), [`ui/dr-ui/src/develop.rs:3909`](../ui/dr-ui/src/develop.rs#L3909), [`ui/dr-ui/src/develop.rs:3925`](../ui/dr-ui/src/develop.rs#L3925), [`ui/dr-ui/src/develop.rs:3957`](../ui/dr-ui/src/develop.rs#L3957), [`ui/dr-ui/src/develop.rs:3961`](../ui/dr-ui/src/develop.rs#L3961), [`ui/dr-ui/src/develop.rs:3980`](../ui/dr-ui/src/develop.rs#L3980), [`ui/dr-ui/src/develop.rs:3996`](../ui/dr-ui/src/develop.rs#L3996), [`ui/dr-ui/src/develop.rs:709`](../ui/dr-ui/src/develop.rs#L709), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:215`](../ui/dr-ui/src/labels.rs#L215), [`ui/dr-ui/src/lib.rs:1536`](../ui/dr-ui/src/lib.rs#L1536), [`ui/dr-ui/src/lib.rs:1579`](../ui/dr-ui/src/lib.rs#L1579), [`ui/dr-ui/src/lib.rs:1587`](../ui/dr-ui/src/lib.rs#L1587), [`ui/dr-ui/src/lib.rs:2573`](../ui/dr-ui/src/lib.rs#L2573), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | +| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-pipeline/src/preset.rs:381`](../core/dr-pipeline/src/preset.rs#L381), [`core/dr-pipeline/src/preset.rs:414`](../core/dr-pipeline/src/preset.rs#L414), [`core/dr-pipeline/src/starter.rs:1`](../core/dr-pipeline/src/starter.rs#L1), [`core/dr-preset-xmp/src/lib.rs:1`](../core/dr-preset-xmp/src/lib.rs#L1), [`core/dr-types/src/settings.rs:272`](../core/dr-types/src/settings.rs#L272), [`ui/dr-ui/src/develop.rs:3796`](../ui/dr-ui/src/develop.rs#L3796), [`ui/dr-ui/src/develop.rs:3817`](../ui/dr-ui/src/develop.rs#L3817), [`ui/dr-ui/src/lib.rs:1503`](../ui/dr-ui/src/lib.rs#L1503), [`ui/dr-ui/src/lib.rs:2188`](../ui/dr-ui/src/lib.rs#L2188), [`ui/dr-ui/src/lib.rs:2217`](../ui/dr-ui/src/lib.rs#L2217), [`ui/dr-ui/src/library.rs:1903`](../ui/dr-ui/src/library.rs#L1903), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library.rs:520`](../ui/dr-ui/src/library.rs#L520), [`ui/dr-ui/src/library_ui.rs:2696`](../ui/dr-ui/src/library_ui.rs#L2696), [`ui/dr-ui/src/library_ui.rs:3096`](../ui/dr-ui/src/library_ui.rs#L3096), [`ui/dr-ui/src/library_ui.rs:494`](../ui/dr-ui/src/library_ui.rs#L494), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/presets.rs:657`](../ui/dr-ui/src/presets.rs#L657), [`ui/dr-ui/src/presets.rs:691`](../ui/dr-ui/src/presets.rs#L691), [`ui/dr-ui/ui/adjust.slint:709`](../ui/dr-ui/ui/adjust.slint#L709), [`ui/dr-ui/ui/adjust.slint:736`](../ui/dr-ui/ui/adjust.slint#L736), [`ui/dr-ui/ui/adjust.slint:779`](../ui/dr-ui/ui/adjust.slint#L779), [`ui/dr-ui/ui/app.slint:1523`](../ui/dr-ui/ui/app.slint#L1523), [`ui/dr-ui/ui/app.slint:2570`](../ui/dr-ui/ui/app.slint#L2570), [`ui/dr-ui/ui/app.slint:727`](../ui/dr-ui/ui/app.slint#L727), [`ui/dr-ui/ui/app.slint:738`](../ui/dr-ui/ui/app.slint#L738), [`ui/dr-ui/ui/library.slint:1495`](../ui/dr-ui/ui/library.slint#L1495), [`ui/dr-ui/ui/library.slint:1502`](../ui/dr-ui/ui/library.slint#L1502), [`ui/dr-ui/ui/library.slint:850`](../ui/dr-ui/ui/library.slint#L850), [`ui/dr-ui/ui/library.slint:866`](../ui/dr-ui/ui/library.slint#L866), [`ui/dr-ui/ui/library.slint:938`](../ui/dr-ui/ui/library.slint#L938), [`ui/dr-ui/ui/library.slint:953`](../ui/dr-ui/ui/library.slint#L953), [`ui/dr-ui/ui/presets.slint:105`](../ui/dr-ui/ui/presets.slint#L105), [`ui/dr-ui/ui/presets.slint:113`](../ui/dr-ui/ui/presets.slint#L113), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/presets.slint:276`](../ui/dr-ui/ui/presets.slint#L276), [`ui/dr-ui/ui/presets.slint:297`](../ui/dr-ui/ui/presets.slint#L297), [`ui/dr-ui/ui/presets.slint:5`](../ui/dr-ui/ui/presets.slint#L5), [`ui/dr-ui/ui/presets.slint:60`](../ui/dr-ui/ui/presets.slint#L60), [`ui/dr-ui/ui/settings.slint:101`](../ui/dr-ui/ui/settings.slint#L101), [`ui/dr-ui/ui/settings.slint:557`](../ui/dr-ui/ui/settings.slint#L557) | +| FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop.rs:3925`](../ui/dr-ui/src/develop.rs#L3925), [`ui/dr-ui/src/develop.rs:3957`](../ui/dr-ui/src/develop.rs#L3957), [`ui/dr-ui/src/lib.rs:1587`](../ui/dr-ui/src/lib.rs#L1587), [`ui/dr-ui/src/lib.rs:2573`](../ui/dr-ui/src/lib.rs#L2573), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | +| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:405`](../core/dr-gpu/src/detail.rs#L405), [`core/dr-gpu/src/detail.rs:623`](../core/dr-gpu/src/detail.rs#L623), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:410`](../core/dr-pipeline/src/detail.rs#L410), [`core/dr-pipeline/src/detail.rs:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:469`](../core/dr-pipeline/src/detail.rs#L469), [`core/dr-pipeline/src/detail.rs:555`](../core/dr-pipeline/src/detail.rs#L555), [`core/dr-pipeline/src/graph.rs:114`](../core/dr-pipeline/src/graph.rs#L114), [`core/dr-pipeline/src/graph.rs:192`](../core/dr-pipeline/src/graph.rs#L192), [`core/dr-pipeline/src/graph.rs:686`](../core/dr-pipeline/src/graph.rs#L686), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:554`](../core/dr-pipeline/src/operation.rs#L554), [`core/dr-pipeline/src/sidecar.rs:184`](../core/dr-pipeline/src/sidecar.rs#L184), [`core/dr-pipeline/src/sidecar.rs:353`](../core/dr-pipeline/src/sidecar.rs#L353), [`core/dr-pipeline/src/sidecar.rs:673`](../core/dr-pipeline/src/sidecar.rs#L673), [`core/dr-pipeline/src/sidecar.rs:809`](../core/dr-pipeline/src/sidecar.rs#L809), [`core/dr-pipeline/src/sidecar.rs:863`](../core/dr-pipeline/src/sidecar.rs#L863), [`core/dr-pipeline/src/sidecar.rs:893`](../core/dr-pipeline/src/sidecar.rs#L893), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2319`](../ui/dr-ui/src/develop.rs#L2319), [`ui/dr-ui/src/develop.rs:2357`](../ui/dr-ui/src/develop.rs#L2357), [`ui/dr-ui/src/develop.rs:2422`](../ui/dr-ui/src/develop.rs#L2422), [`ui/dr-ui/src/develop.rs:2505`](../ui/dr-ui/src/develop.rs#L2505), [`ui/dr-ui/src/develop.rs:2519`](../ui/dr-ui/src/develop.rs#L2519), [`ui/dr-ui/src/develop.rs:796`](../ui/dr-ui/src/develop.rs#L796), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1608`](../ui/dr-ui/src/lib.rs#L1608), [`ui/dr-ui/src/lib.rs:2691`](../ui/dr-ui/src/lib.rs#L2691), [`ui/dr-ui/src/lib.rs:353`](../ui/dr-ui/src/lib.rs#L353), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/src/spots_ui.rs:1`](../ui/dr-ui/src/spots_ui.rs#L1), [`ui/dr-ui/src/spots_ui.rs:265`](../ui/dr-ui/src/spots_ui.rs#L265), [`ui/dr-ui/ui/adjust.slint:810`](../ui/dr-ui/ui/adjust.slint#L810), [`ui/dr-ui/ui/app.slint:138`](../ui/dr-ui/ui/app.slint#L138), [`ui/dr-ui/ui/app.slint:1848`](../ui/dr-ui/ui/app.slint#L1848), [`ui/dr-ui/ui/app.slint:2023`](../ui/dr-ui/ui/app.slint#L2023), [`ui/dr-ui/ui/app.slint:2387`](../ui/dr-ui/ui/app.slint#L2387), [`ui/dr-ui/ui/spots.slint:180`](../ui/dr-ui/ui/spots.slint#L180), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) | +| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2126`](../core/dr-gpu/src/adjust.rs#L2126), [`core/dr-gpu/src/adjust.rs:2203`](../core/dr-gpu/src/adjust.rs#L2203), [`core/dr-gpu/src/adjust.rs:2288`](../core/dr-gpu/src/adjust.rs#L2288), [`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:172`](../core/dr-gpu/src/lib.rs#L172), [`core/dr-gpu/src/lib.rs:192`](../core/dr-gpu/src/lib.rs#L192), [`core/dr-gpu/src/lib.rs:70`](../core/dr-gpu/src/lib.rs#L70), [`core/dr-gpu/src/lib.rs:93`](../core/dr-gpu/src/lib.rs#L93), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:264`](../core/dr-gpu/tests/local_contrast.rs#L264), [`core/dr-gpu/tests/noise_reduction.rs:378`](../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:524`](../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/graph.rs:548`](../core/dr-pipeline/src/graph.rs#L548), [`core/dr-pipeline/src/graph.rs:578`](../core/dr-pipeline/src/graph.rs#L578), [`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:659`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L659), [`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:971`](../core/dr-pipeline/src/ops/local_contrast.rs#L971), [`core/dr-pipeline/src/ops/noise_reduction.rs:700`](../core/dr-pipeline/src/ops/noise_reduction.rs#L700), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2843`](../ui/dr-ui/src/develop.rs#L2843), [`ui/dr-ui/src/develop.rs:4251`](../ui/dr-ui/src/develop.rs#L4251), [`ui/dr-ui/src/develop.rs:4734`](../ui/dr-ui/src/develop.rs#L4734), [`ui/dr-ui/src/develop.rs:4768`](../ui/dr-ui/src/develop.rs#L4768), [`ui/dr-ui/src/lib.rs:838`](../ui/dr-ui/src/lib.rs#L838), [`ui/dr-ui/src/lib.rs:897`](../ui/dr-ui/src/lib.rs#L897), [`ui/dr-ui/src/lib.rs:90`](../ui/dr-ui/src/lib.rs#L90) | | FR-DSP-3 | [`core/dr-gpu/tests/frame_budget.rs:101`](../core/dr-gpu/tests/frame_budget.rs#L101), [`core/dr-gpu/tests/local_contrast.rs:329`](../core/dr-gpu/tests/local_contrast.rs#L329), [`core/dr-pipeline/src/ops/local_contrast.rs:447`](../core/dr-pipeline/src/ops/local_contrast.rs#L447) | | FR-DSP-5 | [`core/dr-gpu/tests/frame_budget.rs:101`](../core/dr-gpu/tests/frame_budget.rs#L101), [`core/dr-gpu/tests/zoom_resolution.rs:135`](../core/dr-gpu/tests/zoom_resolution.rs#L135), [`core/dr-gpu/tests/zoom_resolution.rs:166`](../core/dr-gpu/tests/zoom_resolution.rs#L166), [`core/dr-gpu/tests/zoom_resolution.rs:1`](../core/dr-gpu/tests/zoom_resolution.rs#L1), [`core/dr-gpu/tests/zoom_resolution.rs:216`](../core/dr-gpu/tests/zoom_resolution.rs#L216) | -| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:483`](../core/dr-pipeline/src/operation.rs#L483), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`ui/dr-ui/src/develop.rs:2873`](../ui/dr-ui/src/develop.rs#L2873), [`ui/dr-ui/src/develop.rs:6162`](../ui/dr-ui/src/develop.rs#L6162), [`ui/dr-ui/src/lib.rs:1555`](../ui/dr-ui/src/lib.rs#L1555), [`ui/dr-ui/src/lib.rs:3065`](../ui/dr-ui/src/lib.rs#L3065) | -| 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:2922`](../ui/dr-ui/src/develop.rs#L2922), [`ui/dr-ui/src/develop.rs:5836`](../ui/dr-ui/src/develop.rs#L5836), [`ui/dr-ui/src/develop.rs:5868`](../ui/dr-ui/src/develop.rs#L5868), [`ui/dr-ui/src/develop.rs:720`](../ui/dr-ui/src/develop.rs#L720), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1665`](../ui/dr-ui/src/lib.rs#L1665), [`ui/dr-ui/src/lib.rs:323`](../ui/dr-ui/src/lib.rs#L323), [`ui/dr-ui/ui/app.slint:72`](../ui/dr-ui/ui/app.slint#L72), [`ui/dr-ui/ui/histogram.slint:163`](../ui/dr-ui/ui/histogram.slint#L163), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | -| FR-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:2819`](../ui/dr-ui/src/develop.rs#L2819), [`ui/dr-ui/src/develop.rs:2873`](../ui/dr-ui/src/develop.rs#L2873), [`ui/dr-ui/src/develop.rs:6162`](../ui/dr-ui/src/develop.rs#L6162), [`ui/dr-ui/src/develop.rs:6208`](../ui/dr-ui/src/develop.rs#L6208), [`ui/dr-ui/src/develop.rs:6227`](../ui/dr-ui/src/develop.rs#L6227), [`ui/dr-ui/src/develop.rs:827`](../ui/dr-ui/src/develop.rs#L827), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1516`](../ui/dr-ui/src/lib.rs#L1516), [`ui/dr-ui/src/lib.rs:1555`](../ui/dr-ui/src/lib.rs#L1555), [`ui/dr-ui/src/lib.rs:2978`](../ui/dr-ui/src/lib.rs#L2978), [`ui/dr-ui/src/lib.rs:3065`](../ui/dr-ui/src/lib.rs#L3065), [`ui/dr-ui/ui/app.slint:1696`](../ui/dr-ui/ui/app.slint#L1696), [`ui/dr-ui/ui/app.slint:51`](../ui/dr-ui/ui/app.slint#L51), [`ui/dr-ui/ui/settings.slint:124`](../ui/dr-ui/ui/settings.slint#L124), [`ui/dr-ui/ui/settings.slint:794`](../ui/dr-ui/ui/settings.slint#L794) | +| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:483`](../core/dr-pipeline/src/operation.rs#L483), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`ui/dr-ui/src/develop.rs:2873`](../ui/dr-ui/src/develop.rs#L2873), [`ui/dr-ui/src/develop.rs:6162`](../ui/dr-ui/src/develop.rs#L6162), [`ui/dr-ui/src/lib.rs:1564`](../ui/dr-ui/src/lib.rs#L1564), [`ui/dr-ui/src/lib.rs:3074`](../ui/dr-ui/src/lib.rs#L3074) | +| 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:2922`](../ui/dr-ui/src/develop.rs#L2922), [`ui/dr-ui/src/develop.rs:5836`](../ui/dr-ui/src/develop.rs#L5836), [`ui/dr-ui/src/develop.rs:5868`](../ui/dr-ui/src/develop.rs#L5868), [`ui/dr-ui/src/develop.rs:720`](../ui/dr-ui/src/develop.rs#L720), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1674`](../ui/dr-ui/src/lib.rs#L1674), [`ui/dr-ui/src/lib.rs:332`](../ui/dr-ui/src/lib.rs#L332), [`ui/dr-ui/ui/app.slint:72`](../ui/dr-ui/ui/app.slint#L72), [`ui/dr-ui/ui/histogram.slint:163`](../ui/dr-ui/ui/histogram.slint#L163), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | +| FR-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:2819`](../ui/dr-ui/src/develop.rs#L2819), [`ui/dr-ui/src/develop.rs:2873`](../ui/dr-ui/src/develop.rs#L2873), [`ui/dr-ui/src/develop.rs:6162`](../ui/dr-ui/src/develop.rs#L6162), [`ui/dr-ui/src/develop.rs:6208`](../ui/dr-ui/src/develop.rs#L6208), [`ui/dr-ui/src/develop.rs:6227`](../ui/dr-ui/src/develop.rs#L6227), [`ui/dr-ui/src/develop.rs:827`](../ui/dr-ui/src/develop.rs#L827), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1525`](../ui/dr-ui/src/lib.rs#L1525), [`ui/dr-ui/src/lib.rs:1564`](../ui/dr-ui/src/lib.rs#L1564), [`ui/dr-ui/src/lib.rs:2987`](../ui/dr-ui/src/lib.rs#L2987), [`ui/dr-ui/src/lib.rs:3074`](../ui/dr-ui/src/lib.rs#L3074), [`ui/dr-ui/ui/app.slint:1696`](../ui/dr-ui/ui/app.slint#L1696), [`ui/dr-ui/ui/app.slint:51`](../ui/dr-ui/ui/app.slint#L51), [`ui/dr-ui/ui/settings.slint:124`](../ui/dr-ui/ui/settings.slint#L124), [`ui/dr-ui/ui/settings.slint:794`](../ui/dr-ui/ui/settings.slint#L794) | | 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:2436`](../core/dr-gpu/src/adjust.rs#L2436), [`core/dr-pipeline/src/graph.rs:538`](../core/dr-pipeline/src/graph.rs#L538), [`core/dr-pipeline/src/graph.rs:592`](../core/dr-pipeline/src/graph.rs#L592), [`core/dr-pipeline/src/operation.rs:483`](../core/dr-pipeline/src/operation.rs#L483), [`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:713`](../core/dr-types/src/settings.rs#L713), [`ui/dr-ui/src/develop.rs:6227`](../ui/dr-ui/src/develop.rs#L6227), [`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:182`](../core/dr-export/src/lib.rs#L182), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/size.rs:136`](../core/dr-export/src/size.rs#L136), [`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-export/src/size.rs:84`](../core/dr-export/src/size.rs#L84), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:758`](../core/dr-types/src/settings.rs#L758), [`core/dr-types/src/settings.rs:766`](../core/dr-types/src/settings.rs#L766), [`core/dr-types/src/settings.rs:782`](../core/dr-types/src/settings.rs#L782), [`core/dr-types/src/settings.rs:851`](../core/dr-types/src/settings.rs#L851), [`core/dr-types/src/settings.rs:879`](../core/dr-types/src/settings.rs#L879), [`core/dr-types/src/settings.rs:890`](../core/dr-types/src/settings.rs#L890), [`core/dr-types/src/settings.rs:896`](../core/dr-types/src/settings.rs#L896), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:207`](../ui/dr-ui/src/settings_ui.rs#L207), [`ui/dr-ui/src/settings_ui.rs:220`](../ui/dr-ui/src/settings_ui.rs#L220), [`ui/dr-ui/src/settings_ui.rs:235`](../ui/dr-ui/src/settings_ui.rs#L235), [`ui/dr-ui/src/settings_ui.rs:565`](../ui/dr-ui/src/settings_ui.rs#L565), [`ui/dr-ui/ui/settings.slint:697`](../ui/dr-ui/ui/settings.slint#L697), [`ui/dr-ui/ui/settings.slint:707`](../ui/dr-ui/ui/settings.slint#L707) | | 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-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:434`](../ui/dr-ui/src/lib.rs#L434), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:49`](../ui/dr-ui/src/settings_ui.rs#L49), [`ui/dr-ui/src/settings_ui.rs:679`](../ui/dr-ui/src/settings_ui.rs#L679) | -| 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:942`](../ui/dr-ui/src/export.rs#L942), [`ui/dr-ui/src/lib.rs:213`](../ui/dr-ui/src/lib.rs#L213), [`ui/dr-ui/src/lib.rs:2310`](../ui/dr-ui/src/lib.rs#L2310), [`ui/dr-ui/src/lib.rs:434`](../ui/dr-ui/src/lib.rs#L434), [`ui/dr-ui/src/lib.rs:469`](../ui/dr-ui/src/lib.rs#L469), [`ui/dr-ui/src/lib.rs:496`](../ui/dr-ui/src/lib.rs#L496), [`ui/dr-ui/src/library_ui.rs:3479`](../ui/dr-ui/src/library_ui.rs#L3479), [`ui/dr-ui/src/library_ui.rs:585`](../ui/dr-ui/src/library_ui.rs#L585), [`ui/dr-ui/src/library_ui.rs:6658`](../ui/dr-ui/src/library_ui.rs#L6658), [`ui/dr-ui/src/library_ui.rs:6735`](../ui/dr-ui/src/library_ui.rs#L6735), [`ui/dr-ui/src/library_ui.rs:6747`](../ui/dr-ui/src/library_ui.rs#L6747), [`ui/dr-ui/src/library_ui.rs:685`](../ui/dr-ui/src/library_ui.rs#L685), [`ui/dr-ui/src/library_ui.rs:742`](../ui/dr-ui/src/library_ui.rs#L742), [`ui/dr-ui/ui/app.slint:1063`](../ui/dr-ui/ui/app.slint#L1063), [`ui/dr-ui/ui/app.slint:1533`](../ui/dr-ui/ui/app.slint#L1533), [`ui/dr-ui/ui/library.slint:1505`](../ui/dr-ui/ui/library.slint#L1505), [`ui/dr-ui/ui/library.slint:854`](../ui/dr-ui/ui/library.slint#L854), [`ui/dr-ui/ui/library.slint:967`](../ui/dr-ui/ui/library.slint#L967) | +| 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:443`](../ui/dr-ui/src/lib.rs#L443), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:49`](../ui/dr-ui/src/settings_ui.rs#L49), [`ui/dr-ui/src/settings_ui.rs:679`](../ui/dr-ui/src/settings_ui.rs#L679) | +| 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:942`](../ui/dr-ui/src/export.rs#L942), [`ui/dr-ui/src/lib.rs:222`](../ui/dr-ui/src/lib.rs#L222), [`ui/dr-ui/src/lib.rs:2319`](../ui/dr-ui/src/lib.rs#L2319), [`ui/dr-ui/src/lib.rs:443`](../ui/dr-ui/src/lib.rs#L443), [`ui/dr-ui/src/lib.rs:478`](../ui/dr-ui/src/lib.rs#L478), [`ui/dr-ui/src/lib.rs:505`](../ui/dr-ui/src/lib.rs#L505), [`ui/dr-ui/src/library_ui.rs:3479`](../ui/dr-ui/src/library_ui.rs#L3479), [`ui/dr-ui/src/library_ui.rs:585`](../ui/dr-ui/src/library_ui.rs#L585), [`ui/dr-ui/src/library_ui.rs:6658`](../ui/dr-ui/src/library_ui.rs#L6658), [`ui/dr-ui/src/library_ui.rs:6735`](../ui/dr-ui/src/library_ui.rs#L6735), [`ui/dr-ui/src/library_ui.rs:6747`](../ui/dr-ui/src/library_ui.rs#L6747), [`ui/dr-ui/src/library_ui.rs:685`](../ui/dr-ui/src/library_ui.rs#L685), [`ui/dr-ui/src/library_ui.rs:742`](../ui/dr-ui/src/library_ui.rs#L742), [`ui/dr-ui/ui/app.slint:1063`](../ui/dr-ui/ui/app.slint#L1063), [`ui/dr-ui/ui/app.slint:1533`](../ui/dr-ui/ui/app.slint#L1533), [`ui/dr-ui/ui/library.slint:1505`](../ui/dr-ui/ui/library.slint#L1505), [`ui/dr-ui/ui/library.slint:854`](../ui/dr-ui/ui/library.slint#L854), [`ui/dr-ui/ui/library.slint:967`](../ui/dr-ui/ui/library.slint#L967) | | 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:653`](../core/dr-types/src/lib.rs#L653), [`core/dr-types/src/settings.rs:426`](../core/dr-types/src/settings.rs#L426), [`ui/dr-ui/src/export.rs:619`](../ui/dr-ui/src/export.rs#L619), [`ui/dr-ui/src/export.rs:647`](../ui/dr-ui/src/export.rs#L647), [`ui/dr-ui/src/export.rs:777`](../ui/dr-ui/src/export.rs#L777), [`ui/dr-ui/src/export.rs:794`](../ui/dr-ui/src/export.rs#L794), [`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:1106`](../core/dr-gpu/src/adjust.rs#L1106), [`ui/dr-ui/src/develop.rs:3188`](../ui/dr-ui/src/develop.rs#L3188), [`ui/dr-ui/src/lib.rs:434`](../ui/dr-ui/src/lib.rs#L434) | +| 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:1106`](../core/dr-gpu/src/adjust.rs#L1106), [`ui/dr-ui/src/develop.rs:3188`](../ui/dr-ui/src/develop.rs#L3188), [`ui/dr-ui/src/lib.rs:443`](../ui/dr-ui/src/lib.rs#L443) | | 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/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:355`](../core/dr-sync/src/account.rs#L355), [`ui/dr-ui/src/launch.rs:277`](../ui/dr-ui/src/launch.rs#L277), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:417`](../ui/dr-ui/src/launch_ui.rs#L417) | -| FR-NC-10 | [`core/dr-sync/src/account.rs:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:496`](../ui/dr-ui/src/lib.rs#L496), [`ui/dr-ui/src/library.rs:1101`](../ui/dr-ui/src/library.rs#L1101), [`ui/dr-ui/src/library.rs:1939`](../ui/dr-ui/src/library.rs#L1939), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877), [`ui/dr-ui/src/library_ui.rs:1696`](../ui/dr-ui/src/library_ui.rs#L1696), [`ui/dr-ui/src/library_ui.rs:3479`](../ui/dr-ui/src/library_ui.rs#L3479), [`ui/dr-ui/src/library_ui.rs:526`](../ui/dr-ui/src/library_ui.rs#L526), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-NC-10 | [`core/dr-sync/src/account.rs:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:505`](../ui/dr-ui/src/lib.rs#L505), [`ui/dr-ui/src/library.rs:1101`](../ui/dr-ui/src/library.rs#L1101), [`ui/dr-ui/src/library.rs:1939`](../ui/dr-ui/src/library.rs#L1939), [`ui/dr-ui/src/library.rs:576`](../ui/dr-ui/src/library.rs#L576), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877), [`ui/dr-ui/src/library_ui.rs:1696`](../ui/dr-ui/src/library_ui.rs#L1696), [`ui/dr-ui/src/library_ui.rs:3479`](../ui/dr-ui/src/library_ui.rs#L3479), [`ui/dr-ui/src/library_ui.rs:526`](../ui/dr-ui/src/library_ui.rs#L526), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-NC-12 | [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:40`](../core/dr-sync-nextcloud/src/lib.rs#L40), [`core/dr-sync-nextcloud/src/lib.rs:914`](../core/dr-sync-nextcloud/src/lib.rs#L914), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:81`](../core/dr-sync/src/account.rs#L81), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/lib.rs:51`](../core/dr-sync/src/lib.rs#L51), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`core/dr-sync/src/provider.rs:1`](../core/dr-sync/src/provider.rs#L1), [`core/dr-sync/src/provider.rs:53`](../core/dr-sync/src/provider.rs#L53), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | | FR-NC-13 | [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`ui/dr-ui/src/launch_ui.rs:316`](../ui/dr-ui/src/launch_ui.rs#L316), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | | FR-NC-2 | [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:298`](../core/dr-sync/src/account.rs#L298), [`core/dr-sync/src/account.rs:355`](../core/dr-sync/src/account.rs#L355), [`core/dr-sync/src/account.rs:59`](../core/dr-sync/src/account.rs#L59), [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) | @@ -97,45 +97,45 @@ _None._ | FR-NC-4 | [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-nextcloud/src/propfind.rs:103`](../core/dr-sync-nextcloud/src/propfind.rs#L103), [`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:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61) | | 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:488`](../ui/dr-ui/src/import.rs#L488) | | 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/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1093`](../core/dr-catalog/src/schema.rs#L1093), [`core/dr-catalog/src/schema.rs:692`](../core/dr-catalog/src/schema.rs#L692), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`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:3255`](../ui/dr-ui/src/collections_ui.rs#L3255), [`ui/dr-ui/src/collections_ui.rs:599`](../ui/dr-ui/src/collections_ui.rs#L599), [`ui/dr-ui/src/collections_ui.rs:667`](../ui/dr-ui/src/collections_ui.rs#L667), [`ui/dr-ui/src/lib.rs:2014`](../ui/dr-ui/src/lib.rs#L2014), [`ui/dr-ui/src/lib.rs:3128`](../ui/dr-ui/src/lib.rs#L3128), [`ui/dr-ui/src/library.rs:1565`](../ui/dr-ui/src/library.rs#L1565), [`ui/dr-ui/src/library.rs:1588`](../ui/dr-ui/src/library.rs#L1588), [`ui/dr-ui/src/library.rs:1863`](../ui/dr-ui/src/library.rs#L1863), [`ui/dr-ui/src/library_ui.rs:1121`](../ui/dr-ui/src/library_ui.rs#L1121), [`ui/dr-ui/src/library_ui.rs:1194`](../ui/dr-ui/src/library_ui.rs#L1194), [`ui/dr-ui/src/library_ui.rs:1295`](../ui/dr-ui/src/library_ui.rs#L1295), [`ui/dr-ui/src/library_ui.rs:1350`](../ui/dr-ui/src/library_ui.rs#L1350), [`ui/dr-ui/src/library_ui.rs:1485`](../ui/dr-ui/src/library_ui.rs#L1485), [`ui/dr-ui/src/library_ui.rs:1603`](../ui/dr-ui/src/library_ui.rs#L1603), [`ui/dr-ui/src/library_ui.rs:2171`](../ui/dr-ui/src/library_ui.rs#L2171), [`ui/dr-ui/src/library_ui.rs:289`](../ui/dr-ui/src/library_ui.rs#L289), [`ui/dr-ui/src/library_ui.rs:300`](../ui/dr-ui/src/library_ui.rs#L300), [`ui/dr-ui/src/library_ui.rs:308`](../ui/dr-ui/src/library_ui.rs#L308), [`ui/dr-ui/src/library_ui.rs:320`](../ui/dr-ui/src/library_ui.rs#L320), [`ui/dr-ui/src/library_ui.rs:329`](../ui/dr-ui/src/library_ui.rs#L329), [`ui/dr-ui/src/library_ui.rs:422`](../ui/dr-ui/src/library_ui.rs#L422), [`ui/dr-ui/src/library_ui.rs:432`](../ui/dr-ui/src/library_ui.rs#L432), [`ui/dr-ui/src/library_ui.rs:479`](../ui/dr-ui/src/library_ui.rs#L479), [`ui/dr-ui/src/library_ui.rs:542`](../ui/dr-ui/src/library_ui.rs#L542), [`ui/dr-ui/src/library_ui.rs:5531`](../ui/dr-ui/src/library_ui.rs#L5531), [`ui/dr-ui/src/library_ui.rs:5549`](../ui/dr-ui/src/library_ui.rs#L5549), [`ui/dr-ui/src/library_ui.rs:5561`](../ui/dr-ui/src/library_ui.rs#L5561), [`ui/dr-ui/src/library_ui.rs:573`](../ui/dr-ui/src/library_ui.rs#L573), [`ui/dr-ui/src/library_ui.rs:585`](../ui/dr-ui/src/library_ui.rs#L585), [`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:2560`](../ui/dr-ui/ui/app.slint#L2560), [`ui/dr-ui/ui/app.slint:510`](../ui/dr-ui/ui/app.slint#L510), [`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:1013`](../ui/dr-ui/ui/library.slint#L1013) | +| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1093`](../core/dr-catalog/src/schema.rs#L1093), [`core/dr-catalog/src/schema.rs:692`](../core/dr-catalog/src/schema.rs#L692), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`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:3255`](../ui/dr-ui/src/collections_ui.rs#L3255), [`ui/dr-ui/src/collections_ui.rs:599`](../ui/dr-ui/src/collections_ui.rs#L599), [`ui/dr-ui/src/collections_ui.rs:667`](../ui/dr-ui/src/collections_ui.rs#L667), [`ui/dr-ui/src/lib.rs:2023`](../ui/dr-ui/src/lib.rs#L2023), [`ui/dr-ui/src/lib.rs:3137`](../ui/dr-ui/src/lib.rs#L3137), [`ui/dr-ui/src/library.rs:1565`](../ui/dr-ui/src/library.rs#L1565), [`ui/dr-ui/src/library.rs:1588`](../ui/dr-ui/src/library.rs#L1588), [`ui/dr-ui/src/library.rs:1863`](../ui/dr-ui/src/library.rs#L1863), [`ui/dr-ui/src/library_ui.rs:1121`](../ui/dr-ui/src/library_ui.rs#L1121), [`ui/dr-ui/src/library_ui.rs:1194`](../ui/dr-ui/src/library_ui.rs#L1194), [`ui/dr-ui/src/library_ui.rs:1295`](../ui/dr-ui/src/library_ui.rs#L1295), [`ui/dr-ui/src/library_ui.rs:1350`](../ui/dr-ui/src/library_ui.rs#L1350), [`ui/dr-ui/src/library_ui.rs:1485`](../ui/dr-ui/src/library_ui.rs#L1485), [`ui/dr-ui/src/library_ui.rs:1603`](../ui/dr-ui/src/library_ui.rs#L1603), [`ui/dr-ui/src/library_ui.rs:2171`](../ui/dr-ui/src/library_ui.rs#L2171), [`ui/dr-ui/src/library_ui.rs:289`](../ui/dr-ui/src/library_ui.rs#L289), [`ui/dr-ui/src/library_ui.rs:300`](../ui/dr-ui/src/library_ui.rs#L300), [`ui/dr-ui/src/library_ui.rs:308`](../ui/dr-ui/src/library_ui.rs#L308), [`ui/dr-ui/src/library_ui.rs:320`](../ui/dr-ui/src/library_ui.rs#L320), [`ui/dr-ui/src/library_ui.rs:329`](../ui/dr-ui/src/library_ui.rs#L329), [`ui/dr-ui/src/library_ui.rs:422`](../ui/dr-ui/src/library_ui.rs#L422), [`ui/dr-ui/src/library_ui.rs:432`](../ui/dr-ui/src/library_ui.rs#L432), [`ui/dr-ui/src/library_ui.rs:479`](../ui/dr-ui/src/library_ui.rs#L479), [`ui/dr-ui/src/library_ui.rs:542`](../ui/dr-ui/src/library_ui.rs#L542), [`ui/dr-ui/src/library_ui.rs:5531`](../ui/dr-ui/src/library_ui.rs#L5531), [`ui/dr-ui/src/library_ui.rs:5549`](../ui/dr-ui/src/library_ui.rs#L5549), [`ui/dr-ui/src/library_ui.rs:5561`](../ui/dr-ui/src/library_ui.rs#L5561), [`ui/dr-ui/src/library_ui.rs:573`](../ui/dr-ui/src/library_ui.rs#L573), [`ui/dr-ui/src/library_ui.rs:585`](../ui/dr-ui/src/library_ui.rs#L585), [`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:2560`](../ui/dr-ui/ui/app.slint#L2560), [`ui/dr-ui/ui/app.slint:510`](../ui/dr-ui/ui/app.slint#L510), [`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:1013`](../ui/dr-ui/ui/library.slint#L1013) | | FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1350`](../ui/dr-ui/src/library_ui.rs#L1350), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | | FR-NC-6c | [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync-folder/src/lib.rs:818`](../core/dr-sync-folder/src/lib.rs#L818), [`core/dr-sync-folder/src/lib.rs:857`](../core/dr-sync-folder/src/lib.rs#L857), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1), [`core/dr-sync-nextcloud/src/desktop_client.rs:167`](../core/dr-sync-nextcloud/src/desktop_client.rs#L167), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-sync/src/error.rs:39`](../core/dr-sync/src/error.rs#L39), [`core/dr-sync/src/lib.rs:158`](../core/dr-sync/src/lib.rs#L158), [`core/dr-sync/src/types.rs:101`](../core/dr-sync/src/types.rs#L101), [`core/dr-types/src/lib.rs:120`](../core/dr-types/src/lib.rs#L120), [`core/dr-types/src/lib.rs:202`](../core/dr-types/src/lib.rs#L202), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3255`](../ui/dr-ui/src/collections_ui.rs#L3255), [`ui/dr-ui/src/collections_ui.rs:599`](../ui/dr-ui/src/collections_ui.rs#L599), [`ui/dr-ui/src/collections_ui.rs:667`](../ui/dr-ui/src/collections_ui.rs#L667), [`ui/dr-ui/src/derived_sync.rs:555`](../ui/dr-ui/src/derived_sync.rs#L555), [`ui/dr-ui/src/derived_sync.rs:627`](../ui/dr-ui/src/derived_sync.rs#L627), [`ui/dr-ui/src/library.rs:1685`](../ui/dr-ui/src/library.rs#L1685), [`ui/dr-ui/src/library.rs:1775`](../ui/dr-ui/src/library.rs#L1775), [`ui/dr-ui/src/library.rs:3254`](../ui/dr-ui/src/library.rs#L3254), [`ui/dr-ui/src/library.rs:3592`](../ui/dr-ui/src/library.rs#L3592), [`ui/dr-ui/src/library.rs:981`](../ui/dr-ui/src/library.rs#L981), [`ui/dr-ui/src/library_ui.rs:1121`](../ui/dr-ui/src/library_ui.rs#L1121), [`ui/dr-ui/src/library_ui.rs:1194`](../ui/dr-ui/src/library_ui.rs#L1194), [`ui/dr-ui/src/library_ui.rs:1393`](../ui/dr-ui/src/library_ui.rs#L1393), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`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-6d | [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1) | | FR-NC-7 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:105`](../core/dr-sync-nextcloud/src/lib.rs#L105), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:3472`](../ui/dr-ui/src/library.rs#L3472), [`ui/dr-ui/src/library_ui.rs:3731`](../ui/dr-ui/src/library_ui.rs#L3731), [`ui/dr-ui/ui/settings.slint:387`](../ui/dr-ui/ui/settings.slint#L387) | -| 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:206`](../core/dr-types/src/settings.rs#L206), [`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:1215`](../ui/dr-ui/src/lib.rs#L1215), [`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:122`](../ui/dr-ui/src/import.rs#L122), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import.rs:584`](../ui/dr-ui/src/import.rs#L584), [`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:1215`](../ui/dr-ui/src/lib.rs#L1215) | -| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:119`](../core/dr-pipeline/src/sidecar.rs#L119), [`core/dr-pipeline/src/sidecar.rs:93`](../core/dr-pipeline/src/sidecar.rs#L93), [`ui/dr-ui/src/lib.rs:1984`](../ui/dr-ui/src/lib.rs#L1984), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library_ui.rs:494`](../ui/dr-ui/src/library_ui.rs#L494) | +| 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:206`](../core/dr-types/src/settings.rs#L206), [`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:1224`](../ui/dr-ui/src/lib.rs#L1224), [`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:122`](../ui/dr-ui/src/import.rs#L122), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import.rs:584`](../ui/dr-ui/src/import.rs#L584), [`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:1224`](../ui/dr-ui/src/lib.rs#L1224) | +| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:119`](../core/dr-pipeline/src/sidecar.rs#L119), [`core/dr-pipeline/src/sidecar.rs:93`](../core/dr-pipeline/src/sidecar.rs#L93), [`ui/dr-ui/src/lib.rs:1993`](../ui/dr-ui/src/lib.rs#L1993), [`ui/dr-ui/src/library.rs:492`](../ui/dr-ui/src/library.rs#L492), [`ui/dr-ui/src/library_ui.rs:494`](../ui/dr-ui/src/library_ui.rs#L494) | | FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:635`](../core/dr-catalog/src/schema.rs#L635), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:157`](../core/dr-pipeline/src/sidecar.rs#L157), [`core/dr-pipeline/src/sidecar.rs:184`](../core/dr-pipeline/src/sidecar.rs#L184), [`core/dr-pipeline/src/sidecar.rs:2034`](../core/dr-pipeline/src/sidecar.rs#L2034), [`core/dr-pipeline/src/sidecar.rs:353`](../core/dr-pipeline/src/sidecar.rs#L353), [`core/dr-pipeline/src/sidecar.rs:451`](../core/dr-pipeline/src/sidecar.rs#L451), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/derived_sync.rs:555`](../ui/dr-ui/src/derived_sync.rs#L555), [`ui/dr-ui/src/library.rs:1031`](../ui/dr-ui/src/library.rs#L1031), [`ui/dr-ui/src/library.rs:877`](../ui/dr-ui/src/library.rs#L877) | | FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:54`](../core/dr-types/src/lib.rs#L54), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) | | FR-PLAT-AND-2 | [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-sync-folder/src/lib.rs:242`](../core/dr-sync-folder/src/lib.rs#L242), [`core/dr-sync-folder/src/tests.rs:51`](../core/dr-sync-folder/src/tests.rs#L51), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/error.rs:195`](../core/dr-sync/src/error.rs#L195), [`core/dr-sync/src/scan.rs:175`](../core/dr-sync/src/scan.rs#L175), [`core/dr-sync/src/scan.rs:464`](../core/dr-sync/src/scan.rs#L464), [`core/dr-sync/src/scan.rs:490`](../core/dr-sync/src/scan.rs#L490), [`core/dr-sync/src/scan.rs:519`](../core/dr-sync/src/scan.rs#L519), [`ui/dr-ui/src/library.rs:1222`](../ui/dr-ui/src/library.rs#L1222), [`ui/dr-ui/src/library.rs:1275`](../ui/dr-ui/src/library.rs#L1275), [`ui/dr-ui/src/library.rs:1306`](../ui/dr-ui/src/library.rs#L1306), [`ui/dr-ui/src/library.rs:1411`](../ui/dr-ui/src/library.rs#L1411), [`ui/dr-ui/src/library_ui.rs:1045`](../ui/dr-ui/src/library_ui.rs#L1045), [`ui/dr-ui/src/library_ui.rs:1644`](../ui/dr-ui/src/library_ui.rs#L1644), [`ui/dr-ui/src/library_ui.rs:273`](../ui/dr-ui/src/library_ui.rs#L273), [`ui/dr-ui/src/library_ui.rs:463`](../ui/dr-ui/src/library_ui.rs#L463), [`ui/dr-ui/src/library_ui.rs:966`](../ui/dr-ui/src/library_ui.rs#L966) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/runner.rs:1`](../core/dr-catalog/src/runner.rs#L1) | | FR-PLAT-AND-4 | [`core/dr-catalog/src/runner.rs:1`](../core/dr-catalog/src/runner.rs#L1) | -| FR-PLAT-AND-5 | [`apps/darkroom-android/src/lib.rs:71`](../apps/darkroom-android/src/lib.rs#L71), [`core/dr-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-gpu/src/detail.rs:161`](../core/dr-gpu/src/detail.rs#L161), [`core/dr-gpu/src/detail.rs:547`](../core/dr-gpu/src/detail.rs#L547), [`core/dr-gpu/tests/detail_stage.rs:417`](../core/dr-gpu/tests/detail_stage.rs#L417), [`ui/dr-ui/src/develop.rs:3151`](../ui/dr-ui/src/develop.rs#L3151), [`ui/dr-ui/src/identity_ui.rs:115`](../ui/dr-ui/src/identity_ui.rs#L115), [`ui/dr-ui/src/lib.rs:1039`](../ui/dr-ui/src/lib.rs#L1039), [`ui/dr-ui/src/lib.rs:1468`](../ui/dr-ui/src/lib.rs#L1468), [`ui/dr-ui/src/memory.rs:163`](../ui/dr-ui/src/memory.rs#L163), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | -| FR-PLAT-AND-6 | [`apps/darkroom-android/src/lib.rs:204`](../apps/darkroom-android/src/lib.rs#L204) | -| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:938`](../ui/dr-ui/src/lib.rs#L938), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | +| FR-PLAT-AND-5 | [`apps/darkroom-android/src/lib.rs:71`](../apps/darkroom-android/src/lib.rs#L71), [`core/dr-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-gpu/src/detail.rs:161`](../core/dr-gpu/src/detail.rs#L161), [`core/dr-gpu/src/detail.rs:547`](../core/dr-gpu/src/detail.rs#L547), [`core/dr-gpu/tests/detail_stage.rs:417`](../core/dr-gpu/tests/detail_stage.rs#L417), [`ui/dr-ui/src/develop.rs:3151`](../ui/dr-ui/src/develop.rs#L3151), [`ui/dr-ui/src/identity_ui.rs:115`](../ui/dr-ui/src/identity_ui.rs#L115), [`ui/dr-ui/src/lib.rs:1048`](../ui/dr-ui/src/lib.rs#L1048), [`ui/dr-ui/src/lib.rs:1477`](../ui/dr-ui/src/lib.rs#L1477), [`ui/dr-ui/src/memory.rs:163`](../ui/dr-ui/src/memory.rs#L163), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | +| FR-PLAT-AND-6 | [`apps/darkroom-android/src/lib.rs:231`](../apps/darkroom-android/src/lib.rs#L231) | +| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:947`](../ui/dr-ui/src/lib.rs#L947), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-PLAT-LIN-2 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1) | | FR-PLG-2 | [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/expr.rs:152`](../core/dr-pipeline/src/declared/expr.rs#L152), [`core/dr-pipeline/src/declared/expr.rs:1`](../core/dr-pipeline/src/declared/expr.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:82`](../core/dr-pipeline/src/declared/mod.rs#L82), [`core/dr-pipeline/src/descriptor.rs:15`](../core/dr-pipeline/src/descriptor.rs#L15), [`core/dr-pipeline/src/descriptor.rs:652`](../core/dr-pipeline/src/descriptor.rs#L652), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/tests/declared_parity.rs:1`](../core/dr-pipeline/tests/declared_parity.rs#L1), [`core/dr-pipeline/tests/declared_parity.rs:240`](../core/dr-pipeline/tests/declared_parity.rs#L240), [`core/dr-pipeline/tests/declared_parity.rs:305`](../core/dr-pipeline/tests/declared_parity.rs#L305), [`core/dr-pipeline/tests/declared_parity.rs:358`](../core/dr-pipeline/tests/declared_parity.rs#L358), [`core/dr-pipeline/tests/declared_parity.rs:416`](../core/dr-pipeline/tests/declared_parity.rs#L416) | | FR-PLG-2d | [`core/dr-pipeline/src/declared/decl.rs:112`](../core/dr-pipeline/src/declared/decl.rs#L112), [`core/dr-pipeline/src/declared/decl.rs:152`](../core/dr-pipeline/src/declared/decl.rs#L152), [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/decl.rs:420`](../core/dr-pipeline/src/declared/decl.rs#L420), [`core/dr-pipeline/src/declared/decl.rs:67`](../core/dr-pipeline/src/declared/decl.rs#L67), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:384`](../core/dr-pipeline/src/declared/mod.rs#L384), [`core/dr-pipeline/src/declared/mod.rs:403`](../core/dr-pipeline/src/declared/mod.rs#L403) | | FR-RAW-1 | [`core/dr-decode/src/lib.rs:243`](../core/dr-decode/src/lib.rs#L243), [`core/dr-types/src/lib.rs:130`](../core/dr-types/src/lib.rs#L130), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201) | | 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:213`](../ui/dr-ui/src/lib.rs#L213) | +| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:222`](../ui/dr-ui/src/lib.rs#L222) | | 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:3210`](../ui/dr-ui/src/lib.rs#L3210), [`ui/dr-ui/src/lib.rs:89`](../ui/dr-ui/src/lib.rs#L89), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/identity.slint:181`](../ui/dr-ui/ui/identity.slint#L181), [`ui/dr-ui/ui/library.slint:1091`](../ui/dr-ui/ui/library.slint#L1091) | -| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1008`](../ui/dr-ui/src/collections_ui.rs#L1008), [`ui/dr-ui/src/collections_ui.rs:1018`](../ui/dr-ui/src/collections_ui.rs#L1018), [`ui/dr-ui/src/collections_ui.rs:119`](../ui/dr-ui/src/collections_ui.rs#L119), [`ui/dr-ui/src/collections_ui.rs:1501`](../ui/dr-ui/src/collections_ui.rs#L1501), [`ui/dr-ui/src/collections_ui.rs:1515`](../ui/dr-ui/src/collections_ui.rs#L1515), [`ui/dr-ui/src/collections_ui.rs:160`](../ui/dr-ui/src/collections_ui.rs#L160), [`ui/dr-ui/src/collections_ui.rs:1767`](../ui/dr-ui/src/collections_ui.rs#L1767), [`ui/dr-ui/src/collections_ui.rs:2818`](../ui/dr-ui/src/collections_ui.rs#L2818), [`ui/dr-ui/src/collections_ui.rs:486`](../ui/dr-ui/src/collections_ui.rs#L486), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/lib.rs:89`](../ui/dr-ui/src/lib.rs#L89), [`ui/dr-ui/src/lib.rs:96`](../ui/dr-ui/src/lib.rs#L96), [`ui/dr-ui/src/library_ui.rs:308`](../ui/dr-ui/src/library_ui.rs#L308), [`ui/dr-ui/src/library_ui.rs:5416`](../ui/dr-ui/src/library_ui.rs#L5416), [`ui/dr-ui/src/library_ui.rs:5561`](../ui/dr-ui/src/library_ui.rs#L5561), [`ui/dr-ui/src/library_ui.rs:6766`](../ui/dr-ui/src/library_ui.rs#L6766), [`ui/dr-ui/ui/adjust.slint:1072`](../ui/dr-ui/ui/adjust.slint#L1072), [`ui/dr-ui/ui/adjust.slint:544`](../ui/dr-ui/ui/adjust.slint#L544), [`ui/dr-ui/ui/adjust.slint:741`](../ui/dr-ui/ui/adjust.slint#L741), [`ui/dr-ui/ui/app.slint:2129`](../ui/dr-ui/ui/app.slint#L2129), [`ui/dr-ui/ui/app.slint:540`](../ui/dr-ui/ui/app.slint#L540), [`ui/dr-ui/ui/app.slint:547`](../ui/dr-ui/ui/app.slint#L547), [`ui/dr-ui/ui/app.slint:58`](../ui/dr-ui/ui/app.slint#L58), [`ui/dr-ui/ui/app.slint:898`](../ui/dr-ui/ui/app.slint#L898), [`ui/dr-ui/ui/controls.slint:474`](../ui/dr-ui/ui/controls.slint#L474), [`ui/dr-ui/ui/develop.slint:223`](../ui/dr-ui/ui/develop.slint#L223), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/histogram.slint:187`](../ui/dr-ui/ui/histogram.slint#L187), [`ui/dr-ui/ui/history.slint:118`](../ui/dr-ui/ui/history.slint#L118), [`ui/dr-ui/ui/library.slint:1259`](../ui/dr-ui/ui/library.slint#L1259), [`ui/dr-ui/ui/library.slint:1266`](../ui/dr-ui/ui/library.slint#L1266), [`ui/dr-ui/ui/library.slint:1329`](../ui/dr-ui/ui/library.slint#L1329), [`ui/dr-ui/ui/library.slint:2561`](../ui/dr-ui/ui/library.slint#L2561), [`ui/dr-ui/ui/library.slint:3866`](../ui/dr-ui/ui/library.slint#L3866), [`ui/dr-ui/ui/library.slint:842`](../ui/dr-ui/ui/library.slint#L842), [`ui/dr-ui/ui/library.slint:898`](../ui/dr-ui/ui/library.slint#L898), [`ui/dr-ui/ui/masks.slint:314`](../ui/dr-ui/ui/masks.slint#L314), [`ui/dr-ui/ui/peaking.slint:82`](../ui/dr-ui/ui/peaking.slint#L82), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/settings.slint:110`](../ui/dr-ui/ui/settings.slint#L110), [`ui/dr-ui/ui/spots.slint:88`](../ui/dr-ui/ui/spots.slint#L88) | +| FR-UI-1 | [`ui/dr-ui/src/lib.rs:3219`](../ui/dr-ui/src/lib.rs#L3219), [`ui/dr-ui/src/lib.rs:98`](../ui/dr-ui/src/lib.rs#L98), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/identity.slint:181`](../ui/dr-ui/ui/identity.slint#L181), [`ui/dr-ui/ui/library.slint:1091`](../ui/dr-ui/ui/library.slint#L1091) | +| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1008`](../ui/dr-ui/src/collections_ui.rs#L1008), [`ui/dr-ui/src/collections_ui.rs:1018`](../ui/dr-ui/src/collections_ui.rs#L1018), [`ui/dr-ui/src/collections_ui.rs:119`](../ui/dr-ui/src/collections_ui.rs#L119), [`ui/dr-ui/src/collections_ui.rs:1501`](../ui/dr-ui/src/collections_ui.rs#L1501), [`ui/dr-ui/src/collections_ui.rs:1515`](../ui/dr-ui/src/collections_ui.rs#L1515), [`ui/dr-ui/src/collections_ui.rs:160`](../ui/dr-ui/src/collections_ui.rs#L160), [`ui/dr-ui/src/collections_ui.rs:1767`](../ui/dr-ui/src/collections_ui.rs#L1767), [`ui/dr-ui/src/collections_ui.rs:2818`](../ui/dr-ui/src/collections_ui.rs#L2818), [`ui/dr-ui/src/collections_ui.rs:486`](../ui/dr-ui/src/collections_ui.rs#L486), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/lib.rs:105`](../ui/dr-ui/src/lib.rs#L105), [`ui/dr-ui/src/lib.rs:98`](../ui/dr-ui/src/lib.rs#L98), [`ui/dr-ui/src/library_ui.rs:308`](../ui/dr-ui/src/library_ui.rs#L308), [`ui/dr-ui/src/library_ui.rs:5416`](../ui/dr-ui/src/library_ui.rs#L5416), [`ui/dr-ui/src/library_ui.rs:5561`](../ui/dr-ui/src/library_ui.rs#L5561), [`ui/dr-ui/src/library_ui.rs:6766`](../ui/dr-ui/src/library_ui.rs#L6766), [`ui/dr-ui/ui/adjust.slint:1072`](../ui/dr-ui/ui/adjust.slint#L1072), [`ui/dr-ui/ui/adjust.slint:544`](../ui/dr-ui/ui/adjust.slint#L544), [`ui/dr-ui/ui/adjust.slint:741`](../ui/dr-ui/ui/adjust.slint#L741), [`ui/dr-ui/ui/app.slint:2129`](../ui/dr-ui/ui/app.slint#L2129), [`ui/dr-ui/ui/app.slint:540`](../ui/dr-ui/ui/app.slint#L540), [`ui/dr-ui/ui/app.slint:547`](../ui/dr-ui/ui/app.slint#L547), [`ui/dr-ui/ui/app.slint:58`](../ui/dr-ui/ui/app.slint#L58), [`ui/dr-ui/ui/app.slint:898`](../ui/dr-ui/ui/app.slint#L898), [`ui/dr-ui/ui/controls.slint:474`](../ui/dr-ui/ui/controls.slint#L474), [`ui/dr-ui/ui/develop.slint:223`](../ui/dr-ui/ui/develop.slint#L223), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/histogram.slint:187`](../ui/dr-ui/ui/histogram.slint#L187), [`ui/dr-ui/ui/history.slint:118`](../ui/dr-ui/ui/history.slint#L118), [`ui/dr-ui/ui/library.slint:1259`](../ui/dr-ui/ui/library.slint#L1259), [`ui/dr-ui/ui/library.slint:1266`](../ui/dr-ui/ui/library.slint#L1266), [`ui/dr-ui/ui/library.slint:1329`](../ui/dr-ui/ui/library.slint#L1329), [`ui/dr-ui/ui/library.slint:2561`](../ui/dr-ui/ui/library.slint#L2561), [`ui/dr-ui/ui/library.slint:3866`](../ui/dr-ui/ui/library.slint#L3866), [`ui/dr-ui/ui/library.slint:842`](../ui/dr-ui/ui/library.slint#L842), [`ui/dr-ui/ui/library.slint:898`](../ui/dr-ui/ui/library.slint#L898), [`ui/dr-ui/ui/masks.slint:314`](../ui/dr-ui/ui/masks.slint#L314), [`ui/dr-ui/ui/peaking.slint:82`](../ui/dr-ui/ui/peaking.slint#L82), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/settings.slint:110`](../ui/dr-ui/ui/settings.slint#L110), [`ui/dr-ui/ui/spots.slint:88`](../ui/dr-ui/ui/spots.slint#L88) | | FR-UI-3 | [`ui/dr-ui/src/develop.rs:2248`](../ui/dr-ui/src/develop.rs#L2248), [`ui/dr-ui/src/develop.rs:2357`](../ui/dr-ui/src/develop.rs#L2357), [`ui/dr-ui/src/library_ui.rs:4525`](../ui/dr-ui/src/library_ui.rs#L4525), [`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/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:1945`](../ui/dr-ui/ui/app.slint#L1945), [`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), [`ui/dr-ui/ui/masks.slint:524`](../ui/dr-ui/ui/masks.slint#L524) | | FR-UI-4 | [`tools/traceability/src/gestures.rs:1`](../tools/traceability/src/gestures.rs#L1), [`tools/traceability/src/gestures.rs:366`](../tools/traceability/src/gestures.rs#L366), [`ui/dr-ui/src/collections_ui.rs:1008`](../ui/dr-ui/src/collections_ui.rs#L1008), [`ui/dr-ui/src/collections_ui.rs:1018`](../ui/dr-ui/src/collections_ui.rs#L1018), [`ui/dr-ui/src/collections_ui.rs:119`](../ui/dr-ui/src/collections_ui.rs#L119), [`ui/dr-ui/src/collections_ui.rs:132`](../ui/dr-ui/src/collections_ui.rs#L132), [`ui/dr-ui/src/collections_ui.rs:1501`](../ui/dr-ui/src/collections_ui.rs#L1501), [`ui/dr-ui/src/collections_ui.rs:1515`](../ui/dr-ui/src/collections_ui.rs#L1515), [`ui/dr-ui/src/collections_ui.rs:160`](../ui/dr-ui/src/collections_ui.rs#L160), [`ui/dr-ui/src/collections_ui.rs:1631`](../ui/dr-ui/src/collections_ui.rs#L1631), [`ui/dr-ui/src/collections_ui.rs:1767`](../ui/dr-ui/src/collections_ui.rs#L1767), [`ui/dr-ui/src/collections_ui.rs:1794`](../ui/dr-ui/src/collections_ui.rs#L1794), [`ui/dr-ui/src/collections_ui.rs:1893`](../ui/dr-ui/src/collections_ui.rs#L1893), [`ui/dr-ui/src/collections_ui.rs:2818`](../ui/dr-ui/src/collections_ui.rs#L2818), [`ui/dr-ui/src/collections_ui.rs:486`](../ui/dr-ui/src/collections_ui.rs#L486), [`ui/dr-ui/src/collections_ui.rs:560`](../ui/dr-ui/src/collections_ui.rs#L560), [`ui/dr-ui/src/gesture_book.rs:3`](../ui/dr-ui/src/gesture_book.rs#L3), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/library_ui.rs:4525`](../ui/dr-ui/src/library_ui.rs#L4525), [`ui/dr-ui/src/library_ui.rs:4537`](../ui/dr-ui/src/library_ui.rs#L4537), [`ui/dr-ui/src/library_ui.rs:4589`](../ui/dr-ui/src/library_ui.rs#L4589), [`ui/dr-ui/src/library_ui.rs:4692`](../ui/dr-ui/src/library_ui.rs#L4692), [`ui/dr-ui/src/library_ui.rs:4720`](../ui/dr-ui/src/library_ui.rs#L4720), [`ui/dr-ui/src/library_ui.rs:5549`](../ui/dr-ui/src/library_ui.rs#L5549), [`ui/dr-ui/src/library_ui.rs:5561`](../ui/dr-ui/src/library_ui.rs#L5561), [`ui/dr-ui/ui/app.slint:1785`](../ui/dr-ui/ui/app.slint#L1785), [`ui/dr-ui/ui/app.slint:516`](../ui/dr-ui/ui/app.slint#L516), [`ui/dr-ui/ui/app.slint:547`](../ui/dr-ui/ui/app.slint#L547), [`ui/dr-ui/ui/app.slint:632`](../ui/dr-ui/ui/app.slint#L632), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1067`](../ui/dr-ui/ui/library.slint#L1067), [`ui/dr-ui/ui/library.slint:1234`](../ui/dr-ui/ui/library.slint#L1234), [`ui/dr-ui/ui/library.slint:1259`](../ui/dr-ui/ui/library.slint#L1259), [`ui/dr-ui/ui/library.slint:1266`](../ui/dr-ui/ui/library.slint#L1266), [`ui/dr-ui/ui/library.slint:1322`](../ui/dr-ui/ui/library.slint#L1322), [`ui/dr-ui/ui/library.slint:1329`](../ui/dr-ui/ui/library.slint#L1329), [`ui/dr-ui/ui/library.slint:2561`](../ui/dr-ui/ui/library.slint#L2561), [`ui/dr-ui/ui/library.slint:3866`](../ui/dr-ui/ui/library.slint#L3866), [`ui/dr-ui/ui/library.slint:842`](../ui/dr-ui/ui/library.slint#L842), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892), [`ui/dr-ui/ui/library.slint:898`](../ui/dr-ui/ui/library.slint#L898), [`ui/dr-ui/ui/library.slint:917`](../ui/dr-ui/ui/library.slint#L917) | -| 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:2647`](../ui/dr-ui/src/lib.rs#L2647), [`ui/dr-ui/src/lib.rs:3249`](../ui/dr-ui/src/lib.rs#L3249), [`ui/dr-ui/src/lib.rs:3434`](../ui/dr-ui/src/lib.rs#L3434), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:119`](../ui/dr-ui/ui/app.slint#L119), [`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:2656`](../ui/dr-ui/src/lib.rs#L2656), [`ui/dr-ui/src/lib.rs:3258`](../ui/dr-ui/src/lib.rs#L3258), [`ui/dr-ui/src/lib.rs:3443`](../ui/dr-ui/src/lib.rs#L3443), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:119`](../ui/dr-ui/ui/app.slint#L119), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | | FR-UI-6 | [`ui/dr-ui/ui/widgets.slint:1`](../ui/dr-ui/ui/widgets.slint#L1) | | FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:385`](../core/dr-pipeline/src/framing.rs#L385) | | 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), [`ui/dr-ui/src/library.rs:3069`](../ui/dr-ui/src/library.rs#L3069) | -| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1553`](../ui/dr-ui/src/export.rs#L1553), [`ui/dr-ui/src/export.rs:1579`](../ui/dr-ui/src/export.rs#L1579), [`ui/dr-ui/src/export.rs:409`](../ui/dr-ui/src/export.rs#L409), [`ui/dr-ui/src/export.rs:435`](../ui/dr-ui/src/export.rs#L435), [`ui/dr-ui/src/lib.rs:2344`](../ui/dr-ui/src/lib.rs#L2344), [`ui/dr-ui/ui/app.slint:1074`](../ui/dr-ui/ui/app.slint#L1074), [`ui/dr-ui/ui/library.slint:967`](../ui/dr-ui/ui/library.slint#L967) | +| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1553`](../ui/dr-ui/src/export.rs#L1553), [`ui/dr-ui/src/export.rs:1579`](../ui/dr-ui/src/export.rs#L1579), [`ui/dr-ui/src/export.rs:409`](../ui/dr-ui/src/export.rs#L409), [`ui/dr-ui/src/export.rs:435`](../ui/dr-ui/src/export.rs#L435), [`ui/dr-ui/src/lib.rs:2353`](../ui/dr-ui/src/lib.rs#L2353), [`ui/dr-ui/ui/app.slint:1074`](../ui/dr-ui/ui/app.slint#L1074), [`ui/dr-ui/ui/library.slint:967`](../ui/dr-ui/ui/library.slint#L967) | | 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), [`platform/dr-plat/src/storage.rs:148`](../platform/dr-plat/src/storage.rs#L148), [`ui/dr-ui/src/export.rs:499`](../ui/dr-ui/src/export.rs#L499) | | NFR-OPS-1 | [`tools/traceability/src/gestures.rs:1`](../tools/traceability/src/gestures.rs#L1), [`tools/traceability/src/lib.rs:268`](../tools/traceability/src/lib.rs#L268) | | NFR-OPS-3 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | 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:481`](../tools/traceability/src/lib.rs#L481) | | NFR-P13 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) | -| NFR-P14 | [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`ui/dr-ui/src/develop.rs:3046`](../ui/dr-ui/src/develop.rs#L3046), [`ui/dr-ui/src/lib.rs:1710`](../ui/dr-ui/src/lib.rs#L1710) | -| NFR-P5 | [`core/dr-catalog/src/schema.rs:330`](../core/dr-catalog/src/schema.rs#L330), [`ui/dr-ui/src/library.rs:4132`](../ui/dr-ui/src/library.rs#L4132), [`ui/dr-ui/src/library.rs:5066`](../ui/dr-ui/src/library.rs#L5066), [`ui/dr-ui/src/library_ui.rs:4198`](../ui/dr-ui/src/library_ui.rs#L4198), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) | +| NFR-P14 | [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`ui/dr-ui/src/develop.rs:3046`](../ui/dr-ui/src/develop.rs#L3046), [`ui/dr-ui/src/lib.rs:1719`](../ui/dr-ui/src/lib.rs#L1719) | +| NFR-P5 | [`core/dr-catalog/src/schema.rs:330`](../core/dr-catalog/src/schema.rs#L330), [`ui/dr-ui/src/library.rs:4163`](../ui/dr-ui/src/library.rs#L4163), [`ui/dr-ui/src/library.rs:5097`](../ui/dr-ui/src/library.rs#L5097), [`ui/dr-ui/src/library_ui.rs:4198`](../ui/dr-ui/src/library_ui.rs#L4198), [`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:942`](../ui/dr-ui/src/export.rs#L942), [`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:270`](../core/dr-types/src/lib.rs#L270), [`core/dr-types/src/lib.rs:303`](../core/dr-types/src/lib.rs#L303), [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) | | NFR-PORT-2 | [`core/dr-gpu/src/lib.rs:1`](../core/dr-gpu/src/lib.rs#L1) | @@ -145,7 +145,7 @@ _None._ | NFR-R5 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.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-RES-1 | [`core/dr-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/develop.rs:3151`](../ui/dr-ui/src/develop.rs#L3151), [`ui/dr-ui/src/identity_ui.rs:115`](../ui/dr-ui/src/identity_ui.rs#L115), [`ui/dr-ui/src/lib.rs:81`](../ui/dr-ui/src/lib.rs#L81), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | +| NFR-RES-1 | [`core/dr-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/develop.rs:3151`](../ui/dr-ui/src/develop.rs#L3151), [`ui/dr-ui/src/identity_ui.rs:115`](../ui/dr-ui/src/identity_ui.rs#L115), [`ui/dr-ui/src/lib.rs:90`](../ui/dr-ui/src/lib.rs#L90), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | | NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/schema.rs:692`](../core/dr-catalog/src/schema.rs#L692), [`core/dr-gpu/src/lib.rs:93`](../core/dr-gpu/src/lib.rs#L93), [`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:2964`](../ui/dr-ui/src/library.rs#L2964) | | NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) | | NFR-SEC-2 | [`core/dr-sync/src/account.rs:298`](../core/dr-sync/src/account.rs#L298), [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) | diff --git a/models/scene/categories.txt b/models/scene/categories.txt new file mode 100644 index 0000000..32425fc --- /dev/null +++ b/models/scene/categories.txt @@ -0,0 +1,55 @@ +# Photographic categories, over ADE20K's 150 classes. +# +# The scene tab offers a slider per category, not per class: nobody wants to +# grade "sconce" and "crt screen" separately, and ADE20K's vocabulary is a +# scene-parsing benchmark rather than a photographer's list. This file is the +# translation, and it is data so that changing it is not a code change. +# +# Format: one category per line, `name = class, class, ...`, where each class +# is a name from the model's own `.classes.json`. Names rather than indices +# because an index is silently wrong after a re-export and a name is loudly +# wrong; `SceneModel::from_path` refuses a file naming a class the model does +# not have. +# +# ## Why the list is short, and why "other" is not in it +# +# Weights come from a softmax over all 150 channels summed within each +# category, so the categories listed here plus everything unlisted sum to 1 at +# every pixel. That is what lets the scene tab feather two adjacent categories +# without painting both grades into the overlap. Adding a category takes +# weight from the unlisted remainder rather than from its neighbours, so this +# list can grow without disturbing what is already here. +# +# Classes are assigned to at most one category — an overlap would break the +# partition and double-count the shared class, so the loader rejects it. + +# The one the whole exercise started from. ADE20K's easiest class, and the one +# most often graded on its own in a landscape. +sky = sky + +# Foliage, not "green things": a lawn and a canopy take the same saturation +# and luminance moves far more often than either takes the sky's. +vegetation = tree, grass, plant, flower, palm, field + +# Standing and moving water together. `swimming pool` and `fountain` are here +# rather than under architecture because what a photographer adjusts is the +# water, not the basin. +water = water, sea, river, lake, waterfall, swimming pool, fountain + +# Distant landform. Separate from `ground` because it is usually far, hazy and +# wants dehaze and contrast where a foreground surface wants neither. +terrain = mountain, rock, hill + +# What the photographer is standing on, or would be. Earth and sand sit here +# rather than with terrain for the same near/far reason. +ground = earth, sand, land, dirt track, path, road, sidewalk, runway, floor, step, stairs, stairway + +# Built structure. Deliberately broad: a facade, its railings and its awning +# are one surface as far as a global grade is concerned. +architecture = building, house, skyscraper, wall, tower, bridge, hovel, fence, column, awning, booth, canopy, pier, railing, grandstand + +# Present for the scene tab's "expose people" move, and *not* a replacement for +# the instance model — this is every person in the frame at once, which is the +# right granularity for a global grade and the wrong one for selecting a +# subject. See `models/LICENCE.md`. +person = person From 7312aceded80a1107b952d5681b2fd34f14ddbd3 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 30 Aug 2026 10:48:54 +0200 Subject: [PATCH 6/6] Get the scene model onto the devices that need it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The decoder can load from a path; nothing yet put a file at one. Four packaging routes, and one lookup that finds the result. ## Not `include_bytes!`, unlike the instance model The instance model is 11 MB and compiled in, which was the right call for it: Android hands the app no filesystem path (ARCH §6.9) and 11 MB is tolerable. The scene model is 24 MB, and 35 MB of constants in the binary is paid by every install whether or not the tab is ever opened. So it follows `models/face/` instead — carried as an APK asset, unpacked once at first launch into the shared directory a desktop install already uses, after which every lookup finds it where it finds a desktop user's. Assets are stored rather than deflated in the APK, so unpacking is a copy rather than an inflate. `embedded-scene-model` exists for the desktop build with nowhere else to read from, and for tests wanting the real graph. Off by default, which is the asymmetry with `embedded-model` and the reason for a separate feature. ## Three files, all or none `scene_model` insists on the graph, its vocabulary and the category descriptor together, for the reason `face_models` insists on its pair: a graph alone decodes to 150 anonymous channels. Reporting the set missing beats starting and failing at the first inference. ## The two model sets are not the same kind of thing `install_bundled_models` now carries both, and the distinction is worth keeping in view. Face weights are absent from the repository *by design* — the InsightFace grant is research-only (docs/faces.md §2) — so a build carrying none is ordinary. The scene model is committed, so a build carrying none means a checkout without `git lfs pull`. Neither is fatal. A photo editor that refuses to start over a missing grading feature is worse than one that starts without it, so both report themselves unavailable exactly as face indexing already did. The LFS-pointer guards apply to the `.onnx` only. The vocabulary and the descriptor are legitimately a few kilobytes, and a size check that fails on them would be a guard against the wrong thing. `scene_model` is exported ahead of the tab that will consume it so the packaging added here has something to be verified against — assets written where no lookup looks would be a silent mistake for as long as the tab took to arrive. Co-Authored-By: Claude Opus 5 (1M context) --- apps/darkroom-android/src/lib.rs | 79 +++++++++++++------ docker/android/assemble-apk.sh | 44 ++++++++--- packaging/PKGBUILD | 19 +++++ packaging/flatpak/paris.tourolle.darkroom.yml | 15 ++++ ui/dr-ui/src/lib.rs | 9 +++ ui/dr-ui/src/library.rs | 31 ++++++++ 6 files changed, 160 insertions(+), 37 deletions(-) diff --git a/apps/darkroom-android/src/lib.rs b/apps/darkroom-android/src/lib.rs index f8c417f..ac0b0ec 100644 --- a/apps/darkroom-android/src/lib.rs +++ b/apps/darkroom-android/src/lib.rs @@ -60,7 +60,7 @@ fn android_main(app: slint::android::AndroidApp) { } // After the data dir and before anything asks whether a model is present. - install_bundled_face_models(&app); + install_bundled_models(&app); // Before `init_with_event_listener`, which takes `app` by value and is the // last moment anything can ask the activity a question. Not an ordering @@ -126,38 +126,65 @@ fn android_main(app: slint::android::AndroidApp) { } } -/// Unpack the face models the APK carries, if it carries any. +/// Unpack the models the APK carries, if it carries any. /// /// # Why Android needs this and no other platform does /// -/// The weights are not a build input and are not in the repository — the -/// InsightFace grant is research-only and incompatible with this project's -/// licence (docs/faces.md §2), so a desktop user fetches them, runs -/// `tools/fix-face-model-shapes.sh` over them, and drops the result into -/// `~/.local/share/darkroom/models/`. **That gesture does not exist on -/// Android.** `internal_data_path` is app-private, `run-as` needs a debuggable -/// build, and there is no picker and no fetch in the app, so a phone had no way -/// to acquire a model at all and face indexing reported itself permanently off. +/// A desktop build reads its models from a path — the account's directory, the +/// shared one, or `$XDG_DATA_DIRS` where a package put them. **Android has no +/// such path.** `internal_data_path` is app-private, `run-as` needs a +/// debuggable build, and an asset inside a package is not a path anything can +/// open (ARCH §6.9), so a phone had no way to reach a model at all. /// -/// So a locally-built APK may carry the pair in `assets/models/`, which -/// `assemble-apk.sh` includes when the tree has them and omits when it does -/// not. Nothing changes about what the repository holds or what a published -/// build could redistribute; this only gives a self-built APK the same route a -/// desktop build has always had. +/// So the APK carries them in `assets/models/` and this copies them out, once, +/// into the same shared directory a desktop install uses. After that every +/// lookup in `dr_ui::library` finds them exactly where it finds a desktop +/// user's. /// -/// Absent assets are the ordinary case, not an error — the same quiet "no model -/// installed" state a fresh desktop install is in. +/// # The two sets are not the same kind of thing +/// +/// **Face weights are absent from the repository by design.** The InsightFace +/// grant is research-only and incompatible with this project's licence +/// (docs/faces.md §2), so a desktop user fetches them, runs +/// `tools/fix-face-model-shapes.sh` over them, and drops the result in. A build +/// that carries none is the ordinary case and face indexing simply stays off. +/// +/// **The scene model is committed** (AGPL, compatible — `models/LICENCE.md`), +/// so a build carrying none means a checkout without `git lfs pull` rather than +/// a deliberate omission. It is still not an error here: the scene tab reports +/// itself unavailable the same way face indexing does, because a photo editor +/// that refuses to start over a missing grading feature is worse than one that +/// starts without it. +/// +/// # Why it is not `include_bytes!` like the instance model +/// +/// Size. The instance model is 11 MB and compiled in; the scene model is 24 MB +/// on top of that, and a 35 MB constant in the binary is paid by every install +/// whether or not the tab is opened. Assets are also *stored* rather than +/// deflated in the APK (see `assemble-apk.sh`), so unpacking is a copy rather +/// than an inflate. #[cfg(target_os = "android")] -fn install_bundled_face_models(app: &slint::android::AndroidApp) { +fn install_bundled_models(app: &slint::android::AndroidApp) { use std::io::Read; - // The **shape-fixed** names, matching what `library::face_models` looks - // for: tract cannot parse either InsightFace graph with its dynamic input - // dimension, so what ships here has already been through - // `tools/fix-face-model-shapes.sh`. - const BUNDLED: [(&std::ffi::CStr, &str); 2] = [ + // The face names are the **shape-fixed** exports, matching what + // `library::face_models` looks for: tract cannot parse either InsightFace + // graph with its dynamic input dimension, so what ships here has already + // been through `tools/fix-face-model-shapes.sh`. + // + // The scene entries are three files rather than one because the graph alone + // decodes to 150 anonymous channels — `library::scene_model` wants the + // vocabulary and the category descriptor beside it, and requires all three + // before it reports the tab available. + const BUNDLED: [(&std::ffi::CStr, &str); 5] = [ (c"models/scrfd_500m_640.onnx", "scrfd_500m_640.onnx"), (c"models/arcface_mbf_b1.onnx", "arcface_mbf_b1.onnx"), + (c"models/yolo26s-sem-ade20k.onnx", "yolo26s-sem-ade20k.onnx"), + ( + c"models/yolo26s-sem-ade20k.classes.json", + "yolo26s-sem-ade20k.classes.json", + ), + (c"models/categories.txt", "categories.txt"), ]; let dir = dr_ui::shared_face_models_dir(); @@ -172,7 +199,7 @@ fn install_bundled_face_models(app: &slint::android::AndroidApp) { continue; } let Some(mut asset) = assets.open(asset_path) else { - log::info!("no bundled {name} in this APK; face indexing stays off"); + log::info!("no bundled {name} in this APK; the feature needing it stays off"); continue; }; let mut bytes = Vec::new(); @@ -185,8 +212,8 @@ fn install_bundled_face_models(app: &slint::android::AndroidApp) { return; } // Written under a temporary name and renamed, because - // `library::face_models` decides face indexing is available on - // `is_file()` alone. A truncated write — the process backgrounded and + // `library::face_models` and `library::scene_model` both decide a + // feature is available on `is_file()` alone. A truncated write — the process backgrounded and // killed mid-copy — would otherwise leave a file that passes that test // and fails inside tract, reported to the user as a broken model rather // than a missing one. diff --git a/docker/android/assemble-apk.sh b/docker/android/assemble-apk.sh index 6e11e09..351facd 100755 --- a/docker/android/assemble-apk.sh +++ b/docker/android/assemble-apk.sh @@ -255,25 +255,37 @@ fi cp "${SO}" "${OUT}/staging/lib/${ABI}/libdarkroom.so" cp "${DEX}" "${OUT}/staging/classes.dex" -# The face models. Android has no other route to one — app-private storage is -# not user-reachable and the in-app fetch is unbuilt (docs/faces.md §2.2a) — so +# The models. Android has no other route to one — app-private storage is not +# user-reachable and the in-app fetch is unbuilt (docs/faces.md §2.2a) — so # they go in the APK and `android_main` unpacks them on first launch. The -# source is `models/face/`, shared with the Arch package rather than living -# under this one platform's directory. +# sources are `models/face/` and `models/scene/`, shared with the Arch package +# rather than living under this one platform's directory. +# +# Two directories, and they are not the same kind of thing. The face weights +# are absent from most checkouts by design (research-only grant), so finding +# none is ordinary. The scene model is committed, so finding none means a +# broken checkout — but this script still only warns, because the failure it +# would otherwise cause is at APK build time on a machine that may legitimately +# be building the face-less variant. # # Through the staging directory rather than aapt2's `-A`: the .so and the dex # already go in with `zip` below, and one mechanism for "extra files in the # APK" is easier to follow than two. -ASSETS="${REPO}/models/face" +# # Cleared first: a previous run that died between staging and cleanup would # otherwise leave models in the APK that are no longer in the tree. rm -rf "${OUT}/staging/assets" -if compgen -G "${ASSETS}/*.onnx" >/dev/null; then +mkdir -p "${OUT}/staging/assets/models" +_bundled="" +for _dir in face scene; do + ASSETS="${REPO}/models/${_dir}" + compgen -G "${ASSETS}/*.onnx" >/dev/null || continue # An LFS pointer is ~130 bytes and looks exactly like a model to `cp`. Left # unchecked it reaches the device and fails inside tract, which reports a # broken graph rather than a clone that needs `git lfs pull`. Same guard # dr-segment's build script applies to yolo26n-seg.onnx, and the same - # reason. + # reason. Only the weights are checked: the vocabulary and the category + # descriptor beside them are legitimately a few kilobytes. for m in "${ASSETS}"/*.onnx; do if [[ "$(stat -c%s "${m}")" -lt 100000 ]]; then echo "error: $(basename "${m}") is $(stat -c%s "${m}") bytes — an LFS pointer, not a model." >&2 @@ -281,11 +293,21 @@ if compgen -G "${ASSETS}/*.onnx" >/dev/null; then exit 1 fi done - mkdir -p "${OUT}/staging/assets/models" - cp "${ASSETS}"/*.onnx "${OUT}/staging/assets/models/" - echo " assets: $(ls "${ASSETS}" | grep '\.onnx$' | tr '\n' ' ')" + # The scene model is three files: the graph, its vocabulary, and the + # category descriptor. All three are needed to decode anything, so they + # travel together; README.md is documentation and stays out of the APK. + for f in "${ASSETS}"/*; do + case "$(basename "${f}")" in + README.md) continue ;; + esac + cp "${f}" "${OUT}/staging/assets/models/" + _bundled="${_bundled} $(basename "${f}")" + done +done +if [[ -n "${_bundled}" ]]; then + echo " assets:${_bundled}" else - echo " assets: no face models found (face indexing will be off on the device)" + echo " assets: no models found (face indexing and the scene tab will be off on the device)" fi # -0 "" stores the .so without compression so Android can mmap it directly diff --git a/packaging/PKGBUILD b/packaging/PKGBUILD index f5fde21..5b051a6 100644 --- a/packaging/PKGBUILD +++ b/packaging/PKGBUILD @@ -69,4 +69,23 @@ package() { fi install -Dm644 "${_src}" "${pkgdir}/usr/share/darkroom/models/${_m}" done + + # The scene model, for the per-category grades. Unlike the face weights + # this one is in the repository — AGPL, and GPLv3 §13 permits the + # combination (models/LICENCE.md) — so it is installed unconditionally and + # a pointer here is a broken checkout rather than a licence decision. + # + # Three files: the graph, its vocabulary, and the category descriptor + # grouping ADE20K's 150 classes into what the tab shows. All three, because + # dr_ui::library::scene_model reports the tab unavailable without any one + # of them. Only the graph gets the pointer check — the other two are + # legitimately a few kilobytes. + _src="models/scene/yolo26s-sem-ade20k.onnx" + if [[ "$(stat -c%s "${_src}")" -lt 100000 ]]; then + echo "error: the scene model is an LFS pointer — run: git lfs pull" >&2 + return 1 + fi + for _m in yolo26s-sem-ade20k.onnx yolo26s-sem-ade20k.classes.json categories.txt; do + install -Dm644 "models/scene/${_m}" "${pkgdir}/usr/share/darkroom/models/${_m}" + done } diff --git a/packaging/flatpak/paris.tourolle.darkroom.yml b/packaging/flatpak/paris.tourolle.darkroom.yml index c6b9c27..23dec17 100644 --- a/packaging/flatpak/paris.tourolle.darkroom.yml +++ b/packaging/flatpak/paris.tourolle.darkroom.yml @@ -165,6 +165,21 @@ modules: install -Dm644 "models/face/$m" "/app/share/darkroom/models/$m" done + # The scene model, for the per-category grades. In the repository, unlike + # the face weights — AGPL, and GPLv3 §13 permits the combination + # (models/LICENCE.md) — so it installs unconditionally. Three files: the + # graph, its vocabulary, and the category descriptor; dr_ui reports the + # tab unavailable without any one of them. The pointer check is on the + # graph alone, the other two being legitimately small. + - | + if [ "$(stat -c%s models/scene/yolo26s-sem-ade20k.onnx)" -lt 100000 ]; then + echo "error: the scene model is an LFS pointer — run: git lfs pull" >&2 + exit 1 + fi + for m in yolo26s-sem-ade20k.onnx yolo26s-sem-ade20k.classes.json categories.txt; do + install -Dm644 "models/scene/$m" "/app/share/darkroom/models/$m" + done + - install -Dm644 README.md /app/share/doc/darkroom/README.md sources: diff --git a/ui/dr-ui/src/lib.rs b/ui/dr-ui/src/lib.rs index e7c3364..70d70a2 100644 --- a/ui/dr-ui/src/lib.rs +++ b/ui/dr-ui/src/lib.rs @@ -73,6 +73,15 @@ pub use develop::DevelopSession; /// opened — see `library::shared_face_models_dir`. pub use library::shared_face_models_dir; +/// The scene model's three files, wherever this device keeps them. +/// +/// Public for the same reason as the directory above: the pieces that reach for +/// a model are not all inside this crate. Exported ahead of the scene tab that +/// will consume it so that the packaging and unpacking added alongside it have +/// something to be verified against — `assemble-apk.sh` writing files no lookup +/// looks for would be a silent mistake for as long as the tab took to arrive. +pub use library::scene_model; + pub mod launch; pub mod launch_ui; diff --git a/ui/dr-ui/src/library.rs b/ui/dr-ui/src/library.rs index c14796f..ab4562c 100644 --- a/ui/dr-ui/src/library.rs +++ b/ui/dr-ui/src/library.rs @@ -3846,6 +3846,37 @@ pub fn face_models(account: &Account) -> Option<(PathBuf, PathBuf)> { .or_else(|| system_face_models_dirs().into_iter().find_map(pair)) } +/// The scene model, its vocabulary and its category descriptor, if all three +/// are present. +/// +/// All three or none, for the same reason `face_models` insists on its pair: +/// the graph alone decodes to 150 anonymous channels, and a descriptor naming +/// classes a different model does not have is refused by +/// `dr_segment::scene::parse_categories` anyway. Reporting the set missing is +/// more useful than starting and failing at the first inference. +/// +/// Searched in the same three places, most specific first — the account's own +/// directory, the shared one, then wherever a package installed them. Android +/// only ever finds the second, which is where `install_bundled_models` unpacks +/// the APK's copy before any store opens. +/// +/// Unlike the face weights this model *is* in the repository, so a desktop +/// build from a complete checkout has it. Absent means either a checkout +/// without `git lfs pull` or a package that chose not to carry 24 MB, and the +/// scene tab reports itself unavailable rather than the app refusing to run. +pub fn scene_model(account: &Account) -> Option<(PathBuf, PathBuf, PathBuf)> { + fn set(dir: PathBuf) -> Option<(PathBuf, PathBuf, PathBuf)> { + let model = dir.join("yolo26s-sem-ade20k.onnx"); + let classes = dir.join("yolo26s-sem-ade20k.classes.json"); + let categories = dir.join("categories.txt"); + (model.is_file() && classes.is_file() && categories.is_file()) + .then_some((model, classes, categories)) + } + set(face_models_dir(account)) + .or_else(|| set(shared_face_models_dir())) + .or_else(|| system_face_models_dirs().into_iter().find_map(set)) +} + /// Where a *package* may have installed the models. /// /// `$XDG_DATA_DIRS` rather than a hard-coded `/usr/share`, because that is the