This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass
What I Built
Every October, Johannesburg and Pretoria turn purple. The jacarandas bloom for a few weeks, and one hard storm can strip them. It's the best reason all year to walk your own suburb, if you know which streets are flowering this week.
Jacaranda Scout plans that walk. You tell it how you want to get outside:
easy 4 km walk from Zoo Lake with the dog
It finds the trees most likely to be flowering and plans a walk, run or cycle loop through them. It also picks the best day before the rain comes. Then it asks you to put the phone in your pocket. The screen goes black, and it only speaks when you reach a tree. The point of the app is to spend as little time on it as possible.
Demo
| Plan | Stops | Pocket mode |
|---|---|---|
![]() |
![]() |
![]() |
These screenshots come from a real run on 11 October 2026, peak bloom week:
- Route: a 2.7 km loop through Parkview, about 35 minutes.
- Best day: Monday, because Sunday has 8 mm of rain forecast.
- Bloom chance: the model gives the trees on the route an 83% chance of flowering today.
- Pocket mode: shows how far it is to the next tree and nothing else. Tap twice to get out.
If you'd rather leave the phone at home, the GPX for watch / OsmAnd button exports the loop with the cues as waypoints.
Code
Piwe
/
jacaranda-scout
Find this week's best jacaranda blooms near you , get a walk/run/cycle loop through them, then put the phone away.
Jacaranda Scout
Find this week's best jacaranda blooms near you, get a walk/run/cycle loop through them, then put the phone away.
Built for the dev.to Touch Grass challenge. The planner runs on a laptop with a local open-weight model (Ollama). It plans from open citizen-science data and only speaks up when you reach a tree.
"easy 4 km walk from Zoo Lake with the dog"
│
▼
┌─────────────────────┐ local open-weight LLM (Ollama, JSON-schema constrained)
│ IntentParser │──► {activity: walk, distanceKm: 4, startPlace: "Zoo Lake"}
└─────────────────────┘
│ geocode (OSM Nominatim)
▼
┌─────────────────────┐ iNaturalist sightings (last 21 days) ─► snapshot on disk ─► demo data
│ DataHub │ Open-Meteo 7-day forecast ─► snapshot on disk ─► demo data
└─────────────────────┘ iNaturalist multi-year flowering histogram
│ iNaturalist known trees (any year) ─► TabPFN sidecar: P(flowering today) per tree
│
▼ pure Java, unit-tested
BloomScorer ─► HotspotClusterer ─► LoopPlanner (orienteering: insertion + 2-opt)
SeasonCurve (where…MIT licensed. Spring Boot planner + PWA, a Python TabPFN sidecar, and a Render Blueprint for the whole stack.
How I Built It
The problem: nobody photographs the trees
My first version did the obvious thing. It looked for recent jacaranda sightings on iNaturalist, clustered them into hotspots and routed between them.
That doesn't work in Johannesburg. Within 25 km of Emmarentia there are about 5 jacaranda sightings per season, and none in the last three weeks. The app kept falling back to demo data.
Trees don't move, though. iNaturalist knows where roughly 100 jacarandas stand around Joburg and 360 around Pretoria, photographed in any year. I didn't need a fresh photo. I needed the chance that a known tree is flowering today. That's a table, not a conversation, which is what led me to the second model.
Two models, two jobs
"easy 4 km walk from Zoo Lake with the dog"
│
▼
IntentParser ──── local LLM (Ollama, JSON-schema constrained)
│ → {activity: walk, distanceKm: 4, startPlace: "Zoo Lake"}
▼
DataHub ───────── iNaturalist sightings + known trees, Open-Meteo forecast
│ known trees → TabPFN sidecar → P(flowering today) per tree
▼
BloomScorer → HotspotClusterer → LoopPlanner (insertion + 2-opt)
SeasonCurve, BestDayPicker (storms strip blooms: go before) ← plain Java, unit-tested
│
▼
OSRM street routing over OpenStreetMap
│
▼
FieldGuideWriter ── local LLM writes the briefing + one spoken cue per stop
│
▼
Phone: map → Pocket mode (black screen, GPS geofence, speech) or GPX
1. A small open-weight LLM for language, and only language
The LLM (Ollama, llama3.2:3b or qwen2.5:3b) has two narrow jobs:
- turn a sentence into
{activity, distanceKm, startPlace}, with decoding constrained to a JSON schema - write a short briefing and one sentence per stop
It never chooses the stops. Plain Java computes the scores, clusters, route, season phase and best day. The LLM only gets those numbers to talk about, so it can't invent a street.
If Ollama isn't running, intent parsing falls back to rules and the cues fall back to templates. A pill in the header shows which mode you're in.
2. TabPFN for "is this tree flowering today?"
TabPFN is a transformer pre-trained on millions of synthetic tables. You give it your table as context, and it predicts with no training loop, no tuning and no GPU.
My table is every jacaranda sighting on iNaturalist where someone annotated the plant's phenology: 1,921 rows from six continents. For each row I added:
- location: lat, lon, absolute latitude and elevation
- day of year, shifted half a year in the southern hemisphere so the seasons line up
- weather from Open-Meteo for the weeks before the sighting: 30-day mean max and min temperature, and 60-day rain
The label is simply "Flowers" or not.
To evaluate it honestly, I trained on everything before June 2025 and tested on the 393 sightings after. The baseline is a week-of-year histogram, which is the idea behind the app's own season curve.
| model (all test sightings, n=393) | ROC AUC ↑ | Brier ↓ | accuracy ↑ |
|---|---|---|---|
| season histogram | 0.842 | 0.163 | 0.779 |
| logistic regression | 0.860 | 0.153 | 0.789 |
| TabPFN v2 | 0.908 | 0.126 | 0.842 |
| model (South Africa only, n=55) | ROC AUC ↑ | Brier ↓ | accuracy ↑ |
|---|---|---|---|
| season histogram | 0.964 | 0.119 | 0.836 |
| logistic regression | 0.950 | 0.125 | 0.800 |
| TabPFN v2 | 0.944 | 0.071 | 0.909 |
The South African table is the interesting one. Here the season is so regular that a calendar alone already ranks sightings slightly better than TabPFN. What TabPFN adds is calibration: its Brier score is 40% lower. That's the number I care about, because the planner uses the probability directly as a weight. A known tree counts 0.4 × P(flowering), and a fresh "Flowering" photo still counts 1.0. With 55 test rows, take the SA numbers as indicative.
The model fits and predicts in 38 seconds on a laptop CPU. The sidecar is a small FastAPI service. It warms its cache once at startup, then answers in seconds.
What didn't go perfectly
I ran the whole stack end to end before writing this. The rough edges are worth sharing:
-
The 3B model's cues are grounded but bland. "Look for a purple carpet underfoot" is fine. "Notice the sweet scent of jacaranda after rain" isn't: jacarandas barely smell. In one run, the cue for stop 3 talked about "spot 1". The numbers are always right because Java computes them. A 3B model's prose is the weak spot. A bigger model such as
llama3:8bshould write better cues, but it'll be slower. I haven't benchmarked that yet. - The photos are green. The stops are known trees, so their thumbnails are whatever someone uploaded, often in April. The model says 83% today, but the picture shows leaves. I'd rather show a real photo of the right tree than a stock purple one.
- The loop came out short. I asked for 4 km and got 2.7 km. Near Zoo Lake there just weren't enough likely trees to fill the distance without a detour to nowhere.
- RAM. With TabPFN loaded, the first Ollama call on my 16 GB laptop failed to allocate its buffer. The app logged it, fell back to templates, and the next request worked. That fallback paid for itself on day one.
-
A real bug: a pydantic field named
datehid thedatetype inside its own class, so the sidecar crashed on startup. The fix was one line (dt.date), but I only found it by actually running everything together.
Run it
ollama pull qwen2.5:3b # or any small chat model
cd bloom-model && pip install -r requirements.txt && uvicorn server:app --port 8090
mvn spring-boot:run # JDK 17, then open http://localhost:8080
The README walks through the full end-to-end run, phone access over HTTPS, offline sync and the Render deploy.
Why Does Open Innovation Matter?
- Data is expensive and towers go down. Jacaranda streets are in the suburbs. Mobile data in South Africa isn't cheap, and load-shedding takes towers offline. Both models and a data snapshot run on the laptop, and the phone keeps the last plan offline.
-
Your walking habits stay home. Your start point goes to
localhost:11434, not to a vendor. - The jobs are small. Parsing one sentence and writing six grounded cues doesn't need a frontier model. Paying per call for a hobby app makes no sense either.
- Open data all the way down. The sightings come from iNaturalist (CC-licensed community science), the maps and routing from OpenStreetMap and OSRM, and the weather from Open-Meteo. Every jacaranda you log on iNaturalist makes next week's plan better for everyone.
If you want to share it with a running club, a Render Blueprint deploys the same stack. The planner is public over HTTPS, so phones get GPS. Ollama and the bloom model run as private services next to it.
Prize Categories
- TabPFN: TabPFN v2 (open weights, local CPU) predicts each known tree's chance of flowering today from 1,921 phenology-annotated sightings plus weather. It beats a season baseline and logistic regression on a temporal hold-out (ROC AUC 0.908, Brier 0.126) and is the reason the app works in a city with ~5 sightings a season.
-
Render:
render.yamldeploys the whole stack as a Render Blueprint: the public HTTPS planner (so phones get GPS for Pocket mode), plus Ollama and the TabPFN sidecar as private services on Render's network.
Now go outside
The season is short. Pick a dry day, ask for a loop and put the phone away. If you pass a jacaranda in flower, log it on iNaturalist: it's one more row in the table for next year.
Sightings: iNaturalist contributors (CC licences). Map & routing data © OpenStreetMap contributors (ODbL). Weather: Open-Meteo.com (CC BY 4.0).



Top comments (0)