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:
2026-09-11 22:12:53 +02:00
parent adf5d6cdd9
commit 4f31123b0c
20 changed files with 468 additions and 122 deletions
+14 -5
View File
@@ -269,11 +269,13 @@ fn android_main(app: slint::android::AndroidApp) {
/// thousand of them is an ANR by definition — the system puts "DarkRoom isn't
/// responding" over a window that has never painted, and offers to kill it.
///
/// This copies **41 MB** on the first launch after an install: 24.9 MB of scene
/// This copied **41 MB** on the first launch after an install: 24.9 MB of scene
/// model, 13.6 MB of embedder, 2.5 MB of detector, each read whole out of the
/// APK and written to `/data`. v0.10.0 added the scene model, which is 60% of
/// APK and written to `/data`. v0.10.0 added the scene model, which was 60% of
/// that total; v0.10.0 is the release the ANR appeared in, and the 8,010 minor
/// faults in its report are what 41 MB of freshly touched pages looks like.
/// The two further detectors the settings page offers since have made it
/// 61 MB, which is the same argument with a larger number.
///
/// So it runs on a worker (NFR-ARCH-1: nothing blocking on the UI executor) and
/// this function returns as soon as the thread is running. Nothing on the
@@ -317,8 +319,15 @@ fn unpack_bundled_models(app: &slint::android::AndroidApp) {
// 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] = [
//
// Three detectors, because which one runs is a setting
// (`FaceDetector`, docs/faces.md §12.3) and a tablet has no other way to
// obtain the one it was not shipped with. Twenty megabytes of APK for
// the choice; the embedder is the same for all three.
const BUNDLED: [(&std::ffi::CStr, &str); 7] = [
(c"models/scrfd_500m_640.onnx", "scrfd_500m_640.onnx"),
(c"models/scrfd_2.5g_640.onnx", "scrfd_2.5g_640.onnx"),
(c"models/scrfd_10g_640.onnx", "scrfd_10g_640.onnx"),
(c"models/arcface_mbf_b1.onnx", "arcface_mbf_b1.onnx"),
(c"models/yolo26s-sem-ade20k.onnx", "yolo26s-sem-ade20k.onnx"),
(
@@ -334,8 +343,8 @@ fn unpack_bundled_models(app: &slint::android::AndroidApp) {
for (asset_path, name) in BUNDLED {
let dest = dir.join(name);
// Already unpacked. Not re-read on every launch: this is 41 MB of
// copying across the five entries, and the file does not change without
// Already unpacked. Not re-read on every launch: this is 61 MB of
// copying across the seven entries, and the file does not change without
// the APK changing, at which point the install wiped it anyway. It
// matters more now than it did — a launch that skips every entry here
// costs nothing at all, which is what makes the second launch after an