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:
2026-08-27 13:18:07 +02:00
co-authored by Claude Opus 5
parent b846b312b8
commit eaafacc3fb
10 changed files with 287 additions and 64 deletions
+38
View File
@@ -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.