<?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: Rehan Mehta</title>
    <description>The latest articles on DEV Community by Rehan Mehta (@rehan1604).</description>
    <link>https://dev.to/rehan1604</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%2F4157069%2F378e1c9b-0297-447e-8f87-021be6836979.png</url>
      <title>DEV Community: Rehan Mehta</title>
      <link>https://dev.to/rehan1604</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rehan1604"/>
    <language>en</language>
    <item>
      <title>My laptop has no GPU, so I gave it the night shift</title>
      <dc:creator>Rehan Mehta</dc:creator>
      <pubDate>Thu, 22 Oct 2026 06:23:31 +0000</pubDate>
      <link>https://dev.to/rehan1604/my-laptop-has-no-gpu-so-i-gave-it-the-night-shift-1glp</link>
      <guid>https://dev.to/rehan1604/my-laptop-has-no-gpu-so-i-gave-it-the-night-shift-1glp</guid>
      <description>&lt;p&gt;This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady 🦥&lt;/p&gt;

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

&lt;p&gt;I built Later, a small open-source tool that reads the screenshots you took "for later" while you sleep, and hands you a morning digest of what you meant to remember.&lt;/p&gt;

&lt;p&gt;My screenshots folder did not need more screenshots. It needed someone to read it.&lt;/p&gt;

&lt;p&gt;It had 575 files, the oldest from January 2024. Every one was taken with the same quiet promise: I'll deal with this later. Later never came, because the files are all called Screenshot_20261020-213412.jpg and nobody opens them.&lt;/p&gt;

&lt;p&gt;So now an open-weight model on my own laptop opens them, one by one, overnight. In the morning:&lt;/p&gt;

&lt;p&gt;Do these. Deadlines, exams, events and tickets sorted by date. "Friday 11:59 PM" in a Tuesday group chat becomes Fri 23 Oct, 23:59, in 2 days, with a calendar button and a reminder the day before.&lt;br&gt;
Saved for someday. Articles, recipes, products, places, error messages, loose to-dos.&lt;br&gt;
Shred pile. Expired OTPs, memes, duplicates. Nothing is deleted. You decide.&lt;br&gt;
Search by meaning. "That Wi-Fi password." "Train to Pune."&lt;br&gt;
Tidy, with undo. Screenshot_20261020-213412_Chat.jpg becomes Deadlines/2026-10-23 DBMS assignment 3 deadline.jpg. One command puts everything back.&lt;br&gt;
Demo&lt;/p&gt;

&lt;p&gt;Later runs on your own laptop and opens as a web page on 127.0.0.1. Nothing is hosted, because the whole point is that your screenshots never leave your machine.&lt;/p&gt;

&lt;p&gt;The repo includes later demo-pack, which writes synthetic screenshots (fake OTPs, a fake chat about a deadline, a recipe, a train ticket) so you can try everything without exposing anything real. I did not put my own screenshots in the demo, because they contain OTPs and private chats.&lt;/p&gt;

&lt;p&gt;git clone &lt;a href="https://github.com/Rehan1604/hackw4.git" rel="noopener noreferrer"&gt;https://github.com/Rehan1604/hackw4.git&lt;/a&gt;&lt;br&gt;
cd hackw4&lt;br&gt;
pip install -r requirements.txt&lt;br&gt;
ollama pull gemma3:4b&lt;br&gt;
ollama pull embeddinggemma&lt;br&gt;
python -m later doctor&lt;br&gt;
python -m later demo-pack demo_screenshots --add&lt;br&gt;
python -m later sleep --now --limit 5&lt;br&gt;
python -m later&lt;/p&gt;

&lt;p&gt;The last command opens the morning digest in your browser: the night strip, the Do these list, search, and the tidy preview.&lt;/p&gt;

&lt;p&gt;Code&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Rehan1604" rel="noopener noreferrer"&gt;
        Rehan1604
      &lt;/a&gt; / &lt;a href="https://github.com/Rehan1604/hackw4" rel="noopener noreferrer"&gt;
        hackw4
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Later&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;A sloth that reads the screenshots you took "for later" while you sleep.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Screenshots are the laziest note-taking system ever invented: two buttons and the problem feels handled. Then the file sinks into a folder of a thousand others called &lt;code&gt;Screenshot_20261020-213412.jpg&lt;/code&gt;, and later never comes.&lt;/p&gt;
&lt;p&gt;Later is the lazy second half. Point it at your screenshots folder and go to bed. Overnight, on your own laptop, an open-weight model (Gemma 3) reads every screenshot, one at a time, and works out what you meant to remember. In the morning you get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do these:&lt;/strong&gt; deadlines, exams, events and tickets, sorted by date, each one click away from your calendar (&lt;code&gt;.ics&lt;/code&gt;, with a reminder the day before a deadline).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saved for someday:&lt;/strong&gt; the articles you meant to read, the earbuds you meant to buy, the recipe, the café, the plumber's number, the error message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shred pile:&lt;/strong&gt; expired OTPs…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Rehan1604/hackw4" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;Everything runs on the laptop, with no cloud account and no API key:&lt;/p&gt;

&lt;p&gt;Gemma 3 4B (vision) through Ollama reads each screenshot and returns structured JSON.&lt;br&gt;
EmbeddingGemma turns each result into a vector for search by meaning.&lt;br&gt;
Tesseract OCR reads small text exactly.&lt;br&gt;
SQLite (FTS5 for keywords) stores everything. FastAPI serves the digest on 127.0.0.1, and the page is plain HTML, CSS and JavaScript with no build step.&lt;/p&gt;

&lt;p&gt;My laptop is an Intel i5 with integrated graphics and 16 GB of RAM. On it, Gemma 3 4B averages about 33 seconds per screenshot. In a chat app that would be unusable. For a job that runs while I sleep it does not matter, so I designed around it. The sloth has rules.&lt;/p&gt;

&lt;p&gt;Work while I'm not. later sleep --until 07:00 waits until the keyboard has been quiet for a few minutes, stays off a draining battery, and lowers Ollama's CPU priority. later schedule sets it up once as a nightly Windows task that wakes the laptop.&lt;/p&gt;

&lt;p&gt;Never lose your place. Every screenshot is keyed by a hash of its contents, not its name. Kill it, reboot, rename the files: it resumes where it stopped.&lt;/p&gt;

&lt;p&gt;Don't read anything twice. A 256-bit perceptual hash spots near-duplicates. If the OCR text matches too, the copy reuses the earlier answer with no model call.&lt;/p&gt;

&lt;p&gt;Trust, but verify. Ollama's structured outputs constrain Gemma to a JSON schema, so kind is always one of fifteen known values and every field is present:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
body = {&lt;br&gt;
    "model": "gemma3:4b",&lt;br&gt;
    "messages": [&lt;br&gt;
        {"role": "system", "content": SYSTEM_FULL},&lt;br&gt;
        {"role": "user", "content": prompt, "images": tiles},  # tiles + OCR transcript&lt;br&gt;
    ],&lt;br&gt;
    "format": SCHEMA,  # kind is an enum, every field required&lt;br&gt;
    "stream": False,&lt;br&gt;
    "options": {"temperature": 0, "num_ctx": 4096},&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;A valid shape is not a correct answer, so plain Python checks the content afterwards. Relative dates resolve against when the screenshot was taken, so "Friday" in a Tuesday chat means that Friday. A date years away is dropped. A meme does not get a to-do.&lt;/p&gt;

&lt;p&gt;Two locks on the secrets. A screenshot counts as private if the model says so or a deterministic check fires: OTP wording next to a code, a card number that passes the Luhn checksum, ID numbers, balances, password: lines, lab-report terms. Digits are then masked in the database, thumbnails are blurred on the server, and the full image is sent to the browser only after you click Reveal.&lt;/p&gt;

&lt;p&gt;The night strip. The digest opens with proof of the night's work: a band that shades from night to dawn, one bar per screenshot, as tall as the seconds it took. While the sloth works you can watch the bars appear. It is a progress bar that is honest about being slow. The page loads no external fonts, scripts or images.&lt;/p&gt;

&lt;p&gt;Here is the shape of what the model returns for a chat screenshot (an illustrative example):&lt;/p&gt;

&lt;p&gt;json&lt;br&gt;
{&lt;br&gt;
  "kind": "deadline",&lt;br&gt;
  "title": "DBMS assignment 3 deadline",&lt;br&gt;
  "summary": "Group chat says the DBMS assignment is extended to Friday 11:59 PM.",&lt;br&gt;
  "action": "Submit it on the portal",&lt;br&gt;
  "date": "2026-10-23",&lt;br&gt;
  "time": "23:59",&lt;br&gt;
  "sensitive": false,&lt;br&gt;
  "keep": true,&lt;br&gt;
  "tags": ["dbms", "assignment", "deadline"]&lt;br&gt;
}&lt;br&gt;
What broke along the way&lt;/p&gt;

&lt;p&gt;Four things broke, and each one taught me something.&lt;/p&gt;

&lt;p&gt;I optimised the wrong thing&lt;/p&gt;

&lt;p&gt;My first runs took 60 to 110 seconds per screenshot. I assumed the image was the cheap part and the text was the cost, and I sliced every tall screenshot into two tiles to be sharper. Then I wrote a benchmark instead of guessing. On my CPU, one image takes Gemma about 30 seconds to read, while 450 tokens of text take 1.6 seconds. Extra tiles doubled the cost for almost nothing once OCR was in the loop.&lt;/p&gt;

&lt;p&gt;So Tesseract now reads every screenshot first. If it finds plenty of text, Gemma gets the transcript and never sees the pixels: about 20 seconds. A map, a meme or a photo still gets the image: about 55. Thread count barely mattered either (12 threads took 28 s, 4 took 37 s).&lt;/p&gt;

&lt;p&gt;The model invented dates&lt;/p&gt;

&lt;p&gt;On my first 41 screenshots, Gemma attached a date to 27. That sounded great until I looked: most were ordinary notes and AI chats with no date anywhere on the screen.&lt;/p&gt;

&lt;p&gt;I added one rule. A date has to be backed by the screenshot's own text: a month and day, a numeric date, or a word like "Friday" it can resolve. Re-checking the stored results removed 23 of the 27. The 4 that survived were real. A model that sounds confident about a deadline is more dangerous than one that says nothing.&lt;/p&gt;

&lt;p&gt;My "reset" did not reset&lt;/p&gt;

&lt;p&gt;I had mixed my synthetic demo screenshots into my real database, so I deleted the data folder to start clean. The folder did not exist. Python from the Microsoft Store virtualises %LOCALAPPDATA%, so my files were somewhere else entirely. The fix was boring: set LATER_HOME to a normal folder and keep a separate home for demos. My real data and my demo data can no longer touch.&lt;/p&gt;

&lt;p&gt;The terminal closed in the middle of the night&lt;/p&gt;

&lt;p&gt;I woke up to a closed terminal. Because the queue is keyed by file hash and resumes by design, I re-ran one command and it carried on. One screenshot had failed once and read fine on the retry. That is the whole reason the sloth saves its place after every single screenshot.&lt;/p&gt;

&lt;p&gt;What I would do next&lt;/p&gt;

&lt;p&gt;Honest limits, so you know what you are getting:&lt;/p&gt;

&lt;p&gt;A 4B model makes mistakes. The validation catches invented dates and missed secrets, not every wrong summary. The privacy checks are a safety net, not a guarantee, so look before you share a folder.&lt;br&gt;
It is tuned for English-heavy screenshots from my own phone and laptop. I have not tested other scripts.&lt;br&gt;
The scheduling and keep-awake pieces are Windows-only. The rest should run anywhere Ollama does, but I have only tested Windows.&lt;br&gt;
The tests use a fake Ollama server. They check the plumbing (resume, duplicates, privacy, digest order, calendar files, tidy and undo, the web API). They cannot judge the model. Only real screenshots can, which is what the numbers below are for.&lt;/p&gt;

&lt;p&gt;The idea I most want next: let the digest learn which things I actually open and which I always dismiss. If you build it, contributions are very welcome.&lt;/p&gt;

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

&lt;p&gt;Because screenshots are the most private files you own. They hold OTPs, bank balances, lab reports and your friends' messages. The people in those group chats never agreed to be uploaded anywhere. With an open-weight model, "your data stays on your laptop" is enforced in code: Later refuses to talk to a model server that is not on the same machine unless you explicitly switch that check off.&lt;/p&gt;

&lt;p&gt;Because slowness is free when the weights are yours. A hosted vision API bills per image and is built to answer in a second, which is speed I do not need. On my own laptop, a slower night costs nothing. Open weights let me spend time, which I have plenty of overnight, instead of money.&lt;/p&gt;

&lt;p&gt;Because I can change the brain. One environment variable swaps in gemma3:12b on a machine with more memory. I can cap threads, change how often the image is used, and use EmbeddingGemma's task prefixes for search. None of that is a feature request to anyone.&lt;/p&gt;

&lt;p&gt;Because it works when nothing else does. No internet, no key to leak, no deprecation email. The model only changes when I choose to pull a new one.&lt;/p&gt;

&lt;p&gt;Where closed would have won: a frontier hosted model would read messy screenshots more accurately, and in a second rather than 33. I narrowed that gap with OCR and validation and accepted the rest. The privacy gap is the one I could not have closed any other way.&lt;/p&gt;

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

&lt;p&gt;Setup took a few minutes, once. After that, I pointed Later at my real folder and went to bed. These numbers come straight from python -m later stats:&lt;/p&gt;

&lt;p&gt;575 screenshots, oldest from January 2024. 574 were read by the model and 1 was recognised as a duplicate with no model call. 527 were read in a single run of 4 h 51 min while I slept.&lt;br&gt;
5 h 13 min of CPU work, 32.8 seconds per screenshot, about 10.6 tokens per second. 473,770 tokens read, 75,701 written. Total cost: some electricity.&lt;br&gt;
What was in there: 211 code and error screenshots, 118 "other", 108 chats, 58 loose to-dos, 46 things to read, 15 contacts. Only 17 had a real date and 1 was a true deadline. My folder is not full of deadlines. It is full of things I wanted to find again, which is why search matters more than the calendar button.&lt;br&gt;
45 screenshots were flagged private, blurred and digit-masked.&lt;br&gt;
By hand, at around 20 seconds each (my estimate), that is about 3 h 11 min of opening, reading and deciding. The honest number is higher, because I never did it. The oldest screenshot has been sitting there for almost two years.&lt;/p&gt;

&lt;p&gt;If you have a screenshots folder you are afraid to open, give it to a sloth.&lt;/p&gt;

&lt;p&gt;Prize Categories&lt;/p&gt;

&lt;p&gt;Best Use of Gemma. Gemma 3 4B reads every screenshot (vision plus schema-constrained JSON), and EmbeddingGemma powers search by meaning. Both run locally through Ollama on a laptop CPU.&lt;/p&gt;

&lt;p&gt;__ This project comes from a human. AI helped with its design and code.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>QuoteShield: Turn a Client's WhatsApp Message into a Watertight Scope of Work published: true</title>
      <dc:creator>Rehan Mehta</dc:creator>
      <pubDate>Wed, 14 Oct 2026 19:40:35 +0000</pubDate>
      <link>https://dev.to/rehan1604/quoteshield-turn-a-clients-whatsapp-message-into-a-watertight-scope-of-workpublished-true-aii</link>
      <guid>https://dev.to/rehan1604/quoteshield-turn-a-clients-whatsapp-message-into-a-watertight-scope-of-workpublished-true-aii</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;Every freelancer has had this message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Need an app that tracks my clients — who owes me money, project status, invoice dates. Simple UI, maybe mobile friendly too. Nothing too fancy."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Nothing too fancy" usually turns into three extra screens, a login system, "can you also host it?", and six rounds of "small changes". Most of the work you never get paid for comes from scope creep, and scope creep comes from a scope nobody wrote down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QuoteShield&lt;/strong&gt; fixes that. You paste whatever the client sent you (a WhatsApp message, a rambling email, a voice-note transcript) and in a few seconds you get a professional scope of work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deliverables&lt;/strong&gt; specific enough that both sides know when each one is done. Not "Shop page" but "E-commerce shop page with product grid, category filters, cart and Razorpay checkout".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Milestones&lt;/strong&gt; with testable outcomes instead of "Development".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "Not Included" list&lt;/strong&gt;, which does most of the protecting: hosting, SEO, copywriting, post-launch support, everything a client quietly assumes is free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binding assumptions&lt;/strong&gt; that name what the client must deliver and by when.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An hours estimate&lt;/strong&gt; and a &lt;strong&gt;revision policy&lt;/strong&gt; that defines what actually counts as a revision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can choose a tone (Professional, Friendly or Strict), add your hourly rate to see a live cost range, add your own constraints ("I only do React, no SEO"), and copy the result as plain text or Markdown to paste straight into a proposal.&lt;/p&gt;

&lt;p&gt;It also gives each scope a &lt;strong&gt;health score&lt;/strong&gt; out of 100 based on how complete it is, so you can see a thin one before you send it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who pays for this?&lt;/strong&gt; Freelancers and small agencies who quote work every week. One avoided scope argument pays for a year of a small subscription.&lt;/p&gt;

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

&lt;p&gt;🔗 &lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://quoteshield-ten.vercel.app" rel="noopener noreferrer"&gt;https://quoteshield-ten.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try one of the built-in examples, or paste a real client message you've received.&lt;/p&gt;

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


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Rehan1604" rel="noopener noreferrer"&gt;
        Rehan1604
      &lt;/a&gt; / &lt;a href="https://github.com/Rehan1604/quoteshield" rel="noopener noreferrer"&gt;
        quoteshield
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;⚔️ QuoteShield&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Paste a chaotic client brief — a WhatsApp message, a rambling email, a voice-note transcript — and get a professional scope of work in seconds: specific deliverables, testable milestones, a thorough "not included" list, binding assumptions, an hours estimate and a revision policy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://quoteshield-ten.vercel.app" rel="nofollow noopener noreferrer"&gt;https://quoteshield-ten.vercel.app&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Stack&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React + Vite (Vercel)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI (Render)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI:&lt;/strong&gt; open-weight &lt;code&gt;gpt-oss-20b&lt;/code&gt; on Groq, with automatic fallback to &lt;code&gt;gpt-oss-120b&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run locally&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Backend&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c1"&gt;cd&lt;/span&gt; backend
pip install -r requirements.txt
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; get a free key at https://console.groq.com&lt;/span&gt;
&lt;span class="pl-k"&gt;export&lt;/span&gt; GROQ_API_KEY=your_key        &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; PowerShell: $env:GROQ_API_KEY="your_key"&lt;/span&gt;
uvicorn main:app --reload --port 8000&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Frontend&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c1"&gt;cd&lt;/span&gt; frontend
npm install
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; point at your local backend (defaults to the hosted one)&lt;/span&gt;
&lt;span class="pl-c1"&gt;echo&lt;/span&gt; VITE_API_URL=http://localhost:8000 &lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt; .env.local
npm run dev&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Deploy&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Render&lt;/strong&gt; (root &lt;code&gt;backend&lt;/code&gt;): build &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;, start &lt;code&gt;uvicorn main:app --host 0.0.0.0 --port $PORT&lt;/code&gt;, env var &lt;code&gt;GROQ_API_KEY&lt;/code&gt;. Optional &lt;code&gt;GROQ_MODELS&lt;/code&gt; to override the model list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; (root &lt;code&gt;frontend&lt;/code&gt;): framework…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Rehan1604/quoteshield" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; React + Vite with CSS Modules, deployed on Vercel. A dark UI with the tone selector, rate calculator, example briefs and the scope document view with its health score.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI deployed on &lt;strong&gt;Render&lt;/strong&gt;. It does more than pass the request along:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A strict prompt.&lt;/strong&gt; The model gets seven rules: no vague words like "premium" or "clean" without defining them, every milestone needs a testable outcome, assumptions must name who delivers what and by when, and so on. This is the biggest factor in output quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A model fallback chain.&lt;/strong&gt; The backend tries &lt;code&gt;gpt-oss-20b&lt;/code&gt; first and falls back to &lt;code&gt;gpt-oss-120b&lt;/code&gt;. The list is an environment variable, so swapping models takes no code change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON mode with a retry.&lt;/strong&gt; It asks for structured JSON. If the model rejects JSON mode or returns something that won't parse, it retries in plain mode, then moves to the next model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output normalization.&lt;/strong&gt; LLMs sometimes return &lt;code&gt;"60-80"&lt;/code&gt; for hours, an object where a string should be, or &lt;code&gt;"exclusions"&lt;/code&gt; instead of &lt;code&gt;"not_included"&lt;/code&gt;. The backend reshapes everything into one fixed schema, so the frontend never gets a broken scope.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Render&lt;/strong&gt; hosts the API. A pinned &lt;code&gt;.python-version&lt;/code&gt; keeps builds reproducible, the Groq key lives in Render's environment settings, and the frontend sends a wake-up ping to the Render service when the page loads, so the free tier's cold start is usually over before the user clicks Generate.&lt;/p&gt;

&lt;h3&gt;
  
  
  The road here (the part I learned the most from)
&lt;/h3&gt;

&lt;p&gt;QuoteShield started fully local: &lt;strong&gt;Mistral 7B on Ollama&lt;/strong&gt;, on my laptop's CPU. The output was decent, but each scope took 60–90 seconds and nobody else could use it.&lt;/p&gt;

&lt;p&gt;So I moved inference to an open-weight model on Groq, and a lot broke along the way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Render picked Python 3.14, which had no prebuilt &lt;code&gt;pydantic-core&lt;/code&gt; wheel, so the build tried to compile Rust and failed. Pinning Python 3.11 fixed it.&lt;/li&gt;
&lt;li&gt;CORS preflight requests got &lt;code&gt;400&lt;/code&gt; until the middleware was set up correctly.&lt;/li&gt;
&lt;li&gt;The Groq SDK clashed with the installed &lt;code&gt;httpx&lt;/code&gt; version (&lt;code&gt;unexpected keyword argument 'proxies'&lt;/code&gt;), so I dropped the SDK and call the API directly.&lt;/li&gt;
&lt;li&gt;The first model I switched to had already been &lt;strong&gt;decommissioned&lt;/strong&gt;, and so had the second one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is why the fallback chain exists. A product that depends on one hosted model ID can break without notice, so the code now expects models to come and go.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;No lock-in.&lt;/strong&gt; I've had a model retired out from under me twice while building this project. Because QuoteShield runs on &lt;em&gt;open-weight&lt;/em&gt; models, a retirement is a config change. If a provider drops a model or changes its pricing, I can point at another host or run the same weights myself. With a closed model, the provider's decision would end the product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I can go back to running it myself.&lt;/strong&gt; The first version ran on Mistral 7B entirely on my laptop. A freelancer who doesn't want client briefs leaving their machine can do the same thing, because open weights make that possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The costs work for a small product.&lt;/strong&gt; &lt;code&gt;gpt-oss-20b&lt;/code&gt; costs fractions of a cent per scope. That means I could offer a free tier and still charge a cheap subscription without losing money on every request, which is what a side project needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The weights are inspectable.&lt;/strong&gt; With open weights you can fine-tune on your own past proposals so scopes match how &lt;em&gt;you&lt;/em&gt; quote. That's the next step for QuoteShield.&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>devchallenge</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Doorstep: The AI That Gives You 45 Seconds, Then Tells You to Go Outside</title>
      <dc:creator>Rehan Mehta</dc:creator>
      <pubDate>Thu, 08 Oct 2026 10:34:14 +0000</pubDate>
      <link>https://dev.to/rehan1604/doorstep-the-ai-that-gives-you-45-seconds-then-tells-you-to-go-outside-i7o</link>
      <guid>https://dev.to/rehan1604/doorstep-the-ai-that-gives-you-45-seconds-then-tells-you-to-go-outside-i7o</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every app wants more of your time. &lt;strong&gt;Doorstep wants 45 seconds.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Doorstep is a mission app that is designed to be closed. You tell it how much time and energy you have. An open-weight model running on your own laptop writes you &lt;strong&gt;one small outdoor mission&lt;/strong&gt;. You get &lt;strong&gt;45 seconds of screen&lt;/strong&gt; to read it. Then the app goes black, says &lt;strong&gt;PHONE DOWN&lt;/strong&gt;, and you go outside.&lt;/p&gt;

&lt;p&gt;When you come back, you type what surprised you, say how you felt, and get two honest sentences back. There is no feed, no chat, no streak and no leaderboard. The app succeeds when you stop using it.&lt;/p&gt;

&lt;p&gt;And it learns. The longer you use it, the better it gets at choosing the &lt;em&gt;kind&lt;/em&gt; of mission that actually leaves &lt;em&gt;you&lt;/em&gt; feeling good, and it tells you why.&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%2Fuvhayer5nmrnjsp449ub.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%2Fuvhayer5nmrnjsp449ub.png" alt="Mission card with the 45-second screen budget" width="789" height="666"&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%2Fzkmnv7wopano6xbhy9k3.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%2Fzkmnv7wopano6xbhy9k3.png" alt="What works for you, learned from your own feedback" width="800" height="654"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo was recorded on &lt;code&gt;localhost&lt;/code&gt;, with Wi-Fi switched off mid-take. Missions kept coming because the model runs on the laptop.&lt;/p&gt;

&lt;p&gt;🌐 &lt;strong&gt;Try it fast:&lt;/strong&gt; &lt;a href="https://doorstep-alpha.vercel.app" rel="noopener noreferrer"&gt;doorstep-alpha.vercel.app&lt;/a&gt;. This is a &lt;em&gt;hosted&lt;/em&gt; demo: it calls a hosted model, so the privacy and offline claims below do &lt;strong&gt;not&lt;/strong&gt; apply to it. The free server sleeps, so the first request can take about a minute.&lt;/p&gt;

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


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Rehan1604" rel="noopener noreferrer"&gt;
        Rehan1604
      &lt;/a&gt; / &lt;a href="https://github.com/Rehan1604/doorstep" rel="noopener noreferrer"&gt;
        doorstep
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/Rehan1604/doorstep/screenshots/02-mission.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FRehan1604%2Fdoorstep%2FHEAD%2Fscreenshots%2F02-mission.png" alt="Doorstep mission card" width="260"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🌿 Doorstep&lt;/h1&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;The AI app that is over the moment you cross your own doorstep.&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;Doorstep writes you &lt;strong&gt;one small outdoor mission&lt;/strong&gt; with an open-weight model, gives you &lt;strong&gt;45 seconds of screen&lt;/strong&gt; to read it, then goes black and sends you outside.&lt;br&gt;
It learns what works for you. In local mode, nothing leaves your machine.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/3ee6d3c40cadf6c1f68a8e1b35f0001de32611889583234c9278deef3d17dccb/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f64652d4d49542d3066316131343f7374796c653d666f722d7468652d6261646765"&gt;&lt;img src="https://camo.githubusercontent.com/3ee6d3c40cadf6c1f68a8e1b35f0001de32611889583234c9278deef3d17dccb/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f64652d4d49542d3066316131343f7374796c653d666f722d7468652d6261646765" alt="License"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/2a8bd7b5b79cd6d95117b1f52eeaebdf44cc5fc640122e06da4a95021edf62e4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f41492d72756e732532306c6f63616c6c792d3266366232663f7374796c653d666f722d7468652d6261646765"&gt;&lt;img src="https://camo.githubusercontent.com/2a8bd7b5b79cd6d95117b1f52eeaebdf44cc5fc640122e06da4a95021edf62e4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f41492d72756e732532306c6f63616c6c792d3266366232663f7374796c653d666f722d7468652d6261646765" alt="Local AI"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/8b07b90a74545771242ab3fc852587e3cd0bae7459eb65b7cc1fd16ffdc7d6f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f776f726b732d6f66666c696e652d6237653037613f7374796c653d666f722d7468652d6261646765266c6162656c436f6c6f723d306631613134"&gt;&lt;img src="https://camo.githubusercontent.com/8b07b90a74545771242ab3fc852587e3cd0bae7459eb65b7cc1fd16ffdc7d6f1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f776f726b732d6f66666c696e652d6237653037613f7374796c653d666f722d7468652d6261646765266c6162656c436f6c6f723d306631613134" alt="Offline"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/323155168aeb2a37c0429eb5a37511a4031b84b21f3003d3746a8f3c195b43fd/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d32392532306261636b656e642532302532422532303725323066726f6e74656e642d3266366232663f7374796c653d666f722d7468652d6261646765"&gt;&lt;img src="https://camo.githubusercontent.com/323155168aeb2a37c0429eb5a37511a4031b84b21f3003d3746a8f3c195b43fd/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d32392532306261636b656e642532302532422532303725323066726f6e74656e642d3266366232663f7374796c653d666f722d7468652d6261646765" alt="Tests"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://doorstep-alpha.vercel.app" rel="nofollow noopener noreferrer"&gt;🌐 Live demo&lt;/a&gt; · &lt;a href="https://github.com/Rehan1604/doorstep/YOUR_DEV_POST_URL" rel="noopener noreferrer"&gt;📝 DEV post&lt;/a&gt; · &lt;a href="https://github.com/Rehan1604/doorstep/docs/ML.md" rel="noopener noreferrer"&gt;🧠 The ML&lt;/a&gt; · &lt;a href="https://github.com/Rehan1604/doorstep/docs/PRIVACY.md" rel="noopener noreferrer"&gt;🔒 Privacy&lt;/a&gt; · &lt;a href="https://github.com/Rehan1604/doorstep/docs/ARCHITECTURE.md" rel="noopener noreferrer"&gt;🏗 Architecture&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The problem&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Apps are built to keep you looking at them. Even the ones that say they want you outside keep you scrolling inside.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The idea&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;A hard screen budget.&lt;/strong&gt; Every mission gives you 45 seconds to read it. When the bar hits zero, Doorstep goes black: one instruction, one timer, &lt;strong&gt;PHONE DOWN&lt;/strong&gt;. You go outside. When you come back you type what surprised you, say how you felt, and get two honest sentences back.&lt;/p&gt;
&lt;p&gt;No feed…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Rehan1604/doorstep" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;h3&gt;
  
  
  Step 1: Local first, because of what the app knows
&lt;/h3&gt;

&lt;p&gt;Doorstep handles where you walk, what you notice, and how you feel. That is exactly the kind of data I don't want to send to a stranger's server. So the default path is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;React UI → FastAPI → Ollama → Qwen2.5-1.5B-Instruct → SQLite&lt;/code&gt;, all on one laptop. My machine is an Intel i5-13420H with 16 GB of RAM and &lt;strong&gt;no GPU&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Pick the model with a benchmark, not a vibe
&lt;/h3&gt;

&lt;p&gt;I wrote a benchmark that asks each model for 10 missions and checks the JSON, the safety screen and the clock. On my CPU-only laptop:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Valid JSON&lt;/th&gt;
&lt;th&gt;Safe output&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;th&gt;p90&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;qwen2.5:1.5b-instruct&lt;/strong&gt; ✅&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11.4 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;13.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;phi3.5&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;29.4 s&lt;/td&gt;
&lt;td&gt;41.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both were reliable, so speed decided it. Eleven seconds is still a long wait, so I turned it into a &lt;strong&gt;breathing screen&lt;/strong&gt; instead of a spinner. The delay now reads as part of the design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Never let the model break the app
&lt;/h3&gt;

&lt;p&gt;Model output is constrained to a JSON schema, validated, retried twice, and screened for unsafe suggestions (climbing, private property, wildlife contact). If the model is down, slow, or wrong, the app serves a hand-written fallback mission and labels it &lt;strong&gt;OFFLINE-SAFE MISSION&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Make the AI do something a dictionary can't
&lt;/h3&gt;

&lt;p&gt;A reviewer's fair point: a Python dictionary of tasks could do what the LLM was doing. So I added three pieces of ML that actually depend on &lt;em&gt;you&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🎯 A style bandit.&lt;/strong&gt; Missions have styles (stillness, sensory hunt, close-up, detour, and so on). A small contextual Thompson-sampling bandit learns which style leaves you feeling best. It is pure Python, runs offline, and the card explains each pick: &lt;em&gt;"Close-up has left you feeling better in 3 of 4 missions."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I tested it against random choice with a simulation (500 synthetic people, 20 missions each):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Simulated people&lt;/th&gt;
&lt;th&gt;Random picks their favourite&lt;/th&gt;
&lt;th&gt;Bandit picks it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stable taste&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;43%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different taste per context&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It helps clearly when taste is stable, and barely when taste changes by context. That is a real limit, and it is in the docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧭 An embedding novelty guard.&lt;/strong&gt; Each mission is embedded and compared by meaning to your recent ones. A near-repeat gets regenerated once, and the card shows how new it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧠 Private semantic memory.&lt;/strong&gt; Your notes are embedded locally. When you finish a mission, related earlier notes are retrieved so the reflection can connect today to before: &lt;em&gt;a grey heron by the river&lt;/em&gt;, then weeks later &lt;em&gt;another heron near the water&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: A hosted demo, with honest labels
&lt;/h3&gt;

&lt;p&gt;Judges shouldn't need to install Ollama to try the idea, so I deployed a demo: Vercel (React), Render (FastAPI) and Groq's API with &lt;code&gt;gpt-oss-20b&lt;/code&gt;. The top status strip tells the truth in both modes: &lt;code&gt;AI: LOCAL · DATA: LOCAL&lt;/code&gt; on my laptop, &lt;code&gt;AI: READY · DATA: CLOUD&lt;/code&gt; on the hosted site.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔎 Tip: A Silent Fallback Can Hide a Real Bug
&lt;/h2&gt;

&lt;p&gt;Hosted missions kept coming back as "OFFLINE-SAFE", and nothing crashed, which is exactly why I nearly missed it. I found two problems. In hosted mode I was never telling the model the JSON field names, because JSON mode doesn't enforce a schema. And the provider could reject JSON mode with a 400 whose reason &lt;code&gt;httpx&lt;/code&gt; hides. Now the schema goes into the prompt, the call retries once without JSON mode, and the provider's error text is logged. If you build a fallback, &lt;strong&gt;make it loud in your logs&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔎 Tip: Stored Text Is Untrusted Text
&lt;/h2&gt;

&lt;p&gt;The model's own earlier output and the user's notes get put back into later prompts. I strip anything that could break out of a one-line prompt before it goes in. It is cheap and it closes a real prompt-injection path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Innovation Matters
&lt;/h2&gt;

&lt;p&gt;A closed API would have made this a different app. Because the model, the embedder and the server are open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The data stays home.&lt;/strong&gt; Notes about your walks never leave the laptop in local mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works where there is no signal.&lt;/strong&gt; A park, a trail, a train. The Wi-Fi-off moment in the demo is real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could choose.&lt;/strong&gt; I benchmarked two models and swapped them with one environment variable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could read everything.&lt;/strong&gt; Every prompt is in one file, and the safety screen is code I can inspect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One honest nuance: the hosted demo also uses an open-weight model, but through someone else's server, which gives up the privacy advantage. Open weights give you the &lt;em&gt;choice&lt;/em&gt;. Local mode is what makes that choice private.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🟢 One small mission, 45 seconds of screen, then the app gets out of the way.&lt;/li&gt;
&lt;li&gt;🟢 Runs on a CPU-only laptop with no GPU: ~11 s per mission on &lt;code&gt;qwen2.5:1.5b-instruct&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;🟢 A bandit that learns what works for you, an embedding novelty guard, and private semantic memory.&lt;/li&gt;
&lt;li&gt;🟢 29 backend tests and 7 frontend tests.&lt;/li&gt;
&lt;li&gt;⚠️ A 1.5B model writes valid but often generic missions.&lt;/li&gt;
&lt;li&gt;⚠️ The bandit needs several missions to adapt and barely helps when taste varies by context.&lt;/li&gt;
&lt;li&gt;⚠️ The safety screen is keyword-based and can't guarantee safety.&lt;/li&gt;
&lt;li&gt;⚠️ The hosted demo sends your input to third parties and can reset its data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; one laptop (i5-13420H, 16 GB RAM, CPU only), 10 benchmark runs per model, a &lt;em&gt;synthetic&lt;/em&gt; bandit simulation, and self-testing only. No user study has been run, and the optional voice-note feature was not part of the demo.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The best AI interaction is the one that ends with you leaving the room.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Rehan1604/doorstep" rel="noopener noreferrer"&gt;Doorstep on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; · &lt;a href="https://huggingface.co/Qwen" rel="noopener noreferrer"&gt;Qwen2.5&lt;/a&gt; · &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fastapi.tiangolo.com" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt; · &lt;a href="https://react.dev" rel="noopener noreferrer"&gt;React&lt;/a&gt; · &lt;a href="https://vitejs.dev" rel="noopener noreferrer"&gt;Vite&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>pgaichallenge</category>
      <category>opensource</category>
    </item>
    <item>
      <title>"My local model called a flagged result 'within range', so I stopped letting it do arithmetic"</title>
      <dc:creator>Rehan Mehta</dc:creator>
      <pubDate>Sat, 03 Oct 2026 07:18:04 +0000</pubDate>
      <link>https://dev.to/rehan1604/my-local-model-called-a-flagged-result-within-range-so-i-stopped-letting-it-do-arithmetic-a42</link>
      <guid>https://dev.to/rehan1604/my-local-model-called-a-flagged-result-within-range-so-i-stopped-letting-it-do-arithmetic-a42</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first time I asked a local model to explain a lab report, it told me a result the report had flagged as high was "within the reference range". I ran the same prompt again and got a different answer.&lt;/p&gt;

&lt;p&gt;I was building Plain-Words for Bhavya, a classmate. When a confusing document lands in their family, they ask the family doctor if it's medical, and upload it to Google and try to work it out themselves if it isn't, which they described as very stressful. A tool meant to make that less stressful can't be wrong about the one thing that matters. So most of this project is about what the model isn't allowed to decide.&lt;/p&gt;

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

&lt;p&gt;Plain-Words takes a photo, PDF or pasted text of a document and gives back a plain-language explanation. For a lab report, that's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a short summary of which results fall outside the range printed on the report&lt;/li&gt;
&lt;li&gt;one key point per result, tagged "Above listed range" or "Within listed range"&lt;/li&gt;
&lt;li&gt;a glossary of the terms and units&lt;/li&gt;
&lt;li&gt;specific questions to ask a doctor&lt;/li&gt;
&lt;li&gt;all of it again in Hindi, because that's what an older relative might read&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a history with a delete button and a thumbs up/down on every explanation.&lt;/p&gt;

&lt;p&gt;The version I built and tested runs entirely on my laptop with a local model, and nothing leaves the machine. I also put a hosted demo online, which works differently (see below).&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://plain-words-ox04ps1vb-rehan-6df9.vercel.app?_vercel_share=9PNLhyXGNTZ4cv3g2bGr6V9km6pcsXW2" rel="noopener noreferrer"&gt;https://plain-words-ox04ps1vb-rehan-6df9.vercel.app?_vercel_share=9PNLhyXGNTZ4cv3g2bGr6V9km6pcsXW2&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The hosted demo sends text to a server and to an AI provider, so use it with the fictional sample report only. There's a "Try the fictional sample report" button on the first screen. Photo reading may not work there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first version, which Bhavya tested&lt;/strong&gt; (fictional sample report):&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%2Fygl75ahfbj2jpb5v11nv.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%2Fygl75ahfbj2jpb5v11nv.png" alt="First version of the result screen" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No real patient data is in the repo, the screenshots or this post.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/Rehan1604/plain-words" rel="noopener noreferrer"&gt;https://github.com/Rehan1604/plain-words&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT-licensed. The Gemma model keeps its own terms.&lt;/p&gt;

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

&lt;p&gt;The front end is React and Vite. Behind it is FastAPI, with Tesseract for photos, SQLite for history and a model behind a small &lt;code&gt;LLMProvider&lt;/code&gt; interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;phone browser → FastAPI → PDF text / Tesseract OCR
                           ├→ range parser (plain code): above / below / within
                           └→ LLMProvider → Gemma 3 4B (local, Ollama): glossary + Hindi glossary
                          → SQLite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The decision that matters most is what the model doesn't get to do.&lt;/strong&gt; My first attempts simply asked Gemma to explain a sample HbA1c report. On a report that flagged the result high, it told me it was within the reference range, and in a bare prompt it drifted into diet advice. A small model on a CPU isn't a reliable number comparer.&lt;/p&gt;

&lt;p&gt;So the app doesn't ask it to. A parser reads each result and the reference range printed next to it, and plain code decides above, below or within. That output becomes the summary, the key points and the doctor questions, with no model involved. The model only writes the glossary, and the Hindi version of it. For documents with no numbers, like a notice or a form, the model writes the summary instead. That path is less reliable, and I haven't tested it much.&lt;/p&gt;

&lt;p&gt;Each glossary entry quotes a line from the document, and the code checks the line really appears there. That proves the quote exists, not that the explanation is right. The meanings are the model's general knowledge and can be wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The local model.&lt;/strong&gt; Gemma 3 4B through Ollama, on the CPU. I'd planned around having a GPU, but the laptop I built this on has only Intel UHD graphics (i5-13420H, 16 GB RAM). The 4B model at 4-bit quantization ran at about 12.7 tokens per second, which is slow but usable. I compared it with Qwen 2.5 3B, which was faster at about 15.9 tokens per second. Its Hindi was gibberish: it talked about an injection and "five to ten months" for a blood sugar value that had neither in it. For a tool that's supposed to calm someone down, that settled it.&lt;/p&gt;

&lt;p&gt;Gemma comes under Google's own Gemma Terms of Use, not a standard open-source license, so I call it open-weight, not open-source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hosted demo.&lt;/strong&gt; Because everything sits behind &lt;code&gt;LLMProvider&lt;/code&gt;, the hosted option was a new provider class plus a few config values, selected with one environment variable (&lt;code&gt;LLM_PROVIDER=groq&lt;/code&gt;). The backend runs on Render and calls &lt;code&gt;openai/gpt-oss-20b&lt;/code&gt;, an open-weight model, through Groq. I haven't measured speed or Hindi quality on that model, so every timing and Hindi claim in this post is about local Gemma.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed (local).&lt;/strong&gt; After I added the guardrails, a full run took 262 seconds. I'd made the model write a summary, key points and doctor questions, then replaced all of it with code anyway. Leaving only the glossary to the model brought it to about 35 seconds. The Hindi step fell from 52 to about 19 seconds for the same reason.&lt;/p&gt;

&lt;p&gt;There are 10 automated tests: the range parser, the Hindi templates and the API round trip with a fake model. I checked the photo path, the real model output and the Hindi quality by hand.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy, when you run it locally.&lt;/strong&gt; A lab report is the kind of document I'd hesitate to paste into a cloud service. In the local version the photo, the extracted text and the history stay on one machine. The hosted demo gives that up, which is why it carries a warning and I only use the fictional report there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; The local version has no API bill and no account. A student laptop is enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Picking and swapping the model.&lt;/strong&gt; I put Gemma and Qwen side by side on the same task and chose the one that handled Hindi. Later I added a hosted open-weight model behind the same interface without touching the rest of the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Designing around the weaknesses.&lt;/strong&gt; The model is swappable and runs where I choose, so I could build around what it gets wrong instead of waiting for a better release.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing It With Bhavya
&lt;/h2&gt;

&lt;p&gt;Bhavya tried the local version on an Android phone over Wi-Fi, using the sample report (fictional data) and the in-app camera. What follows is paraphrased from their written feedback, which they cleaned up afterwards, so none of it is a direct quote:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It worked. The OCR read most of the text, though a couple of numbers and formatting spots were harder.&lt;/li&gt;
&lt;li&gt;They waited and wondered whether it was still processing.&lt;/li&gt;
&lt;li&gt;They found the result much easier to understand than the original report.&lt;/li&gt;
&lt;li&gt;The Hindi sounded natural rather than word-for-word, and leaving some medical words in English was fine. They thought it would help parents and grandparents.&lt;/li&gt;
&lt;li&gt;They had to ask what the "ask your doctor" lines meant, and I explained they weren't a diagnosis. They also weren't sure at first whether the numbers were normal until they read below.&lt;/li&gt;
&lt;li&gt;They'd use it again for blood-test reports, prescriptions and relatives' documents, to get the meaning without looking up every word.&lt;/li&gt;
&lt;li&gt;They suggested a clearer progress state, and said to keep the warning to check the original document.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One tester and one sample document, so I wouldn't call this validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed After That
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bhavya's feedback (paraphrased)&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wondered whether it was frozen during the wait&lt;/td&gt;
&lt;td&gt;Spinner and a "still working, not frozen" message, plus a note that photos take longer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasn't sure if numbers were normal until reading below&lt;/td&gt;
&lt;td&gt;An "Above / Within listed range" badge on every result, instead of color alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Had to ask what the doctor lines meant&lt;/td&gt;
&lt;td&gt;A one-line "not a diagnosis" note under that heading, in English and Hindi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photos can misread numbers; keep the check-the-original warning&lt;/td&gt;
&lt;td&gt;A panel showing the exact text read from the photo, with a note to compare it against the paper&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Afterwards I also redesigned the interface (range bars, summary counts, a one-tap sample button, dark mode). That wasn't based on Bhavya's feedback, and Bhavya hasn't seen these changes yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Got Wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I let the model write the whole explanation, then replaced most of it with code anyway. That was the 262-second version.&lt;/li&gt;
&lt;li&gt;I asked the model to write Hindi for sentences the code had already computed. It returned English. I switched to fixed Hindi templates for those parts.&lt;/li&gt;
&lt;li&gt;Gemma sometimes fell into a repetition loop while writing JSON, and Ollama killed the request with a "token repeat limit reached" error. I capped the output length and retry when that happens.&lt;/li&gt;
&lt;li&gt;One glossary entry in Hindi contradicted itself, saying "after eating" and "after not eating for 8 hours". The 8 hours wasn't in the report at all. I tightened the prompt against adding numbers, but it can still happen.&lt;/li&gt;
&lt;li&gt;My test script looked dead because it printed nothing for over a minute, so I added a heartbeat line. Bhavya hit the same wall in the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Didn't Do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I haven't tested it on real reports, only a fictional sample, and only with one tester.&lt;/li&gt;
&lt;li&gt;The parser handles two line formats. Other layouts fall back to the model alone, which is less reliable.&lt;/li&gt;
&lt;li&gt;Bhavya was the only Hindi reader.&lt;/li&gt;
&lt;li&gt;The glossary meanings are the model's general knowledge. Nobody has checked them against a medical source.&lt;/li&gt;
&lt;li&gt;The hosted backend has no login or rate limiting, and its CORS setting is wide open. Anyone with the URL could call it, so the Groq key's usage is capped on their side.&lt;/li&gt;
&lt;li&gt;I haven't measured the hosted model's speed or Hindi, and photo reading there is untested.&lt;/li&gt;
&lt;li&gt;No staged progress bar yet (reading the photo, then explaining it).&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; Gemma 3 4B, an open-weight model, runs locally through Ollama and writes the glossary, the Hindi glossary and the summary for documents without numbers. Code, not the model, compares the numbers, because Gemma got that wrong. (The hosted demo uses a different model, so to see Gemma in action, run the local version.)&lt;/p&gt;

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