<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Manswi Pradhan</title>
    <description>The latest articles on DEV Community by Manswi Pradhan (@newton_lebniz).</description>
    <link>https://dev.to/newton_lebniz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4154248%2F8146ea21-8628-4850-a3a0-81006b9c1a77.jpg</url>
      <title>DEV Community: Manswi Pradhan</title>
      <link>https://dev.to/newton_lebniz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/newton_lebniz"/>
    <language>en</language>
    <item>
      <title>I built a ghost that haunts my desktop until it can see my work</title>
      <dc:creator>Manswi Pradhan</dc:creator>
      <pubDate>Sat, 31 Oct 2026 15:27:55 +0000</pubDate>
      <link>https://dev.to/newton_lebniz/i-built-a-ghost-that-haunts-my-desktop-until-it-can-see-my-work-39f4</link>
      <guid>https://dev.to/newton_lebniz/i-built-a-ghost-that-haunts-my-desktop-until-it-can-see-my-work-39f4</guid>
      <description>&lt;p&gt;&lt;em&gt;My submission for the Hacktoberfest Open-Source AI Challenge: Ghost in the Machine. Gemma 3 4B, Ollama and PySide6. Nothing leaves my laptop.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A reminder asks one thing of you: a tap. Snooze, dismiss, "done". None of them checks anything and I am very good at tapping.&lt;/p&gt;

&lt;p&gt;So I built Haunt, a small ghost that appears over whatever I am doing, says something unsettling and does not leave until it can see that the work is done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You add a task. Every few minutes the ghost appears on top of your other windows and speaks a line written by Gemma. If you ignore it, it comes back angrier: a whisper first, then eerie, then furious.&lt;/p&gt;

&lt;p&gt;When you click &lt;strong&gt;I did it&lt;/strong&gt;, the ghost says &lt;em&gt;"Hold still. Let me look at your screen..."&lt;/em&gt; and checks. If it does not see the work, it tells you what it does see and it stays. Something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I see a YouTube video of cats. That is not "Finish the report".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you honestly did the work and the model disagrees, an "I swear I did" button appears after two failed claims. A wrong judgement should never trap you.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it sees
&lt;/h2&gt;

&lt;p&gt;Here is what happens when you click &lt;strong&gt;I did it&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The ghost hides before capturing the screen, so it does not photograph itself.&lt;/li&gt;
&lt;li&gt;Haunt captures visual evidence from the desktop.&lt;/li&gt;
&lt;li&gt;The screenshot and task context are sent to Gemma 3 4B through Ollama.&lt;/li&gt;
&lt;li&gt;The model evaluates whether the screenshot supports the completion claim.&lt;/li&gt;
&lt;li&gt;Haunt processes the verdict. If the evidence is insufficient, the ghost responds and remains part of the interacti
on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model's job is not to read your mind. It evaluates what is visible on the screen.&lt;/p&gt;

&lt;p&gt;That distinction matters: a screenshot is evidence of a moment, not proof of everything you have done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not everything needs a model
&lt;/h2&gt;

&lt;p&gt;If a task is tied to a file or a repo, Haunt does not need to look at anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python haunt.py add &lt;span class="s2"&gt;"Write tomorrow's notes"&lt;/span&gt; &lt;span class="nt"&gt;--proof&lt;/span&gt; file:~/notes.txt
python haunt.py add &lt;span class="s2"&gt;"Commit the lab work"&lt;/span&gt; &lt;span class="nt"&gt;--proof&lt;/span&gt; git:~/lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first finishes the moment the file is saved after the task was created. The second finishes when a new commit lands. If a file was saved, I do not need a 4B vision model to tell me. Checking that is cheaper and more reliable than asking a model to interpret pixels, so the model only handles the tasks that really are visual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does the judge work?
&lt;/h2&gt;

&lt;p&gt;I tested the judge using 10 screenshots: 5 representing completed tasks and 5 representing incomplete tasks.&lt;/p&gt;

&lt;p&gt;This was an initial sanity check to see whether the model could distinguish relevant work from unrelated desktop activity.&lt;/p&gt;

&lt;p&gt;I did not retain the individual verdict counts needed to report a reliable confusion matrix, so I am not presenting invented accuracy numbers. Ten screenshots would also be far too small a dataset to establish general reliability.&lt;/p&gt;

