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:
@@ -40,6 +40,17 @@ automatically. To force a rebuild without a content change, run the workflow fro
|
||||
The job runs on the host runner rather than in a container, because it needs the Docker daemon and
|
||||
the runner host's cached registry credentials.
|
||||
|
||||
## Face models
|
||||
|
||||
`package.sh` bundles `apps/darkroom-android/android/assets/models/` into the APK, and the entry point
|
||||
unpacks it on first launch. **The models are in LFS**, so a clone without `git lfs pull` has 130-byte
|
||||
pointers there; the build detects that and stops rather than shipping them.
|
||||
|
||||
This exists because Android offers no other route to a model: the directory the app reads from is
|
||||
inside app-private storage, `run-as` needs a debuggable build, and the app has no picker and no
|
||||
fetch. docs/faces.md §2.2a is the decision and its limits — these files come back out before anything
|
||||
is published.
|
||||
|
||||
## Pinned versions
|
||||
|
||||
| Component | Version | Why this one |
|
||||
|
||||
@@ -153,6 +153,37 @@ fi
|
||||
cp "${SO}" "${OUT}/staging/lib/${ABI}/libdarkroom.so"
|
||||
cp "${DEX}" "${OUT}/staging/classes.dex"
|
||||
|
||||
# The face models. Android has no other route to one — app-private storage is
|
||||
# not user-reachable and the in-app fetch is unbuilt (docs/faces.md §2.2a) — so
|
||||
# they go in the APK and `android_main` unpacks them on first launch.
|
||||
#
|
||||
# Through the staging directory rather than aapt2's `-A`: the .so and the dex
|
||||
# already go in with `zip` below, and one mechanism for "extra files in the
|
||||
# APK" is easier to follow than two.
|
||||
ASSETS="${REPO}/apps/darkroom-android/android/assets"
|
||||
# Cleared first: a previous run that died between staging and cleanup would
|
||||
# otherwise leave models in the APK that are no longer in the tree.
|
||||
rm -rf "${OUT}/staging/assets"
|
||||
if compgen -G "${ASSETS}/models/*.onnx" >/dev/null; then
|
||||
# An LFS pointer is ~130 bytes and looks exactly like a model to `cp`. Left
|
||||
# unchecked it reaches the device and fails inside tract, which reports a
|
||||
# broken graph rather than a clone that needs `git lfs pull`. Same guard
|
||||
# dr-segment's build script applies to yolo26n-seg.onnx, and the same
|
||||
# reason.
|
||||
for m in "${ASSETS}"/models/*.onnx; do
|
||||
if [[ "$(stat -c%s "${m}")" -lt 100000 ]]; then
|
||||
echo "error: $(basename "${m}") is $(stat -c%s "${m}") bytes — an LFS pointer, not a model." >&2
|
||||
echo " run: git lfs pull" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
mkdir -p "${OUT}/staging/assets"
|
||||
cp -r "${ASSETS}/models" "${OUT}/staging/assets/"
|
||||
echo " assets: $(ls "${ASSETS}/models" | tr '\n' ' ')"
|
||||
else
|
||||
echo " assets: no face models found (face indexing will be off on the device)"
|
||||
fi
|
||||
|
||||
# -0 "" stores the .so without compression so Android can mmap it directly
|
||||
# (extractNativeLibs=false territory); for a 37 MB library that also keeps
|
||||
# install times sane.
|
||||
@@ -160,6 +191,13 @@ cd "${OUT}/staging"
|
||||
cp "${OUT}/base.apk" "${OUT}/unaligned.apk"
|
||||
zip -q -0 -X "${OUT}/unaligned.apk" "lib/${ABI}/libdarkroom.so"
|
||||
zip -q -X "${OUT}/unaligned.apk" classes.dex
|
||||
# Stored, not deflated: an ONNX graph is mostly incompressible float data, so
|
||||
# deflating it buys a few percent and costs the whole file being inflated into
|
||||
# RAM on the way out. AAssetManager reads a stored entry straight from the
|
||||
# mapped APK.
|
||||
if [[ -d assets ]]; then
|
||||
zip -q -0 -X -r "${OUT}/unaligned.apk" assets
|
||||
fi
|
||||
|
||||
# zipalign before signing: apksigner preserves alignment, the reverse order
|
||||
# invalidates the signature.
|
||||
|
||||
Reference in New Issue
Block a user