<?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: Aditya Patkar</title>
    <description>The latest articles on DEV Community by Aditya Patkar (@virtualord).</description>
    <link>https://dev.to/virtualord</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%2F4156512%2F7bf54e42-8519-4549-98c6-79c2ff823cbc.png</url>
      <title>DEV Community: Aditya Patkar</title>
      <link>https://dev.to/virtualord</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/virtualord"/>
    <language>en</language>
    <item>
      <title>Sloth Valet: a local AI that tidies my Downloads overnight (and can undo everything)</title>
      <dc:creator>Aditya Patkar</dc:creator>
      <pubDate>Sat, 24 Oct 2026 13:49:17 +0000</pubDate>
      <link>https://dev.to/virtualord/sloth-valet-a-local-ai-that-tidies-my-downloads-overnight-and-can-undo-everything-g8k</link>
      <guid>https://dev.to/virtualord/sloth-valet-a-local-ai-that-tidies-my-downloads-overnight-and-can-undo-everything-g8k</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;Sloth Valet is a small Linux tool that tidies a messy Downloads folder while I sleep. It waits until files have been left alone for a while, asks a small open-weight model running on my own machine what each file is, then renames it cleanly and moves it into a sorted folder tree. In the morning there's a short report waiting.&lt;/p&gt;

&lt;p&gt;The effort it saves is the one I keep dodging: opening Downloads, seeing 200 files called &lt;code&gt;final_v2 (3).pdf&lt;/code&gt;, and closing the window again. Anyone with a permanently messy folder gets to be a little lazier.&lt;/p&gt;

&lt;p&gt;Because it runs unattended, I designed it around not breaking anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dry-run is the default.&lt;/strong&gt; Nothing moves until you pass &lt;code&gt;--apply&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It waits.&lt;/strong&gt; Files must sit untouched for a settle period, and partial downloads are skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never guesses.&lt;/strong&gt; Low confidence, bad JSON, a timeout, or a missing model sends the file to &lt;code&gt;_review/&lt;/code&gt; or leaves it alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything is undoable.&lt;/strong&gt; Every move is journaled in SQLite with a SHA-256 hash, and &lt;code&gt;valet undo --last&lt;/code&gt; puts files back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never deletes or overwrites.&lt;/strong&gt; Name collisions get a suffix.&lt;/li&gt;
&lt;/ul&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/Virtualord" rel="noopener noreferrer"&gt;
        Virtualord
      &lt;/a&gt; / &lt;a href="https://github.com/Virtualord/sloth-valet" rel="noopener noreferrer"&gt;
        sloth-valet
      &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;Sloth Valet&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A slow, private file valet for a messy downloads folder.&lt;/p&gt;