&lt;p&gt;A more rigorous evaluation would use a larger, labelled dataset and report true positives, false positives, true negatives, false negatives, precision and recall.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The buttons did nothing.&lt;/strong&gt; The ghost's buttons looked fine and did not respond to clicks. The tests were green the whole time, because they click by calling &lt;code&gt;button.click()&lt;/code&gt; in code, which skips the window system entirely. I isolated it with a minimal Qt button test and then the real window. The culprit was the &lt;code&gt;Qt.Tool&lt;/code&gt; window flag. Removing it fixed clicking in my WSLg setup. I only tested that one setup, so other desktops may behave differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A newer PySide6 crashed the app.&lt;/strong&gt; PySide6 6.12.0 crashed Haunt on my machine. I found that 6.8.3 works in a clean environment and pinned it in &lt;code&gt;requirements.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;localhost&lt;/code&gt; was not where Ollama lived.&lt;/strong&gt; I develop in WSL while Ollama runs on the Windows host, and &lt;code&gt;localhost&lt;/code&gt; inside WSL did not reach it. I point Haunt at the host's address with &lt;code&gt;OLLAMA_HOST&lt;/code&gt;. That address is specific to my setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "voice" was a beep.&lt;/strong&gt; My first version called &lt;code&gt;QApplication.beep()&lt;/code&gt; and I was calling that speech. The real fix was Windows' built-in &lt;code&gt;System.Speech&lt;/code&gt;, driven from PowerShell on a background thread so the ghost never freezes while it talks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it had to be local
&lt;/h2&gt;

&lt;p&gt;This project exists because of a conflict. A ghost that watches your screen sees your mail, your chats and whatever you would rather not show anyone. That is a reasonable thing to build only if the screenshot goes to your own machine and nowhere else.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; The screenshot is held in memory and sent to Ollama on my own computer. After the one-time model download, nothing in Haunt calls out to the internet and there is no API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every look is free.&lt;/strong&gt; A ghost that checks you repeatedly is only affordable when a look costs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I can swap the model.&lt;/strong&gt; &lt;code&gt;HAUNT_MODEL&lt;/code&gt; or &lt;code&gt;--model&lt;/code&gt; picks any vision-capable model from &lt;code&gt;ollama list&lt;/code&gt;. I used a 4B model because my laptop has 16 GB of RAM and no GPU, and a bigger one would be impractical. The ghost covers the wait with "Hold still. Let me look."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It degrades instead of dying.&lt;/strong&gt; If Ollama is not running, the ghost uses built-in lines and says so in the window ("voice: built-in"), rather than pretending.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it can't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A screenshot is one moment, not an hour of work. Opening the right window would fool it.&lt;/li&gt;
&lt;li&gt;A 4B model can accept irrelevant evidence and reject real work. That is why the honour button exists.&lt;/li&gt;
&lt;li&gt;Speech uses the Windows speech engine, which is built in but is not an open-weight model. On other systems the ghost is silent.&lt;/li&gt;
&lt;li&gt;Local is not the same as secure. A screenshot can still contain sensitive things.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/newton-lebniz/haunt
&lt;span class="nb"&gt;cd &lt;/span&gt;haunt
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
ollama pull gemma3:4b
python haunt.py check
python haunt.py add &lt;span class="s2"&gt;"Finish the report"&lt;/span&gt; &lt;span class="nt"&gt;--expect&lt;/span&gt; &lt;span class="s2"&gt;"The report open with a finished conclusion"&lt;/span&gt; &lt;span class="nt"&gt;--first&lt;/span&gt; 0
python haunt.py run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/newton-lebniz/haunt" rel="noopener noreferrer"&gt;https://github.com/newton-lebniz/haunt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Landing page: &lt;a href="https://newton-lebniz.github.io/haunt/" rel="noopener noreferrer"&gt;https://newton-lebniz.github.io/haunt/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;How I built it: I used AI assistants for code and debugging. I ran it, tested it and fixed the real-desktop problems on my own machine.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A ghost that watches your screen should not live on someone else's server. This one lives on mine, and it still will not let me off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge:&lt;/strong&gt; Hacktoberfest Open-Source AI Challenge&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Week 4 Theme:&lt;/strong&gt; Ghost in the Machine&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Category:&lt;/strong&gt; Gemma&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Inbox Sloth: a local Gemma that reads your inbox so you don’t have to</title>
      <dc:creator>Manswi Pradhan</dc:creator>
      <pubDate>Sun, 25 Oct 2026 09:05:10 +0000</pubDate>
      <link>https://dev.to/newton_lebniz/inbox-sloth-a-local-gemma-that-reads-your-inbox-so-you-dont-have-to-1po9</link>
      <guid>https://dev.to/newton_lebniz/inbox-sloth-a-local-gemma-that-reads-your-inbox-so-you-dont-have-to-1po9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;My professor emails on Monday: &lt;em&gt;assignment due Friday.&lt;/em&gt; On Tuesday: &lt;em&gt;"correction, because of the holiday it's now Thursday."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An email summariser gives me two accurate summaries and zero help. I still have to notice they're about the same assignment, remember which one wins, and move the date in my head.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inbox Sloth&lt;/strong&gt; does that part for me. It reads a mailbox with &lt;strong&gt;Gemma running on my own laptop&lt;/strong&gt;, turns emails into &lt;em&gt;commitments&lt;/em&gt; (what, by when, which email said so), and notices when a later email changes an earlier one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Submit Probability Assignment 3&lt;/strong&gt;: Thu 22 Oct, 11:59 PM. &lt;em&gt;Changed (was Fri 23 Oct).&lt;/em&gt;&lt;br&gt;
