This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass
TL;DR: Touch Grass is a one-screen outdoor planner. Gemma 2 2B runs inside your browser tab (WebLLM + WebGPU) and writes a structured plan for the time you have. Then the screen goes dark green and becomes a timer with a checklist of things to spot. The model's JSON is constrained by a schema whose bird names are an
enumbuilt from verified local data, so it can choose a bird but it can't invent one. It works offline, there's no server, and it costs nothing to run.Demo: https://ess-cor.github.io/touch-grass-chennai/ · No WebGPU? See a real Gemma 2 plan
What I Built
I live in Chennai. Most evenings I mean to go outside. Then I open my phone "for a minute" to check the weather, and the minute turns into an hour. The apps that promise to get you outside don't help much either: step counters, route apps and nature feeds all want your attention too.
So I built the opposite: an app whose job is to be the shortest part of your time outside.
- Tell it four things (about 30 seconds): where you are, how long you have (15 min to 2 hours), what you feel like (birding, walking, gardening, the beach, cycling) and what it's like out there. One tap on Live weather fills that in from open data.
- Gemma writes a plan, on your device: a short title, three timed steps, 3 or 4 things to spot with a hint on where to look, a seasonal tip, and what to bring.
- Press "Go outside". The screen turns dark green and shows a big countdown ring and two tick-off lists, Spot and Bring. Read it once, put the phone in your pocket, go.
- Come back, press "I'm back", and you get a walk card: an image drawn on your phone showing how long you were out, what you spotted and one line you wrote. Share it or keep it. A small private log ("This week: 3 times outside, 85 min, 5 things spotted") never leaves the device.
It's for students and office workers in big, hot, busy cities, people like me. It's built around Chennai's seasons, because a plan that ignores the northeast monsoon or a 38°C May afternoon isn't a useful plan here. For any other city it still works, but it's careful not to pretend it knows your local wildlife (more on that below).
Demo
👉 Live: https://ess-cor.github.io/touch-grass-chennai/
Best in a recent Chrome or Edge on desktop or Android, where WebGPU is available. The first time, pick a model to download:
- Gemma 2 2B Instruct (about 1.4 GB, the default and the better planner)
- Qwen2.5 0.5B Instruct (about 270 MB, a "lite" option for phones with less memory)
After that it starts from your browser's cache, even with no signal. If your browser has no WebGPU, you still get a plan from the built-in rule-based planner. There's also a "See a real Gemma 2 plan" link that shows an unedited Gemma 2 2B answer to the app's own prompt, so judges on a laptop without WebGPU can still see what the model does.
| 1 · Gemma's plan | 2 · Outside mode | 3 · Walk card |
|---|---|---|
![]() |
![]() |
![]() |
Screenshots are from a headless test run of the live code (the plan is the recorded Gemma sample; the timer and the walk card were simulated), not from a real walk.
Code
Touch Grass 🌿
Plan in 30 seconds. Spend the rest outside.
Touch Grass is a one-screen outdoor planner. You tell it where you are, how long you have, what you feel like (birding, walking, gardening, beach, cycling) and what the weather is doing. Gemma 2 2B, running inside your browser, writes a short structured plan: a title, three timed steps, three or four things to spot, a seasonal tip and what to bring. Then you press Go outside, the screen turns into a calm timer with tick-off checklists, and when you're back you get a walk card image, drawn on your phone, that you can share or keep.
Live demo: https://ess-cor.github.io/touch-grass-chennai/
(No WebGPU? Tap “See a real Gemma 2 plan”, or open ?sample.)
Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 1: Touch Grass.
What makes
…Plain HTML, CSS and JavaScript: no build step, no framework, no backend. MIT licensed. node --test tests/planner.test.mjs runs 9 tests on the planner, the schema, the validator and the safety rules.
How I Built It
form ──▶ planner.js ──▶ prompt + JSON Schema (spot names = enum of verified species)
│
▼
WebLLM (WebGPU) · Gemma 2 2B · XGrammar-constrained decoding
│ streamed JSON
▼
validate + allowlist ──▶ safety guardrails in code ──▶ plan card
│ (if anything fails)
▼
rule-based planner on the same seasonal data
│
▼
Go outside: timer + Spot/Bring checklists ──▶ canvas walk card (local)
1. Gemma 2 runs in the browser tab
The core is WebLLM from the MLC team, an open-source (Apache-2.0) engine that runs open-weight models on your GPU through WebGPU. GitHub Pages serves static files, and everything else happens on your device.
const webllm = await import("https://cdn.jsdelivr.net/npm/@mlc-ai/web-llm@0.2.85/lib/index.js");
// Not every GPU supports 16-bit floats in shaders, so pick the right build of the same model.
const adapter = await navigator.gpu.requestAdapter();
const id = adapter.features.has("shader-f16") ? "gemma-2-2b-it-q4f16_1-MLC" : "gemma-2-2b-it-q4f32_1-MLC";
const engine = await webllm.CreateMLCEngine(id, { initProgressCallback: (p) => showProgress(p.progress) });
Why Gemma 2 2B? I wanted the smallest model that could still follow a strict format and write a warm, short sentence. 2B is small enough to download once and run on a decent laptop or phone, and good enough at short, friendly instructions. Swapping it is one string (that's how the Qwen lite option works). One quirk: Gemma's chat template has no system role, so all the instructions go in the user turn.
2. The trick: the model can choose a bird, but it can't invent one
Ask a 2B model for "birds to look for in Chennai in October" and it will happily invent a species or two. That's not OK in an app that sends you outside to look for them.
So the app keeps a small, hand-written file of seasonal notes for Chennai: four seasons, the birds you can expect in each, a one-line field note per bird ("pumps its long tail up and down near drains, puddles and streams"), what to plant now, and some green spaces. For each request it builds a JSON Schema where the spot names are an enum of only this season's birds plus things you can find anywhere ("a spider web", "something growing through concrete"):
spot: {
type: "array", minItems: 3, maxItems: 4,
items: {
type: "object",
properties: {
name: { type: "string", enum: facts.spotOptions }, // this season's birds + generic finds
hint: { type: "string" },
},
required: ["name", "hint"],
},
},
WebLLM compiles that schema into a grammar (with XGrammar) and masks every token that would break it while Gemma is generating:
const chunks = await engine.chat.completions.create({
messages: [{ role: "user", content: buildPrompt(input, new Date(), weather) }],
response_format: { type: "json_object", schema: JSON.stringify(planSchema(facts)) },
temperature: 0.6,
stream: true,
});
So Gemma does what small models are good at: choosing from good options and explaining them in plain words for your situation. It never gets the chance to make up a species. For cities with no local data, the enum has no species at all, and the prompt says so.
The output is streamed, so you watch the JSON appear token by token. Then a validator double-checks it (dropping anything outside the allowlist, plus duplicates) before it's shown. The Spot checklist and the walk card only ever use those verified names.
3. Let the code do the maths and the safety
I ran the real prompt through Gemma 2 2B many times while building this (more on how below), and the misses were useful:
- It sent me to Guindy National Park on a 30-minute plan. Now named places are only offered for outings of an hour or more. Shorter plans are told to stay within a five-minute walk.
-
It was bad at time. One plan gave "listen for birds: 1 minute" out of 30. Now the code splits the time (
timeBudget(30)→ 6 min out, 18 main, 6 back) and the prompt hands Gemma the numbers. - It said "Spring is a great time to plant" for Bengaluru in October. The prompt now includes the month, and cities without data get a noticing tip, not a planting tip.
-
The free-text steps aren't constrained. In the recorded sample, step 2 mentions "sparrows", which aren't on the list. The structured
spotlist is the part the app trusts. I left that sample unedited on purpose.
Safety is asked for in the prompt and enforced in code after the model answers. If it's hot or midday and no step mentions shade, the app adds one. In a storm it adds a doorstep version. At the beach it always adds "stay out of the water", because the currents on Chennai's beaches are dangerous. In rain it always adds an umbrella to Bring.
4. Three layers, so it never just says no
- Schema-constrained JSON from Gemma (the normal path).
- If the grammar can't be used, the same prompt without the grammar, then a JSON extractor plus the same validator.
- If WebGPU isn't there or the answer is still unusable, a rule-based planner built on the same seasonal data. It's less chatty, but it always gives a sensible plan.
The label at the top of every plan says which one you got ("✨ Planned by Gemma 2 2B on this device · schema-constrained JSON", or "Built-in planner"), so it never pretends.
5. Offline, private, and the walk card
A service worker caches the app and the WebLLM library, and WebLLM keeps the model weights in the browser's cache. The app remembers which model you loaded and starts it from cache next time. The timer runs on timestamps, so it stays right even if your screen is off for the whole walk.
The walk card is drawn on a <canvas> on your phone and shared with the Web Share API (or saved as a PNG). Nothing is uploaded unless you choose to share it.
Live weather is optional and uses Open-Meteo: open data, no API key, no account. One tap geocodes your city, reads the current temperature, feels-like temperature and weather code, picks the matching condition chip and gives the model a line like "30°C (feels like 34°C), partly cloudy".
How I tested it (and what I couldn't)
My build environment has no GPU, so I couldn't run WebGPU inference there. Instead I ran the same Gemma 2 2B Instruct weights (4-bit GGUF) with llama.cpp, using the app's exact prompt and the same JSON Schema as a grammar, on a 2-core ARM machine with no GPU. It still generated about 13 tokens per second, and a full plan took about 35–40 seconds on CPU. Every lesson in section 3 came from those runs, and the "See a real Gemma 2 plan" sample is one of them, unedited. In the browser I tested the non-AI flow end to end with headless Chromium: plan, go outside, tick things off, come back, walk card. The WebGPU path should still be checked on a real device with a GPU.
Why Does Open Innovation Matter?
For this project, open weights aren't a nice extra. They're the only reason the idea works.
It runs where the signal doesn't. A planner that needs an API call is useless at the edge of a marsh or on the beach with one bar. With Gemma on the device, the whole thing works offline after the first load.
Your routine stays yours. The app knows your city, when you go out, for how long, and what you saw. That's a fairly personal picture of someone's day. Here, none of it is sent anywhere, because there's no server to send it to. Even the weather call only sends a city name, and only when you tap.
Open weights let me control how it generates, not just what I ask. With a closed chat API I'd send a prompt and hope. With an open model running in an open engine, I get the tokens one by one, so I can plug in a grammar and make "never invent a species" a hard rule instead of a polite request. That one feature is the heart of this app, and it depends on having the model in my hands.
It costs nothing to run, for anyone. I'm a student. If every plan was an API call, I'd have to charge people, show ads, or shut it down when my credits ran out. With open weights, open data and GitHub Pages, it's free to host and free to use, however many people use it.
Anyone can fork it for their own place. Someone in Bengaluru or Kochi can add their city's birds and planting calendar as a season file, and the enum, the prompt and the fallback all pick it up. No permission, no API key. Or they can swap in a different open model with one line.
There are honest trade-offs. The first download is big, WebGPU isn't on every phone yet, and a 2B model is nowhere near as clever as the biggest closed models. But for "give me three good steps to get outside in the next 30 minutes, and don't make things up", it doesn't need to be. It needs to be private, free, fast, and there when you have no signal. Open models are.
What's Next
- More cities through community-made season files (Bengaluru and Kochi first).
- Bird calls as well as sightings, with an open audio model, so "a bird call you can't place" can become a name.
- Try Gemma 3 builds in WebLLM as they arrive, and compare plan quality and speed on phones.
- A field report, once I've used the new version on a few real evenings.
How this was made
I built this with a lot of help from an AI coding assistant. It wrote and tested much of the code and helped draft this post. I made the decisions about the idea, the model, the grounding approach and what to cut, and every claim about how Gemma behaved comes from real runs of the app's prompt.
Prize Categories
Best Use of Gemma: Gemma 2 2B Instruct is the default model and runs fully on-device in the browser through WebLLM. It writes every plan as schema-constrained JSON, choosing what you should look for from a verified, per-season list. That makes the small open model trustworthy enough to send someone outside with.






Top comments (0)