Give the phone the model it had no way to obtain
Face indexing was compiled into the APK all along — dr-ui takes dr-face with `inference` on every target, so SCRFD, alignment, MBF, calibration and clustering were all in there. What was missing was the weights, and on Android there was no way to supply them. Route C (docs/faces.md §2.2) says the user obtains the model and the app loads it. On a desktop that is a real gesture: drop two files in ~/.local/share/darkroom/models/ and indexing starts working. On Android it is not a gesture at all. `internal_data_path` is app-private, `run-as` needs a debuggable build, and the in-app fetch route C specifies was never built — so the settings page reported "no face model is installed" on every launch with nothing behind the message. Not "off until you supply weights"; off. So the shape-fixed pair goes into LFS under the APK's assets, assemble-apk.sh copies it into the package, and `android_main` unpacks it to the shared models directory before anything asks whether a model is present. Three things that are not incidental: The models directory is now shared across accounts rather than per-account. Weights are identified by `faces.model_id`, not by who is signed in, so two accounts had no reason to hold two copies — and the unpack runs before any session exists to key a per-account path off. `face_models` still prefers a per-account directory when one is populated, so anyone mid-migration keeps the ability to pin one library to its own pair. The unpack writes under a temporary name and renames. `face_models` decides availability on `is_file()` alone, so a copy truncated by the process being killed would leave a file that passes that test and fails inside tract — reported to the user as a broken model rather than a missing one. assemble-apk.sh refuses an LFS pointer. At ~130 bytes it looks exactly like a model to `cp`, and unchecked it reaches the device and fails in the graph loader instead of telling someone to run `git lfs pull` — the same guard dr-segment's build script applies to yolo26n-seg.onnx. The licensing half is unchanged and recorded in §2.2a: the InsightFace grant is research-only, this is a private repository and a self-installed build, and these files come back out before anything is published. The weights are still not a cargo build input — dr-face has no `models/` directory and no `embedded-model` feature, and nothing in the build reads them. The APK assembly step copies two files and is the only thing in the tree that knows they exist. Verified on device: both models unpack on first launch (2524817 and 13616095 bytes) and the APK carries them at assets/models/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# APK assets
|
||||
|
||||
`models/` holds the two face models, in LFS. `docker/android/assemble-apk.sh` copies them into the
|
||||
APK and `android_main` unpacks them to the shared models directory on first launch, which is the only
|
||||
way a phone can be handed a model at all — app-private storage has no user-reachable route into it,
|
||||
and the in-app fetch docs/faces.md §2.2 specifies is unbuilt.
|
||||
|
||||
models/scrfd_500m_640.onnx 2.5 MB
|
||||
models/arcface_mbf_b1.onnx 13 MB
|
||||
|
||||
**A clone without git-lfs gets a ~130-byte pointer file where each model should be.** `assemble-apk.sh`
|
||||
checks for exactly that and refuses, rather than bundling the pointer and failing inside tract on the
|
||||
device — the same guard `dr-segment`'s build script applies to `yolo26n-seg.onnx`. Fix it with
|
||||
`git lfs pull`.
|
||||
|
||||
These are not what InsightFace ships. They came from `buffalo_sc.zip` and `buffalo_s.zip` on the
|
||||
InsightFace v0.7 release with their input dimensions pinned, because tract cannot parse either graph
|
||||
while they are dynamic:
|
||||
|
||||
./tools/fix-face-model-shapes.sh det_500m.onnx models/scrfd_500m_640.onnx --input input.1=1,3,640,640
|
||||
./tools/fix-face-model-shapes.sh w600k_mbf.onnx models/arcface_mbf_b1.onnx --dim None=1
|
||||
|
||||
The weights carry a non-commercial research-only grant. They are here because this is a private
|
||||
repository and a self-installed build; they come back out before anything is published, and the
|
||||
restriction binds whoever uses the app, not only the project. docs/faces.md §2.2a is the decision.
|
||||
Binary file not shown.
Binary file not shown.
@@ -48,6 +48,9 @@ fn android_main(app: slint::android::AndroidApp) {
|
||||
None => log::error!("no internal data path; settings will not persist"),
|
||||
}
|
||||
|
||||
// After the data dir and before anything asks whether a model is present.
|
||||
install_bundled_face_models(&app);
|
||||
|
||||
if let Err(e) = slint::android::init(app) {
|
||||
log::error!("Slint Android backend failed to initialise: {e}");
|
||||
return;
|
||||
@@ -61,3 +64,78 @@ fn android_main(app: slint::android::AndroidApp) {
|
||||
log::error!("DarkRoom exited with error: {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Unpack the face models the APK carries, if it carries any.
|
||||
///
|
||||
/// # Why Android needs this and no other platform does
|
||||
///
|
||||
/// The weights are not a build input and are not in the repository — the
|
||||
/// InsightFace grant is research-only and incompatible with this project's
|
||||
/// licence (docs/faces.md §2), so a desktop user fetches them, runs
|
||||
/// `tools/fix-face-model-shapes.sh` over them, and drops the result into
|
||||
/// `~/.local/share/darkroom/models/`. **That gesture does not exist on
|
||||
/// Android.** `internal_data_path` is app-private, `run-as` needs a debuggable
|
||||
/// build, and there is no picker and no fetch in the app, so a phone had no way
|
||||
/// to acquire a model at all and face indexing reported itself permanently off.
|
||||
///
|
||||
/// So a locally-built APK may carry the pair in `assets/models/`, which
|
||||
/// `assemble-apk.sh` includes when the tree has them and omits when it does
|
||||
/// not. Nothing changes about what the repository holds or what a published
|
||||
/// build could redistribute; this only gives a self-built APK the same route a
|
||||
/// desktop build has always had.
|
||||
///
|
||||
/// Absent assets are the ordinary case, not an error — the same quiet "no model
|
||||
/// installed" state a fresh desktop install is in.
|
||||
#[cfg(target_os = "android")]
|
||||
fn install_bundled_face_models(app: &slint::android::AndroidApp) {
|
||||
use std::io::Read;
|
||||
|
||||
// The **shape-fixed** names, matching what `library::face_models` looks
|
||||
// for: tract cannot parse either InsightFace graph with its dynamic input
|
||||
// dimension, so what ships here has already been through
|
||||
// `tools/fix-face-model-shapes.sh`.
|
||||
const BUNDLED: [(&std::ffi::CStr, &str); 2] = [
|
||||
(c"models/scrfd_500m_640.onnx", "scrfd_500m_640.onnx"),
|
||||
(c"models/arcface_mbf_b1.onnx", "arcface_mbf_b1.onnx"),
|
||||
];
|
||||
|
||||
let dir = dr_ui::shared_face_models_dir();
|
||||
let assets = app.asset_manager();
|
||||
|
||||
for (asset_path, name) in BUNDLED {
|
||||
let dest = dir.join(name);
|
||||
// Already unpacked. Not re-read on every launch: this is 15 MB through
|
||||
// a decompressor on the startup path, and the file does not change
|
||||
// without the APK changing, at which point the install wiped it anyway.
|
||||
if dest.is_file() {
|
||||
continue;
|
||||
}
|
||||
let Some(mut asset) = assets.open(asset_path) else {
|
||||
log::info!("no bundled {name} in this APK; face indexing stays off");
|
||||
continue;
|
||||
};
|
||||
let mut bytes = Vec::new();
|
||||
if let Err(e) = asset.read_to_end(&mut bytes) {
|
||||
log::error!("bundled {name} could not be read: {e}");
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = std::fs::create_dir_all(&dir) {
|
||||
log::error!("cannot create {}: {e}", dir.display());
|
||||
return;
|
||||
}
|
||||
// Written under a temporary name and renamed, because
|
||||
// `library::face_models` decides face indexing is available on
|
||||
// `is_file()` alone. A truncated write — the process backgrounded and
|
||||
// killed mid-copy — would otherwise leave a file that passes that test
|
||||
// and fails inside tract, reported to the user as a broken model rather
|
||||
// than a missing one.
|
||||
let part = dir.join(format!("{name}.part"));
|
||||
match std::fs::write(&part, &bytes).and_then(|()| std::fs::rename(&part, &dest)) {
|
||||
Ok(()) => log::info!("installed bundled {name} ({} bytes)", bytes.len()),
|
||||
Err(e) => {
|
||||
log::error!("cannot install {name}: {e}");
|
||||
let _ = std::fs::remove_file(&part);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user