9b674a88d81db75c51546517b250b23be8253c71
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7312aceded |
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> |
||
|
|
79b7f54e04 |
Tell javac the Java sources are UTF-8
The APK stopped building the moment the tree gained Java with prose in it: 55 errors, every one "unmappable character (0x94)", every one from a comment. The code was fine. javac reads sources in the *platform* encoding, and the build container sets no locale, so that is US-ASCII. Every curly quote and em dash in a doc comment is then unrepresentable. The repository is UTF-8 throughout, so this says so rather than asking one file's prose to be typed in ASCII to suit a default nobody chose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
24bf5be574 |
Compile our own Java into the APK, so the classes Android constructs can exist
The APK's only dex was Slint's. `assemble-apk.sh` found `classes.dex` under the android-activity backend's build directory and copied it in, and there was no `javac` step and no `d8` of anything of ours — package.sh's header said so outright, on the reasoning that the app has no Java because android-activity calls `android_main` directly. That reasoning holds for everything the app *calls* and fails for everything Android *constructs*. A `ContentProvider` is instantiated by the system from its manifest entry; nothing in the process ever reaches its constructor, so there is no JNI route to writing one in Rust. The launch `Intent` is the same shape of problem from the other end: it arrives through `Activity.getIntent()`, and the activity android-activity hands out is a stock `NativeActivity` rather than a subclass with room for code. FR-PLAT-AND-6 needs both, and FR-PLAT-AND-4 needs a foreground `Service`, which is a third. So: everything under `apps/darkroom-android/android/java/` goes through javac against `android.jar`, and d8 merges the classes with Slint's finished dex into one `classes.dex`. Merging rather than emitting a second dex keeps the staging and zip steps as they are — multidex is native at API 28, but two files to keep in step buys nothing at this size. The step is skipped when the tree holds no Java, which is the state this commit leaves it in. Nothing about the APK changes until a `.java` file appears. `-source 8 -target 8 -bootclasspath android.jar` is not caution about language features. It is the last combination in which javac allows the boot class path to be replaced: from `-target 9` the flag is rejected, the platform classes come from the JDK instead of from android.jar, and the build stays green while the device raises `NoClassDefFoundError` for a class Android never shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f9e331eed2 |
Let a test build be opened up, when asked
`adb shell run-as` refuses on a release build — "package not debuggable" — and the app's private storage is then unreachable from the host. That storage is where the face shards, the thumbnail store and the catalog live, so when a device disagrees with the desktop about what it has synced there is no way to find out which of them is right. An evening was spent guessing at exactly that. `DARKROOM_DEBUGGABLE=1 ./docker/android/package.sh --install` now sets `android:debuggable` through aapt2's `--debug-mode`, and nothing else changes. Set through aapt2 rather than written into `AndroidManifest.xml` on purpose: the flag then exists only for the build that asked for it, and a release build cannot inherit it because somebody forgot to take it out again. A debuggable APK lets any process on the device read this app's files, so it belongs on a test tablet and nowhere else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2d95807542 |
Package the models on every platform, not just the phone
The Android bundling landed the weights under that platform's asset directory, which was the wrong home the moment a second packager wanted them. `makepkg -si` produced a desktop install with no model at all — the same "no face model is installed" the phone used to show, for the same reason: nothing put the files anywhere the app looks. So `models/face/` at the root is the one copy, and both packagers read it: assemble-apk.sh bundles it as APK assets, and the PKGBUILD installs it to /usr/share/darkroom/models. Both refuse an LFS pointer rather than shipping a 130-byte file that fails inside the graph loader on a user's machine. `face_models` now searches three places, most specific first: the account's own directory, the shared user directory, then $XDG_DATA_DIRS. So a packaged pair is found automatically and a pair the user placed by hand still outranks it — which is what keeps a deliberate choice of weights from being overridden by an upgrade. $XDG_DATA_DIRS rather than a hard-coded /usr/share: that is the variable a distribution, a prefix install or a Nix-style store already sets to say where its data went, and its documented default is exactly the two paths that would otherwise have been hard-coded. Empty on Android, which has no such directories — there the APK's copy is unpacked into the shared user directory instead, because an asset inside a package is not a path anything can read from. Verified: the APK still carries both models at assets/models/, the PKGBUILD parses and installs from the new path, 467 tests pass. Includes the pkgver 0.6.0 → 0.7.0 bump that was already sitting uncommitted in the working tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
eaafacc3fb |
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> |
||
|
|
40e6334bb1 |
Sign the APK with a real key when one is configured
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Desktop (Linux) (push) Successful in 21m23s
Build and test / Layer separation (push) Successful in 29s
Traceability / Requirement traces (push) Failing after 28s
Build and test / Android (aarch64) (push) Failing after 33m28s
The APK has been debug-signed with a key generated on the spot, which is right for putting a build on a test device and useless for anything else: a different signature every run, so nothing can ever update in place. Four secrets now select a real signature -- ANDROID_KEYSTORE_BASE64 and its password, alias and key password. The names are JellyTau's, because that repo already signs its Android build this way against this same runner and one convention across both is one thing to remember. Absence of the secrets is not an error. A fork or a branch build has no access to them and should still produce an installable APK, so the debug path stays exactly as it was. The reverse is an error: if a keystore is supplied and cannot be read, the build fails rather than quietly falling back to a debug key, because a release that is silently debug-signed is worse than no release. Passwords reach apksigner and keytool as `env:`, never `pass:`. `pass:` puts the password in the process table for anything on the box to read. The keystore is written to a 0700 mktemp directory and never into the workspace, which is both what actions/cache saves and what the upload step globs. Also: upload-artifact drops from v4 to v3. v4 was a guess about what this Gitea supports. v3 is what JellyTau uploads its APK with on this runner today, which makes it the version known to work rather than the one that ought to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5741ec5e00 |
Lift the APK assembly out of package.sh so CI can run it too
package.sh does two things: it decides how the host reaches the image, and it assembles an APK once inside it. Only the first half is host-specific. CI already runs in that image, so the second half was about to be copied into a workflow step -- two copies of aapt2/zipalign/apksigner ordering, drifting apart at whatever rate the toolchain moves. So it moves to docker/android/assemble-apk.sh, which assumes it is inside the image and takes its paths from the environment, because the callers disagree about them: the container mounts the repo at /work, the runner checks it out wherever it likes. Every default reproduces what package.sh did, so the host path is unchanged. Two things stop being hard-coded on the way. The build-tools version and the compile SDK are resolved from what is installed rather than written out as 36.0.0 and android-36 -- the versions are Dockerfile ARGs, and a second copy is a second thing to miss when they move. --min-sdk-version now comes from that same ARG instead of a literal 28, which is the number the API-level check in CI already reads. The intermediates are removed at the end. They were harmless in a cache directory nobody looks at; beside a published artefact they are four more files for a glob to pick up by mistake. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |