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:
@@ -55,6 +55,13 @@ use dr_decode::{Metadata, PreviewSize};
|
||||
|
||||
pub use develop::DevelopSession;
|
||||
|
||||
/// Where a model has to land for face indexing to find it, on any account.
|
||||
///
|
||||
/// Public for the Android entry point, which is the only caller that knows the
|
||||
/// APK may carry a bundled copy and has to write it out before any store is
|
||||
/// opened — see `library::shared_face_models_dir`.
|
||||
pub use library::shared_face_models_dir;
|
||||
|
||||
pub mod launch;
|
||||
pub mod launch_ui;
|
||||
|
||||
|
||||
+49
-24
@@ -916,32 +916,38 @@ pub fn catalog_path(server: &str, user_id: &str) -> PathBuf {
|
||||
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
|
||||
.collect();
|
||||
|
||||
// **Not the cache directory, and on Android that distinction is the whole
|
||||
// point.** Neither `XDG_DATA_HOME` nor `HOME` is set there, so this used
|
||||
// to fall through to `temp_dir()` — which Android resolves to the app's
|
||||
// *cache*, a directory the system deletes without asking under storage
|
||||
// pressure.
|
||||
//
|
||||
// What sits beside this catalog is not disposable. `sidecars/` is the
|
||||
// commit point for every rating and edit made offline (see
|
||||
// `sidecar_cache`), and `outbox/` holds exports the user has been told
|
||||
// succeeded. A day of culling on a train, evicted by the OS before it ever
|
||||
// reached the server, is the worst failure this application can have, and
|
||||
// it would be silent.
|
||||
//
|
||||
// `SessionStore::data_dir()` is the persistent per-app directory the
|
||||
// Android entry point establishes before anything opens a store. On a
|
||||
// desktop it is the XDG config directory, and the two lines below keep the
|
||||
// established XDG *data* location there rather than moving anyone's
|
||||
// catalog.
|
||||
data_root()
|
||||
.join(format!("{slug}-{user_id}"))
|
||||
.join("catalog.sqlite")
|
||||
}
|
||||
|
||||
/// The directory every account's data hangs off.
|
||||
///
|
||||
/// **Not the cache directory, and on Android that distinction is the whole
|
||||
/// point.** Neither `XDG_DATA_HOME` nor `HOME` is set there, so this used
|
||||
/// to fall through to `temp_dir()` — which Android resolves to the app's
|
||||
/// *cache*, a directory the system deletes without asking under storage
|
||||
/// pressure.
|
||||
///
|
||||
/// What sits beside a catalog is not disposable. `sidecars/` is the
|
||||
/// commit point for every rating and edit made offline (see
|
||||
/// `sidecar_cache`), and `outbox/` holds exports the user has been told
|
||||
/// succeeded. A day of culling on a train, evicted by the OS before it ever
|
||||
/// reached the server, is the worst failure this application can have, and
|
||||
/// it would be silent.
|
||||
///
|
||||
/// `SessionStore::data_dir()` is the persistent per-app directory the
|
||||
/// Android entry point establishes before anything opens a store. On a
|
||||
/// desktop it is the XDG config directory, and the two lines below keep the
|
||||
/// established XDG *data* location there rather than moving anyone's
|
||||
/// catalog.
|
||||
fn data_root() -> PathBuf {
|
||||
let base = std::env::var_os("XDG_DATA_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share")))
|
||||
.unwrap_or_else(dr_sync_nextcloud::session::SessionStore::data_dir);
|
||||
|
||||
base.join("darkroom")
|
||||
.join(format!("{slug}-{user_id}"))
|
||||
.join("catalog.sqlite")
|
||||
}
|
||||
|
||||
/// TRACES: FR-NC-10 | NFR-R1
|
||||
@@ -2906,6 +2912,18 @@ pub fn face_models_dir(server: &str, user_id: &str) -> PathBuf {
|
||||
.unwrap_or_else(|| std::env::temp_dir().join("darkroom-models"))
|
||||
}
|
||||
|
||||
/// Where face models live for *every* account on this device.
|
||||
///
|
||||
/// Account-independent, unlike the catalog: a model is identified by
|
||||
/// `faces.model_id` (catalog.md §10.1) and not by who is signed in, so two
|
||||
/// accounts have no reason to hold two 15 MB copies of the same weights. This
|
||||
/// is also the only directory an Android build can populate for itself — the
|
||||
/// entry point extracts the APK's bundled copy here before any store opens,
|
||||
/// and at that moment no session exists to key a per-account path off.
|
||||
pub fn shared_face_models_dir() -> PathBuf {
|
||||
data_root().join("models")
|
||||
}
|
||||
|
||||
/// The detector and embedder files, if both are present.
|
||||
///
|
||||
/// Both or neither: an embedder with no detector has nothing to embed, and a
|
||||
@@ -2915,11 +2933,18 @@ pub fn face_models_dir(server: &str, user_id: &str) -> PathBuf {
|
||||
/// The names are the **shape-fixed** exports, not what InsightFace ships:
|
||||
/// `tools/fix-face-model-shapes.sh` has to run over the originals first,
|
||||
/// because tract cannot parse either graph with a dynamic input.
|
||||
///
|
||||
/// The per-account directory wins over the shared one so a library can be
|
||||
/// pinned to its own weights — a re-index after a model swap is a `model_id`
|
||||
/// change, and someone mid-migration needs the old pair to stay put for one
|
||||
/// account without holding back the other.
|
||||
pub fn face_models(server: &str, user_id: &str) -> Option<(PathBuf, PathBuf)> {
|
||||
let dir = face_models_dir(server, user_id);
|
||||
let detector = dir.join("scrfd_500m_640.onnx");
|
||||
let embedder = dir.join("arcface_mbf_b1.onnx");
|
||||
(detector.is_file() && embedder.is_file()).then_some((detector, embedder))
|
||||
fn pair(dir: PathBuf) -> Option<(PathBuf, PathBuf)> {
|
||||
let detector = dir.join("scrfd_500m_640.onnx");
|
||||
let embedder = dir.join("arcface_mbf_b1.onnx");
|
||||
(detector.is_file() && embedder.is_file()).then_some((detector, embedder))
|
||||
}
|
||||
pair(face_models_dir(server, user_id)).or_else(|| pair(shared_face_models_dir()))
|
||||
}
|
||||
|
||||
/// One grid cell's data, read from the catalog.
|
||||
|
||||
Reference in New Issue
Block a user