This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass
What I Built
I built Mitti — a Balcony Garden Oracle for 22°N. It tells you what to plant this week on your balcony in Kolkata, based on your actual sunlight, pot size, and direction — not US frost dates.
Who is it for? 10M families who don't have a backyard. They have an 8" tub, a 12" pot, or an 18" bosta crate on a north-facing balcony that gets 2 hours of sun.
How it gets you off-screen?
Screen time < 1 minute. 3 taps: Which way does it face? N/E/S/W. How many hours direct sun? 0-8h slider. What pot size? Small/Medium/Large. Optional: upload balcony photo — Mitti estimates light locally. Badge: Photo considered • local only. Photo never leaves your laptop.
Click What should I plant this week? → Returns 3 cards in late monsoon context:
- পুদিনা Pudina - Mint (shade-tolerant) - 15 days
- পুঁই শাক Pui Shak - Malabar Spinach (humidity lover) - needs trellis
- লঙ্কা Lanka - Chili (high sun, fruiting)
Then you close the laptop and go touch actual mitti. Journal is offline in localStorage.
Leaf Doctor: Upload a sick leaf → diagnosis on-device, in Bangla + English, with kitchen fix. Output: Overwatering + fungal spots → বেশি জল + ছত্রাক — দুদিন জল দেবেন না, নিম তেল স্প্রে করুন
Demo
Live: Local PWA, fully offline after first load — runs on your laptop
# Terminal 1 — allow browser to talk to Ollama
OLLAMA_ORIGINS=* ollama serve
# Terminal 2 — pull open-weight models
ollama pull moondream
ollama pull llama3.2:latest
# Terminal 3 — serve Mitti
python3 -m http.server 8000
Top bar turns green: Ollama: live • local
30-sec flow:
- Set balcony: N facing, 4hr direct sun, Medium 12" pot + upload balcony photo
- Click Generate → "Late monsoon air = fast germination. Your 4hr N light + medium pot = perfect for these 3"
- Shows Pudina / Pui Shak / Lanka cards with depth, spacing, harvest days
- Leaf Doctor: upload leaf with brown edge →
Overwatering + fungal spots→ remedy in Bangla
Code
Full app is a single index.html — no build step, no node_modules, no API keys.
mitti-local/
└── index.html # React + Tailwind via CDN, all logic inside
Core open-weight calls:
// 1. Detect local models — fallback chain after mllama error
const { models } = await fetch("http://localhost:11434/api/tags").then(r=>r.json())
// My Ollama 0.40.1 threw: "unknown model architecture: 'mllama'" for llama3.2-vision:11b
// So we try in order: moondream -> llava:7b -> llama3.2-vision:11b
// 2. Text — what to plant this week
await fetch("http://localhost:11434/api/generate", {
method: "POST",
body: JSON.stringify({
model: "llama3.2:latest",
prompt: `You are Mitti, oracle for Kolkata 22°N late monsoon 31C 84% hum. Balcony: ${direction}, ${sunHours}hrs, ${potSize}. Suggest 3 plants THIS week in Bengal with Bengali names. Return JSON.`,
format: "json",
stream: false
})
})
// 3. Vision — leaf diagnosis, stays on device
await fetch("http://localhost:11434/api/chat", {
method: "POST",
body: JSON.stringify({
model: "moondream",
messages: [{
role: "user",
content: "Diagnose Bengal balcony leaf. Return {issue, cause, remedy_en, remedy_bn}",
images: [base64]
}],
stream: false
})
})
Offline build mitti-local.zip works with WiFi off after ollama pull
How I Built It
The stack is embarrassingly simple — and that is the point.
One file. No backend. Two open-weight models running on Metal.
index.html (React + Tailwind via CDN)
├─> localStorage (journal, balcony config)
├─> FileReader.readAsDataURL (balcony photo → base64)
├─> fetch → Ollama /api/tags (detect what is installed)
├─> fetch → Ollama /api/generate (llama3.2 for planting advice)
└─> fetch → Ollama /api/chat (moondream for leaf diagnosis)
Model 1 — llama3.2:latest (2.0 GB, Q4_K_M, 2B params) — The Text Oracle
Why this model and not GPT? Three reasons: speed, JSON reliability, and 22°N prompting.
On Apple M4 Metal, it responds in <2 seconds for 3 plant cards. Prompt is engineered for Bengal, not USDA zones:
"You are Mitti, oracle for Kolkata 22°N, late monsoon 31°C 84% humidity, balcony N facing 4hrs direct sun, medium 12-inch pot. Suggest exactly 3 plants to sow THIS week in Bengal. Use only: Palak, Dhonepata, Pudina, Lal Shak, Pui Shak, Notey, Begun, Lanka, Kochu. Consider monsoon humidity = fast germination but fungal risk. Return JSON array: [{bn, en, whyNow, depth, spacing, water, harvest, companion}]"
Then local filter: if sunHours < 2 + pot=small, remove Begun/Lanka (needs 6hr + large bosta). If direction=N + sunHours=2, only allow Pudina, Pui Shak, Kochu. This filtering is hard-coded in JS — the model suggests, local logic validates. No frost dates anywhere.
Model 2 — moondream (1.7 GB, 1.7B params, Apache 2.0) — The Eye
I started with llama3.2-vision:11b. It crashed:
500 Internal Server Error
llama-server has terminated
unknown model architecture: 'mllama'
Ollama 0.40.1
Mllama architecture wasn't supported in my Ollama build. Instead of giving up, I built a fallback chain:
const tryModels = ["moondream", "llava:7b", "llava:13b", "llama3.2-vision:11b"]
for (let m of tryModels) {
try { await fetch(... model: m ...) ; break }
catch { continue }
}
moondream won — 1.7B, trained specifically for edge vision, great at close-up leaf texture. On M4 it diagnoses overwatering + fungal spots in ~4 seconds. Input pipeline:
input type="file" → FileReader → base64 string → JSON images: [base64] → Ollama chat → local
The image never leaves the device. Badge Checked locally • No cloud is not marketing — it is if (res.ok) setBadge("Checked locally • No cloud").
Ollama + llama.cpp + Metal
Ollama is Go wrapper around llama.cpp with Metal GPU support. From my logs:
inference compute id=0 library=Metal compute=0.0 name=MTL0 total="11.8 GiB"
model type=Llama embedding=2048
OLLAMA_ORIGINS=* ollama serve is critical — without it, browser fetch to 127.0.0.1:11434 is blocked by CORS. Serving Mitti via python3 -m http.server gives it a proper origin. If fetch fails, app degrades gracefully to a mock Bengal pool (still monsoon-correct).
Bengal logic that no US model has:
- Watering: "Keep moist, not wet" vs "Let top dry" vs "Heavy drinker" based on humidity. Para tip: "Don't water leaves after 6pm in monsoon. Fungus loves night wetness. Water soil at 7am."
- Companion: Pudina + Begun? No — Pudina spreads, keep separate. Pui Shak + jaali + Begun = good, Pui climbs, Begun shades roots.
- Pot mapping: 8" tub = herbs only (Pudina, Dhonepata). 12" pot = most greens. 18" bosta crate = Begun, Kochu, Pui with trellis.
Journal is localStorage.setItem("mitti-journal", JSON.stringify(entries)). No DB, no login. Session journal clears when tab closes — intentional friction to go offline.
Why Does Open Innovation Matter?
Closed APIs would have made Mitti technically possible but culturally dead. Open weights made it Bengali.
1. Privacy is not a feature — it is the whole product.
A mother in Baguiati will not upload a photo of her balcony to OpenAI. That photo has her kids' clothes drying, her flat number on the wall, her mother's face. She will upload a sick leaf to something that says Checked locally • No cloud because the photo literally never leaves FileReader. With moondream running on her son's M4 MacBook, diagnosis happens in 127.0.0.1. No S3 bucket, no log retention, no "we may use your data to improve our models."
I tried to do this with a closed vision API first. The first thing the docs said: "Images are stored for 30 days for abuse monitoring." My para aunty said: "Na baba, thak." No, son, leave it. Open innovation made trust possible.
2. Frost is in the training data. Monsoon is not.
GPT-4, Claude, Gemini — all know "last frost date April 15, plant tomatoes after." None know that in Kolkata, late monsoon is the best time to sow Pui Shak because humidity = 84% = seeds germinate in 3 days instead of 7, but you must not water leaves after 6pm because Alternaria fungus explodes in night wetness.
That knowledge is not in Common Crawl. It is in para gossip, in 70-year-old uncles who say "Bosta te Begun bhalo hoy" — brinjal grows well in fruit crates. With open-weight models, I could inject that knowledge in the system prompt and then add a local JS filter that enforces it. I own the prompt. I own the filter. I own the fallback.
With a closed API, you get a 2000-token system prompt limit and a content filter that flags "neem oil 1 tsp" as medical advice. With llama3.2, I can write a 400-token prompt in Bangla-English mix and it just works.
3. It works where WiFi doesn't — which is exactly where you garden.
My balcony has no WiFi. Many buildings in Kolkata have patchy data. The exact place you need a garden oracle — standing in 31°C humidity with soil in your hands — is the place where a closed API times out.
After ollama pull moondream once, Mitti works in airplane mode. I tested it: turn off WiFi, generate planting advice, diagnose leaf — all <5 seconds on Metal. A closed API dies on the balcony. Open weights thrive on the balcony. That is the whole Touch Grass brief.
4. Forkable for their 22°N — not just my 22°N.
Open innovation means a student in Chennai can fork Mitti tomorrow, replace Pudina, Pui Shak, Lanka with Agathi, Muringa, Vendakka, change N facing 2hrs to West facing 5hrs + sea breeze, and run ollama pull moondream on his 8GB IdeaPad. No API key, no $20/month, no rate limit, no credit card.
A closed API charges $0.01 per image. For 10M balcony families uploading 2 leaf photos a week, that is $800K/month leaving Bengal. Open weights keep it in Bengal.
This is why I built Mitti on a MacBook and not on Vercel. Open innovation didn't make Mitti cheaper. It made it ours.
And ours means you can fork it. Tomorrow someone in Howrah will add Kochu stems, someone in Sylhet will add their jaali trellis pattern, someone will teach moondream what overwatering looks like on Lal Shak — not just lettuce. That fork won't need my permission. It won't need an API key or a credit card. It will just need a bosta crate and some mitti.
I built Mitti to be outgrown. The best thing that can happen is it stops being my balcony oracle and becomes your para's oracle. That's Touch Grass. That's why it runs on your laptop, not someone else's cloud.



Top comments (0)