Let the user choose which SCRFD finds their faces
faces.md §12.3 measured what the cheapest detector costs: the small faces in every group shot, and a dog embedded a dozen times. Which trade is right depends on the machine doing the sweep — a desktop left overnight and a tablet on a battery want different answers — so the detector is now a per-device setting, Fast / Balanced / Thorough on the settings page beside the indexing button, persisted with the rest of the settings file. A detector is half of a model id. Every face, marker, shard and calibration is keyed on faces.model_id precisely so that a model change is a new id and a re-index rather than a silent change under existing data, and a detector change is a model change: it decides which faces exist and where the landmarks that align them land. So each choice names its own pipeline. 500M keeps the bare "w600k_mbf" every existing library was written under, so an upgrade disturbs nothing; the others are qualified. Choosing one restarts coverage from zero under the new id, the sweep re-detects, confirmed names carry across by box overlap, and the sync shards are keyed by the same id so a peer on another setting neither adopts nor pollutes them. The library controller carries the id into the sync the same way it carries the cache budget, because the sync starts from places that have no settings in reach. All three shape-fixed exports ship — APK, Arch, Flatpak — since a tablet has no other way to obtain the one it was not installed with; the APK grows by twenty megabytes for the choice.
This commit is contained in:
+61
-27
@@ -21,10 +21,14 @@ use crate::{AppWindow, IdentityFace, IdentityPerson};
|
||||
|
||||
/// Which model's faces the screen is looking at.
|
||||
///
|
||||
/// A constant for now because exactly one model is supported at a time; it is
|
||||
/// named rather than inlined because every query in this file is keyed on it,
|
||||
/// and a library indexed across a model change holds faces from both.
|
||||
pub const MODEL_ID: &str = "w600k_mbf";
|
||||
/// Every query in this file is keyed on it, and a library indexed across a
|
||||
/// detector change holds faces from both pipelines: the screen shows the one
|
||||
/// the settings page currently names (`FaceDetector::model_id`), read at each
|
||||
/// use rather than captured once, because the page can change it while the
|
||||
/// screen is open.
|
||||
pub fn model_id(settings: &crate::settings_ui::SettingsController) -> String {
|
||||
settings.snapshot().faces.detector.model_id().to_string()
|
||||
}
|
||||
|
||||
/// Screen state that outlives a single callback.
|
||||
#[derive(Default)]
|
||||
@@ -148,6 +152,7 @@ pub fn refresh(
|
||||
ctl: &Rc<IdentityController>,
|
||||
catalog: &Rc<RefCell<Option<Catalog>>>,
|
||||
store: Option<Rc<ThumbStore>>,
|
||||
model_id: &str,
|
||||
) {
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else {
|
||||
@@ -156,7 +161,7 @@ pub fn refresh(
|
||||
return;
|
||||
};
|
||||
|
||||
let view = match identity::load_people(cat, MODEL_ID) {
|
||||
let view = match identity::load_people(cat, model_id) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log::warn!("identity: reading people: {e}");
|
||||
@@ -268,7 +273,7 @@ pub fn refresh(
|
||||
|
||||
window.set_identity_picked(ctl.picked.borrow().len() as i32);
|
||||
drop(borrow);
|
||||
refresh_coverage(window, catalog, store.as_deref());
|
||||
refresh_coverage(window, catalog, store.as_deref(), model_id);
|
||||
|
||||
// Last, so a portrait cannot delay anything above it.
|
||||
if let Some(store) = store {
|
||||
@@ -379,13 +384,14 @@ pub fn refresh_coverage(
|
||||
window: &AppWindow,
|
||||
catalog: &Rc<RefCell<Option<Catalog>>>,
|
||||
store: Option<&ThumbStore>,
|
||||
model_id: &str,
|
||||
) {
|
||||
let borrow = catalog.borrow();
|
||||
let (Some(cat), Some(store)) = (borrow.as_ref(), store) else {
|
||||
window.set_identity_coverage(Default::default());
|
||||
return;
|
||||
};
|
||||
match crate::faces::audit(cat, store, MODEL_ID) {
|
||||
match crate::faces::audit(cat, store, model_id) {
|
||||
Ok(a) => {
|
||||
window.set_identity_coverage(a.summary().into());
|
||||
// Complete means nothing left to index, not "every image has a
|
||||
@@ -591,8 +597,8 @@ pub fn wire<S, M, P>(
|
||||
// a rejection changes two counts — and a model patched by hand would
|
||||
// drift from the catalog in exactly the cases that matter.
|
||||
macro_rules! reload {
|
||||
($w:expr, $ctl:expr, $catalog:expr, $store:expr) => {
|
||||
refresh(&$w, &$ctl, &$catalog, $store())
|
||||
($w:expr, $ctl:expr, $catalog:expr, $store:expr, $settings:expr) => {
|
||||
refresh(&$w, &$ctl, &$catalog, $store(), &model_id(&$settings))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -601,6 +607,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
let models_present = models.clone();
|
||||
window.on_identity_open(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
@@ -615,7 +622,7 @@ pub fn wire<S, M, P>(
|
||||
// A fact about the filesystem, so it is re-checked on every open
|
||||
// rather than cached: the user may have just put the models there.
|
||||
w.set_identity_model_missing(models_present().is_none());
|
||||
reload!(w, ctl, catalog, store);
|
||||
reload!(w, ctl, catalog, store, settings);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -642,6 +649,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
window.on_identity_person_picked(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// Before the selection moves: a name typed into the field and never
|
||||
@@ -653,7 +661,7 @@ pub fn wire<S, M, P>(
|
||||
// The offer was about the person being navigated away from. Left
|
||||
// up, its "Merge" would fold whoever is selected *now*.
|
||||
clear_merge_offer(&w, &ctl);
|
||||
reload!(w, ctl, catalog, store);
|
||||
reload!(w, ctl, catalog, store, settings);
|
||||
reset_name_field(&w);
|
||||
});
|
||||
}
|
||||
@@ -663,6 +671,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
window.on_identity_rename(move |name| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(person) = ctl.selected.get() else {
|
||||
@@ -697,7 +706,7 @@ pub fn wire<S, M, P>(
|
||||
Err(e) => log::warn!("identity: looking for a namesake: {e}"),
|
||||
}
|
||||
}
|
||||
reload!(w, ctl, catalog, store);
|
||||
reload!(w, ctl, catalog, store, settings);
|
||||
// `rename` trims; the field should show what was actually stored
|
||||
// rather than the spacing the user happened to type.
|
||||
reset_name_field(&w);
|
||||
@@ -714,6 +723,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
window.on_identity_merge_accept(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let (Some(target), Some(source)) = (ctl.merge_offer.get(), ctl.selected.get()) else {
|
||||
@@ -733,7 +743,7 @@ pub fn wire<S, M, P>(
|
||||
}
|
||||
}
|
||||
clear_merge_offer(&w, &ctl);
|
||||
reload!(w, ctl, catalog, store);
|
||||
reload!(w, ctl, catalog, store, settings);
|
||||
reset_name_field(&w);
|
||||
});
|
||||
}
|
||||
@@ -767,6 +777,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
window.on_identity_confirm_face(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(person) = ctl.selected.get() else {
|
||||
@@ -777,7 +788,7 @@ pub fn wire<S, M, P>(
|
||||
log::warn!("identity: confirm: {e}");
|
||||
}
|
||||
}
|
||||
reload!(w, ctl, catalog, store);
|
||||
reload!(w, ctl, catalog, store, settings);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -786,6 +797,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
window.on_identity_reject_face(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(person) = ctl.selected.get() else {
|
||||
@@ -796,7 +808,7 @@ pub fn wire<S, M, P>(
|
||||
log::warn!("identity: reject: {e}");
|
||||
}
|
||||
}
|
||||
reload!(w, ctl, catalog, store);
|
||||
reload!(w, ctl, catalog, store, settings);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -827,6 +839,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
window.on_identity_confirm_all(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(person) = ctl.selected.get() else {
|
||||
@@ -838,7 +851,7 @@ pub fn wire<S, M, P>(
|
||||
Err(e) => log::warn!("identity: confirm all: {e}"),
|
||||
}
|
||||
}
|
||||
reload!(w, ctl, catalog, store);
|
||||
reload!(w, ctl, catalog, store, settings);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -847,6 +860,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
window.on_identity_split_picked(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(person) = ctl.selected.get() else {
|
||||
@@ -870,7 +884,7 @@ pub fn wire<S, M, P>(
|
||||
}
|
||||
}
|
||||
ctl.clear_picks();
|
||||
reload!(w, ctl, catalog, store);
|
||||
reload!(w, ctl, catalog, store, settings);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -893,7 +907,7 @@ pub fn wire<S, M, P>(
|
||||
w.set_identity_previewing(true);
|
||||
*ctl.preview.borrow_mut() = Some(crate::faces::spawn_grouping_preview(
|
||||
catalog_path,
|
||||
MODEL_ID.to_string(),
|
||||
model_id(&settings),
|
||||
settings.snapshot().faces,
|
||||
));
|
||||
|
||||
@@ -960,7 +974,7 @@ pub fn wire<S, M, P>(
|
||||
w.set_identity_regroup_status("regrouping…".into());
|
||||
*ctl.regroup.borrow_mut() = Some(crate::faces::spawn_recluster(
|
||||
catalog_path,
|
||||
MODEL_ID.to_string(),
|
||||
model_id(&settings_for_regroup),
|
||||
settings_for_regroup.snapshot().faces,
|
||||
));
|
||||
|
||||
@@ -972,6 +986,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl_tick = ctl.clone();
|
||||
let catalog_tick = catalog.clone();
|
||||
let store_tick = store.clone();
|
||||
let settings_tick = settings_for_regroup.clone();
|
||||
timer.start(
|
||||
slint::TimerMode::Repeated,
|
||||
Duration::from_millis(100),
|
||||
@@ -1020,7 +1035,13 @@ pub fn wire<S, M, P>(
|
||||
// makes new people; a stale cache would draw the
|
||||
// previous pass's faces beside the new groups.
|
||||
ctl_tick.covers.borrow_mut().clear();
|
||||
refresh(&w, &ctl_tick, &catalog_tick, store_tick());
|
||||
refresh(
|
||||
&w,
|
||||
&ctl_tick,
|
||||
&catalog_tick,
|
||||
store_tick(),
|
||||
&model_id(&settings_tick),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1036,6 +1057,7 @@ pub fn wire<S, M, P>(
|
||||
let models = models.clone();
|
||||
let paths = paths.clone();
|
||||
let gpu = gpu.clone();
|
||||
let settings_for_sweep = settings.clone();
|
||||
window.on_identity_index(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if ctl.sweep.borrow().is_some() {
|
||||
@@ -1066,7 +1088,7 @@ pub fn wire<S, M, P>(
|
||||
store_dir,
|
||||
detector,
|
||||
embedder,
|
||||
MODEL_ID.to_string(),
|
||||
model_id(&settings_for_sweep),
|
||||
dr_face::DetectOptions::default(),
|
||||
gpu,
|
||||
));
|
||||
@@ -1083,6 +1105,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl_tick = ctl.clone();
|
||||
let catalog_tick = catalog.clone();
|
||||
let store_tick = store.clone();
|
||||
let settings_tick = settings_for_sweep.clone();
|
||||
timer.start(
|
||||
slint::TimerMode::Repeated,
|
||||
Duration::from_millis(250),
|
||||
@@ -1165,7 +1188,13 @@ pub fn wire<S, M, P>(
|
||||
// grouped, so a sweep ending with an unchanged people
|
||||
// rail is expected. The coverage line is what shows it
|
||||
// worked, and Regroup is the next step.
|
||||
refresh(&w, &ctl_tick, &catalog_tick, store_tick());
|
||||
refresh(
|
||||
&w,
|
||||
&ctl_tick,
|
||||
&catalog_tick,
|
||||
store_tick(),
|
||||
&model_id(&settings_tick),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1180,6 +1209,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
window.on_identity_stop_indexing(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// Dropping the receiver *is* the cancellation: the worker's next
|
||||
@@ -1190,7 +1220,7 @@ pub fn wire<S, M, P>(
|
||||
a.finish("stopped");
|
||||
}
|
||||
w.set_identity_indexing(false);
|
||||
reload!(w, ctl, catalog, store);
|
||||
reload!(w, ctl, catalog, store, settings);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1198,9 +1228,10 @@ pub fn wire<S, M, P>(
|
||||
let weak = window.as_weak();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
window.on_identity_check_coverage(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
refresh_coverage(&w, &catalog, store().as_deref());
|
||||
refresh_coverage(&w, &catalog, store().as_deref(), &model_id(&settings));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1209,6 +1240,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
window.on_identity_ignore_person(move |id, on| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let person = PersonId(id.max(0) as u64);
|
||||
@@ -1225,7 +1257,7 @@ pub fn wire<S, M, P>(
|
||||
ctl.selected.set(None);
|
||||
ctl.clear_picks();
|
||||
}
|
||||
reload!(w, ctl, catalog, store);
|
||||
reload!(w, ctl, catalog, store, settings);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1234,10 +1266,11 @@ pub fn wire<S, M, P>(
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
window.on_identity_toggle_show_ignored(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
ctl.show_ignored.set(!ctl.show_ignored.get());
|
||||
reload!(w, ctl, catalog, store);
|
||||
reload!(w, ctl, catalog, store, settings);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1246,6 +1279,7 @@ pub fn wire<S, M, P>(
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
let store = store.clone();
|
||||
let settings = settings.clone();
|
||||
window.on_identity_delete_all(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(cat) = catalog.borrow().as_ref() {
|
||||
@@ -1257,7 +1291,7 @@ pub fn wire<S, M, P>(
|
||||
ctl.selected.set(None);
|
||||
ctl.clear_picks();
|
||||
ctl.covers.borrow_mut().clear();
|
||||
reload!(w, ctl, catalog, store);
|
||||
reload!(w, ctl, catalog, store, settings);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user