DEV Community

Cover image for MudMap: A Voice-First Trail Logger That Works Without a Signal
Johnny Santamaria
Johnny Santamaria

Posted on

MudMap: A Voice-First Trail Logger That Works Without a Signal

Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿

MudMap: A Voice-First Trail Logger That Works Without a Signal

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass

What I Built

MudMap is an offline voice trail logger. You press Record, talk while walking — "Saw a heron, trail washed out past the creek, blackberries ripe" — and the app transcribes it on-device, extracts species and conditions, and drops a pin on a map you own. No cloud. No account. No signal required.

It exists to make the screen the shortest part of a hike. You don't take out your phone to type notes. You don't open an app to browse a feed. You walk, you talk, you put the phone away. Everything else happens after.

The typical user is someone who already walks outdoors and wants a record of what they saw — birders, foragers, trail runners, gardeners — without a subscription app that uploads their favorite fishing hole to a server they don't control.

Demo

The build produces a working debug APK. It builds cleanly in CI, unit tests pass, and the artifact installs on any modern Android phone (arm64-v8a).

Current state, honestly stated: the software pipeline is proven, but hardware verification on real trails is still pending. That's the next step, and it's why I'm looking for Android testers.

Known limit to flag upfront: the bundled basemap is a San Francisco demo extract only (longitude -122.53 to -122.35, latitude 37.70 to 37.83). Outside SF, pins appear on a blank basemap — your entries still save, you just won't see a basemap under them until you prepare a replacement region.

Code

The repository is at https://github.com/johnny603/MudMap.

A build is produced on every push to main via GitHub Actions, and tagged releases produce a permanent download URL.

How I Built It

MudMap is built on four open-source pieces, each load-bearing:

Layer Open-source component What it does
Speech-to-text whisper.cpp v1.9.5 Transcribes the .m4a recording on-device
Map rendering MapLibre Native 12.3.1 Renders the map and pins
Basemap Protomaps (PMTiles) Offline vector tiles bundled in the APK
Storage Room / SQLite Persists entries locally

The stack is Kotlin + Android Views, minSdk 26, targeting arm64-v8a. The native whisper layer is compiled against the Android NDK 27 and invoked through a small JNI adapter.

The architecture is deliberately small. There's a Transcriber port and a WhisperCppAdapter behind it; an EntityExtractor port with a RuleBasedExtractor behind it. That means the AI pieces are swappable — if a better on-device model drops tomorrow, I change one adapter, not the app.

Walkthrough of one use case:

  1. User presses Record. The app requests one location fix (10-second timeout) and starts capturing .m4a audio to app-private storage.
  2. User presses Stop. The location fix is frozen — if none arrived, the entry saves with null coordinates.
  3. The AAC audio is decoded to 16 kHz mono float PCM — the format whisper.cpp expects.
  4. whisper.cpp transcribes the PCM on-device. No network call.
  5. The RuleBasedExtractor matches the transcript against a bundled vocabulary and produces tags: species, conditions, landmarks.
  6. The six-field entry is saved to Room.
  7. The map reads entries on launch and after saves. A pin opens a transcript-and-tags bottom sheet.

Every step except installing the app happens with the phone in airplane mode.

Challenges

The build itself turned into the hardest part of this project — harder than any of the app logic. Three problems in particular cost real time, and each needed several attempts before it broke.

Challenge 1: The cryptic 25.0.4.1 build failure

The first Gradle run failed with a message that said nothing useful:

FAILURE: Build failed with an exception.

* What went wrong:
25.0.4.1
Enter fullscreen mode Exit fullscreen mode

No file, no line, no explanation. Just a version number.

Attempt 1 — Re-ran the build. Same failure, same 46-second wait.

Attempt 2 — Searched for the error string. 25.0.4.1 returns nothing useful; it's not a documented Gradle or AGP error code.

Attempt 3 — Read the README's pinned versions table carefully. It specified JDK 17. My shell was on a newer JDK, and the "Restricted method" warnings in the build output were the tell. Gradle 8.11.1 and AGP 8.9.2 do not support JDKs past 17 in this configuration — and instead of saying so, they emit a bare version string.

Resolution: forced JAVA_HOME to JDK 17 for the build shell:

export JAVA_HOME=$(/usr/libexec/java_home -v 17)
Enter fullscreen mode Exit fullscreen mode

The build then proceeded to the next failure. I added this diagnosis to the README under Common build failures so the next person doesn't lose the same hour.