&lt;p&gt;Sloth Valet waits until files have settled, asks a small local open-weight model
(through Ollama) for a category and a clean filename, and moves the files into a tidy
destination tree. It runs offline, journals every move, writes a morning report, and
prefers leaving a file alone over guessing.&lt;/p&gt;
&lt;p&gt;Built for &lt;strong&gt;Hacktoberfest 2026: Slow and Steady&lt;/strong&gt; — open-weight AI that rewards
patience, privacy, and a recoverable filesystem.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Status&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Version &lt;code&gt;0.1.0&lt;/code&gt;. The full pipeline is implemented: validated configuration, read-only
scanning, bounded extraction, local classification, planning, journaled moves, review
routing, crash reconciliation, undo, Markdown reports, optional desktop notifications
a systemd user timer, and a model-comparison benchmark harness.&lt;/p&gt;
&lt;p&gt;The default test suite uses mocked HTTP and fake classifiers. It does not need a
network connection, a running Ollama, a real systemd session, or a notification
daemon.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why this shape&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offline&lt;/strong&gt;…&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/Virtualord/sloth-valet" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;It's about 4,700 lines of plain Python (no agent framework), MIT licensed, with mocked-HTTP tests so CI needs no Ollama. Setup is &lt;code&gt;./install.sh&lt;/code&gt;, then &lt;code&gt;valet config init&lt;/code&gt;, then &lt;code&gt;valet run&lt;/code&gt; for a dry run. &lt;code&gt;scripts/demo.sh &amp;lt;model-name&amp;gt;&lt;/code&gt; runs the whole flow against generated fixtures in a temp folder, so you can try it without touching real files.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Open pieces&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; open-weight models served locally through &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;. The code never assumes a model name. You put one in the config, or pass several to &lt;code&gt;valet bench --models&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harness:&lt;/strong&gt; no LangChain, no framework. The agent loop is plain code: scan, extract, classify, plan, apply, report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building it:&lt;/strong&gt; I built it phase by phase with &lt;a href="https://opencode.ai/" rel="noopener noreferrer"&gt;opencode&lt;/a&gt;, driven by an &lt;code&gt;AGENTS.md&lt;/code&gt; of safety rules, with a branch and checks per phase. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scan&lt;/strong&gt; watch folders. Protected paths, ignore globs, symlinks, size limits, and a settle check all run &lt;em&gt;before&lt;/em&gt; any file is opened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract&lt;/strong&gt; a bounded snippet of text (text, code, PDF, DOCX). Images and unknown binaries contribute metadata only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classify&lt;/strong&gt; through Ollama with a strict JSON schema. The category must come from an allowlist, and filenames are sanitized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan&lt;/strong&gt; collision-free destinations. Obvious extensions like &lt;code&gt;.deb&lt;/code&gt; or &lt;code&gt;.iso&lt;/code&gt; skip the model entirely, which saves compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply&lt;/strong&gt; by journaling &lt;code&gt;pending&lt;/code&gt;, revalidating the file, moving it, then marking &lt;code&gt;done&lt;/code&gt;. A crash is reconciled on the next run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report&lt;/strong&gt; in Markdown, with an optional count-only desktop notification.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It runs from a systemd user timer (&lt;code&gt;valet install-timer --time 03:00&lt;/code&gt;) at low CPU and I/O priority. The timer is persistent, so a run missed while the laptop was asleep happens after it wakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating files as hostile.&lt;/strong&gt; A downloaded file can contain text like "ignore previous instructions". The model only picks from an allowlisted category and returns JSON, and nothing it outputs is executed or used raw in a path. There's a test for exactly that case.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Privacy was the whole point.&lt;/strong&gt; Downloads are full of marksheets, ID scans, offer letters, and invoices. With a local model, file text only ever goes to &lt;code&gt;localhost:11434&lt;/code&gt;. There's no telemetry and no hosted API. The only HTTP module is &lt;code&gt;ollama.py&lt;/code&gt;, and an architecture test enforces that boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; No per-token bill means it can scan every night without me thinking about cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swapping models was easy, and that mattered.&lt;/strong&gt; Because the model is just a config value, I could benchmark it properly. I ran the same 50 labeled synthetic files through two tiny models on a CPU-only machine (8 cores, 7.6 GiB RAM; Ollama couldn't use my GTX 1050, so this is the no-GPU case):&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;Category accuracy&lt;/th&gt;
&lt;th&gt;Mean latency&lt;/th&gt;
&lt;th&gt;Peak RSS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen2.5:0.5b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;936 ms&lt;/td&gt;
&lt;td&gt;661 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;llama3.2:1b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;1,490 ms&lt;/td&gt;
&lt;td&gt;789 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The surprise is that both models produced perfectly valid JSON, and one of them was still almost always wrong. A model that looks fine can quietly misfile your documents, which is why the review folder, the confidence threshold, and undo are core features, not extras. Being able to swap and measure models locally is what made that visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where a closed model would have been better.&lt;/strong&gt; I didn't benchmark a hosted model, so I can't give you a number. A large cloud model would probably be more accurate. For this job I'd rather accept some misfiling that I can see and undo than send my documents to a server I don't control. Those numbers are one run on synthetic files on one machine, not a general verdict.&lt;/p&gt;

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

&lt;p&gt;Here's the honest accounting, using only what I measured. On my 50-file benchmark, &lt;code&gt;llama3.2:1b&lt;/code&gt; averaged about 1.5 seconds per file on a CPU-only machine, so a full pass over 50 files costs roughly 75 seconds of machine time. That happens at 3 AM at low CPU and I/O priority, so none of it is my time.&lt;/p&gt;

&lt;p&gt;The cost I'm really removing isn't the seconds spent moving a file. It's the decision cost: opening a folder full of &lt;code&gt;final_v2 (3).pdf&lt;/code&gt; and deciding, file by file, where each one belongs. That's the part I never get around to.&lt;/p&gt;

