Get the scene model onto the devices that need it

The decoder can load from a path; nothing yet put a file at one. Four
packaging routes, and one lookup that finds the result.

## Not `include_bytes!`, unlike the instance model

The instance model is 11 MB and compiled in, which was the right call for
it: Android hands the app no filesystem path (ARCH §6.9) and 11 MB is
tolerable. The scene model is 24 MB, and 35 MB of constants in the binary
is paid by every install whether or not the tab is ever opened.

So it follows `models/face/` instead — carried as an APK asset, unpacked
once at first launch into the shared directory a desktop install already
uses, after which every lookup finds it where it finds a desktop user's.
Assets are stored rather than deflated in the APK, so unpacking is a copy
rather than an inflate.

`embedded-scene-model` exists for the desktop build with nowhere else to
read from, and for tests wanting the real graph. Off by default, which is
the asymmetry with `embedded-model` and the reason for a separate
feature.

## Three files, all or none

`scene_model` insists on the graph, its vocabulary and the category
descriptor together, for the reason `face_models` insists on its pair: a
graph alone decodes to 150 anonymous channels. Reporting the set missing
beats starting and failing at the first inference.

## The two model sets are not the same kind of thing

`install_bundled_models` now carries both, and the distinction is worth
keeping in view. Face weights are absent from the repository *by design*
— the InsightFace grant is research-only (docs/faces.md §2) — so a build
carrying none is ordinary. The scene model is committed, so a build
carrying none means a checkout without `git lfs pull`.

Neither is fatal. A photo editor that refuses to start over a missing
grading feature is worse than one that starts without it, so both report
themselves unavailable exactly as face indexing already did.

The LFS-pointer guards apply to the `.onnx` only. The vocabulary and the
descriptor are legitimately a few kilobytes, and a size check that fails
on them would be a guard against the wrong thing.

`scene_model` is exported ahead of the tab that will consume it so the
packaging added here has something to be verified against — assets
written where no lookup looks would be a silent mistake for as long as
the tab took to arrive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 10:48:54 +02:00
co-authored by Claude Opus 5
parent 8df6000e4b
commit 7312aceded
6 changed files with 160 additions and 37 deletions
+33 -11
View File
@@ -255,25 +255,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
# The 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. The
# source is `models/face/`, shared with the Arch package rather than living
# under this one platform's directory.
# sources are `models/face/` and `models/scene/`, shared with the Arch package
# rather than living under this one platform's directory.
#
# Two directories, and they are not the same kind of thing. The face weights
# are absent from most checkouts by design (research-only grant), so finding
# none is ordinary. The scene model is committed, so finding none means a
# broken checkout — but this script still only warns, because the failure it
# would otherwise cause is at APK build time on a machine that may legitimately
# be building the face-less variant.
#
# 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}/models/face"
#
# 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}/*.onnx" >/dev/null; then
mkdir -p "${OUT}/staging/assets/models"
_bundled=""
for _dir in face scene; do
ASSETS="${REPO}/models/${_dir}"
compgen -G "${ASSETS}/*.onnx" >/dev/null || continue
# 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.
# reason. Only the weights are checked: the vocabulary and the category
# descriptor beside them are legitimately a few kilobytes.
for m in "${ASSETS}"/*.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
@@ -281,11 +293,21 @@ if compgen -G "${ASSETS}/*.onnx" >/dev/null; then
exit 1
fi
done
mkdir -p "${OUT}/staging/assets/models"
cp "${ASSETS}"/*.onnx "${OUT}/staging/assets/models/"
echo " assets: $(ls "${ASSETS}" | grep '\.onnx$' | tr '\n' ' ')"
# The scene model is three files: the graph, its vocabulary, and the
# category descriptor. All three are needed to decode anything, so they
# travel together; README.md is documentation and stays out of the APK.
for f in "${ASSETS}"/*; do
case "$(basename "${f}")" in
README.md) continue ;;
esac
cp "${f}" "${OUT}/staging/assets/models/"
_bundled="${_bundled} $(basename "${f}")"
done
done
if [[ -n "${_bundled}" ]]; then
echo " assets:${_bundled}"
else
echo " assets: no face models found (face indexing will be off on the device)"
echo " assets: no models found (face indexing and the scene tab will be off on the device)"
fi
# -0 "" stores the .so without compression so Android can mmap it directly