This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass
What I Built
Trailcard is a local-first web app that uses Gemma 4 (running on your own machine via Ollama) to write a printable one-page outdoor quest card — then checks your photos when you get back.
The pitch in one sentence: the screen is the shortest part.
You open the app, describe where you're headed and how long you have, and a small open-weight model writes a card of observation tasks tailored to your season, habitat and energy level. You print it, pocket the phone, and go outside. When you return you log what you found (tick items manually, or upload a photo and let the model judge it), and the model writes a short field-journal entry from your finds.
Everything — model inference, photo storage, the journal — lives on 127.0.0.1. No account, no cloud, nothing to log in to.
Demo
Here's a real card the model wrote for a forest trail in autumn (3.3 seconds on an RTX 4080 Laptop after the model warmed up):
Autumn Whispers in the Woods
The air is crisp, the leaves are turning, and the forest has a story to tell. Slow your pace and let your senses guide you through this golden afternoon.
- 🔍 Notice the way the sunlight filters through the canopy
- 👂 Pause to catch the forest's quiet conversation
- 🤚 Discover a patch of bark with unexpected texture
- 👃 Inhale the scent of damp earth and decay
- 🔍 Spot three distinct shapes made by fallen foliage
- 📷 Capture a photograph of an interesting colour contrast
- 🧘 Find a space to simply be present
Sit spot: find a comfortable spot and spend five minutes just noticing the slow, silent rhythm of the forest.
Sunset about 18:39 local time. Be back at least 30 minutes before.
Photo check results on real Wikimedia photos:
- 🍁 Red maple leaf vs. "find three shades of brown" → yes (the bark background had plenty of brown, model was lenient but consistent across 5 runs)
- 🚗 Parked car vs. same task → no ✓
- 🌨️ Snowy panorama vs. "capture a shadow stretching long" → partly (unticked, left for you to decide) ✓
Repo: https://github.com/tharun634/trailcard
# Quick start ollama pull gemma4:e4b pip install -e ".[dev]" python -m trailcard # → http://127.0.0.1:8765 pytest # 31 tests, no GPU or network needed The Code
src/trailcard/ astro.py — offline sunrise/sunset + season (no network, no DB) quest.py — Gemma writes a JSON-schema-constrained quest card identify.py — Gemma vision checks photos; journal writer server.py — FastAPI: /api/quests, /card/{id} (printable), /api/journal store.py — SQLite journal (local only) static/ — plain HTML/CSS/JS, no build step Tests (31, all mocked — no GPU or network needed to run them):
# conftest.py: FakeOllama via httpx.MockTransport class FakeOllama: def handler(self, request): body = json.loads(request.content) props = body["format"]["properties"] if "matches" in props: return vision_verdict elif "entry" in props: return journal_entry else: return quest_card How I Built It
The core loop
A quest card is a JSON object with title, intro, a list of items (each with prompt, hint and kind — look/listen/touch/smell/photo), a sit_spot and a safety_note. Gemma is given a strict JSON schema via Ollama's format field, a context paragraph (season, habitat, minutes, energy, sunset note) and a prompt that forbids touching unknown plants, mushrooms, animals or litter.
The server validates the output (minimum 4 items, non-empty title, item counts capped, degenerate hints dropped) and falls back to a static template when the model is down or returns junk. The UI shows whether the card came from the model or the template.
The Ollama think: false trick (and why it matters)
Gemma 4 is a thinking model. By default, Ollama applies the JSON schema grammar only after the closing thinking tag — so if the model decides to answer directly without thinking, the schema is never applied and you get bare text back with HTTP 200 and no error in the log.
Sending think: false disables this path and applies the grammar from token one. For structured-output calls where I don't need the chain-of-thought (card generation, photo checks, journal entries), this is strictly better. I learned about this edge case from @homelabpm's excellent post — worth reading if you're building anything on Ollama with Gemma 4.
Deterministic photo checks
The vision check runs at temperature 0 so the same photo against the same task always gives the same answer. I verified this by sending the same image five times and confirming identical responses each time.
Only a clear yes auto-ticks the item in the UI. A partly verdict is shown with its reasoning but left for you to decide. This matters because the model is genuinely lenient on borderline cases (a red leaf on bark will often pass "find three shades of brown"), and you know what you saw better than the model does.
Offline-first daylight maths
astro.py computes sunrise, sunset and season from latitude/longitude and date with no network or database — just the standard solar declination and hour-angle formulas. Cross-checked against Open-Meteo for Seattle (47.6°N, Oct 7: sunrise 07:16, sunset 18:36) and Sydney (33.9°S, Oct 7: spring, sunrise 06:24, sunset 19:02), both within 3-5 minutes. Sydney in October correctly comes out as spring, not autumn.
The UTC-offset field in the UI defaults to the browser's own zone, so if you're planning a hike somewhere else you can adjust it. I found this bug when running from India (+5:30) while testing a Seattle card: the UI showed Seattle's sunset as 07:09.
Privacy by construction
The one place where data could leave the machine is the optional weather checkbox (off by default), which rounds your coordinates to one decimal (~11 km) before sending them to Open-Meteo. Photos are downscaled to 768px and re-encoded server-side, which strips all EXIF including GPS. The journal is SQLite in a local data/ directory.
Screen-time tracking (the honest version)
The "screen time on this card" counter uses the Page Visibility API to accumulate seconds the tab was visible, stored per quest in localStorage. It's shown next to the minutes-outside field when you log your return. It only covers screen time in this tab, for this card — not your phone in your pocket — but it's the number that the user chose to track, and it's computed entirely client-side.
Why Does Open Innovation Matter?
The features I care most about in Trailcard — season-aware cards for your specific afternoon, photo checks of where you actually went, a journal that stays private — are only possible because the weights are open.
A cloud API for this would mean your afternoon coordinates, your habitat, your photo of a patch of bark, and your notes go to a server. For a "get off the screen and outside" app, that is a particularly bad trade. The data is personal in the quiet way that notebooks are personal: it doesn't need to be secret to be yours.
Open weights also mean the capability scales with hardware, not with a billing tier. The same gemma4:e4b that writes a solid card in 3 seconds on an RTX 4080 runs (slowly) on a CPU. A smaller gemma4:e2b trades quality for speed. The model is swappable via an environment variable. None of that is true if the capability lives behind an API.
There's a quieter reason too. This project was built in a few days, by one person, using a model that didn't exist a year ago, with a Pillow resize and a SQLite file as the only dependencies beyond the LLM. The gap between "idea" and "running" is small enough that it's worth trying things. That's what open models change.
My Agent Session
This project was built with the help of an AI coding agent (Pi / Antigravity). The agent wrote the initial scaffolding, tests, and templates; I directed the design decisions, reviewed every file, caught the wrong-timezone bug live in the browser harness test, and ran the real-model validation.
Prize Categories
- Best Use of Gemma — Gemma 4 (e4b, multimodal) via Ollama is the only AI in the stack. It writes the quest card (constrained JSON with schema validation and fallback), checks photos (vision, temperature 0), and writes the journal entry. All three roles, one open-weight model, local GPU.
- Entire (Share Your Agent Session) — built with an AI coding agent; full session embedded above.