Evidence: "Please submit Probability Assignment 3 by Thursday 22 October, 11:59 PM instead."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I open one page and see what to do first, what's due this week, what clashes, and which dates moved and are worth double-checking.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reads email, extracts commitments:&lt;/strong&gt; assignments, meetings, exams, replies, with deadlines resolved to real dates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracks them across emails:&lt;/strong&gt; a correction updates the original task and keeps the history; an older email can never overwrite a newer one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plans your day and week:&lt;/strong&gt; ranked by urgency first, then by what you said matters (a student's exam, a freelancer's client reply). Every ranking shows its reasons, so you can disagree with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flags trouble:&lt;/strong&gt; overlapping events, overloaded days, moved dates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shows its evidence:&lt;/strong&gt; every task links to the sentence it came from, highlighted in the original email.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admits what it ignored:&lt;/strong&gt; informational emails (a mess menu, an instruction tweak) create no task and appear under "Ignored by the sloth" with the reason, so nothing disappears silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyc3hozqr124tqbg7wnh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyc3hozqr124tqbg7wnh.png" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2dks3imbvrq1iknk2w9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2dks3imbvrq1iknk2w9.png" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2qwfpslg167cufnwe999.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2qwfpslg167cufnwe999.png" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95tlz5zzxyl5a8wiaiz3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95tlz5zzxyl5a8wiaiz3.png" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run it yourself in a few minutes (the &lt;code&gt;demo&lt;/code&gt; command runs the real model on 18 sample emails and scores it against ground truth):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:1b
&lt;span class="nb"&gt;cd &lt;/span&gt;backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest &lt;span class="nt"&gt;-q&lt;/span&gt;              &lt;span class="c"&gt;# 132 tests, no Ollama needed&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; app.cli demo           &lt;span class="c"&gt;# real model run, scored against ground truth&lt;/span&gt;
uvicorn app.main:app             &lt;span class="c"&gt;# http://localhost:8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/newton-lebniz/inbox-sloth" rel="noopener noreferrer"&gt;https://github.com/newton-lebniz/inbox-sloth&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; &lt;code&gt;gemma3:1b&lt;/code&gt; via Ollama (JSON-schema constrained output, temperature 0), FastAPI, SQLite, plain HTML/JS, pytest. It runs on a CPU-only laptop with 16 GB RAM.&lt;/p&gt;

&lt;p&gt;The key design decision: &lt;strong&gt;the model is a noisy witness.&lt;/strong&gt; The model &lt;em&gt;reads&lt;/em&gt;; code &lt;em&gt;decides&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email -&amp;gt; Gemma (local, JSON-schema output) -&amp;gt; checks in plain code -&amp;gt; commitment tracker -&amp;gt; plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dates are never computed by the model.&lt;/strong&gt; It copies the words ("Thursday 5 PM"); a deterministic resolver turns them into a date relative to when the email was sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A date has to be in the email.&lt;/strong&gt; If the model's date words don't appear there, they're replaced by the email's own date (flagged "Date unsure") or dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A task needs a cue:&lt;/strong&gt; an action, deadline, meeting or exam word, in a sentence that isn't about the past or a cancellation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence is chosen by code&lt;/strong&gt;, not trusted from the model's quote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kind and title are normalised&lt;/strong&gt; from the subject: a quiz is an exam, and a meeting is named after the meeting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety net:&lt;/strong&gt; if the model returns nothing for an email, rules may propose one task, subject to the same checks, and the UI marks it "found by rules".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Valid model answers are cached by (model, prompt, email), so re-analysing all 18 emails after a code change takes about a second instead of three minutes. That is what made fast iteration possible on a CPU.&lt;/p&gt;

&lt;h3&gt;
  
  
  The part I almost didn't write
&lt;/h3&gt;

