//! 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 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"]; /// The id of the face detector LightCraft runs (see [`yunet`]). pub const YUNET_ID: &str = "yunet-2023mar"; pub const YUNET_SHA256: &str = "8f2383e4dd3cfbb4553ea8718107fc0423210dc964f9f4280604804ed2552fa4"; pub const SFACE_SHA256: &str = "0ba9fbfa01b5270c96627c4ef784da859931e02f04419c829e83484087c34e79"; pub const AURAFACE_SHA256: &str = "a7933ea5330113b01c9b60351d8f4c33003f145d8470ac5f0e52ee2effe25c60"; fn licence(name: &str, commercial: Commercial, url: &str, notice: &str) -> Licence { Licence { name: name.into(), commercial, url: Some(url.into()), notice: notice.into() } } /// YuNet 2023mar (OpenCV Zoo): a small face detector. pub fn yunet() -> ModelManifest { ModelManifest { id: YUNET_ID.into(), name: "YuNet (face detector)".into(), version: "2023mar".into(), role: Role::Detector, licence: licence( "MIT", Commercial::Yes, "https://github.com/opencv/opencv_zoo/blob/main/models/face_detection_yunet/LICENSE", "MIT licence, copyright Shiqi Yu. A separate opt-in download.", ), source: Some("https://github.com/opencv/opencv_zoo/tree/main/models/face_detection_yunet".into()), sha256: Some(YUNET_SHA256.into()), size_bytes: Some(232_589), provenance: "Trained on WIDER FACE, whose terms are not settled by the weights' MIT licence. A detector: it finds faces, it does not identify anyone." .into(), input: InputSpec { width: 640, height: 640, colour: Colour::Bgr, mean: [0.0; 3], std: [1.0; 3], resize: Resize::Letterbox }, output: OutputSpec::Detector { decoder: "yunet-v2".into() }, thresholds: Thresholds { match_cosine: None, score: Some(0.6), nms_iou: Some(0.3) }, speed: None, } } /// SFace 2021dec (OpenCV Zoo). Its match threshold is a starting point: on 755 named faces (busts, matched across /// shots more than five seconds apart) 0.55 gave 97.5% right suggestions for 88% of faces; `faces.evaluate` checks it on /// your own photos. pub fn sface() -> ModelManifest { ModelManifest { id: "sface-2021dec".into(), name: "SFace (face recogniser)".into(), version: "2021dec".into(), role: Role::Embedder, licence: licence( "Apache-2.0 (as labelled by OpenCV Zoo)", Commercial::Unknown, "https://github.com/opencv/opencv_zoo/tree/main/models/face_recognition_sface", "Labelled Apache-2.0, but what it was trained on is not documented, and two questions about commercial use (opencv_zoo issues 313 and 318) are unanswered. Fine to try for yourself; do not redistribute.", ), source: Some("https://github.com/opencv/opencv_zoo/tree/main/models/face_recognition_sface".into()), sha256: Some(SFACE_SHA256.into()), size_bytes: Some(38_696_353), provenance: "Undocumented. The original SFace repository mentions CASIA-WebFace, VGGFace2 and MS1MV2.".into(), input: InputSpec { width: 112, height: 112, colour: Colour::Rgb, mean: [0.0; 3], std: [1.0; 3], resize: Resize::Stretch }, output: OutputSpec::Embedding { dim: 128 }, thresholds: Thresholds { match_cosine: Some(0.55), ..Thresholds::default() }, speed: Some(6.9), } } /// AuraFace v1 `glintr100` (fal.ai). Starting-point threshold: on the same 755 faces 0.40 gave 96.6% right suggestions /// for 56% of faces (0.30: 97.3% for 85%). pub fn auraface() -> ModelManifest { ModelManifest { id: "auraface-v1".into(), name: "AuraFace v1 (face recogniser)".into(), version: "1".into(), role: Role::Embedder, licence: licence( "Apache-2.0", Commercial::Yes, "https://huggingface.co/fal/AuraFace-v1", "Apache-2.0. Its training data is described only as a commercial dataset, with no dataset named and no consent statement, and it covers some ethnicities less well. About 261 MB, and slow on a CPU.", ), source: Some("https://huggingface.co/fal/AuraFace-v1".into()), sha256: Some(AURAFACE_SHA256.into()), size_bytes: Some(260_694_151), provenance: "Undisclosed: \"a commercial dataset comprising face images from various sources\".".into(), input: InputSpec::default(), output: OutputSpec::Embedding { dim: 512 }, thresholds: Thresholds { match_cosine: Some(0.40), ..Thresholds::default() }, speed: Some(1.0), } } /// Every model we know by hash. pub fn all() -> Vec { vec![yunet(), sface(), auraface()] } /// The known model whose file has this SHA-256 (lowercase hex). pub fn lookup(sha256: &str) -> Option { all().into_iter().find(|m| m.sha256.as_deref() == Some(sha256)) } /// Where the user's "Download" button fetches a model from. The URL is pinned to a commit of the model's own /// repository, and what arrives is checked against the manifest's size and SHA-256 before anything uses it. #[derive(Clone, Debug, PartialEq)] pub struct Download { pub id: String, pub url: String, /// The name the file is kept under while it waits for the user to accept its terms. pub file_name: String, pub size_bytes: u64, pub sha256: String, } /// Pinned download addresses by model id. Only models whose terms allow us to point at them are here: a model /// that is marked non-commercial is never offered for download (the user brings the file themselves). const SOURCES: &[(&str, &str, &str)] = &[ ( YUNET_ID, "https://github.com/opencv/opencv_zoo/raw/25f423d0e04c31a17254620e58febd7386da523b/models/face_detection_yunet/face_detection_yunet_2023mar.onnx", "face_detection_yunet_2023mar.onnx", ), ( "sface-2021dec", "https://github.com/opencv/opencv_zoo/raw/25f423d0e04c31a17254620e58febd7386da523b/models/face_recognition_sface/face_recognition_sface_2021dec.onnx", "face_recognition_sface_2021dec.onnx", ), ("auraface-v1", "https://huggingface.co/fal/AuraFace-v1/resolve/af6d057c9b0ec4071d4c49c80e3539258798b609/glintr100.onnx", "glintr100.onnx"), ]; /// How to download the model with this id, if LightCraft offers to. pub fn download(id: &str) -> Option { let (_, url, file_name) = SOURCES.iter().find(|(i, _, _)| *i == id)?; let m = all().into_iter().find(|m| m.id == id)?; if m.licence.commercial == Commercial::No { return None; } Some(Download { id: m.id, url: (*url).into(), file_name: (*file_name).into(), size_bytes: m.size_bytes?, sha256: m.sha256? }) } /// "github.com" for a download address: what the user is told the file comes from. pub fn host(url: &str) -> &str { url.strip_prefix("https://").and_then(|r| r.split('/').next()).unwrap_or("") } impl Download { /// The address without the file name (the downloader's "mirror": it fetches `/`). pub fn base(&self) -> &str { self.url.strip_suffix(self.file_name.as_str()).map_or(self.url.as_str(), |b| b.trim_end_matches('/')) } /// The site the file comes from, as the user is told. pub fn host(&self) -> &str { host(&self.url) } } #[cfg(test)] mod tests { use super::*; use crate::manifest::validate; #[test] fn known_models_are_valid_and_unique() { let all = all(); for m in &all { assert_eq!(validate(m), Ok(()), "{}", m.id); } let mut ids: Vec<_> = all.iter().map(|m| &m.id).collect(); ids.sort(); ids.dedup(); assert_eq!(ids.len(), all.len(), "unique ids"); let mut hashes: Vec<_> = all.iter().filter_map(|m| m.sha256.as_deref()).collect(); hashes.sort(); hashes.dedup(); assert_eq!(hashes.len(), all.len(), "unique hashes"); } #[test] fn lookup_finds_by_hash() { assert_eq!(lookup(AURAFACE_SHA256).map(|m| m.id), Some("auraface-v1".to_string())); assert_eq!(lookup(YUNET_SHA256).map(|m| m.role), Some(Role::Detector)); assert!(lookup(&"0".repeat(64)).is_none()); assert!(lookup("").is_none()); } #[test] fn downloads_are_pinned_checked_and_only_for_models_we_may_point_at() { for m in all() { let Some(d) = download(&m.id) else { continue }; assert!(d.url.starts_with("https://"), "{}: https only", d.id); assert!(!d.url.contains("/main/") && !d.url.contains("/master/"), "{}: pinned to a commit, not a branch", d.id); assert!(d.file_name.ends_with(".onnx") && !d.file_name.contains(['/', '\\']), "{}", d.file_name); assert!(d.url.ends_with(&format!("/{}", d.file_name)), "{}: the address ends in the file's name", d.id); assert!(d.base().starts_with("https://") && !d.base().ends_with('/') && !d.base().ends_with(".onnx"), "{}", d.base()); assert_eq!((Some(d.size_bytes), Some(d.sha256.as_str())), (m.size_bytes, m.sha256.as_deref()), "{}", d.id); assert!(m.licence.commercial != Commercial::No, "{} is non-commercial and must not be offered", d.id); } assert_eq!(download("yunet-2023mar").map(|d| d.host().to_string()).as_deref(), Some("github.com")); assert_eq!(download("sface-2021dec").map(|d| host(&d.url).to_string()).as_deref(), Some("github.com")); assert_eq!(download("auraface-v1").map(|d| host(&d.url).to_string()).as_deref(), Some("huggingface.co")); // unknown ids and path tricks have nothing to download for id in ["nope", "", "../sface-2021dec", "SFACE-2021DEC"] { assert!(download(id).is_none(), "{id}"); } // every address in the table belongs to a known model for (id, _, _) in SOURCES { assert!(all().iter().any(|m| m.id == *id), "{id}"); } } #[test] fn unresolved_models_say_so() { assert_eq!(sface().licence.commercial, Commercial::Unknown); assert!(sface().provenance.to_lowercase().contains("undocumented")); assert!(auraface().provenance.to_lowercase().contains("undisclosed")); } }