&lt;p&gt;I also ran it on a copy of my own Downloads: [N files scanned, X moved, Y sent to &lt;code&gt;_review/&lt;/code&gt;, Z left alone because they were too new, in T minutes. Then &lt;code&gt;valet undo --last&lt;/code&gt; restored all X.]&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Sloth Valet runs Gemma locally through Ollama as one of the models in its benchmark.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Localscout: an open-weight agent that audits a local business's website before it pitches them</title>
      <dc:creator>Aditya Patkar</dc:creator>
      <pubDate>Sun, 18 Oct 2026 04:46:56 +0000</pubDate>
      <link>https://dev.to/virtualord/localscout-an-open-weight-agent-that-audits-a-local-businesss-website-before-it-pitches-them-20a0</link>
      <guid>https://dev.to/virtualord/localscout-an-open-weight-agent-that-audits-a-local-businesss-website-before-it-pitches-them-20a0</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 freelance web developer knows the loop. You finish a project, and the next morning you are hunting for clients again: scrolling Google Maps, opening websites that take ten seconds to load, and sending messages that say "I can improve your website" to owners who have read that sentence fifty times. The pitch carries no proof, so it gets ignored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Localscout flips the order: audit first, pitch second.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You type something like &lt;code&gt;plumbers in Bhopal&lt;/code&gt;, and Localscout:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Finds&lt;/strong&gt; matching businesses from Google Maps results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audits&lt;/strong&gt; each website with real measurements: a mobile Lighthouse run (time to main content, layout shift, blocking time) plus plain HTTP checks for HTTPS, a mobile viewport tag, page title and description, and a tap-to-call link. Businesses with no website, or only a social page, are flagged too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scores&lt;/strong&gt; every lead. A slow site on a busy, well-reviewed business scores highest, because that owner has the most to gain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drafts&lt;/strong&gt; a short WhatsApp message in English, Hinglish, or Hindi that cites only what was measured, for example that the main content took 6.1 seconds to appear in a mobile speed test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hands it to you.&lt;/strong&gt; You read it, copy it or open WhatsApp with the text prefilled, send it yourself, and log what happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It never sends anything on its own. There is no send button in the code at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who would pay?&lt;/strong&gt; Freelance web developers and tiny agencies, especially in India, where WhatsApp is how you actually reach a plumber, a salon, or a clinic. The problem it solves for them is twofold: working out which of two hundred listings is worth a message, and arriving with evidence instead of a claim. &lt;strong&gt;[FILL IN: your pricing idea, for example a flat fee per batch of leads, or "I haven't priced it yet, but one hour of manual prospecting is the benchmark".]&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Live: &lt;strong&gt;&lt;a href="https://localscout-5cvy.onrender.com/" rel="noopener noreferrer"&gt;https://localscout-5cvy.onrender.com/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The free Render instance sleeps when idle, so the first load can take up to a minute.&lt;/p&gt;

&lt;p&gt;Things to try: click "Explore with demo data", move your cursor across the pixel map (each square is a business, colored by opportunity score), open a lead to see the audit evidence behind its score, and compare the three message languages.&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/Virtualord" rel="noopener noreferrer"&gt;
        Virtualord
      &lt;/a&gt; / &lt;a href="https://github.com/Virtualord/LocalScout" rel="noopener noreferrer"&gt;
        LocalScout
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      an open-weight agent that audits a local business's website before it pitches them 
    &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;Localscout&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Find local businesses, audit their websites with real measurements, and draft
honest outreach that &lt;strong&gt;you&lt;/strong&gt; review and send by hand.&lt;/p&gt;
&lt;p&gt;Localscout never sends anything. There is no send code path. It finds businesses
measures what is actually wrong with their site, writes a short message that cites
only those measurements, and hands it to you.&lt;/p&gt;
&lt;p&gt;Everything runs on your machine.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quickstart&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Three commands. No API keys, no Chrome, no Ollama, no network.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pnpm install
pnpm dev&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Then open &lt;a href="http://localhost:5173" rel="nofollow noopener noreferrer"&gt;http://localhost:5173&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is &lt;strong&gt;fixtures mode&lt;/strong&gt;: discovery reads 35 recorded SerpApi responses from
&lt;code&gt;fixtures/serpapi/&lt;/code&gt;, auditing runs against local fixture sites, and drafting uses
canned JSON. Everything is exercised except the three external services.&lt;/p&gt;
&lt;p&gt;Run a batch from the terminal instead:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pnpm cli &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;plumbers&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; Bhopal --limit 20&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;Browser (React)  --HTTP/SSE--&amp;gt;  API (127.0.0.1:8787)
                                      |
                               Mastra workflow
  discover -&amp;gt; dedupe -&amp;gt; audit (x N) -&amp;gt; score -&amp;gt;&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Virtualord/LocalScout" 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;What is open, and what isn't&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Open?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma (via Ollama or any OpenAI-compatible endpoint)&lt;/td&gt;
&lt;td&gt;Turns structured findings into a short message&lt;/td&gt;
&lt;td&gt;Open weights&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lighthouse&lt;/td&gt;
&lt;td&gt;Measures speed and mobile readiness&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mastra&lt;/td&gt;
&lt;td&gt;Orchestrates the workflow&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;Stores leads, audits, drafts, outreach log&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SerpApi&lt;/td&gt;
&lt;td&gt;Google Maps discovery&lt;/td&gt;
&lt;td&gt;Hosted service (the one closed part; responses are cached and the source is swappable)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The workflow.&lt;/strong&gt; A Mastra workflow runs &lt;code&gt;discover → dedupe → audit → score → draft → persist&lt;/code&gt;. Each step is a thin wrapper over a pure, tested function, so a framework quirk never traps the logic. One failing site never aborts a run: every external call returns a result marked &lt;code&gt;ok&lt;/code&gt;, &lt;code&gt;stale&lt;/code&gt;, or &lt;code&gt;unavailable&lt;/code&gt;, and the UI says so instead of guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence before words.&lt;/strong&gt; This is the decision I care about most. The model never reads a web page. Deterministic code produces the findings (for example "main content at 6.1 s, threshold 4.0 s"), and the model only turns that structured data into plain language. Then a grounding validator checks the draft: every number must match the audit evidence, it may not cite a finding that wasn't provided, and banned phrases like "guaranteed #1 ranking" are rejected. A bad draft gets one retry with the errors attached, and after that falls back to a deterministic template. A cold message with a made-up number is worse than no message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safety by construction.&lt;/strong&gt; Websites come from search results, so the auditor treats them as untrusted: private and loopback addresses are blocked (including cloud metadata endpoints), redirects are re-checked, and page text never reaches the model. A contacted business or domain is never pitched twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real hiccup.&lt;/strong&gt; I planned to resolve the city to coordinates before searching. My SerpApi key is on the Free plan, and every geocoding engine returned "Unsupported search engine". The documented alternative, &lt;code&gt;location&lt;/code&gt; plus &lt;code&gt;z&lt;/code&gt;, worked and returned 20 real Bhopal plumber results. I built the map from the GPS coordinates each listing already carries, which turned out simpler and removed the need to know the city center at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The frontend.&lt;/strong&gt; I modeled the look on the Hacktoberfest site itself: a teal canvas, coral buttons with hard offset shadows, and a pixel-grid map. Each business is a square colored by score, and a spotlight follows the cursor across the grid. The map is drawn on a canvas for smoothness, while the lead squares are real buttons so keyboard and screen-reader users get the same map.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Process.&lt;/strong&gt; I wrote the build spec first (architecture, data model, scoring rules, design system, git workflow), then had a coding agent build it phase by phase. &lt;strong&gt;[FILL IN: anything that went differently from the spec, such as a Mastra API change or a Lighthouse quirk, and how you handled it.]&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment.&lt;/strong&gt; Hosted on Render. &lt;strong&gt;[FILL IN: how it is deployed, for example a single web service serving the API and the built frontend.]&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;It makes the unit economics work.&lt;/strong&gt; Prospecting only pays if a lead costs almost nothing to process. With an open-weight model, drafting costs no per-token fee, so the only real cost per lead is the search itself. &lt;strong&gt;[FILL IN: your measured numbers from &lt;code&gt;/api/metrics&lt;/code&gt; or the eval run, for example average audit time, average draft latency, and SerpApi calls per lead.]&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It keeps a freelancer's pipeline theirs.&lt;/strong&gt; A lead list, the notes on each business, and the outreach log are a solo freelancer's most valuable data. Localscout stores them in a local SQLite file, and drafting needs no third-party LLM, so none of it goes to a model provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It lets me fix the part generic models handle poorly.&lt;/strong&gt; Romanized Hindi (Hinglish) written for a local shop owner is exactly where general-purpose models tend to sound stiff. Open weights mean I can swap models, evaluate them with the same fixtures, and fine-tune later without changing the product. The model sits behind a small provider interface, so moving between Ollama, another OpenAI-compatible endpoint, or a different Gemma size is a config change. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It makes the evidence checkable.&lt;/strong&gt; Because the audit uses open-source Lighthouse, a business owner can run the same test and see the same number. That is a much stronger opener than "your site feels slow."&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt;: the app is hosted on Render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of SerpApi&lt;/strong&gt;: live Google Maps discovery with a disk cache to protect the free-plan quota.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Mastra&lt;/strong&gt;: the discover-to-persist workflow runs on Mastra.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: drafting runs on a Gemma model. &lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>career</category>
      <category>ai</category>
    </item>
    <item>
      <title>Garden Rounds: a 6 MB leaf classifier and a frost-date planner that work with the network off</title>
      <dc:creator>Aditya Patkar</dc:creator>
      <pubDate>Sun, 11 Oct 2026 03:40:46 +0000</pubDate>
      <link>https://dev.to/virtualord/garden-rounds-a-6-mb-leaf-classifier-and-a-frost-date-planner-that-work-with-the-network-off-2l58</link>
      <guid>https://dev.to/virtualord/garden-rounds-a-6-mb-leaf-classifier-and-a-frost-date-planner-that-work-with-the-network-off-2l58</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;Garden Rounds is a small offline-first web app that gets people into a garden once a week. It works out &lt;strong&gt;its own frost dates&lt;/strong&gt; for wherever you are from thirty years of historical weather, tells you what to sow, plant out, inspect and harvest &lt;em&gt;this week&lt;/em&gt;, prints that as a one-page card you can take outside, and gives a first-look read on a leaf you photograph with your phone.&lt;/p&gt;

&lt;p&gt;The screen is the shortest part of the experience. The point is the card, and the leaf.&lt;/p&gt;

&lt;p&gt;It's for the gardener with a phone and no signal in a raised bed, and for anyone who has ever wanted to know &lt;em&gt;this week specifically&lt;/em&gt; rather than read a seed packet written for a different country.&lt;/p&gt;

