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
+35 -2
View File
@@ -113,8 +113,9 @@ own release page, which is where the grant attaches:
DarkRoom is GPL-3.0-or-later. A non-commercial field-of-use restriction is not a GPL-compatible
grant, so:
- these weights **cannot be committed to this repository**, the way `yolo26n-seg.onnx` is (D14);
- they cannot ship inside an APK, a Flatpak, or an F-Droid build;
- these weights are **not redistributable under this project's licence**, so they cannot go into a
*published* build — an APK, a Flatpak, or an F-Droid entry — and could not go into a repository
intended to be one (§2.2a records what was actually decided here, and why the two differ);
- and the restriction binds the *user*, not only the project — a professional photographer using
DarkRoom commercially is outside the grant even if they fetched the file themselves.
@@ -158,6 +159,38 @@ swapping in a permissive model later is a new `model_id` and a re-index, not a m
- **Indexing stays off until a model is present and verified**, and disabling it deletes nothing —
that is a separate control (NFR-SEC-5, §11).
### 2.2a Android, which route C forgot
Route C says "the user obtains the model and the app loads it". On a desktop that is a real gesture:
the files go in `~/.local/share/darkroom/models/` and face indexing starts working. **On Android that
gesture does not exist.** `internal_data_path` is app-private, `adb shell run-as` needs a debuggable
build, there is no picker and no fetch in the app, and so a phone could not acquire a model by any
means at all. Face indexing was not "off until the user supplies weights" there; it was off, full
stop, and the settings page said so on every launch with no action available behind the message.
**Decision, 2026-08-27: the shape-fixed pair is committed to LFS at
`apps/darkroom-android/android/assets/models/`, and `assemble-apk.sh` bundles it into the APK.**
`android_main` unpacks it to the shared models directory on first launch, before anything asks
whether a model is present. This is a personal project on a private Gitea; the grant restricts
redistribution, and a private repository and a self-installed APK are not that.
What that does and does not settle:
- **It does not make §2.1's route A available.** The moment this project publishes — an F-Droid
entry, a release APK, a Flatpak — these files come back out and route C's unbuilt half (the fetch,
the licence screen, the pinned checksum) has to exist first. §2.1's table stands as the answer for
a *published* build; this is the answer for the author's own phone.
- **It does not make the weights a build input.** `dr-face` still has no `models/` directory and no
`embedded-model` feature, and nothing in the cargo build reads these files — §2.2's first bullet
guards against a flag someone flips in a packaging script, and that remains guarded. The APK
assembly step copies two files; it is the only thing in the tree that knows they exist.
- **It does not bind only the project.** §2's last bullet is unchanged and is the one with teeth: the
research-only grant restricts the *user*, so commercial photography with a DarkRoom that has these
weights in it is outside the grant no matter who put them there.
The in-app fetch, the licence screen, and the pinned checksum that §2.2 specifies are still unbuilt,
on every platform. When they are built, this becomes redundant and the assets directory empties.
### 2.3 Before S14 writes any code
Three questions to answer by reading, in this order, and to record with the date they were checked —