From 7845d515e84c7744a5245a02bcf45f1a44826ba4 Mon Sep 17 00:00:00 2001 From: Daniel Voshart <205798369+voshart@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:17:58 -0400 Subject: [PATCH] Faces: nothing is bundled, so YuNet is not reported as installed The model list marked the YuNet detector as bundled with LightCraft and installed, but no model ships inside LightCraft (the weights are opt-in downloads or files the user adds). Drop the notion: a model is installed when its folder is, it can be removed like any other, and the licence notice no longer says it is bundled. Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/face_models.rs | 12 ++---------- crates/engine/src/tests_face_models.rs | 3 +-- crates/faces/src/known.rs | 10 +++------- crates/ui-egui/src/panels/faces.rs | 12 +++++------- 4 files changed, 11 insertions(+), 26 deletions(-) diff --git a/crates/engine/src/cmd/face_models.rs b/crates/engine/src/cmd/face_models.rs index de4a8d225..1ba0db0f4 100644 --- a/crates/engine/src/cmd/face_models.rs +++ b/crates/engine/src/cmd/face_models.rs @@ -88,7 +88,6 @@ fn installed_models(dir: &Path) -> Vec { } fn row(m: &ModelManifest, installed: bool, selected: bool, accepted: &Value) -> Value { - let bundled = known::BUNDLED.contains(&m.id.as_str()); json!({ "id": m.id, "name": m.name, @@ -99,8 +98,7 @@ fn row(m: &ModelManifest, installed: bool, selected: bool, accepted: &Value) -> "sizeBytes": m.size_bytes, "sha256": m.sha256, "known": known::all().iter().any(|k| k.id == m.id), - "bundled": bundled, - "installed": installed || bundled, + "installed": installed, "selected": selected, "accepted": accepted, }) @@ -192,9 +190,6 @@ fn install(s: &mut Session, p: &Value) -> Result { Suggestion::Known(m) | Suggestion::Draft { manifest: m, .. } => m, Suggestion::Unsupported(why) => return Err(bad(C, format!("this model cannot be used yet: {why}"))), }; - if known::BUNDLED.contains(&m.id.as_str()) { - return Err(bad(C, "this model is already included with LightCraft")); - } if m.role != Role::Embedder { return Err(bad(C, "only face recognition models can be installed for now")); } @@ -228,9 +223,6 @@ fn remove(s: &mut Session, p: &Value) -> Result { if !manifest::valid_id(id) { return Err(bad(C, "not a model id")); } - if known::BUNDLED.contains(&id) { - return Err(bad(C, "this model is part of LightCraft and cannot be removed")); - } let home = dir.join(id); // only a folder that is a model (it has a manifest) is ever deleted if !home.join("face-model.json").is_file() { @@ -274,7 +266,7 @@ fn enable(s: &mut Session, p: &Value) -> Result { pub fn specs() -> Vec { vec![ - cmd!(query "faces.models.list", "Face Models", [], None, "{} → {dir, enabled, embedder, runtime, models: [{id, name, role, licence{name, commercial, url, notice}, provenance, source, sizeBytes, known, bundled, installed, selected}]}", always, list), + cmd!(query "faces.models.list", "Face Models", [], None, "{} → {dir, enabled, embedder, runtime, models: [{id, name, role, licence{name, commercial, url, notice}, provenance, source, sizeBytes, known, installed, selected}]}", always, list), cmd!(query "faces.models.inspect", "Inspect Face Model File", [], None, "{path} → what a .onnx file is: {kind: known | draft | unsupported, model, assumptions, reason, alreadyInstalled}; installs nothing", always, inspect), cmd!(query "faces.models.install", "Install Face Model", [], None, "{path, acknowledged: true} — copy a .onnx face recognition model into the models folder. `acknowledged` must be true: the user has been shown its licence (see inspect) and accepted it", always, install), cmd!(query "faces.models.remove", "Remove Face Model", [], None, "{id} — delete an installed model", always, remove), diff --git a/crates/engine/src/tests_face_models.rs b/crates/engine/src/tests_face_models.rs index 1296ac882..e002b5c62 100644 --- a/crates/engine/src/tests_face_models.rs +++ b/crates/engine/src/tests_face_models.rs @@ -37,8 +37,7 @@ fn the_list_knows_the_known_models_and_is_honest_about_the_runtime() { let l = s.execute("faces.models.list", &json!({})).unwrap(); assert_eq!(l["enabled"], false); assert_eq!(l["runtime"], false); - assert_eq!(find(&l, "yunet-2023mar")["bundled"], true); - assert_eq!(find(&l, "yunet-2023mar")["installed"], true); + assert_eq!(find(&l, "yunet-2023mar")["installed"], false, "nothing ships with LightCraft"); assert_eq!(find(&l, "auraface-v1")["installed"], false); assert_eq!(find(&l, "auraface-v1")["licence"]["commercial"], "yes"); assert_eq!(find(&l, "sface-2021dec")["licence"]["commercial"], "unknown"); diff --git a/crates/faces/src/known.rs b/crates/faces/src/known.rs index a3e563ede..b5c73d5fc 100644 --- a/crates/faces/src/known.rs +++ b/crates/faces/src/known.rs @@ -1,17 +1,13 @@ //! The models LightCraft recognises by their SHA-256, with what is honestly known about each. //! //! The licence and provenance texts here are what the user reads before enabling a model, so they say what -//! is *not* known too. Weights are never bundled except where the bundling decision says so (the YuNet -//! detector, 232 KB, MIT); everything else is an opt-in the user installs themselves. +//! is *not* known too. Weights are never part of LightCraft: every model is an opt-in the user installs. use crate::manifest::{Colour, Commercial, InputSpec, Licence, ModelManifest, OutputSpec, Resize, Role, Thresholds}; /// Detector output decoders built into LightCraft. pub const DECODERS: &[&str] = &["yunet-v2"]; -/// Models that ship inside LightCraft (they are not installed by the user). -pub const BUNDLED: &[&str] = &["yunet-2023mar"]; - pub const YUNET_SHA256: &str = "8f2383e4dd3cfbb4553ea8718107fc0423210dc964f9f4280604804ed2552fa4"; pub const SFACE_SHA256: &str = "0ba9fbfa01b5270c96627c4ef784da859931e02f04419c829e83484087c34e79"; pub const AURAFACE_SHA256: &str = "a7933ea5330113b01c9b60351d8f4c33003f145d8470ac5f0e52ee2effe25c60"; @@ -20,7 +16,7 @@ fn licence(name: &str, commercial: Commercial, url: &str, notice: &str) -> Licen Licence { name: name.into(), commercial, url: Some(url.into()), notice: notice.into() } } -/// YuNet 2023mar (OpenCV Zoo): the face detector LightCraft bundles. +/// YuNet 2023mar (OpenCV Zoo): a small face detector. pub fn yunet() -> ModelManifest { ModelManifest { id: "yunet-2023mar".into(), @@ -31,7 +27,7 @@ pub fn yunet() -> ModelManifest { "MIT", Commercial::Yes, "https://github.com/opencv/opencv_zoo/blob/main/models/face_detection_yunet/LICENSE", - "MIT licence, copyright Shiqi Yu. Bundled with LightCraft.", + "MIT licence, copyright Shiqi Yu.", ), source: Some("https://github.com/opencv/opencv_zoo/tree/main/models/face_detection_yunet".into()), sha256: Some(YUNET_SHA256.into()), diff --git a/crates/ui-egui/src/panels/faces.rs b/crates/ui-egui/src/panels/faces.rs index ab321affa..5dad9bfcf 100644 --- a/crates/ui-egui/src/panels/faces.rs +++ b/crates/ui-egui/src/panels/faces.rs @@ -145,13 +145,11 @@ fn model_row(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, m: &Value) }); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if installed { - if m["bundled"] != true { - let r = ui.button("Remove"); - register(ui.ctx(), format!("faces:remove:{id}"), r.rect); - if r.clicked() { - let _ = app.run("faces.models.remove", json!({"id": id})); - app.caches.faces_epoch += 1; - } + let r = ui.button("Remove"); + register(ui.ctx(), format!("faces:remove:{id}"), r.rect); + if r.clicked() { + let _ = app.run("faces.models.remove", json!({"id": id})); + app.caches.faces_epoch += 1; } if !selected { let r = ui.button("Use");