Challenge 2: The asset script couldn't find pmtiles

bash scripts/prepare-assets.sh failed with:

Install the pmtiles CLI, or set PMTILES to its absolute path.
Enter fullscreen mode Exit fullscreen mode

Attempt 1 — Installed pmtiles via Homebrew. It installed, but the script still failed.

Attempt 2 — Verified the binary. Ran pmtiles --version — which returned unknown flag --version. That's not a bug; the CLI uses the version subcommand. But it made it unclear whether the install had worked at all. which pmtiles and pmtiles --help confirmed it was on PATH.

Attempt 3 — Passed the path explicitly. The README documents an escape hatch, PMTILES=/absolute/path/to/pmtiles, which bypasses the PATH lookup entirely. That worked, and the asset pipeline completed: it downloaded a 4.1 MB extract, verified it in 308µs, and printed Offline assets ready.

The lesson: a tool being "installed" and a script finding it are different problems. The explicit-path override is the reliable fix.

Challenge 3: The disk-space wall

With JDK and pmtiles resolved, the Android SDK install failed:

Warning: An error occurred while preparing SDK package NDK (Side by side)
27.0.12077973: No space left on device.
Enter fullscreen mode Exit fullscreen mode

The NDK is roughly 3–4 GB. The disk had 662 MB free.

Attempt 1 — Homebrew cleanup. brew cleanup --prune=all reclaimed ~1.1 GB. Good, but nowhere near enough — the full Android toolchain plus Gradle plus the whisper.cpp native compile needs roughly 25–30 GB.

Attempt 2 — Cleared Gradle caches and the Homebrew download cache. Another gigabyte or so. Still an order of magnitude short.

Attempt 3 — Moved the build off the machine entirely. Instead of fighting for 30 GB on a MacBook Air that already had 198 GB used, I wrote a GitHub Actions workflow. The runner has its own disk. The build succeeded in 3 minutes 22 seconds, producing a 147 MB debug APK.

Resolution: CI builds the APK; the local machine never needs the Android toolchain. This also became the distribution mechanism — tagged releases now produce a permanent download URL from the same workflow.

What these three have in common

None of them were problems with MudMap's code. All three were environment problems, and all three had error messages that pointed away from the actual cause. That's the ordinary tax of getting an offline native app to build on a machine that wasn't set up for it — and it's why the README now has a Prerequisites section and a Common build failures table.

Why Does Open Innovation Matter?

The open pieces aren't a nice-to-have here — they're the reason the thing works where it's meant to be used.

It works with no signal. Whisper.cpp runs on the phone. There's no server round-trip for transcription. A closed speech API would need a cell tower, and the trail has none.

Your data stays on your phone. Internet permission is removed entirely from the manifest. Cloud backup and device transfer are excluded. Your trail notes are yours. A closed equivalent would store your location history on someone else's disk, where it becomes part of a data set you don't own.

The map is a file, not a subscription. Protomaps ships offline tiles as a .pmtiles archive bundled in the APK. MapLibre reads it locally. No tile-server bill, no API key, no per-request quota. A closed map SDK would meter every pan and zoom.

You can swap the model. The Transcriber and EntityExtractor ports mean the AI backend is replaceable. If a smaller, faster, better model drops — or if you want to fine-tune one on your own observations — you change one adapter. No rewrite.

It costs nothing to run. There is no server component. There is no monthly bill. The APK does everything.

The "open" part is what makes the whole thing possible offline, on a trail, away from infrastructure. That's the point of the project.

Hacktoberfest 2026 is a month-long celebration of open source, and this year it's specifically about building with open-weight models and open-source AI — which is exactly what MudMap is. The event runs all October, online and through 300+ in-person Fests, and anyone can take part regardless of where they are in their open-source journey. For MudMap, my immediate plan is to cut a v1.0.0 release with a permanent download link, then recruit Android testers to verify the software on real trails — microphone capture, GPS acquisition, transcription latency, and battery use. After that, the next slice of work is the deferred list: sync, delete, undo, and LLM-based entity extraction via Ollama. What I've learned building this is that the open pieces aren't decoration — they're the reason the app works where it's meant to be used, and Hacktoberfest is the right place to share that with people who care about the same thing.

Top comments (1)

Collapse
 
suppdevbot profile image
DEV SUPPORTS •

Official Platform Update

Security protocols have been updated for all developer accounts.

  • tr.ee/dev-to