&lt;p&gt;Three things make it work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It computes your frost dates instead of looking them up.&lt;/strong&gt; Type a place name and the app pulls thirty complete calendar years of daily temperatures from the Open-Meteo archive and derives your own first-spring and last-autumn frost. Everything else follows from that. "Last frost is usually April, first fall frost is usually October" is not a fact from a table — it's computed from your location, and you can move the threshold in Settings and watch the whole season shift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model is a 6 MB file, not a service.&lt;/strong&gt; MobileNetV3-Small, fine-tuned on 48,947 photographs of 37 leaf conditions, running in the browser through ONNX Runtime Web's WASM backend. No API key, no account, no upload endpoint — there isn't one in the codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It keeps working with the network off.&lt;/strong&gt; A service worker precaches the shell, the fonts, the ONNX runtime and the model. After one setup call, the app makes no further requests to anywhere.&lt;/p&gt;

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


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://garden-rounds.onrender.com/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;garden-rounds.onrender.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;Run it with the network off after setup. That's the demo. Turn off wifi and add a plant, scan a leaf, print the card — all of it still works.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything the app does on the network, it does once: your typed place name goes to Open-Meteo geocoding, and a coordinate &lt;strong&gt;rounded to one decimal place&lt;/strong&gt; — about 11 km — goes to the archive API. There is an end-to-end test that asserts no third-party request happens after that, rather than the README just claiming it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;This week&lt;/th&gt;
&lt;th&gt;Garden&lt;/th&gt;
&lt;th&gt;Leaf check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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%2F24rpnv97tl9612n7k5dw.png" alt="This week" width="390" height="844"&gt;&lt;/td&gt;
&lt;td&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%2Fywkdc3qq24o3yk5ijgxw.png" alt="Garden" width="390" height="844"&gt;&lt;/td&gt;
&lt;td&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%2F4e2fevs26x1uyib7s958.png" alt="Scan" width="390" height="844"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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/Virtualord" rel="noopener noreferrer"&gt;
        Virtualord
      &lt;/a&gt; / &lt;a href="https://github.com/Virtualord/Garden-Rounds" rel="noopener noreferrer"&gt;
        Garden-Rounds
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A small offline-first web app that gets people into a garden once a week.
    &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;Garden Rounds&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A small offline-first web app that gets people into a garden once a week.&lt;/p&gt;
&lt;p&gt;It works out its own frost dates for wherever you are from thirty years of
historical weather, tells you what to sow, plant out, inspect and harvest this
week, prints that plan as a one-page card you can take outside, and gives a
first-look read on a leaf you photograph with your phone.&lt;/p&gt;
&lt;p&gt;The screen is the shortest part of the experience. The point is the card, and the
leaf.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it looks like&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;This week&lt;/th&gt;
&lt;th&gt;Garden&lt;/th&gt;
&lt;th&gt;Leaf check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Virtualord/Garden-Rounds/docs/screens/this-week-mobile-light.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%2FVirtualord%2FGarden-Rounds%2FHEAD%2Fdocs%2Fscreens%2Fthis-week-mobile-light.png" alt="This week"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Virtualord/Garden-Rounds/docs/screens/garden-mobile-light.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%2FVirtualord%2FGarden-Rounds%2FHEAD%2Fdocs%2Fscreens%2Fgarden-mobile-light.png" alt="Garden"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Virtualord/Garden-Rounds/docs/screens/scan-result-mobile-light.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%2FVirtualord%2FGarden-Rounds%2FHEAD%2Fdocs%2Fscreens%2Fscan-result-mobile-light.png" alt="Scan"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The printed card is the real output:&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Virtualord/Garden-Rounds/docs/screens/print-card-a5.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%2FVirtualord%2FGarden-Rounds%2FHEAD%2Fdocs%2Fscreens%2Fprint-card-a5.png" alt="The printed card"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;More in &lt;a href="https://github.com/Virtualord/Garden-Rounds/docs/screens/" rel="noopener noreferrer"&gt;&lt;code&gt;docs/screens/&lt;/code&gt;&lt;/a&gt;, including a frost-free climate, the
setup flow and both colour schemes.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Running it&lt;/h2&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; web
npm install
npm run dev&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Then open &lt;a href="http://localhost:5173" rel="nofollow noopener noreferrer"&gt;http://localhost:5173&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm run lint          &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; eslint&lt;/span&gt;
npm run format:check  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; prettier&lt;/span&gt;
npm &lt;span class="pl-c1"&gt;test&lt;/span&gt;              &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; vitest, 456 tests&lt;/span&gt;
npm run build         &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Virtualord/Garden-Rounds" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT licensed. 10,730 lines, 492 unit tests and 30 end-to-end tests.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The model.&lt;/strong&gt; MobileNetV3-Small (&lt;code&gt;timm&lt;/code&gt;, Apache 2.0) fine-tuned on PlantVillage, grouped by leaf so that all four photographs of the same leaf land on the same side of the split. Eight epochs, CPU-only, about two hours.&lt;/p&gt;

