Skip to content

First-run download UX: byte-weighted progress + ETA, and fix Control startup permissions#63

Merged
ivan-digital merged 5 commits into
mainfrom
feat/byte-weighted-download-progress
Jul 25, 2026
Merged

First-run download UX: byte-weighted progress + ETA, and fix Control startup permissions#63
ivan-digital merged 5 commits into
mainfrom
feat/byte-weighted-download-progress

Conversation

@ivan-digital

Copy link
Copy Markdown
Member

Summary

Fixes two first-run complaints on the Control demo: the model download "takes ages" (really: the progress bar looked frozen) and a "media permission not granted" note that appeared before the app had asked for anything.

SDK — byte-weighted download progress (849e7e5)

The bar was file-count weighted, so on the default manifest it sprinted through the small JSON assets and then sat still for minutes on the two files that are ~93% of the bytes, then reset to zero for the LLM phase.

  • ModelManager.Progress carries cumulative totalBytesDownloaded/totalBytes, seeded from published sizes and corrected to the real Content-Length as each file starts. Cached files are excluded, so the bar measures the transfer, not the manifest.
  • plannedModelBytes() / plannedLlmBytes() size the whole download up front, so the pipeline and LLM phases render as one continuous 0→100 bar.
  • ModelDownloadWorker reports byte-weighted percent plus rate and ETA via detailLine() (412 / 580 MB · 3.6 MB/s · 2 min left), clamped monotonic. ETA minutes round rather than truncate.

control-demo — startup permissions + download detail (a68d857)

buildContextPhrases() runs during startup to bias the STT beam with the on-device music library, and it called listMusic(), which reports a denial to the feed — but permissions were only requested on the first mic tap, so a fresh install always showed the note before any dialog.

  • Request permissions in onCreate as one batch, including POST_NOTIFICATIONS (declared but never requested, so the download notification was silently dropped).
  • Startup biasing reads through a silent listMusicOrEmpty(); the music tools still report a denial, because there it's what made the command fail.
  • Rebuild biasing in onRequestPermissionsResult when the media grant lands; a micRequested flag stops a startup grant from switching the mic on unasked.
  • Setup panel shows bytes/rate/ETA; first-run copy corrected ~800 MB → ~600 MB (the Pocket path).

On Android 13 the media permission is READ_MEDIA_AUDIO, surfaced under "Music and audio", not the pre-13 "Files and media" group — which is why it looked absent in App Info.

Test plan

  • ./gradlew :sdk:test — 87 pass, incl. new byte-weighting/monotonicity and plannedModelBytes cache-state coverage.
  • ./gradlew :sdk:connectedDebugAndroidTest (arm64 emulator, excl. the multi-GB Nemotron/Parakeet-TDT suites that exceed the AVD data partition) — 34 pass, 0 fail; exercises the rewritten download path incl. modelDownloadIsCached / corruptModelFileTriggersRedownload.
  • ./gradlew :control-demo:compileDebugKotlin + :control-demo:assembleDebug — clean.
  • On-emulator: fresh install shows the permission dialog at startup with no phantom note, and the setup panel renders the byte/rate/ETA line.

ivan-digital added 5 commits July 23, 2026 08:56
The download bar was file-count weighted, so on the default manifest it
sprinted through the small JSON assets and then sat still for minutes on the
two files that are ~93% of the bytes, then reset to zero for the LLM phase.

- ModelManager.Progress carries cumulative totalBytesDownloaded/totalBytes,
  seeded from published sizes and corrected to the real Content-Length as each
  file starts; cached files are excluded so the bar measures the transfer.
- plannedModelBytes()/plannedLlmBytes() size the whole download up front so the
  pipeline and LLM phases render as one continuous 0..100 bar instead of two
  sweeps that reset in the middle.
- ModelDownloadWorker reports byte-weighted percent plus transfer rate and ETA
  via detailLine(), clamped monotonic. ETA minutes round rather than truncate.

Tests cover byte-weighted monotonicity across the phase handover and the cache
states plannedModelBytes must distinguish (fresh, cached, stale, in-progress).
…e/ETA

The "media permission not granted" note appeared on a fresh install before the
app had asked for anything: buildContextPhrases() runs during startup to bias
the STT beam with the on-device music library, and it called listMusic(), which
reports a denial to the feed — but permissions were only requested on the first
mic tap.

- Request permissions in onCreate as one batch, including POST_NOTIFICATIONS
  (previously declared but never requested, so the foreground download
  notification was silently dropped).
- Startup biasing reads through the silent listMusicOrEmpty(); the music tools
  still report a denial, because there it is what made the command fail.
- Rebuild biasing in onRequestPermissionsResult when the media grant lands, so
  track titles reach the decoder without an app restart. A micRequested flag
  keeps a startup grant from switching the microphone on unasked.
- Setup panel shows byte counts, rate and ETA from ModelDownloadWorker; the
  first-run size copy is corrected from ~800 MB to ~600 MB (the Pocket path).

On Android 13 the media permission is READ_MEDIA_AUDIO, which the system
surfaces under "Music and audio", not the pre-13 "Files and media" group.
…front

Pocket is the Control demo's default TTS, but its files were absent from
EXPECTED_SIZES, so the byte total under-counted by ~126 MB and then visibly
grew as each file self-corrected to its Content-Length. Pin the six meaningful
files' published sizes so the total is stable from the first frame.
Point it at two Kokoro model directories (published fp32, then a candidate) via
-e kokoroModelDir / -e kokoroDumpDir and diff the dumps. Logs a half-second RMS
profile rather than one aggregate figure, because a reduced-precision build can
measure clean on a one-second reply and still collapse past two seconds — the
vocoder sums over the time axis, so the failure scales with utterance length.
Gated behind assumeTrue, so it is a no-op without the model-dir argument.
@ivan-digital
ivan-digital merged commit 9dcfc68 into main Jul 25, 2026
3 checks passed
@ivan-digital
ivan-digital deleted the feat/byte-weighted-download-progress branch July 26, 2026 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant