Frostline: a planting planner that decides on your own machine
This is a submission for Hacktoberfest Open-Source AI Challenge: Week 1
Theme: Touch Grass.
What I built
I have a small balcony in Delhi NCR. Every October I do the same thing: buy coriander seeds, sow them, and watch them bolt in the heat — because October in Delhi is not autumn, it is a second summer with shorter days. The advice I was following came from gardening sites written for England.
Frostline answers one question — what should I sow this week, where I actually live — and it answers it from my own weather history, on my own laptop, with no account and no API keys.
You open it, pick your town, press one button. It tells you the next seven days of temperature for your coordinates and which of 30 crops fit that window. This week for Delhi it says: bush beans, cucumber, okra, marigold, basil, mint, bottle gourd and sunflower all fit at 100%. Radish, fenugreek, spinach, coriander and mustard greens score zero, and the app says so out loud instead of hedging.
Then you close the laptop and go plant.
The demo
Here is the app running — 29 seconds, no mockups:
One honest note about the clip: TabPFN refits on every run, and on this two-core box a full fit takes minutes, so the recording shows the button press and then cuts the wait. What appears after the cut is the output of a completed run — the same run whose numbers are below, rendered by the app itself. Nothing in the clip is staged, but the waiting is not real-time.
Why open innovation matters for this
A planting calendar is a small, personal thing. The closed version of it is worse in five specific ways, and every one of them is why this is built the way it is.
The model runs on the machine. TabPFN does its inference on CPU, in the same process as the web server. My latitude, my garden, my plans are never sent anywhere to be scored. I do not have to trust a vendor's retention policy to ask what to plant.
It works with the network unplugged. Weather history is cached in one SQLite file — 5,761 days of daily min/max for Delhi, back to 2011. After the first fetch, the whole planner runs offline. I tested this properly: I stubbed the archive fetcher so it raised on every call, and the app served the cached history and produced the full plan anyway. That matters for a tool you would actually take outside.
The advice is inspectable. The crop table is a plain Python list of temperature envelopes, not a black box. If okra is mis-scored for your microclimate, you open frostline/crops.py and change a number. No retraining, no waiting on a vendor.
The model is swappable, and optional. Gemma sits behind a single function. If Ollama is not running, Frostline falls back to a deterministic template-written plan and keeps working. Nothing 500s because a model is missing.
It costs nothing to run. No per-request price for asking what to plant, and no ceiling on how often you ask.
How it works
Open-Meteo archive (free, no key) → SQLite cache → 20 engineered features
→ TabPFN regressor (CPU, local) → 7-day min/max forecast
→ crop scoring against temperature envelopes
→ Gemma 3 4B via Ollama writes the plan in plain language
Two things I care about in the design.
The forecast is recursive. TabPFN predicts day one, that prediction is fed back in as a lag feature, then day two, and so on for seven days. The 20 features are the day-of-year (raw, plus sine/cosine so December and January are neighbours), year, lags at 1/2/3/7 days, and rolling windows up to 30 days.
The model has to justify itself. Frostline holds out the last seven days of history, predicts them, and scores the mean absolute error against a day-of-year climatology baseline. The UI has a panel literally titled "Did the model earn its place?" — if the model cannot beat "the average for this date over 15 years", that is what it says.
Did the model earn its place?
Yes — and here is the number that decides it. Frostline holds out the last seven days of real history, predicts them, and scores itself against the simplest thing that could work: the average for that same date over 15 years.
Held-out week: 3–9 October 2026. 5,724 days of training history, fitted on the most recent 2,500.
- Min temperature: 1.024 °C MAE, against 2.797 °C for the climatology baseline — 63% better
- Max temperature: 1.152 °C MAE, against 1.808 °C — 36% better
The model also earns its place in shape, not just in error. A day-of-year average cannot see a trend by construction — it is the same number every year for that date. This week Delhi cools from 32.7 °C down to 28.2 °C, and TabPFN called the slide; the baseline sat flat at 32 °C for all seven days. That is the difference between a calendar and a forecast.
The caveat, stated plainly. A seven-day holdout is a small sample, and the two targets disagree about how much the training cap matters. Re-running the same holdout with a 200-row cap gives 57% on min temperature but 76% on max — so read "36% better on max temperature" as a point estimate from one week, not a settled figure. A walk-forward evaluation over many holdout windows is the honest next step; at roughly six minutes per fit on this CPU it is an overnight job, not a footnote.
Both numbers are reproducible on your own machine:
.venv/bin/python scripts/evaluate.py
What broke
Five things, all worth writing down.
Gemma invented weather. My first prompt got back a plan that warned about frost protection. There were zero frost-risk nights in the window — the coldest night was 19.9 °C. A small model will happily describe a hazard the data says is absent, and that contradiction is the first thing a reader notices. The fix was to put every fact in as a list, state the exact date range and that there is no data beyond it, and forbid the specific invention: "Frost-risk nights: NONE. Do not mention frost, frost cloth, or frost protection." It now writes "no risk of anything needing protection from the cold."
The plan was a week stale. The cache top-up had an early return: if more than five years of history was already cached, it skipped fetching entirely. So the app kept serving whatever week the cache was first filled in. On 11 October it produced a forecast for 4–10 October, and Gemma faithfully wrote "start planting your bush beans today, October 4th." The model was not wrong — my pipeline was handing it last week's data. Now every run requests only the missing tail, which also makes the offline path explicit instead of accidental.
A 4B model needs 4.1 GB of RAM and does not negotiate. The Ollama runner kept dying mid-load. I initially blamed the CPU repack path, and I was wrong — the log line still read REPACK = 1 with the flag set, and the real cause was the OOM killer: the process was inside a 4 GB memory cgroup. It now runs detached with a 6 GB budget. I mention this because "run a model locally" has a resource floor, and the honest number is more useful than a mystery crash.
The bug that only existed once the model worked. _run() in forecast.py built its recursive walk from a variable it had never been passed. Every code path that could reach it was gated behind a working TabPFN licence, so it was unreachable — and therefore invisible — until the day the licence cleared. The first real run fitted the model, then died with NameError: name 'df' is not defined, and the broad exception handler I wrote on purpose ("the model is not usable here, never a broken request") swallowed it and quietly served the climatology fallback. The fallback design that makes the app robust is the same design that hid the bug. I only caught it because I read the run log instead of the UI.
TabPFN on a 2-core ARM box is not fast. The full run — two evaluation fits, two product fits, and seven recursive one-day-ahead predictions — took 68 minutes on two CPU cores. TabPFN is a transformer that does its learning in-context, so every prediction re-reads the whole training set: the cost is per-prediction, not per-training-run. Lowering the row cap to a few hundred cuts that dramatically, and on this holdout it did not hurt accuracy. I shipped the 2,500-row default because it is the stronger claim, and I would rather state the honest runtime than pretend a 7-day forecast is free.
Honest gaps
- The TabPFN licence needs a one-time interactive acceptance per model version, and the weights are 876 MB. That is a real gate on anyone cloning this — on a fresh machine the accuracy panel honestly reads "TabPFN is not active, so there is no model to score" until that is done. The app never hard-fails either way: no TabPFN token falls back to climatology, no Ollama falls back to a template plan, and both fallbacks are labelled in the UI rather than hidden.
- Gemma 3 4B is the largest model this machine can serve.
Try it
The code is here: https://github.com/amandevelops/frostline
git clone https://github.com/amandevelops/frostline.git && cd frostline
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
printf 'TABPFN_TOKEN=%s\n' "<your key>" > .env # free: ux.priorlabs.ai -> Licenses tab
.venv/bin/python scripts/evaluate.py # prints the accuracy table above
.venv/bin/python server.py # then open http://127.0.0.1:8000
The server is one standard-library file — no framework, no build step, no container. On a slow machine set FROSTLINE_MAX_ROWS=400 in .env and a full run drops from about an hour to a couple of minutes.
Built with TabPFN (Prior Labs, CPU inference), Gemma 3 via Ollama, and Open-Meteo's free archive.

Top comments (0)