&lt;p&gt;On Day 1, my evaluator said: &lt;strong&gt;10 of 12 commitments found, 6 of 12 dates right.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then I read the rows. It was comparing &lt;em&gt;"Pay semester fee"&lt;/em&gt; with &lt;em&gt;"Submit data structures exam"&lt;/em&gt;. My matcher accepted the best title match even when the best was bad, so the score looked better than the output. After fixing the evaluator and re-scoring the same real output:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day 1 (real gemma3:1b run)&lt;/th&gt;
&lt;th&gt;reported&lt;/th&gt;
&lt;th&gt;actual&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Commitments found&lt;/td&gt;
&lt;td&gt;10/12&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6/12&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dates correct&lt;/td&gt;
&lt;td&gt;6/12&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/12&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False positives&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What the real run showed: for 4 of the 14 actionable emails, including the deadline correction, no commitment came out. It invented tasks for informational emails ("Khichdi and papad dinner" from a mess-menu notice). It rewrote dates, and a quiz became "Attend class".&lt;/p&gt;

&lt;p&gt;So the interesting problem wasn't prompting. It was building something that stays correct when the model doesn't. With the checks above, same prompt:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Day 1 (re-scored)&lt;/th&gt;
&lt;th&gt;After the code checks (real Gemma run)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Found&lt;/td&gt;
&lt;td&gt;6/12&lt;/td&gt;
&lt;td&gt;12/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dates correct&lt;/td&gt;
&lt;td&gt;5/12&lt;/td&gt;
&lt;td&gt;12/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kinds correct&lt;/td&gt;
&lt;td&gt;5/12&lt;/td&gt;
&lt;td&gt;12/12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An ablation (on recorded Gemma answers) shows where it comes from: without the rule safety net, 10/12 found and 9/12 dates; without the cue check, the false positives come back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Honest caveats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;I wrote the rules &lt;em&gt;after&lt;/em&gt; seeing these 18 emails, so this is a development-set score, not a claim that it generalises.&lt;/li&gt;
&lt;li&gt;I then wrote 12 differently-worded emails and ran them through the rules alone (all handled), and tried adversarial ones. That found two real flaws ("Thanks for attending the workshop on Monday" became a future task; an email's own subject could act as evidence for a title copied from it). Both are fixed and tested. But I wrote the rules and the tests, so treat this as a smoke test, not proof.&lt;/li&gt;
&lt;li&gt;All emails are synthetic. No real mailbox was used.&lt;/li&gt;
&lt;li&gt;A 1B model misses things; the rule fallback proposes at most one task per email. A cancellation is ignored rather than applied to an existing task. English only, no Gmail sync yet (imported messages only).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Email is the most private text most people have: grades, client names, medical appointments, offers. I didn't want to ship that to a third-party API just to find out when my lab report is due.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local inference:&lt;/strong&gt; after mail is fetched, analysis never leaves the machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free after setup:&lt;/strong&gt; no per-email bill, so running it over a whole mailbox is a non-event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swappable:&lt;/strong&gt; one environment variable (&lt;code&gt;SLOTH_MODEL=gemma3:4b&lt;/code&gt;) changes the model. That mattered, because a 1B model made mistakes and the architecture had to survive them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspectable and reproducible:&lt;/strong&gt; open weights at temperature 0 let me cache answers and replay them, so I could compare Day 1 and Day 2 on identical model output. A hosted model that changes under you can't give you that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;The saving is the part I used to do by hand: noticing that Tuesday's email overrides Monday's, and re-checking my own list. The honest limit: I haven't run it on a real mailbox for a week, only on 18 synthetic emails, so I can't claim hours saved. What I can show is that on those emails it found every commitment and applied the date correction, and that for every task it shows the sentence it came from, so checking it takes seconds.&lt;/p&gt;

&lt;p&gt;What I learned: (1) a metric you wrote in ten minutes is a hypothesis, and mine flattered me until I read the rows; (2) let the model read and let code decide; (3) make every judgment visible. The evidence sentence, the ranking reasons and the ignored list made the tool trustworthy and also debuggable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>I built a WhatsApp shop assistant that runs on a CPU-only laptop. The hard part was getting it to say "I don't know."</title>
      <dc:creator>Manswi Pradhan</dc:creator>
      <pubDate>Sun, 18 Oct 2026 07:20:39 +0000</pubDate>
      <link>https://dev.to/newton_lebniz/i-built-a-whatsapp-shop-assistant-that-runs-on-a-cpu-only-laptop-the-hard-part-was-getting-it-to-443m</link>
      <guid>https://dev.to/newton_lebniz/i-built-a-whatsapp-shop-assistant-that-runs-on-a-cpu-only-laptop-the-hard-part-was-getting-it-to-443m</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;If you've ever bought jewellery over WhatsApp or Instagram DMs, you know the script. You send "price?" The seller sends photos. You ask what it's made of, what size it is, whether it comes in another colour. Multiply that by every buyer and every item, and a solo seller spends more time retyping answers than making things.&lt;/p&gt;

&lt;p&gt;SellMate takes that job off their plate. You drop in a photo and a few keywords, and it writes the listing. Buyers get a chat page where they can ask questions. Anything the assistant can't answer lands in the seller's inbox instead of being guessed. It runs entirely on my laptop (16 GB RAM, no GPU) using two small open models through Ollama.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Photos and keywords in, listing out.&lt;/strong&gt; Title, description, highlights, tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It asks instead of guessing.&lt;/strong&gt; Material, size and delivery come back as questions for the seller, who types the answers once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A claim guard.&lt;/strong&gt; A photo shows colours, not materials. If the model writes "silver" or "hand-tied" and the seller never said so, plain code removes or softens it and asks the seller.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A shop page.&lt;/strong&gt; Buyers ask questions; the assistant answers only from the listing and the seller's own details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A seller inbox.&lt;/strong&gt; Anything it couldn't answer, with a count if several buyers asked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready to post.&lt;/strong&gt; WhatsApp, Instagram and Reddit versions, one click to copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fexlrygkhiw24mczp3bqu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fexlrygkhiw24mczp3bqu.png" alt="the shop page with a short buyer chat: price answered, size passed to the seller" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Who would pay for this
&lt;/h3&gt;

&lt;p&gt;A solo seller with a catalogue they keep re-describing in DMs is the person I'd start with. I haven't priced SellMate yet. I want to test two separate sources of value: whether generating listings saves enough time to be worth paying for and whether routing unanswered buyer questions to a seller inbox is useful enough on its own. I'd talk to a few handmade-product sellers, let them try both workflows and ask what they'd actually pay before setting a price.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Usage Example
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Drop in a bracelet photo, type "handmade bracelets", a price and a category.&lt;/li&gt;
&lt;li&gt;Read the listing. A red box shows what the claim guard removed; a yellow box shows what to check.&lt;/li&gt;
&lt;li&gt;Type what's true about the item and press &lt;strong&gt;Rewrite&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Copy the WhatsApp, Instagram or Reddit version.&lt;/li&gt;
&lt;li&gt;On the shop page, ask the price (instant), then something the listing doesn't cover (it passes the question to the seller instead of guessing).&lt;/li&gt;
&lt;li&gt;Back on the seller desk, the question is waiting in the inbox.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3h8qofyy9oboc9zwhzkb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3h8qofyy9oboc9zwhzkb.png" alt="the seller desk showing the " width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6jzbfqm4klncfqh4g4du.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6jzbfqm4klncfqh4g4du.png" alt="the ready-to-post tabs" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/newton-lebniz/SellMate" rel="noopener noreferrer"&gt;Github Repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You need Python 3.10+ and &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull moondream
ollama pull gemma3:1b
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
uvicorn backend.app.main:app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;http://127.0.0.1:8000&lt;/code&gt; (seller desk) and &lt;code&gt;http://127.0.0.1:8000/shop.html&lt;/code&gt; (what buyers see).&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;A 1B-parameter text model writes well but can't see images. A vision model that can see is heavy on a laptop with no GPU. So I used two small open models, each doing one job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Moondream&lt;/strong&gt; looks at the photo and describes how it &lt;em&gt;looks&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 3 1B&lt;/strong&gt; writes the listing from that description plus what the seller typed.
On my machine, one photo took about 22 seconds to look at and 18 seconds to write. That's slow next to a hosted API and I'll come back to that.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Buyers use a separate shop page. Their questions go to Gemma again, but it only sees the one to three products that match the question and it's told to answer only from the seller's facts. If it doesn't know, it says so, tells the buyer the question was passed on and the seller sees it in an inbox. The backend is FastAPI, the frontend is plain HTML, CSS and JavaScript and the catalog is a JSON file, because one seller on one laptop doesn't need a database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three things that broke
&lt;/h3&gt;

&lt;p&gt;This is the part I learned the most from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. 23 tests passed and the real model still failed.&lt;/strong&gt; On Day 1 my tests passed. Then I ran SellMate for real and got &lt;code&gt;Ollama returned an error (400)&lt;/code&gt;. &lt;code&gt;gemma3:1b&lt;/code&gt; is text-only and rejects image input. My tests stubbed the model, so they could never have caught it. Worse, the status light said &lt;code&gt;gemma3:1b ready&lt;/code&gt;, because the health check only knew the model was &lt;em&gt;downloaded&lt;/em&gt;, not that it could &lt;em&gt;see&lt;/em&gt;. The fix was the two-model pipeline, a health check that reports each model separately and a test that asserts the text model never receives an image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. I asked the model to grade its own homework.&lt;/strong&gt; For the inbox, I had Gemma return &lt;code&gt;answered_from_catalog: true/false&lt;/code&gt; alongside every answer. On the first live test, a buyer asked "What's the price?" and Gemma answered correctly ("The price is 100"), and the question still landed in the seller's inbox, next to repeats of the same material question.&lt;/p&gt;

&lt;p&gt;A 1B model can't be trusted to judge its own answer. The fix had two parts: plain price questions no longer go to the model at all (the answer comes from the price field, instantly and always correct), and the "needs the seller" decision now comes from plain code that reads what the answer says. Repeated questions are counted instead of repeated.&lt;/p&gt;

&lt;p&gt;My first version of that fix was wrong in the other direction. When I tried it on realistic replies, "You can contact the seller to order it" was treated as "I don't know", which is the original bug again, and "Sorry, I couldn't find that in the details" slipped through, which means a buyer's question never reaches the seller. Both failures cost something: noise teaches the seller to stop checking the inbox, and a missed question is a lost sale. Pointing at the seller now only counts when it's how the reply &lt;em&gt;opens&lt;/em&gt;, and "Is the price negotiable?" no longer gets answered with just the price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A colour became a metal.&lt;/strong&gt; This is the one that matters for a shop. My bracelet photo showed three bracelets: gold, silver and white. Moondream said so, correctly, as colours. Gemma wrote:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Handcrafted Silver Bracelet Trio&lt;/strong&gt; ... tags: &lt;code&gt;silver bracelet&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and a description claiming a "hand-tied design". Meanwhile the seller's own notes said the material was unconfirmed. A buyer reads "silver bracelet" as the metal. Telling the model "don't invent materials" in the prompt didn't stop it.&lt;/p&gt;

&lt;p&gt;So I added a claim guard: plain code that runs on every generated listing and compares its claims against what the &lt;strong&gt;seller&lt;/strong&gt; supplied (never the photo notes):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Title&lt;/td&gt;
&lt;td&gt;Handcrafted Silver Bracelet Trio&lt;/td&gt;
&lt;td&gt;Handcrafted Silver-coloured Bracelet Trio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tag&lt;/td&gt;
&lt;td&gt;&lt;code&gt;silver bracelet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;silver-coloured bracelet&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"hand-tied design"&lt;/td&gt;
&lt;td&gt;in the description&lt;/td&gt;
&lt;td&gt;removed, and the seller is asked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Handmade"&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;kept (the seller typed it)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once the seller confirms "sterling silver, hand-tied with cotton thread" and presses &lt;em&gt;Rewrite&lt;/em&gt;, the guard stops removing those claims. The guard is a word list, not understanding, so it won't catch everything, and I say so in the README. But it's the difference between a tool that &lt;em&gt;tries&lt;/em&gt; to be honest and one that's checked.&lt;/p&gt;

&lt;p&gt;I wrote regression tests for all three problems, using the real bracelet listing as a fixture. The test suite is now 168 tests. A regression test that can't fail isn't one, so &lt;code&gt;python scripts/mutation_check.py&lt;/code&gt; puts nine of the bugs back, one at a time, in a scratch copy and checks that the tests fail each time. They do. (My first run missed one, which is how I found a test that wasn't testing what it claimed.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Where it falls short
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slow.&lt;/strong&gt; About 40 seconds for one photo on my CPU. Three photos would take longer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The claim guard is a word list.&lt;/strong&gt; A claim in words it doesn't know gets through, so read the listing before you post it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The inbox reads the wording of the answer.&lt;/strong&gt; An oddly phrased "I don't know" could slip through.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No login.&lt;/strong&gt; It's a single-seller local tool. Don't put it on the internet as it is.&lt;/p&gt;
&lt;h3&gt;
  
  
  What I'd do next
&lt;/h3&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A faster vision step. 22 seconds per photo is the biggest wait.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Let sellers teach it their own words, so it learns that "kada" is a bangle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A login, so it can leave my laptop.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It costs nothing per message.&lt;/strong&gt; A seller with thin margins can't take a per-token bill on every "what's it made of?"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The photos and chats never leave the laptop.&lt;/strong&gt; AI inference runs locally through Ollama. Product photos are re-encoded before inference to strip EXIF metadata and a regression test checks a GPS-tagged image.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I could change the architecture when something broke.&lt;/strong&gt; When I found my text model couldn't read images, the fix was to add a second model and change one environment variable. With a closed API I'd have been choosing between vendors, not designing a pipeline.&lt;br&gt;
Where a closed model would very likely have been better: speed and caption detail. I didn't benchmark one, so that's an expectation, not a measurement. But the failures I hit weren't the kind a bigger model fixes. A colour read as a metal and a model grading its own answer are problems of process, and I could only fix them because I controlled the whole pipeline.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built SellMate with Claude as a coding assistant. The bugs described above were found by running it on my own laptop and the commit history in the repo shows each fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;My first tester was my roommate. She tried SellMate on a crochet toy she'd made, even though she wasn't planning to sell it. She tested the listing generation, and then I played buyer and asked whether I could request a customization.&lt;/p&gt;

&lt;p&gt;The listing didn't mention customization, so SellMate didn't make up an answer. It passed my question to the seller inbox instead. That let us try both sides of the workflow, and she loved it. Her reaction was that this would make things a lot easier.&lt;/p&gt;

&lt;p&gt;It was one informal test, not a real sale or proof that sellers will pay for it. My next step is to test SellMate with someone who regularly sells handmade products and find out whether it saves enough time to be worth using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 3 (1B) runs locally through Ollama, on a CPU-only laptop, and writes every listing and answers every buyer question.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>OUTSIDE: a local Gemma picks your next quest and tells you to put the phone away</title>
      <dc:creator>Manswi Pradhan</dc:creator>
      <pubDate>Sun, 11 Oct 2026 17:30:30 +0000</pubDate>
      <link>https://dev.to/newton_lebniz/outside-a-local-gemma-picks-your-next-quest-and-tells-you-to-put-the-phone-away-53k9</link>
      <guid>https://dev.to/newton_lebniz/outside-a-local-gemma-picks-your-next-quest-and-tells-you-to-put-the-phone-away-53k9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;OUTSIDE tells you what to do outside right now.&lt;/p&gt;

&lt;p&gt;It reads the time, the weather, how long until sunset, any event nearby and what you have already done today, then gives you one quest. At 5:45 PM on a clear day the quest is "find an open western view and watch the sunset for five minutes." If cloud rolls in, that quest is cancelled and replaced with something that does not need the sky. If a stargazing night is 3 km away then it becomes a special quest worth 500 XP.&lt;/p&gt;

&lt;p&gt;When you press Start, the screen says "Put your phone away" and shows a timer. That screen is the whole idea. The app has one job, which is to get you out of the door and then it gets out of your way.&lt;/p&gt;

&lt;p&gt;It is for anyone who opens their phone to decide what to do and ends up scrolling instead. There is no feed and no list to choose from, just one concrete thing to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmzfn6ecgizynvze2157f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmzfn6ecgizynvze2157f.png" alt="A sample quest" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo follows one evening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A live quest for the current weather.&lt;/li&gt;
&lt;li&gt;A simulated 5:45 PM with a clear sky: a sunset quest, with the minutes until sunset on the card.&lt;/li&gt;
&lt;li&gt;Heavy cloud switched on: the banner reads "The sky isn't cooperating", the sunset quest is struck through and a different quest replaces it.&lt;/li&gt;
&lt;li&gt;Demo events switched on: a stargazing night 3.2 km away becomes a gold-outlined special quest, 500 XP, about 38 minutes on foot.&lt;/li&gt;
&lt;li&gt;Start the quest, see the "Put your phone away" screen, finish, collect the XP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The weather and time controls exist only for demos. They let me show a cloudy evening without waiting for one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taking it outside
&lt;/h2&gt;

&lt;p&gt;So I majorly had the idea of this app due to my friend,as she always says that she has nothing to do outside and so she just ends up scrolling all day.&lt;br&gt;
So I tried it with her yesterday during the evening and we got the task to watch the sunset. We went and watched the sunset ,talked a bit and she said she enjoyed it, and even felt calm.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx916ao9nueo1v78latj0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx916ao9nueo1v78latj0.jpg" alt="The picture we took later" width="591" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/newton-lebniz/outside" rel="noopener noreferrer"&gt;Github Repo Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To run it you need Python 3.10+ and &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:1b
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
uvicorn backend.main:app &lt;span class="nt"&gt;--reload&lt;/span&gt;   &lt;span class="c"&gt;# then open http://127.0.0.1:8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The model:&lt;/strong&gt; Gemma 3 1B, running locally through Ollama. Ollama's &lt;code&gt;format&lt;/code&gt; option takes a JSON schema, so the model's reply is forced into the shape the app needs: quest type, title, action, duration and a one-sentence reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The stack:&lt;/strong&gt; a FastAPI backend and a plain HTML, CSS and JavaScript frontend with no build step. Weather and sunset time come from Open-Meteo, which is free and needs no key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The loop:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;location + time + weather + sunset + events + today's history
      -&amp;gt; context engine
      -&amp;gt; local Gemma (JSON schema)
      -&amp;gt; rule check
      -&amp;gt; quest   (or a rule-based quest after two rejections)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The model proposes and the code checks.&lt;/strong&gt; I did not trust a 1B model with the rules, so there is a validator between the model and the screen. If a quest breaks a rule, the model is asked once more with the reason. If it breaks a rule again, a rule-based generator answers instead and the card says so. The rules are small: no sky quest under heavy cloud or in the rain, no run on a hot day or after you already ran, short quests when it is wet, and an event quest has to name the real event.&lt;/p&gt;

&lt;p&gt;My first runs showed why this matters. An event quest came back one minute long. A sunset quest came back as "Observe the evening sky." Neither broke a rule I had written, so I added rules. Actions must be one or two concrete sentences, and an event quest's duration now comes from the walking time, calculated in code.&lt;/p&gt;

&lt;p&gt;The next problem was subtler. The model labelled 6 of my 7 test quests "nature", including the sunset one. That mattered because the "no sky quests under cloud" rule only checks quests labelled "sky", so a mislabelled stargazing quest would have walked straight past it. Now the prompt lists the quest types that are allowed right now, and code relabels any quest that mentions a sunset or stars as a sky quest before it is checked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing.&lt;/strong&gt; I run the same seven situations through the model: a clear morning, a hot afternoon after a run, a clear sunset, a cloudy sunset, a stargazing event nearby, rain, and a cloudy night. Here is Gemma 3 1B before and after that fix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Labelled "nature"&lt;/th&gt;
&lt;th&gt;Situation checks passed&lt;/th&gt;
&lt;th&gt;Answered by the model&lt;/th&gt;
&lt;th&gt;Right on first try&lt;/th&gt;
&lt;th&gt;Median latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Before the fix&lt;/td&gt;
&lt;td&gt;6 of 7&lt;/td&gt;
&lt;td&gt;5 of 6&lt;/td&gt;
&lt;td&gt;7 of 7&lt;/td&gt;
&lt;td&gt;6 of 7&lt;/td&gt;
&lt;td&gt;9.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After the fix&lt;/td&gt;
&lt;td&gt;4 of 7&lt;/td&gt;
&lt;td&gt;6 of 6&lt;/td&gt;
&lt;td&gt;7 of 7&lt;/td&gt;
&lt;td&gt;6 of 7&lt;/td&gt;
&lt;td&gt;11.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Situation checks passed" is a low bar, because the validator enforces most of them. It tells me the whole system behaves, not that the model is good by itself. What separates models is how often they need a retry or the fallback, and how good the text is. &lt;br&gt;
I only tested Gemma 3 1B. The code lets you swap models with one environment variable, but I have not compared other models yet, so that is a feature of the design, not a result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;A quest app needs your location, the times you are free and the places you go. I did not want to send that to a hosted API just to be told to go and look at a tree. With an open-weight model the quest generator runs on my own machine, so none of those prompts leave it.&lt;/p&gt;

&lt;p&gt;The limit of that claim matters, so here it is: the weather lookup goes to Open-Meteo, which receives my coordinates rounded to about 1 km, and it needs internet. The model does not.&lt;/p&gt;

&lt;p&gt;Three other things I got from an open model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Swapping the model is one environment variable&lt;/strong&gt; (&lt;code&gt;OUTSIDE_MODEL&lt;/code&gt;), with the same prompt, schema and tests for every model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A quest costs nothing to generate.&lt;/strong&gt; Nobody has to decide whether a tap is worth a bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constrained output.&lt;/strong&gt; The reply has to match my schema, and I could test it against fixed situations on my own hardware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A larger closed model would do some things better than my 1B model, and I would rather say so. Several of my test quests sent people to "the nearby creek" or "a quiet bench near a small green space". The model has no map, so it invents places. At night, my cloudy-night quest told the user to watch "the shadows beneath the trees" even though I had told the model it was dark. A bigger model would likely follow that. The validator catches what I thought to write a rule for, and nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Did Not Work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Completion is an honor-system button. Nothing checks that you went outside.&lt;/li&gt;
&lt;li&gt;Events are demo data. I did not find a free events API worth using, so events are a list, and the app labels demo events "(demo event)" on the card.&lt;/li&gt;
&lt;li&gt;A quest takes about 11 seconds on my laptop with the 1B model, so the loading screen cycles through what the app is doing.&lt;/li&gt;
&lt;li&gt;I wanted Jupiter in the sky quests and cut it to finish the core loop. Sky quests use sunset time, cloud cover and time of day.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; The quest generator is Gemma 3 1B running locally through Ollama with schema-constrained output. I tested it against seven fixed situations and measured how often its first answer passed my rules.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
