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) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 10:05:04 +02:00
co-authored by Claude Opus 5
parent 9e519eb8a6
commit e26f71d15d
9 changed files with 14 additions and 14 deletions
+2 -2
View File
@@ -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}");
-54
View File
@@ -1,54 +0,0 @@
# 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.
## The grant
**Ultralytics releases YOLO under AGPL-3.0**, and the weights carry the same
grant as the framework — the HuggingFace repository declares `agpl-3.0` for the
checkpoints themselves, not merely for the training code. A commercial licence
is offered separately; DarkRoom does not use it and does not need it.
## What that means for DarkRoom
DarkRoom is GPL-3.0-or-later. **GPLv3 §13 explicitly permits combination with
AGPL-3.0 code**, so redistributing these weights inside this repository is
allowed — this is *not* the situation the InsightFace "buffalo" weights would
have created, where a non-commercial research grant is simply incompatible with
the project's licence and with F-Droid, Flatpak and Play distribution
(NFR-COMPAT-2, D13).
The consequence, and it is a real one: **the combined work is effectively
AGPL-3.0.** §13's permission runs one way — the AGPL's §13 network-use condition
attaches to the portion under that licence. For a local-first desktop and
Android photo editor that condition has no practical bite, because there is no
network service offering the combined work to remote users. It would acquire
bite the moment any hosted or server-side rendering appeared, and that is the
thing to remember rather than rediscover.
This was decided deliberately (D14), not arrived at by accident, and
`docs/segmentation.md` §7 records the reasoning.
## Class vocabulary — a caveat worth reading
`docs/segmentation.md` §4 specified YOLO **pretrained on ADE20K**, whose 150
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.
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.
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.
@@ -1,82 +0,0 @@
[
"person",
"bicycle",
"car",
"motorcycle",
"airplane",
"bus",
"train",
"truck",
"boat",
"traffic light",
"fire hydrant",
"stop sign",
"parking meter",
"bench",
"bird",
"cat",
"dog",
"horse",
"sheep",
"cow",
"elephant",
"bear",
"zebra",
"giraffe",
"backpack",
"umbrella",
"handbag",
"tie",
"suitcase",
"frisbee",
"skis",
"snowboard",
"sports ball",
"kite",
"baseball bat",
"baseball glove",
"skateboard",
"surfboard",
"tennis racket",
"bottle",
"wine glass",
"cup",
"fork",
"knife",
"spoon",
"bowl",
"banana",
"apple",
"sandwich",
"orange",
"broccoli",
"carrot",
"hot dog",
"pizza",
"donut",
"cake",
"chair",
"couch",
"potted plant",
"bed",
"dining table",
"toilet",
"tv",
"laptop",
"mouse",
"remote",
"keyboard",
"cell phone",
"microwave",
"oven",
"toaster",
"sink",
"refrigerator",
"book",
"clock",
"vase",
"scissors",
"teddy bear",
"hair drier",
"toothbrush"
]
Binary file not shown.
+1 -1
View File
@@ -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;
+4 -4
View File
@@ -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<Arc<str>>,
}
/// 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.