This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass
What I Built
In Port Harcourt, the light goes and the room changes in about two seconds. The fan stops. The router dies. The laptop drops to battery. Then somebody shouts "on the gen!"
I used to just sit there. Phone on mobile data, laptop on battery, waiting for the gen to come on so I could keep working.
That gap, between NEPA taking light and someone starting the generator, is the one moment in my day when the screen is already losing. Light don go uses it. When my laptop switches to battery for more than 30 seconds, a full-screen page opens on top of whatever I'm doing:

The real outage at 12:31 on 8 October. The big line is Gemma's. The line above it is code.
It gives me one plan, a 60-second stretch timer (stand up, reach up, roll the shoulders, touch the toes, go outside), and a button: "I don stand up. Close am." When the timer ends, it says "You self don try." The page doesn't open again until the next cut.
The sky follows the situation. It is warm in the day and counts down to rain or sunset. It is dark at night, with the plan kept near the house:

Day and night are simulated runs (ldg simulate). The nudges are real Gemma output from those runs, taken from the log.
The page works with no internet, because the router is already off: it uses no web fonts and loads nothing from a CDN.
It is for anyone who works from a laptop where the power cuts are routine: Nigeria, load-shedding in South Africa, a lot of India and Pakistan. You don't need to explain the problem to any of them.
Words you may not know (Nigerian English and Pidgin):
| Word | Meaning |
|---|---|
| NEPA | What Nigerians still call the power company (the old National Electric Power Authority). "NEPA takes light" means the grid power goes off. |
| Light don go | "The power is gone." |
| Gen, "on the gen" | The petrol generator most homes start by hand when the grid fails. "On the gen" means start it. |
| Hold the gen | Keep the generator off. "₦1,500 saved every hour e rest" means each hour it stays off saves ₦1,500 of petrol. |
| Abeg · waka · close am · fit | Please · walk · close it · may ("rain fit start" = rain may start) |
| You self don try | "You have done well." |
| Mineral · kiosk · junction | Soft drink · small roadside shop · the street corner where food sellers sit |
| WAT | West Africa Time, UTC+1 |
Demo
Watch on YouTube: https://youtu.be/Zr-YILDu6JI
Real outages so far, from logs/outages.jsonl (8 and 9 October 2026, WAT):
| Time | Event | What the tool did |
|---|---|---|
| 12:23 | NEPA cut | Rain path, indoor activity. Gemma timed out; fallback sentence shown |
| 12:25 | Power back after 123 s | Logged (the gen) |
| 12:31 | NEPA cut again | Gemma: "Abeg shut that laptop, waka go stand by the window stretch back and legs, rain dey down." Passed the guard first try |
| 12:34 | Power back after 225 s | Logged |
| 12:53 | NEPA cut | First cut with the full-screen page. Gemma: "Abeg shut that laptop, walk to the kiosk for cold mineral and come back quick." Passed first try. Facts line: rain around 15:00, about 2 hours left |
| 13:34 | NEPA cut, laptop asleep | Timed out, fallback logged. The Windows event log shows the laptop was in Modern Standby: it woke for a moment at the cut, went back to sleep mid-call, and woke again at 13:56. The model was still loaded. Nobody was at the screen |
| 9 Oct, 07:33 | NEPA cut | Gemma: "Abeg shut that laptop, walk to the kiosk find cold mineral and come back quick." Passed first try |
What I did when it told me to go. I closed my laptop, took a walk outside in my neighbourhood in Port Harcourt, visited my cousins and played with their children. That was the 07:33 cut on 9 October. The light came back at about 12:05.
The first real outage found a bug. More on that below.
Code
dannwaneri
/
light-don-go
When NEPA takes light, a local Gemma tells you to go outside before you on the gen. Hacktoberfest 2026 Touch Grass.
Light don go
When NEPA takes light, and before anyone starts the generator, your laptop tells you to go outside, then gets out of the way.
- Detects the cut: the laptop drops to battery for 30 s or more (no inverter in this house).
- Works with the router dead: the forecast is saved while power is on; after the cut nothing touches the network.
- Code decides, Gemma phrases: rules compute sunset (NOAA formula, offline), the dry window and ₦ saved, and write the facts line. A local open-weight Gemma writes only the nudge, and a guard rejects it if it has any number, more than 20 words, or mentions light/the generator. One retry, then a fixed sentence.
Words you may not know (Nigerian English and Pidgin):
| Word | Meaning |
|---|---|
| NEPA | What Nigerians still call the power company (the old National Electric Power Authority). "NEPA takes light" means the grid power goes |
How I Built It
Detection. There is no inverter in my house, so when NEPA goes, the laptop really does go to battery. psutil reads the power state every 5 seconds. A drop shorter than 30 seconds is a flicker. A second cut within 15 minutes gets logged but not nudged, so NEPA flashing the light on and off doesn't spam you.
Weather without internet. When the light goes, the router goes with it. So while power is on, the tool saves the hourly Open-Meteo forecast every 30 minutes. After the cut, it reads only that file. Sunset comes from the NOAA solar formula, computed on the laptop. It gives 18:21 for Port Harcourt on 7 October; Open-Meteo says 18:21 and sunrise-sunset.org says 18:22.
One rule had to change for the rainy season. I first counted an hour as "wet" at a 50% chance of rain. In October in Port Harcourt, almost every hour is over 50%, so the tool kept everyone indoors all day. Now an hour is wet at 70%, or at 0.5 mm of expected rain.
The model: Gemma 4 E2B, on my CPU. gemma4:e2b-it-qat through Ollama. It is a 4.3 GB download. My laptop is an i5-1135G7 with no NVIDIA GPU. Measured: 14 s cold, 3.6 to 4.0 s warm, about 15 to 20 tokens per second.
What the model is allowed to do. This is the part I care about most.
My first version let Gemma write the whole message from the facts. In 4 test runs it dropped every number, and twice it turned the meaning upside down:
"E be like NEPA light don come Port Harcourt…"
That means the light came back.
"Close your laptop now, before the generator dies…"
The generator wasn't on.
A check on the numbers could not catch either of these, because neither sentence had a number in it.
So I split the job:
- Code writes the facts line. It says what happened, when the rain or sunset comes, the minutes left, and the naira saved. Every number in the message comes from code.
- Gemma writes only the nudge. It sees three things: the place, the situation ("raining now", "dry daylight", "after dark") and an activity picked by rule. It sees no numbers at all.
- A guard checks the nudge. It rejects the nudge if it has any number, more than 20 words, or any mention of NEPA, light, power or the generator. Gemma gets one retry, with the reason. If the retry fails too, a fixed sentence is used. That is two model calls at most, and never a loop.
After the split, 9 of 9 test runs passed first time, with no meaning errors that I saw.
What the guard can't do. It checks numbers and topic, not sense. In one test run Gemma wrote "watch the rain inside the phone", and it passed. A bigger model would make fewer sentences like that, but it would not run on this laptop. I chose the small model, and I accept that one of its sentences will sometimes be odd.
The bug a real outage found. At 12:23 the light went, the tool caught it, and Gemma timed out. I measured before changing anything. ollama ps showed that the model had just been loaded, by the request that failed. Ollama unloads an idle model after 5 minutes. My warm-up had run at 11:53, so at 12:23 the tool hit a cold start, on battery, and took more than 20 s. The fix is one setting: keep_alive: -1, which keeps Gemma in RAM (3.9 GB of my 15.7). At the next cut, at 12:31, Gemma answered first time. One later cut, at 13:34, also timed out, but the cause was different: the laptop was asleep (Windows Modern Standby). I checked the event log before blaming the fix.
So the defaults broke the one promise the tool makes, which is to work when the light goes. I would not have found that in a simulation.
The page. One HTML file with the outage data written in as JSON. Edge opens it with --app --start-fullscreen, so there is no address bar and no tabs. The model's sentence goes in as data and is never inserted as HTML. A test feeds it a nudge containing </script> and checks that the page does not break.
Tests. There are 98 tests, including one that blocks every network socket and still runs the outage path to the end. I also broke the guard on purpose 8 times (wider tolerances, checks removed), and a test failed every time.
Why Does Open Innovation Matter?
Because the network dies with the light.
When NEPA takes light, my router is off until someone starts the gen. A cloud API is unreachable at the exact moment this tool needs to speak. A model on my laptop answers in 4 seconds with no signal. Another reason was not needed.
It also costs nothing per outage, which matters when outages happen every day. And because Ollama and the model are open, I could see why the first real call failed: an idle model unloads after 5 minutes, and I could change that setting. With a closed API, a timeout is just a timeout.
Prize Categories
- Best Use of Gemma. Gemma 4 E2B (QAT) runs locally through Ollama, offline, on a CPU-only laptop. It is the only part of the system that writes text, and the guard limits it to what a small model does well.
The tool's best result so far isn't in the logs. It's a morning with my cousins' children that I would have spent waiting for the gen.