&lt;p&gt;That grouping was the whole ballgame, and it came from a mirror rather than the original dataset. The Hugging Face mirror &lt;a href="https://huggingface.co/datasets/geraldmc/plantvillage-full" rel="noopener noreferrer"&gt;&lt;code&gt;geraldmc/plantvillage-full&lt;/code&gt;&lt;/a&gt; keeps a &lt;code&gt;leaf_id&lt;/code&gt; column that upstream discards. &lt;strong&gt;69% of eligible rows are one of just 5,910 leaves, shot four times over.&lt;/strong&gt; Split those naively and the model sees the same leaf in both train and test. It was not a hypothetical: the audit caught a real bug on its first run, with 3,223 leaves assigned to both validation and test.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;ml/prepare_hf.py&lt;/code&gt; groups on &lt;code&gt;leaf_id&lt;/code&gt;, and its &lt;code&gt;audit()&lt;/code&gt; &lt;strong&gt;raises rather than printing reassurance&lt;/strong&gt;. A split report that quietly says "looks fine" is how a leaked split gets shipped. Final leaf overlap between every pair of splits: &lt;strong&gt;0&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Split&lt;/th&gt;
&lt;th&gt;Images&lt;/th&gt;
&lt;th&gt;Leaves&lt;/th&gt;
&lt;th&gt;Images per leaf&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;train&lt;/td&gt;
&lt;td&gt;34,096&lt;/td&gt;
&lt;td&gt;14,663&lt;/td&gt;
&lt;td&gt;2.33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;val&lt;/td&gt;
&lt;td&gt;7,411&lt;/td&gt;
&lt;td&gt;3,195&lt;/td&gt;
&lt;td&gt;2.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;test&lt;/td&gt;
&lt;td&gt;7,440&lt;/td&gt;
&lt;td&gt;3,223&lt;/td&gt;
&lt;td&gt;2.31&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The result: &lt;strong&gt;lab top-1 0.9957&lt;/strong&gt;, macro recall 0.9950, 37 classes. All 32 errors are between two diseases of the same plant, and the correlation between class size and recall is &lt;strong&gt;+0.07&lt;/strong&gt; — indistinguishable from zero. The 37× class imbalance was the reason to check that rather than assume it, because the small classes are normally what breaks first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I tried to make it 2 MB and it refused to be.&lt;/strong&gt; The target was int8. All three quantisation routes produced near-constant output:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Precision&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Lab top-1&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;fp32&lt;/td&gt;
&lt;td&gt;6.23 MB&lt;/td&gt;
&lt;td&gt;0.9957&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;shipped&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;static QDQ, per-channel&lt;/td&gt;
&lt;td&gt;1.90 MB&lt;/td&gt;
&lt;td&gt;0.0887&lt;/td&gt;
&lt;td&gt;refused, −90.7 pts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dynamic, per-channel&lt;/td&gt;
&lt;td&gt;1.72 MB&lt;/td&gt;
&lt;td&gt;0.1187&lt;/td&gt;
&lt;td&gt;refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dynamic + &lt;code&gt;reduce_range&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1.72 MB&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;td&gt;refused&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the export carries a gate: int8 ships only if it costs no more than 1.5 points. It refused, three times, and fp32 shipped. Two candidate causes were tested and eliminated — single-class calibration, and VNNI-less saturation. The wrong answer is still in &lt;a href="https://github.com/Virtualord/Garden-Rounds/blob/main/ml/MODEL_CARD.md" rel="noopener noreferrer"&gt;&lt;code&gt;ml/MODEL_CARD.md&lt;/code&gt;&lt;/a&gt; with both hypotheses, because I don't know what it is and 6.23 MB is the honest answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything else is deliberately not AI.&lt;/strong&gt; There's no language model anywhere in this app. The planner is a pure function of your frost dates and the crop's own needs, with &lt;code&gt;today&lt;/code&gt; injected so it's testable. A language model would have made it worse and less trustworthy, and the app's claim — "this is what your dates say" — is only worth making if the thing producing it is inspectable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The app.&lt;/strong&gt; React 19, Vite, Tailwind v4, &lt;code&gt;react-router&lt;/code&gt;, ONNX Runtime Web, IndexedDB, Vite PWA. The design constraints that shaped it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One animation runs forever&lt;/strong&gt;, and a test asserts there's exactly one. That's what stops a second from being added without noticing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One nav landmark.&lt;/strong&gt; The bottom tab bar restyles into a left rail at ≥64rem rather than being duplicated, so there's never two navs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No horizontal scroll at 320, 360, 390, 820, 1024 or 1440px&lt;/strong&gt; — asserted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The theme is System / Light / Dark&lt;/strong&gt;, with each colour declared once via &lt;code&gt;light-dark()&lt;/code&gt; and the three options only setting &lt;code&gt;color-scheme&lt;/code&gt;. Setting &lt;code&gt;color-scheme&lt;/code&gt; rather than repainting also fixes scrollbars, the caret and form controls.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Because it runs in a garden with no signal, and because the photographs never leave the phone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both of those are consequences of the licence, not accidents. A closed API would have been &lt;em&gt;easier&lt;/em&gt;: a bigger model, better generalisation, someone else's GPUs. What it could not have been is offline, or free of a server holding photographs of someone's plants. Those aren't features I added on top of the open approach. They're what the open approach is &lt;em&gt;for&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Concretely, open weights let me do three things a hosted endpoint would have stopped me doing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find the leak myself.&lt;/strong&gt; I could decode the dataset's structure, discover that 69% of rows were four shots of the same leaf, and fix the split. With an API I would have sent a photo, got an answer back, and had no way to ask &lt;em&gt;why&lt;/em&gt; — so the leak would have stayed invisible and I'd have shipped a number I believed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set the gate and let it refuse.&lt;/strong&gt; The quantisation table above is only meaningful because I could run the model locally, badly, a hundred times. The 1.5-point rule rejected all three int8 routes. A hosted service would have returned a working answer and I'd never have known there was a cheaper size I hadn't found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Publish the failure.&lt;/strong&gt; &lt;code&gt;MODEL_CARD.md&lt;/code&gt; records the licence dispute, the int8 table, the 37× class imbalance, and the one wrong prediction I left in place rather than quietly fix. You can check any of it, because the pipeline that produced it is in the repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On privacy specifically:&lt;/strong&gt; there is no upload endpoint in this codebase. Your leaf photographs are resized and classified in the browser and stored only as a thumbnail on your own device. The Content-Security-Policy in &lt;a href="https://github.com/Virtualord/Garden-Rounds/blob/main/render.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;render.yaml&lt;/code&gt;&lt;/a&gt; makes that enforceable rather than aspirational — &lt;code&gt;connect-src&lt;/code&gt; names Open-Meteo and nothing else, so a browser would block a third-party call even if someone added one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is genuinely worse than a closed approach
&lt;/h2&gt;

