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.

Plan tab showing

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.

  1. 🔍 Notice the way the sunlight filters through the canopy
  2. 👂 Pause to catch the forest's quiet conversation
  3. 🤚 Discover a patch of bark with unexpected texture
  4. 👃 Inhale the scent of damp earth and decay
  5. 🔍 Spot three distinct shapes made by fallen foliage
  6. 📷 Capture a photograph of an interesting colour contrast
  7. 🧘 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.

Building Trailcard: Local Gemma 4 Outdoor Quest Cards (Hacktoberfest Week 1)
You

Build a project for the Hacktoberfest Open-Source AI Challenge Week 1 (Touch Grass theme). Use open-source AI at the core. Work locally first - I'll give you a GitHub repo at the end. pyenv/npm available. Use DevRelay skill for MLH interaction and submission.

Agent

Researched the challenge (theme: get people off screens and outside using open-source AI), checked MLH event (id 01a0f81e..., external submission via DEV post), checked hardware (RTX 4080 12GB VRAM, 32GB RAM, Python 3.13). Proposed and recommended Trailcard: a local-first app where Gemma 4 writes a printable outdoor quest card, user goes outside, then logs photos on return which Gemma vision checks.

▸ get_challenges {}
{ }
Challenge id 79: Hacktoberfest Open-Source AI Challenge Week 1, theme Touch Grass, due Oct 11 2026, Best Use of Gemma prize $200
You

Go with Trailcard. Install Ollama and gemma4:e4b.

Agent

Installed Ollama 0.40.0 via winget, pulled gemma4:e4b (6.6GB). Verified: 100% GPU on RTX 4080, structured JSON output works with think:false, cold load ~80s, warm card ~3s per generation.

▸ bash winget install Ollama.Ollama && ollama pull gemma4:e4b
$ winget install Ollama.Ollama && ollama pull gemma4:e4b
Ollama 0.40.0 installed, gemma4:e4b pulled successfully
Agent

Built full project: astro.py (offline sunrise/sunset via solar declination formulas, hemisphere-aware season - Sydney Oct 7 correctly spring not autumn), quest.py (JSON-schema-constrained card generation, prompt safety rules forbidding touching unknown plants/animals/litter, template fallback), identify.py (vision check temperature=0, journal writer), store.py (SQLite local only), server.py (FastAPI, Jinja printable card, multipart photo upload 12MB cap, XSS-safe via Jinja autoescape), static HTML/CSS/JS frontend (no build step). 31 tests via FakeOllama httpx.MockTransport - no GPU or network needed.

Show more
▸ bash pytest
$ pytest
31 passed, 1 warning in 0.61s
Agent

Real-model validation with live Gemma 4: generated cards for Discovery Park Seattle (autumn, 3.3s warm), Bondi Beach Sydney (correctly shows spring due to hemisphere flip), London streets. Vision tested with real Wikimedia photos: maple leaf -> yes, parked car -> no, snowy panorama -> partly (unticked, user decides). Fixed: hints that just echo the sense word are dropped (e.g. 'Look' as a hint for a look task). Prompt updated to forbid litter pickup suggestions.

Agent

Found and fixed wrong-timezone sunset bug via browser harness test: server was using browser UTC offset (India +5:30) for a Seattle card, showing sunset as 07:09 instead of 18:39. Fix: added explicit UTC-offset field to the UI, prefilled from browser but editable for planning trips elsewhere. Set photo check to temperature=0 (greedy) for deterministic verdicts: 5 identical runs on same photo confirmed. Only clear yes auto-ticks; partly verdict shown but left for user.

▸ bash browser-harness: set tz=-7, generate card
$ browser-harness: set tz=-7, generate card
Sunset about 18:39 local time. Be back at least 30 minutes before.
Agent

Key technical insight discovered via community research (dev.to/homelabpm): Ollama applies JSON schema grammar ONLY after the closing thinking tag on Gemma 4. If the model skips thinking and answers directly, the schema is never enforced and you get bare text with HTTP 200 and no error. Sending think:false applies the grammar from token 1. All Trailcard structured-output calls already use think:false - this is why they reliably work. Shared this finding in the DEV post.

You

I have registered for the event. Push to https://github.com/tharun634/trailcard and submit.

Agent

Pushed 4 commits to github.com/tharun634/trailcard. Created DEV draft article (id 4815346) with all required challenge sections: What I Built, Demo, Code, How I Built It, Why Open Innovation Matters, Agent Session, Prize Categories. Tagged devchallenge and hf26challenge. Prize categories entered: Best Use of Gemma (sole AI in stack - writes cards, checks photos, writes journal) and Entire (agent session sharing).

▸ create_article {"title": "I built Trailcard...", "published": false, "tags": ["devchallenge"...
{ "title": "I built Trailcard...", "published": false, "tags": [ "devchallenge", "hf26challenge" ] }
Draft created, id 4815346, edit_url provided

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.