Every autumn I go into the woods looking for porcini (Boletus edulis and its cousins). Every porcini hunter asks the same two questions: is it worth going this weekend, and where? And the answers have to work where porcini grow, which is exactly where your phone shows "No service".
Porcini Radar is a web app that installs on your phone and helps with both questions. Once installed, it works fully offline:
📡 Radar. It estimates the porcini "flush" for the next 16 days from the weather of the last three weeks: rain, soil temperature and soil moisture. Foragers' rule of thumb says porcini come up 10–15 days after a good soaking, if the soil stays mild and damp. You save your spots with the GPS while you are in the woods; at home you check them all the night before.
💬 Ask. An open-weight LLM (Google's Gemma) runs inside the phone's browser, on the CPU, with no connection. Ask "I'm in the woods now, where do I look?" and you get an answer even in airplane mode.
📓 Diary. Log each outing with a photo, GPS position, woodland type and how many porcini you found. The app stores the radar score of that day, and "Did the radar get it right?" compares the scores of good outings with empty ones, so over time you find out whether the heuristic works for your woods.
🧭 Compass and SOS. "Back to the car" and "take me to that spot" give you an arrow, a distance and a buzz on arrival, using only GPS and the phone's compass. The SOS card shows your coordinates in large type, calls 112 and sends your position by SMS, which gets through when the signal is too weak for internet.
The app is for anyone who forages, hikes or just wants a reason to spend a Saturday in a beech wood instead of on the sofa. It is in English and Italian and follows the phone's language.
What it deliberately does not do: tell you whether a mushroom is edible. Small models make mistakes, and with Amanita phalloides a mistake kills. If you ask about eating a mushroom, the app (not the model) tells you to take it to a mycological inspection service. In Italy the ASL Ispettorato Micologico checks them for free.
Open it on an Android phone with Chrome, then use "Install app" from the ⋮ menu. In the Ask tab, download the model once over Wi-Fi (~720 MB). After that, airplane mode is fine.
Radar – estimates the porcini "flush" for the next 16 days at your spots, from rain, soil temperature and soil moisture (Open-Meteo). You save spots with the GPS while you are in the woods; at home you check them all.
Diary – log each outing with photo, GPS position, woodland type and how many porcini you found. Everything stays on the phone (IndexedDB) and works offline. Each entry stores that day's radar score, and "Did the radar get it right?" compares the scores of outings with porcini against empty ones.
Plain HTML, CSS and JavaScript, no build step, hosted on GitHub Pages.
How I Built It
The stack
Gemma, Google's open-weight model: Gemma 3 1B (Q4_0 GGUF, ~720 MB) by default, Gemma 2 2B (Q4_K_M, ~1.7 GB) as an option for stronger phones.
wllama, which is llama.cpp compiled to WebAssembly, running the model on the phone's CPU inside the browser.
Open-Meteo for 31 days of weather history plus 16 days of forecast, including soil temperature at 6 cm and soil moisture. It is free and needs no API key.
A PWA: a service worker caches the app, IndexedDB keeps spots, diary and photos on the phone, and the model lives in the browser's storage.
I built it together with Claude Code, from the first idea through the WebGPU dead ends to the switch to llama.cpp.
Getting an LLM to run on a mid-range phone took three attempts
My phone is a Poco F3 (Snapdragon 870, Adreno 650 GPU). The plan was "WebGPU, obviously". Reality disagreed.
Attempt 1: WebLLM + Gemma 2 2B on the GPU. It downloaded and then crashed Chrome on the first question. The model plus a 4096-token KV cache was more GPU memory than Android lets one tab have. With the context cut to 1024 tokens it loaded, but the GPU driver reset mid-answer (A valid external Instance reference no longer exists) and froze the screen. Splitting the prompt into smaller chunks to shorten each GPU dispatch didn't save it.
Attempt 2: WebLLM + Gemma 3 1B. It is smaller, so it should fit. It answered with <image_soft_token><image_soft_token>…. I reproduced it on my laptop, and the cause is in the model's config: it pairs an 8192-token context with Gemma 3's 512-token sliding window, and WebLLM can run only one of the two. Turn the window off and the output degenerates past ~500 tokens. Turn it on and the engine crashes.
Attempt 3: llama.cpp on the CPU. llama.cpp supports Gemma 3's sliding-window attention properly, and running on the CPU means no GPU driver can reset. The catch: WebAssembly needs SharedArrayBuffer to use more than one thread, which requires cross-origin isolation headers, and GitHub Pages can't set headers. The service worker can, because it already sits between the page and the network:
functionwithIsolation(res){constheaders=newHeaders(res.headers);headers.set('Cross-Origin-Opener-Policy','same-origin');// "credentialless" still allows the cross-origin fetches (Open-Meteo, Hugging Face)headers.set('Cross-Origin-Embedder-Policy','credentialless');returnnewResponse(res.body,{status:res.status,statusText:res.statusText,headers});}
Result: 4 threads on the Snapdragon's fast cores, and the answer starts after a few seconds. It runs on a phone that came out in 2021, with no signal.
Who decides what: the code for the verdict, the model for the words
The first answers were fluent and wrong. Asked "should I go today?" while the data said "too dry", Gemma 3 1B replied "Yes, today is essential". Another time it turned "radar" into "poor visibility". A 1B model writes well but doesn't reason reliably over numbers, and no prompt tweak fixed that.
So I stopped asking the model to decide. The verdict is computed in code and shown first, so it always matches the data:
📡 Here (205 m): it is too dry here for porcini and it will not improve in the coming days… This is lowland, where porcini are rare: only in oak and chestnut woods. Among your spots, the best time is Sat 17 Oct at "Beech wood".
The model adds what it is good at: practical tips on which woods, altitude, slope and what to look for. Things that helped a small model behave:
instructions in the user's language;
the data as plain sentences ("damp soil, 24% water"), not scores to misread;
one worked example of the expected answer;
no mention of topics it shouldn't bring up. When the prompt told it "never say a mushroom is edible, refer to the ASL", it brought up the ASL in every answer. The edibility warning now comes from the app instead.
The radar
The radar is a heuristic, and I say so in the app. The score (0–100) is:
the rain of 7–21 days before, weighted most between 10 and 14 days;
× how suitable the soil temperature is (ideal 11–18 °C);
× the soil moisture of the last 3 days, because rain two weeks ago is useless if the soil has dried out since;
− penalties for nights below 3 °C and dry, warm weeks.
That is why the diary matters: "Did the radar get it right?" turns every outing, including the empty ones, into a check of the model against reality.
Why Does Open Innovation Matter?
Because the woods have no signal, and foragers have secrets.
It works where it is needed. Apps that identify mushrooms with a cloud model go silent at the edge of the forest. With an open-weight model the "AI" is a file on my phone, and airplane mode changes nothing.
My spots stay mine. A porcini spot is handed down like a family recipe, and nobody wants its exact coordinates on someone else's server. Spots, diary and photos never leave the phone, and the AI never sees the network. The only network calls are the one-time model download and the weather download, which sends the spot's coordinates to Open-Meteo with no account attached.
I could fix it. When the GPU path failed, open code let me find out why: I read WebLLM's source and found the sliding-window conflict, I patched a prefill limit in a vendored copy to test a theory, and I switched to llama.cpp when that didn't work. With a closed API, "it crashes on my phone" is the end of the story.
No cost, no keys, no quota. The app can stay a free static page forever.
Open data too. Open-Meteo's free weather and soil data is what makes the radar possible.
Prize Categories
Best Use of Gemma: Gemma 3 1B and Gemma 2 2B run fully locally, in the phone's browser, offline, through llama.cpp.
Stay safe out there: never eat a wild mushroom that hasn't been checked by an expert. 🍄