Repository navigation
feat(app): let the learner point the app at a self-hosted server - #15
Merged
Merged
Conversation
Settings > Advanced > Server takes a custom base URL, checks it answers like a Glosso server (/health and /v1/config) and switches every network path to it: GlossoApi reads the server at request time, so content, cloud scoring, lesson and model downloads, product config, statistics and sign-in follow. Switching drops what came from the previous server (queued downloads, HTTP cache and content version, lesson packs, cached config and account, linked Google account, unsent events) and keeps the scoring model only when its digest is pinned or published by the new server. Progress stays. The build's glossoApiBase / GLOSSO_API_BASE stays the default. About shows the server when it is not the default. The F-Droid recipe drops TetheredNet. Claude-Session: https://claude.ai/code/session_01FcVpZoyr8VdRLp1QecX1Ng
On a server switch, downloaded lesson packs now stay until the new server's manifest answers. Packs whose content fingerprint (assetVersion) matches it exactly are kept; any other pack, and every pack when the server publishes no fingerprint or cannot be reached, is deleted as before. The rest of the switch (HTTP cache, config, account, events) is unchanged. Claude-Session: https://claude.ai/code/session_01FcVpZoyr8VdRLp1QecX1Ng
IgnacioLD
added a commit
that referenced
this pull request
Nov 10, 2026
…ice scoring memory (#16) Play Console (2.5.4, Android vitals) warns that the R8 setup may use more memory because optimized resource shrinking is off, and says to move to AGP 9. Google starts enforcing these memory and code optimization thresholds in February 2027. This PR upgrades the build, removes keep rules that are no longer needed, and lowers the memory that on-device scoring holds. There are three commits, one per concern. ## 1. AGP 8.9.1 -> 9.3.3, Gradle 8.11.1 -> 9.7.1 - **Why 9.3.3 and not 9.5.1:** 9.3.3 is the newest AGP inside Kotlin 2.4.20's tested range (AGP 8.5.2-9.3.1, Gradle 7.6.3-9.7.0). 9.4.x and 9.5.x are outside that range, and 9.5.x was released less than two weeks ago. AGP 9.3 needs at least Gradle 9.5. I picked Gradle 9.7.1, the version `deploy/Dockerfile` already builds the server with. Both picks are patch releases of tested versions. The wrapper jar matches the official sha256. - **KMP modules:** `:engine` and `:shared` now use `com.android.kotlin.multiplatform.library`, because AGP 9 rejects `com.android.library` next to the KMP plugin. These stay as they were: the opt-in Android target (`-Pglosso.android`), `onnxMain`, `kotlin.mpp.applyDefaultHierarchyTemplate=false` and JVM target 17. - **`:androidApp`:** uses AGP's built-in Kotlin, so the `kotlin-android` plugin is gone and the JVM target follows `compileOptions`. `resourceConfigurations` is replaced by `androidResources.localeFilters`. Both flavors, the licensing gate per flavor and the release `-P` inputs are unchanged. - **Test moves:** - `shared/src/androidUnitTest` is now `androidHostTest`, so the task is `:shared:testAndroidHostTest`. - `BottomActionBarTest`, `GlossoArtworkTest` and `PlayerPointerTest` stopped compiling in `:androidApp` once the UI moved to `:shared`: they use `internal` API, and one still imported Coil 2. That broke `connectedAndroidTest` and `tools/score-parity/run-parity-test.sh` on main. They now live in `shared/src/androidDeviceTest` (`:shared:connectedAndroidDeviceTest`). - **Gradle 9.7 deprecations fixed:** delegated `by getting/creating/registering` in `engine`, `shared` and `androidApp`, and `Task.project` read at execution time by the licensing tasks. - **Licensing:** the locks and `licenses.json` are regenerated with `tools/licensing/collect.py`. `kotlin-stdlib-common:2.4.20` now resolves. - **Docs:** README and CONTRIBUTING list `:shared:testAndroidHostTest`. ## 2. R8 - Optimized resource shrinking is AGP 9's default. `isMinifyEnabled` and `isShrinkResources` were already on for release. - **Rules removed from `proguard-rules.pro`:** - the kotlinx.serialization block, which the library already ships; - `-keep class androidx.credentials.playservices.** { *; }`, because the AAR ships a narrower rule; - the Coil 2 rules, which named classes that no longer exist; - every blanket `-dontwarn`, because R8 reports no missing class without them; - the duplicate native-method rule; - `-renamesourcefileattribute`, so R8 9 writes its mapping id as the source file. - **Rule kept:** `ai.onnxruntime.**`. The JNI layer looks up classes, fields and constructors by name, and the AAR ships rules only for its telemetry classes. | Release, unsigned | before (main d70a496) | after | |---|---|---| | playstore APK size | 138,753,248 B | 138,421,983 B | | playstore referenced methods | 30,511 | 26,178 (-14%) | | playstore dex | 5,119,168 B | 4,621,976 B | | playstore `res/` files / resources.arsc | 277 / 113,228 B | 13 / 44,704 B | | fdroid APK size | 138,232,808 B | 138,227,046 B | | fdroid referenced methods | 25,514 | 23,963 | | fdroid dex | 4,463,520 B | 4,291,760 B | | playstore AAB | 60,310,866 B | 59,993,510 B | Methods were counted with `apkanalyzer dex references`. Most of the APK is the ONNX Runtime `.so` for 4 ABIs (about 135 MB), so the total size barely moves. ## 3. On-device scoring memory - **Model load:** the model was already loaded from the file path, not through a ByteArray. - **Session options:** the session is still created once and kept. It now runs with `setCPUArenaAllocator(false)` and `session.use_device_allocator_for_initializers=1`. These change only where tensors are allocated, not the math. - **Background release:** `OnDeviceScorer` releases the model on `TRIM_MEMORY_BACKGROUND` and reloads it on the next take. A take holds the load lock while it runs, so the session cannot be closed under it. - **Not changed:** the model stays loaded when the learner leaves practice but stays in the app, to avoid reloading it on every screen. - **Bit-exact check:** a SHA-256 over the recognition (IPA, per-token confidences, timings) of `score-parity/fixtures/good.wav` was `1f3c5e77...` before and after. `tools/engine-bench/run.sh` output is identical line for line between main and this branch. **Memory on emulator-5558** (arm64, 1080x2340 at 480 dpi), debug build. A scratch instrumented probe (not committed) walks through the phases while `adb shell dumpsys meminfo me.shirobyte42.glosso` is sampled every 0.5 s. Values are MB at the end of each phase: | Phase | PSS before | PSS after | Native heap before | Native heap after | Java heap before -> after | |---|---|---|---|---|---| | idle | 183 | 187 | 4 | 6 | 9 -> 10 | | model loaded | 589 | 592 | 400 | 400 | 9 -> 10 | | after 3 scored takes | 610 | 574 | 416 | 378 | 13 -> 13 | | second recognizer loaded (peak) | 1018 | 918 | 826 | 724 | 11 -> 10 | | second recognizer closed | 639 | 588 | 447 | 394 | 10 -> 10 | | `onTrimMemory(TRIM_MEMORY_BACKGROUND)` | 638 | 230 | 447 | 36 | 10 -> 10 | Scoring latency for the fixture was unchanged within noise (321/291/268 ms before, 337/310/265 ms after). ## Verification (rebased on main d70a496) - **Unit tests:** - `./gradlew :engine:jvmTest`: 463 passed - `LC_ALL=en_US.UTF-8 ./gradlew :server:test`: 347 passed - `:androidApp:testFdroidDebugUnitTest` / `testPlaystoreDebugUnitTest`: 296 / 295 passed - `:shared:testAndroidHostTest`: 146 passed. The same test files ran as `testDebugUnitTest` before the move: 142 on the old base, plus the tests that came with #15. - **Lint:** `:androidApp:lintFdroidDebug` and `lintPlaystoreDebug` report 0 errors. Warnings went from 73 to 11 per flavor. The one new warning is `OldTargetApi`: the newer lint knows API 37, and raising `targetSdk` is a separate product change. - **iOS:** `:engine:compileKotlinIosSimulatorArm64` and `:shared:compileKotlinIosSimulatorArm64` pass. - **Server image path:** `./gradlew :server:installDist -Pglosso.android=false -Pglosso.ios=false` passes without the Android SDK in the environment. Docker is not available on this machine, so `docker build` was not run. The Dockerfile already uses Gradle 9.7.1. - **Release builds:** `assemblePlaystoreRelease`, `assembleFdroidRelease` and `bundlePlaystoreRelease` pass. - **Release on device:** I used a temporary local setup, not committed: `testBuildType = "release"`, debug signing, and keep rules for the test harness only. `ScoreParityTest` passed on the R8-minified playstore release on emulator-5558: good.wav scored 100 / PERFECT with delta 0, and silence, noise and low were not evaluable. ONNX Runtime loads and scores under the new rules. `OfflineReferenceParityTest` skipped because `thirty.wav` is not staged anywhere. - **Debug on device:** `tools/score-parity/run-parity-test.sh --serial emulator-5558` passed. `:shared:connectedAndroidDeviceTest` ran 6 tests and all passed. - **Engine bench:** `GLOSSO_MODEL=<abs>/wav2vec2_phoneme_int8_dyn_pertensor.onnx tools/engine-bench/run.sh` gives native >= 98 for 30/45 and REFSCORE 22/28 (>= 98), identical on main and this branch. The bench's own 0.9 rate gate already fails on main; this PR does not change that. - **gitleaks:** `gitleaks dir . --redact` reports only the 2 known false positives. ## Notes - AGP prints an informational warning that `:engine`'s `commonTest` has no Android host test. Turning one on fails to compile, because `ParityPlatform` has no Android `actual`. The old `androidUnitTest` variant never compiled either. - No NDK r28 is installed locally, so "Unable to strip libonnxruntime.so" is printed, as it was before. The libraries are already stripped. https://claude.ai/code/session_01FcVpZoyr8VdRLp1QecX1Ng --------- Co-authored-by: IgnacioLD <178957758+IgnacioLD@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
F-Droid labels the app TetheredNet because the server is fixed at build time (
glossoApiBase). This adds an in-app server setting so anyone can use their own self-hosted Glosso server, and drops TetheredNet from the F-Droid recipe.What
ServerScreen): shows the server in use, takes a custom address, "Check and use", and "Use the default server". The address is normalized (bare host meanshttps://, lowercase, no trailing slash, no query/fragment/user info).https://is required; debug builds also accepthttp://tolocalhost,127.x,10.0.2.2and private LAN ranges (release Android denies cleartext anyway).probeServer):/healthmust answer{"status":"ok", ..., "assets": ...}and/v1/configa JSON object. Clear errors for unreachable vs "answers, but not a Glosso server". The probe client sends no device id.GlossoApinow takesserverBase: () -> Stringand reads it per request (ServerPreferences.base, storeglosso_server, keybase_url). That covers content, cloud scoring, asset/lesson/model downloads and their.sha256,/v1/config,/v1/me, events, crash traces and Google sign-in. Each existing client (every ViewModel keeps its own) notices the switch and forgets its manifest and content version.ServerSwitch) drops everything from the previous server: active downloads, HTTP cache + content version (ApiCache.forget), cached config and account (ProductConfigRepository.forget, and an in-flight refresh from the old server is discarded), the linked Google account shown on the phone, unsent events and pending crash trace. The scoring model is kept only if its SHA-256 is the pinned release digest or the one the new server publishes (OnDeviceScorer.onServerChanged). Downloaded lesson packs are kept only when the new server's manifest publishes exactly the content fingerprint (assetVersion) they were downloaded for (OfflineStore.keepOnly); a different fingerprint, none, or an unreachable server deletes them. Progress is local and kept. The switch runsNonCancellable. Screens reload throughcontentReloadSignal(now includes the server).glossoApiBase(Android) /GLOSSO_API_BASE(iOS) stays the default; iOS uses the same setting through:shared.Server: ...when it is not the default. Newserverscreen name added to the analytics catalogue.docs/FDROID.mdexplains why TetheredNet no longer applies;metadata/me.shirobyte42.glosso.ymldrops it; README has a "Use your own server" section.Verification
:engine:jvmTest463/0 failed,:shared:testDebugUnitTest146/0,:androidApp:testFdroidDebugUnitTest296/0,:androidApp:testPlaystoreDebugUnitTest295/0.ServerSwitchTest: same fingerprint keeps the pack; different, missing or unreachable deletes it (the keep test fails against the old always-delete behaviour).ServerSettingTest(normalization, invalid input, http rules, private ranges, persistence and default fallback) andServerSwitchTest(probe OK / not Glosso / unreachable on a MockEngine; switch A -> B -> default: downloads cancelled, account forgotten, config and content version from B only, every request after the switch goes to B with no stale?v=).:androidApp:lintFdroidDebug: 0 errors, 73 warnings, none in changed files (GradleDependency, AGP version, UseKtx, VectorPath, launcher icon, ObsoleteSdkInt).:androidApp:assembleFdroidDebug,:shared:compileKotlinIosSimulatorArm64pass.gitleaks dir . --redact: only the two known false positives.http://example.orgrefused with the https message;http://10.0.2.2:8080/accepted and normalized; the local server log shows config, manifest, LESSON, STORY, DIALOGUE, EXERCISE and asset requests; the setting survives a reinstall and cold start; the installed model was kept (pinned digest); "Use the default server" switched back with 0 further requests to the local server.https://claude.ai/code/session_01FcVpZoyr8VdRLp1QecX1Ng