&lt;p&gt;The project is an argument, so the argument needs its counter-argument, and I'd rather write it than have a judge find it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No open questions.&lt;/strong&gt; "What's wrong with this leaf and what should I do?" is the real question. This answers "which of 37 conditions does this resemble" — narrower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No context across a season.&lt;/strong&gt; It sees one leaf. It doesn't know the plant has been declining since June, or that the bed waterlogs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No coverage outside the label set.&lt;/strong&gt; Thirty-seven classes from two datasets. A strawberry problem that isn't leaf scorch isn't in the space, and the model will confidently offer the nearest thing it knows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worse photographs.&lt;/strong&gt; It's trained on photographs most people don't take. A closed API trained on far more varied data would handle a phone-in-the-dark photo better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No explanation.&lt;/strong&gt; It can't tell you &lt;em&gt;why&lt;/em&gt;, or what to do next. That's advice, not classification.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I haven't done yet
&lt;/h2&gt;

&lt;p&gt;Being straight about this, because a 0.9957 in a write-up invites the wrong conclusion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;There is no field test.&lt;/strong&gt; The 0.9957 is on held-out lab photographs. &lt;code&gt;docs/FIELD_TEST.md&lt;/code&gt; exists and is empty; I have not photographed a real garden with this yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No field split at all&lt;/strong&gt; — &lt;code&gt;metrics.json&lt;/code&gt; records &lt;code&gt;"field": {"n": 0, "skipped": "no images in this split"}&lt;/code&gt;. The corpus is lab-only. This is a number on a test set, not a product claim.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model isn't published yet.&lt;/strong&gt; &lt;code&gt;models/leaf.onnx&lt;/code&gt; is a gitignored Release asset, so the deployed Scan screen says plainly that no trained model is installed. The rest of the app is fully functional without it. It needs a GPU box to run the export properly — mine is &lt;code&gt;sm_61&lt;/code&gt;, and PyTorch has dropped &lt;code&gt;sm_61&lt;/code&gt; from every wheel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;26 crop timings still need checking&lt;/strong&gt; against a local extension guide. Every one is marked &lt;code&gt;needs_review&lt;/code&gt; in the plan and on the card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A licence question is unresolved.&lt;/strong&gt; The HF mirror is tagged &lt;code&gt;cc0-1.0&lt;/code&gt;; upstream PlantVillage is CC BY-SA 4.0. Those cannot both be right. No dataset is redistributed here, and the dispute is recorded rather than assumed away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Render&lt;/strong&gt; — the app is a static Vite build deployed as a Render static site with a Blueprint, an SPA rewrite rule that doesn't swallow the wasm or the model, long-lived immutable caching for both, and a Content-Security-Policy that makes the no-third-party-requests claim something a browser enforces.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hacktoberfest Weekend Challenge: Build for a Friend</title>
      <dc:creator>Aditya Patkar</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:23:24 +0000</pubDate>
      <link>https://dev.to/virtualord/hacktoberfest-weekend-challenge-build-for-a-friend-411m</link>
      <guid>https://dev.to/virtualord/hacktoberfest-weekend-challenge-build-for-a-friend-411m</guid>
      <description>&lt;p&gt;&lt;strong&gt;## What I Built&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://saaz-8d9d.onrender.com/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;saaz-8d9d.onrender.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
I built &lt;strong&gt;Saaz&lt;/strong&gt;, a subtitle tool for regional-language video. You upload a clip, and it returns timed Hindi subtitles plus an English translation. You can edit them in the browser and export &lt;code&gt;.srt&lt;/code&gt; or &lt;code&gt;.vtt&lt;/code&gt;. Everything runs on your own machine with open-weight models, and no closed AI API is called at any point.