Top comments (33)
Hey Daniel. Very good work! For some reason, I thought the title said that your laptop physically dragged you outside for a second lol.
But I hope your doing well on your end. Sorry if I have been leaving you out in some way. Just want to let you know that I am still here :)
Haha, it’s not that strong yet! 😂 Good to hear from you, Francis and no worries at all. Hope you’re doing well too.
Without GPU, Gemma is too slow. E2B is usable, but it too is slow.
Best of luck 👍️
Thanks Fayaz! You’re right, on CPU it’s slow. I measured 15–20 tokens/s on my i5. It works here because Gemma only writes one sentence of about 25 tokens. The real outage calls took 6 to 9 seconds and the tool already waits 30 s to make sure it’s a real cut, not a flicker.
For a chat app I’d want a GPU. Good luck with Birds.Quest too, I liked the “predictions are not sightings” rule.
Yeah, I learnt the hard way!
My i7 with 4 cores (3.4GHz each) took quite some time to auto write my last afternoon's bird watching session.
Fortunately, the real thing will run on DigitalOcean Serverless GPU, costing probably a few cents each time! 🙃
I'm thinking about fine tuning a smaller model (sub 1B) for my purpose, just for the fun of it.
I'll post about it if I get good results 🥰
Ha, a whole afternoon’s walk is a lot more text than my one sentence 😄 A sub-1B fine-tune sounds fun. A small model trained only on your blog’s style could be quick even on CPU. Do post it, I’d like to see the results!!!
I used another open model that takes GPS entry and predicts birds in that path. It's only a 15MB Model! Can even run on a low end phone browser.
That gave me the idea actually. If a 15MB open model can do that so well, something like a 300MB model should be able to write the posts just fine.
A 15MB model for that is impressive! I think it works so well because it’s choosing from a fixed list of species.
Writing is open-ended and that’s where I saw small models slip.
Gemma E2B was fine at tone but got facts wrong when it wrote the whole message.
What fixed it for me was splitting the job: code supplies the facts, the model only phrases them.
Your setup already has that shape, with the 15MB model giving the bird list. A ~300MB writer that only phrases that list might work well.
daniel, this is a masterclass in constraint-driven, local-first engineering. massive respect. 🐯🔥
as a developer who builds strictly under heavy constraints (like running ai tools on a $150 phone), i deeply appreciate the architectural choices you made here.
splitting the job so the code handles the facts (numbers, time, naira saved) and gemma only handles the phrasing is brilliant. it’s the exact right way to use a small, local model: leverage its tone while completely eliminating its tendency to hallucinate facts. the guardrail rejecting numbers and specific keywords is a beautiful, simple, and effective safety net.
also, discovering the
keep_alive: -1fix through a real-world cold-start timeout is the kind of battle-tested wisdom you can't get from simulations. keeping that 3.9gb model resident in ram to guarantee the tool works exactly when the grid fails is top-tier systems thinking.turning a frustrating daily reality into a moment of intentional rest is not just great engineering; it's great design. incredible work, and keep shipping! 💻🌍
Thanks Harun! 🙏 The keep_alive one hurt to find, but I’m glad a real outage caught it and not a judge. The guard isn’t perfect: it catches numbers and off-topic words, not sense, so the odd sentence still slips through. What AI tools are you running on the $150 phone?
daniel, glad the
keep_alivefix resonated! real-world outages are the ultimate debugging tool. 🐯to answer your question about the $150 phone: it’s all about ruthless optimization and avoiding heavy local inference when possible.
the goal is never to force the hardware to do what it can't. it's about designing the software to perfectly match the hardware's reality. keep building! 💻🌍
The test that blocks every network socket is a strong fit for the promise of this app. I’m curious about a long outage: does an expired cached forecast switch the plan to a conservative fallback, or is its age shown with the suggestion?
Good question. If the cached forecast is older than 6 hours, the tool treats it as missing: it drops rain from the plan and falls back to sunset only, which the laptop computes itself. It doesn’t show the forecast’s age, though, and you’re right that it should. In practice the nudge fires once, 30 seconds into the cut, while the forecast is at most 30 minutes old, so a long outage doesn’t make that first nudge stale. Showing the age is on my list.
Thanks, that answers it. I'd pictured the nudge updating throughout the outage, so the one-shot timing is an important distinction. Showing the forecast age alongside the sunset-only fallback would make the behaviour easy to understand. Appreciate the detailed explanation!
This is genuinely excellent.
The core insight is perfect: the short window between NEPA taking light and someone starting the gen is the one moment the screen is already losing. Turning that into a forced, offline, culturally fluent nudge to go outside is both clever and kind.
What I especially respect is the discipline in the architecture. Letting code own every number and fact, restricting Gemma to the nudge only, and putting a hard guard on length + forbidden topics is exactly how you make a small local model reliable. The real-outage debugging (cold start from
keep_alive, Modern Standby) shows the difference between something that looks good in simulation and something that actually works when the light goes.Also loved the cultural grounding. No one who has lived with routine power cuts needs the problem explained, and the Pidgin makes it feel native rather than imposed.
One of the strongest Touch Grass submissions I’ve seen. The best result really is the morning spent with your cousins’ children instead of waiting for the gen.
Thank you, Anne! This made my day. And yes, the morning with my cousins’ children is still the best result in the logs 😄
The separation between code-generated facts and model-generated wording is probably the most important design decision here. It reminds me of a problem in edge-based industrial monitoring: a model can generate a perfectly fluent response while getting the underlying state wrong, which is much worse than a slightly awkward sentence.
I’d be curious how you’d handle semantic validation as the project grows. Your current guard catches numbers and restricted topics, but it can’t detect a recommendation that contradicts the actual weather or situation. A rule-based validator could check the proposed activity against the same structured state used to generate the facts, leaving Gemma responsible only for phrasing. That would preserve the local-first approach without adding another model call.
Also, the laptop sleep issue is a good reminder that keeping a model resident solves only one part of reliability. Power-state transitions and process availability are part of the system contract too. Real outages exposed exactly the kind of failure a clean simulation might miss.
Thanks Sina. I think this is really the crux of the matter - fluency without accuracy. (The solution you're proposing is partly already implemented, actually - instead of "choosing" an activity, an activity is "picked" for Gemma. Rules "pick" an indoor activity when it's raining, a home-close activity after sunset, or an outdoor activity when the weather is dry.
Gemma is then given the picked activity to articulate, and it's up to her to do it well. But Gemma is not perfect: rather than correctly articulating the picked activity (outdoor), she'll in the wrong cases articulate some activity very poorly (given as "watch the rain inside the phone"), or articulate some other activity altogether. A corrective could be a rule to check the nudge is still talking about the picked activity - this would keep Gemma on track without needing a model call. I think you're right; reliability is what we're after. Keeping the model loaded may solve one class of problems, but the laptop going to sleep suggests the power state is part of the contract.
That makes sense. I’d treat the picked activity as an invariant rather than trying to judge whether the whole sentence sounds reasonable. For example, a lightweight semantic check could verify that the nudge still expresses the intended action, while rejecting outputs that introduce a different activity. The tricky part is that keyword matching alone can be brittle: “take a walk” and “step outside” express the same intent, while “watch the rain inside the phone” shares the topic but loses the intended action.
This is where a small set of activity-specific acceptance rules could be more useful than another model call. Given the offline constraint, I’d also keep the existing deterministic fallback whenever validation fails, rather than letting a second attempt become another reliability dependency. The goal isn’t to make Gemma perfect; it’s to make sure an imperfect response can’t break the behavior the system promises.
An invariant is a better frame for me. And, "Is this chosen action still being reported?", followed by, "Does this output sound reasonable?" but latter is inappropriate. (Each of the activities in the list can have a few words that show they survived doing it - junction, plantain, fish, veranda, rain, sit, for example.
Anything more than these words is failure.) Today, the guard gives Gemma one chance to retry, with an explanation of the failure, before returning the fixed-sentence output. That's a reliability dependency, I accept. If the CPU is slow, it can double the delay. So, probably better to skip the retry after a single failed check. Yes, a bad answer cannot undermine the behavior the system promises. I may steal this line.
“Love that the guard blocks numbers and any mention of the generator, and still let ‘watch the rain inside the phone’ through. Rejecting digits while approving nonsense is a very specific kind of quality control 😄. Genuinely though, the code-writes-facts, model-writes-nudge split is the smartest part of this. Have you thought about a tiny allowlist of activity phrases so the model only picks tone, not content?”
You're right, the guard is rejecting valid digits but allowing nonsense. It's currently based on using fixed lists of allowed words with the choice based on the weather and time and then providing only the chosen activity to Gemma. It doesn't include any check that the sentence actually states an activity, which is why the sentence inside the phone got through. So we're in agreement on what to change, I just haven't done it yet 😅
This is such a creative and practical use of AI! I love how you turned a frustrating power outage into an opportunity to disconnect, get outside, and reconnect with people. The decision to run Gemma locally and keep the tool functional without internet makes the project even more impressive. Sometimes the best measure of technology isn’t how much work it helps us do, but how effectively it reminds us to step away and enjoy life.
Thank you, Marie. This one's near and dear to my heart also - my favorite thing in the logs was not a number, but rather the morning I spent outside with my cousins' kids.
The 30-second battery threshold is the smart part. I tried something similar with a power-loss webhook on a UPS and kept getting false triggers from brief dips — 30 seconds is long enough to filter noise but short enough that you're not already deep back into work by the time it fires. Using Gemma for the nudges instead of hardcoded strings is a nice touch, though I'd want to see what happens when battery is already at 12% and the model tells you to go look at stars. Real outages are the only honest test bed for this kind of thing, so respect for running it on actual NEPA cuts instead of ju...
Thanks, you found the case I dealt with. The code triggers the nudge when the battery is 15% or less. The text is "Battery 12%: sleep the laptop now." Your screenshot at night is exactly that nudge: stars, 12%, "sleep the laptop now." That 30 second was generated by the same false trigger that was impacting the UPS.
Ahhh, reading this really shows how tough it is. Infrastructure is such a huge burden. I hope the power supply in your country gets better soon. I haven’t experienced a major blackout in over 20 years here. Keep up the great work 💪, good luck!
Thank you! 20 years without a major blackout sounds like a dream from here 😄 We’ve learned to work around it. This project was me trying to turn the outage into something good instead of just waiting for the gen. Thanks for reading and for the support!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.