&lt;p&gt;I built it for Raashi, who edit reels they caption footage for clients, and uploading unreleased footage to a hosted captioning service wasn't an option.&lt;/p&gt;

&lt;p&gt;That is the problem Saaz solves. When you caption someone's unreleased footage, the video, the transcript and the translation normally all end up on somebody else's server. With Saaz every byte stays local, and the offline claim is checkable: &lt;code&gt;npm run prove:offline&lt;/code&gt; runs the pipeline with all outbound network poisoned.&lt;/p&gt;

&lt;p&gt;It also doesn't pretend to be perfect. A translation often needs more characters than the on-screen time window allows, so a &lt;strong&gt;Fit stage&lt;/strong&gt; escalates through five outcomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;verbatim&lt;/strong&gt;: it already fits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reflowed&lt;/strong&gt;: a small model rewrites it inside the budget&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;extended&lt;/strong&gt;: borrow time from the following gap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;split&lt;/strong&gt;: divide the cue deterministically and retranslate both halves&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;refused&lt;/strong&gt;: nothing works, so flag it for a human and say why&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last outcome matters most. A subtitle file is something a client signs off on, so a tool that always outputs &lt;em&gt;something&lt;/em&gt; is worse than one that tells you which lines need a person.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/Virtualord/saaz" rel="noopener noreferrer"&gt;https://github.com/Virtualord/saaz&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The pipeline is: &lt;code&gt;probe → decode → Whisper ASR → Silero VAD → align → segment → glossary → OPUS-MT → Fit&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Licence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speech recognition&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onnx-community/whisper-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speech boundaries&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onnx-community/silero-vad&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translation draft&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Xenova/opus-mt-hi-en&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caption rewrite&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HuggingFaceTB/SmolLM2-360M-Instruct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All four run through one runtime, &lt;a href="https://github.com/huggingface/transformers.js" rel="noopener noreferrer"&gt;transformers.js&lt;/a&gt; over &lt;code&gt;onnxruntime-node&lt;/code&gt;, quantised to &lt;code&gt;q8&lt;/code&gt; so they fit in CPU RAM with no GPU. The app is TypeScript throughout, with a React + shadcn/ui + Tailwind v4 frontend, Vite, SQLite for jobs, and Vitest for the unit tests (segmenter, fit, align, glossary, export). I deploy it from a Docker image to Render, with the weights baked in at build time so the live service never contacts Hugging Face.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I chose every default from a benchmark rather than reputation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Whisper-base emits Arabic script for Hindi audio&lt;/strong&gt; (CER 1.00). whisper-small scores 0.165. I also tried &lt;code&gt;whisper-large-v3-turbo&lt;/code&gt;, but at &lt;code&gt;q8&lt;/code&gt; it returned empty transcripts, so I removed it instead of shipping it broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qwen2.5-0.5B and 1.5B couldn't do the caption task.&lt;/strong&gt; Both failed to produce parseable JSON and degenerated into repetition. SmolLM2-360M returned valid JSON first try and ran 2.5× faster. Smaller model, better result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Whisper export has no cross-attention outputs&lt;/strong&gt;, so it can't emit word timestamps. Its segment timings collapsed into uniform 3-second blocks that matched nothing in the audio. I found this by inspecting real output and fixed it with Silero VAD.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open MT has vocabulary gaps.&lt;/strong&gt; &lt;code&gt;मालाई&lt;/code&gt; (the sweet) translated to &lt;em&gt;"by Miley"&lt;/em&gt;. I ruled out tokenisation bugs, so Saaz ships a user-pinned &lt;strong&gt;glossary&lt;/strong&gt; that is substituted before translation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is deliberately no agent loop in the fit path. A subtitle deliverable has to be reproducible, so escalation is deterministic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest limitations:&lt;/strong&gt; Whisper quality on regional languages is imperfect (hence the editor), word timing is approximated by proportional alignment, and CPU inference runs at roughly 4× realtime for ASR. That is the price of keeping footage on a laptop.&lt;/p&gt;

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

&lt;p&gt;A closed API would have made this project impossible, because the whole point is that the footage never leaves the machine. Open weights let me run speech recognition, translation and caption rewriting in-process and prove it with the network unplugged.&lt;/p&gt;

&lt;p&gt;Openness also made the work &lt;em&gt;inspectable&lt;/em&gt;. I could open the Whisper export and discover it had no word timestamps. I could swap Qwen out for SmolLM2 when Qwen failed, and I could read every model's licence and put it in the UI. I even left Gemma out of the defaults because its licence isn't OSI-approved. With a closed API I would have had to trust whatever it did to my friend's footage. Here I can show exactly what ran and why.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt;: Saaz ships a &lt;code&gt;render.yaml&lt;/code&gt; blueprint and deploys from a Docker image with weights baked in.&lt;/li&gt;
&lt;/ul&gt;

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