<?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: Anand B</title>
    <description>The latest articles on DEV Community by Anand B (@anandb71).</description>
    <link>https://dev.to/anandb71</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%2F4157484%2Fd501aa8b-1936-457c-81d6-efd24bed6aa6.jpg</url>
      <title>DEV Community: Anand B</title>
      <link>https://dev.to/anandb71</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anandb71"/>
    <language>en</language>
    <item>
      <title>logsloth reads the failing CI log so I don't have to</title>
      <dc:creator>Anand B</dc:creator>
      <pubDate>Fri, 23 Oct 2026 05:34:58 +0000</pubDate>
      <link>https://dev.to/anandb71/logsloth-reads-the-failing-ci-log-so-i-dont-have-to-3ocf</link>
      <guid>https://dev.to/anandb71/logsloth-reads-the-failing-ci-log-so-i-dont-have-to-3ocf</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;I'm Anand Biju. I built logsloth, a small command line tool for the most boring part of my week: a red CI run.&lt;/p&gt;

&lt;p&gt;The red X is never the bad part. The bad part is opening the log. You get a few hundred lines of checkout, cache restore, dependency install and cleanup, and somewhere in the middle is the one line that matters. The last line, &lt;code&gt;Process completed with exit code 1&lt;/code&gt;, tells you nothing. So you scroll, you search for "error", you hit forty false matches, and ten minutes later you find out a package was missing.&lt;/p&gt;

&lt;p&gt;logsloth does that scrolling. Give it a log file, pipe a command into it, or point it at a failed GitHub Actions run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;logsloth ci.log
npm &lt;span class="nb"&gt;test &lt;/span&gt;2&amp;gt;&amp;amp;1 | logsloth
logsloth &lt;span class="nt"&gt;--gh&lt;/span&gt; https://github.com/owner/repo/actions/runs/1234567890
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It prints a short card with four lines. CULPRIT is the real error line. WHERE is the file and line. WHY is one sentence on what went wrong. FIX is the next thing to do. Below that are the handful of log lines it used as evidence, with their original line numbers, so you can check it instead of trusting it. &lt;code&gt;--md&lt;/code&gt; prints the same thing as Markdown for a PR comment, and &lt;code&gt;--json&lt;/code&gt; is there for scripts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;These are all real runs on my machine. The first three use real logs from public projects, pulled with &lt;code&gt;gh run view --log-failed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A ruff-format pre-commit failure in psf/requests. The log is 284 lines and the run ends with about forty lines of git cleanup after the error. logsloth skips that and lands on the hook that failed:&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%2Fvexb8qu1oajwn5xnbsvt.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%2Fvexb8qu1oajwn5xnbsvt.png" alt="pre-commit failure in requests" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A mypy error in pallets/flask. The error message wraps over three lines, and logsloth joins them back together:&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%2F1itdgmyk16bduaponzpb.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%2F1itdgmyk16bduaponzpb.png" alt="mypy error in flask" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A failing vitest assertion in vitejs/vite. The log is 653 lines. Here Gemma got distracted by a &lt;code&gt;pnpm store prune&lt;/code&gt; line in the excerpt and suggested running it. The validator threw that out, and the card shows the built-in hint instead. The footer says so.&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%2Fnfw5qfqb4z1rosdhhpvg.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%2Fnfw5qfqb4z1rosdhhpvg.png" alt="vitest assertion in vite" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And one of the bundled examples, a pytest run where &lt;code&gt;httpx&lt;/code&gt; was never installed:&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%2Fkjifsklgdfx4cws6nev7.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%2Fkjifsklgdfx4cws6nev7.png" alt="pytest missing module" width="800" height="351"&gt;&lt;/a&gt;&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/Anandb71" rel="noopener noreferrer"&gt;
        Anandb71
      &lt;/a&gt; / &lt;a href="https://github.com/Anandb71/logsloth" rel="noopener noreferrer"&gt;
        logsloth
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Reads a failing CI log so you don't have to. Code finds the evidence, a local Gemma explains it.
    &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;logsloth&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A small CLI that reads a long failing CI, test, or build log and tells you the one thing that broke.&lt;/p&gt;

&lt;p&gt;Code finds the evidence and states the facts. A local open-weight model (gemma3:1b via Ollama) only explains why it failed and suggests a fix. The model never sees the whole log.&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Anandb71/logsloth/docs/demo.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%2FAnandb71%2Flogsloth%2FHEAD%2Fdocs%2Fdemo.png" alt="demo"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;When CI goes red you get a few hundred lines of setup, cache restores, and cleanup, with the real error somewhere in the middle. Finding it is boring and you do it many times a week. logsloth does the scrolling for you and runs entirely on your machine, so private logs never leave it.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&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;pip install -e &lt;span class="pl-c1"&gt;.&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You also need &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt; and the default model:&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;ollama pull gemma3:1b&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;OLLAMA_HOST&lt;/code&gt; defaults to &lt;code&gt;http://127.0.0.1:11434&lt;/code&gt;.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&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-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; a log file&lt;/span&gt;
logsloth path/to/ci.log
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; piped from a local command&lt;/span&gt;
npm &lt;span class="pl-c1"&gt;test&lt;/span&gt; &lt;span class="pl-k"&gt;2&amp;gt;&amp;amp;1&lt;/span&gt; &lt;span class="pl-k"&gt;|&lt;/span&gt; logsloth

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; a failed&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/Anandb71/logsloth" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:1b
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
logsloth examples/pytest_httpx.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;python -m pytest -q&lt;/code&gt; runs 47 tests. They never call the model or the network.&lt;/p&gt;

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

&lt;p&gt;The rule I followed: code finds the facts, the model only explains them.&lt;/p&gt;

&lt;p&gt;My first version let Gemma 3 1B write everything. It went badly. On a cargo error it wrote the literal text "one sentence: what broke" as the culprit. On a jest log it pointed at the test file instead of the file that was missing the import. On the real flask and requests logs it blamed &lt;code&gt;git config&lt;/code&gt; and &lt;code&gt;safe.directory&lt;/code&gt;, because the cleanup lines at the end of a GitHub log look like errors. That third one is why I stopped testing only on my own example logs.&lt;/p&gt;

&lt;p&gt;So the work is split.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Triage is plain Python.&lt;/strong&gt; It strips ANSI colours, including the literal &lt;code&gt;^[[36m&lt;/code&gt; that &lt;code&gt;gh run view --log-failed&lt;/code&gt; leaves in, plus the timestamps and the &lt;code&gt;job&amp;lt;TAB&amp;gt;step&lt;/code&gt; prefix on every line. It folds repeated lines and then scores lines against known failure patterns: Python tracebacks and pytest &lt;code&gt;E&lt;/code&gt; lines, &lt;code&gt;Cannot find module&lt;/code&gt;, rust &lt;code&gt;error[E...]&lt;/code&gt; with its &lt;code&gt;--&amp;gt;&lt;/code&gt; location, mypy and ruff style &lt;code&gt;path:line: error:&lt;/code&gt;, pre-commit hooks that print &lt;code&gt;Failed&lt;/code&gt;, pip's &lt;code&gt;No matching distribution&lt;/code&gt;, docker's &lt;code&gt;failed to solve&lt;/code&gt;, and a few more. The final &lt;code&gt;exit code 1&lt;/code&gt; line can never be the main finding if anything else matched. CULPRIT and WHERE come straight from this step, so they are always lines that really are in the log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma only writes why and fix.&lt;/strong&gt; It gets a short excerpt, between 6 and 15 lines in the runs above, not the whole log. The prompt only includes the rules that apply to this failure. When I listed every rule at once, the 1B model copied them, and suggested "install httpx using pip or npm".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every answer gets checked.&lt;/strong&gt; Why and fix are validated separately. They have to be non-empty and short. They can't repeat the prompt or the example. They can't tell a git story when the failure isn't about git. They have to name the real package, symbol or hook. If the fix says to install something, that name has to appear in the log. That check caught Gemma telling me to "include lodoc" when the log said &lt;code&gt;lodash&lt;/code&gt;. A field that fails is replaced by a built-in hint for that error type, and the footer says which parts came from the model and which from the hint, for example &lt;code&gt;why from gemma3:1b, fix from hint&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In my final runs over the seven logs, Gemma's why was kept in six and its fix in three. On the vite log both fields fell back to hints. On this shared, CPU-only box a call took between 1.9 and 34 seconds, and most were around 10.&lt;/p&gt;

&lt;p&gt;It is standard-library Python. Ollama serves the model over HTTP on localhost. I built it this week with the Cursor CLI agent doing a lot of the typing, while I wrote the spec, ran it against real logs, and kept tightening the validator.&lt;/p&gt;

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

&lt;p&gt;CI logs are full of things you shouldn't paste into a hosted chatbot: internal hostnames, paths, package names from a private registry, sometimes a token someone forgot to mask. With an open-weight model on my own machine, the log stays here. Nothing gets uploaded, there's no API key to manage, and running it a hundred times a day costs nothing.&lt;/p&gt;

&lt;p&gt;It also changed how I built it. A 1B model running locally is fast and cheap enough to call on every failure, but it is wrong often enough that I had to treat its output as a suggestion to check, not an answer. Because I control the prompt, the temperature, the retry and the validation, I could build that checking around it. Swapping in a different model is one flag, &lt;code&gt;--model&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Before: open the run, expand the failed step, scroll past setup, search for "error", skip the false matches, find the real line, then work out which file it means.&lt;/p&gt;

&lt;p&gt;After: &lt;code&gt;logsloth --gh &amp;lt;run url&amp;gt;&lt;/code&gt;. I read four lines, then check the evidence lines if I don't believe them. On the 653-line vite log the card had the failing spec file and line, and the expected and received values, in the evidence block. I didn't have to open the log.&lt;/p&gt;

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

&lt;p&gt;Best Use of Gemma. logsloth runs Gemma 3 1B (&lt;code&gt;gemma3:1b&lt;/code&gt;) locally through Ollama. It writes the why and fix, and the validator decides which of its answers to keep.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>Portion Cut: paste your exam portion, read only the pages that matter (EmbeddingGemma, in the browser)</title>
      <dc:creator>Anand B</dc:creator>
      <pubDate>Tue, 20 Oct 2026 04:13:22 +0000</pubDate>
      <link>https://dev.to/anandb71/portion-cut-paste-your-exam-portion-read-only-the-pages-that-matter-embeddinggemma-in-the-4plg</link>
      <guid>https://dev.to/anandb71/portion-cut-paste-your-exam-portion-read-only-the-pages-that-matter-embeddinggemma-in-the-4plg</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;Every semester goes the same way. The professor posts the exam portion as a block of syllabus text, the textbook is a 400-page PDF, and the night before the exam I am flipping between the two trying to work out which chapters actually count. The book never uses the syllabus's wording, half the topics are spread across three chapters, and at least one line on the portion is not in the book at all. You only find that out after twenty minutes of searching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Portion Cut&lt;/strong&gt; does that matching for you. Paste the portion, drop in the textbook PDF, and it tells you which pages to read, grouped by syllabus line, with the section names next to each range. It then builds a smaller PDF with only those pages. When a syllabus line probably is not covered by the book, it says so plainly instead of guessing, so you know to look somewhere else.&lt;/p&gt;

&lt;p&gt;It is a tool for studying less, which felt right for Sloth Day. It does not summarise the book or write notes for you. It just stops you reading pages you did not need to read.&lt;/p&gt;

&lt;p&gt;Everything runs in the browser tab. The PDF is never uploaded, there is no backend, and there is no API key.&lt;/p&gt;

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

&lt;p&gt;Live: &lt;strong&gt;&lt;a href="https://anandb71.github.io/portion-cut/" rel="noopener noreferrer"&gt;https://anandb71.github.io/portion-cut/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;?demo=1&lt;/code&gt; to try it without the 220 MB model download. Demo mode uses page vectors for a sample book that were computed by the same model with the same settings. It is labelled on the page and cannot build the cut PDF, because there is no PDF loaded.&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%2Fwkiorfww4rdb34iv3sps.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%2Fwkiorfww4rdb34iv3sps.png" alt="Home screen in demo mode" width="800" height="1244"&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%2Fq73vuhde0uqjhgfdijv0.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%2Fq73vuhde0uqjhgfdijv0.png" alt="Reading list for a Python exam portion" width="800" height="2607"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real run, with the model doing the work in the browser on the full 397-page book:&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%2F9gx2c1kyvbh6u090hzz0.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%2F9gx2c1kyvbh6u090hzz0.png" alt="Real run in progress" width="800" height="1244"&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%2F56e0hrrd2cdwoqdo1awg.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%2F56e0hrrd2cdwoqdo1awg.png" alt="Real run results" width="800" height="2515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On that run, Portion Cut said to read 99 of 397 pages, found 7 of the 9 syllabus lines, and flagged the other two (sets and regular expressions) as probably not in the book. The book's table of contents confirms neither is covered. The raw log is in &lt;a href="https://github.com/Anandb71/portion-cut/blob/main/screenshots/browser-real-1.json" rel="noopener noreferrer"&gt;&lt;code&gt;screenshots/browser-real-1.json&lt;/code&gt;&lt;/a&gt;.&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/Anandb71" rel="noopener noreferrer"&gt;
        Anandb71
      &lt;/a&gt; / &lt;a href="https://github.com/Anandb71/portion-cut" rel="noopener noreferrer"&gt;
        portion-cut
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Paste your exam portion, drop the textbook PDF, read only the pages that matter. EmbeddingGemma runs in the browser.
    &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;Portion Cut&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Paste your exam portion, drop the textbook PDF, and Portion Cut tells you which pages to read. It also builds a smaller PDF with only those pages, and it says plainly when a syllabus line is probably not in the book at all.&lt;/p&gt;
&lt;p&gt;Everything runs in the browser tab. The PDF is never uploaded. There is no backend and no API key.&lt;/p&gt;
&lt;p&gt;Live: &lt;a href="https://anandb71.github.io/portion-cut/" rel="nofollow noopener noreferrer"&gt;https://anandb71.github.io/portion-cut/&lt;/a&gt; (add &lt;code&gt;?demo=1&lt;/code&gt; to try it without downloading the model)&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why an open-weight model, on the device&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The matching is done by &lt;a href="https://huggingface.co/google/embeddinggemma-300m" rel="nofollow noopener noreferrer"&gt;EmbeddingGemma 300M&lt;/a&gt;, Google's open-weight embedding model, using the 4-bit ONNX build from &lt;a href="https://huggingface.co/onnx-community/embeddinggemma-300m-ONNX" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;onnx-community/embeddinggemma-300m-ONNX&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://huggingface.co/docs/transformers.js" rel="nofollow noopener noreferrer"&gt;Transformers.js&lt;/a&gt; on WebAssembly.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The textbook stays with you.&lt;/strong&gt; Course PDFs are often shared files you should not be pushing to someone else's server. Here the text is pulled out with pdf.js in the tab, embedded in a Web Worker, and stored only in this browser's IndexedDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No per-page&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/Anandb71/portion-cut" 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;The core is &lt;a href="https://huggingface.co/google/embeddinggemma-300m" rel="noopener noreferrer"&gt;EmbeddingGemma 300M&lt;/a&gt;, Google's open-weight embedding model, using the 4-bit ONNX build from &lt;a href="https://huggingface.co/onnx-community/embeddinggemma-300m-ONNX" rel="noopener noreferrer"&gt;&lt;code&gt;onnx-community/embeddinggemma-300m-ONNX&lt;/code&gt;&lt;/a&gt; and run with &lt;a href="https://huggingface.co/docs/transformers.js" rel="noopener noreferrer"&gt;Transformers.js&lt;/a&gt; on WebAssembly inside a Web Worker.&lt;/p&gt;

&lt;p&gt;The pipeline is short:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read.&lt;/strong&gt; pdf.js pulls the text out of every page. Blank pages, the table of contents and the index are skipped with simple rules. PDF bookmarks become section page ranges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed.&lt;/strong&gt; Each page goes through EmbeddingGemma with its document prompt, truncated to 512 tokens and, using the model's Matryoshka training, cut down to 256 dimensions so the per-book cache stays small. This is the slow part. The vectors are saved in IndexedDB, so the second portion for the same book is instant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match.&lt;/strong&gt; Each syllabus line is embedded with the query prompt. Comma lists are split into facets, so &lt;code&gt;List: slicing, comprehensions&lt;/code&gt; also searches for &lt;code&gt;List slicing&lt;/code&gt; and &lt;code&gt;List comprehensions&lt;/code&gt; separately. Otherwise one strong facet would hide the others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide.&lt;/strong&gt; This part is plain code, not the model. A line is reported as "probably not in this book" when its best page does not stand out from the rest of the book, meaning the best score is less than 3.35 standard deviations above the average page. Otherwise it keeps the pages close to its best score, plus the best pages for each facet, and snaps them to whole bookmarked sections, because nobody reads page 137 on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cut.&lt;/strong&gt; pdf-lib assembles the smaller PDF in the tab.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Vite, TypeScript and Vitest. The decision logic in &lt;code&gt;src/match.ts&lt;/code&gt; has unit tests, and GitHub Actions runs the tests and deploys to Pages on every push.&lt;/p&gt;

&lt;h3&gt;
  
  
  Did it actually work?
&lt;/h3&gt;

&lt;p&gt;I wanted a number I could defend, so I measured it on one free textbook, OpenStax's &lt;em&gt;Introduction to Python Programming&lt;/em&gt; (397 pages), against two portions copied word for word from a public university Python syllabus. I labelled which sections each line needed by hand, from the book's contents, before running the model. I tuned the settings on the first portion only and kept the second for a held-out check.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Modules 3â€“5 (tuning)&lt;/th&gt;
&lt;th&gt;Modules 6â€“7 (held out)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pages to read&lt;/td&gt;
&lt;td&gt;99 of 397&lt;/td&gt;
&lt;td&gt;75 of 397&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needed sections found&lt;/td&gt;
&lt;td&gt;18 of 21&lt;/td&gt;
&lt;td&gt;12 of 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kept pages that are on topic&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;89%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing lines flagged&lt;/td&gt;
&lt;td&gt;2 of 2&lt;/td&gt;
&lt;td&gt;1 of 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines wrongly flagged as missing&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For comparison, a keyword (BM25) baseline with the same page rules, tuned the same way, found 8 of the 16 needed sections on the held-out portion and did not flag any of the missing lines. Keyword search falls apart exactly where the syllabus and the book use different words, which is most of the time.&lt;/p&gt;

&lt;p&gt;To be honest about the limits: this is one book and two portions. The "not in this book" threshold was chosen after looking at scores from both, so those three catches are a fit, not a test. It also misses some sections; 12 of 16 on the held-out portion means you should still glance at the contents page.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The book stays on your machine.&lt;/strong&gt; Course PDFs are often files a classmate shared, or a scan the department put on a drive. Uploading them to someone else's API is not great. Here the text is extracted, embedded and cached without leaving the tab. The network panel shows nothing going out after the model download.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing to run, including at 1 AM.&lt;/strong&gt; A 400-page book is 400 embedding calls, and every new textbook is another 400. With the weights in the browser that costs me nothing to host and the student nothing to use. A static GitHub Pages site is the whole deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works offline after the first visit.&lt;/strong&gt; The model is cached by the browser and a small service worker keeps the app shell, so it still works on hostel Wi-Fi or with no connection at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open weights let me fix things myself.&lt;/strong&gt; The default 4-bit model file used an operator the WebAssembly runtime does not support, so it would not load. Because the weights are open, I switched to the &lt;code&gt;model_no_gather_q4&lt;/code&gt; variant in the same repository and carried on. With a closed endpoint I would have been waiting on someone else. The same goes for Matryoshka truncation: I could choose 256 dimensions because I control how the vectors are stored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is slow, and that is fine.&lt;/strong&gt; The first read of the full book took about 15 minutes on CPU with four WebAssembly threads. A hosted API would be faster. But it is a one-time cost per book, it runs while you make tea, and every portion after that is instant. Slow and steady, as promised.&lt;/p&gt;

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

&lt;p&gt;On the sample portion, the reading list covered 99 of 397 pages, so about three quarters of the book could be skipped, and two syllabus lines were flagged before I wasted time hunting for them. On the held-out portion it was 75 pages. I have only tried it on this sample book and syllabus so far.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: EmbeddingGemma 300M does all of the matching, on the device, in the browser.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>KadaCard: WhatsApp listings from a product photo, scored offline with open-weight CLIP</title>
      <dc:creator>Anand B</dc:creator>
      <pubDate>Tue, 13 Oct 2026 04:16:42 +0000</pubDate>
      <link>https://dev.to/anandb71/kadacard-whatsapp-listings-from-a-product-photo-scored-offline-with-open-weight-clip-4ee5</link>
      <guid>https://dev.to/anandb71/kadacard-whatsapp-listings-from-a-product-photo-scored-offline-with-open-weight-clip-4ee5</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;&lt;strong&gt;KadaCard&lt;/strong&gt; (കട means shop) is a small browser tool for Kerala micro-sellers. You photograph what you sell — banana chips, pickle, spices, handloom, bakery — and an open-weight CLIP model running on your phone picks a niche. The app then fills a bilingual English and Malayalam WhatsApp listing template. You type the price and WhatsApp number, tap copy, and paste into WhatsApp Business.&lt;/p&gt;

&lt;p&gt;It is aimed at the people I actually see selling from home in Kochi: a neighbour with a jar of mango pickle, someone packing banana chips for Onam orders, a handloom stall that still writes every Instagram caption by hand. They do not need another SaaS account. They need something that works on a phone, keeps product photos private, and costs nothing to run.&lt;/p&gt;

&lt;p&gt;There is no backend. Photos are scored in the tab and discarded. Only the listing text you choose to save stays in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I have not put this in front of a paying user yet. That is next, once the write-up is out.&lt;/p&gt;

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

&lt;p&gt;Live: &lt;strong&gt;&lt;a href="https://anandb71.github.io/kadacard/" rel="noopener noreferrer"&gt;https://anandb71.github.io/kadacard/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;?demo=1&lt;/code&gt; to try the flow without downloading the model. Demo mode is labeled on the page and uses a sample match.&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%2F0igw64ctm68662y8kjo4.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%2F0igw64ctm68662y8kjo4.png" alt="Home screen" width="800" height="3495"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnandb71%2Fkadacard%2Fmain%2Fscreenshots%2F02-demo-listing.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%2Fraw.githubusercontent.com%2FAnandb71%2Fkadacard%2Fmain%2Fscreenshots%2F02-demo-listing.png" alt="Demo listing filled from a banana chips photo" width="800" height="4394"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnandb71%2Fkadacard%2Fmain%2Fscreenshots%2F03-history.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%2Fraw.githubusercontent.com%2FAnandb71%2Fkadacard%2Fmain%2Fscreenshots%2F03-history.png" alt="Saved listing history" width="800" height="5036"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Real model runs on the same fixtures. A mango pickle jar is classified as pickles at 99%. A banana chips pack is classified as banana chips at 100%. A handloom textile is classified as handloom at 100%.&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%2Fraw.githubusercontent.com%2FAnandb71%2Fkadacard%2Fmain%2Fscreenshots%2F04-real-clip-pickle.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%2Fraw.githubusercontent.com%2FAnandb71%2Fkadacard%2Fmain%2Fscreenshots%2F04-real-clip-pickle.png" alt="Real CLIP: pickle jar → Pickles &amp;amp; preserves 99%" width="800" height="4918"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnandb71%2Fkadacard%2Fmain%2Fscreenshots%2F05-real-clip-chips.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%2Fraw.githubusercontent.com%2FAnandb71%2Fkadacard%2Fmain%2Fscreenshots%2F05-real-clip-chips.png" alt="Real CLIP: banana chips → Banana chips &amp;amp; snacks 100%" width="800" height="4964"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAnandb71%2Fkadacard%2Fmain%2Fscreenshots%2F06-real-clip-handloom.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%2Fraw.githubusercontent.com%2FAnandb71%2Fkadacard%2Fmain%2Fscreenshots%2F06-real-clip-handloom.png" alt="Real CLIP: handloom → Handloom &amp;amp; textiles 100%" width="800" height="4964"&gt;&lt;/a&gt;&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/Anandb71" rel="noopener noreferrer"&gt;
        Anandb71
      &lt;/a&gt; / &lt;a href="https://github.com/Anandb71/kadacard" rel="noopener noreferrer"&gt;
        kadacard
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      KadaCard — offline WhatsApp product listings for Kerala sellers, powered by on-device open-weight CLIP
    &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;KadaCard&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;KadaCard (കട, a shop) helps a Kerala micro-seller turn a product photo into a WhatsApp listing. Homemade snacks, pickles, spices, coconut products, handloom, bakery, produce, crafts, and oils each have a short bilingual template. The seller types the price and phone number, then copies the text. There is no backend.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why open-weight and on-device&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The category check is &lt;a href="https://huggingface.co/Xenova/clip-vit-base-patch32" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;Xenova/clip-vit-base-patch32&lt;/code&gt;&lt;/a&gt;, an ONNX build of CLIP, loaded with &lt;a href="https://huggingface.co/docs/transformers.js" rel="nofollow noopener noreferrer"&gt;Transformers.js&lt;/a&gt;. The weights are open, so the check does not depend on a proprietary vision API, an account, or a server that can see the product photo.&lt;/p&gt;
&lt;p&gt;The model is downloaded once and cached in the browser. A small service worker then keeps the app shell available, so later visits can open offline. The first visit still needs a connection to fetch the model.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Privacy&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Photos are read locally, resized in memory, scored on the device, and discarded. They are not…&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/Anandb71/kadacard" 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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; &lt;a href="https://huggingface.co/Xenova/clip-vit-base-patch32" rel="noopener noreferrer"&gt;&lt;code&gt;Xenova/clip-vit-base-patch32&lt;/code&gt;&lt;/a&gt;, an ONNX export of open-weight CLIP, run with &lt;a href="https://github.com/huggingface/transformers.js" rel="noopener noreferrer"&gt;transformers.js&lt;/a&gt; (&lt;code&gt;@huggingface/transformers&lt;/code&gt;) on WebAssembly in the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classification:&lt;/strong&gt; zero-shot image classification against nine seller niches (banana chips, pickles, spices, coconut, handloom, bakery, produce, crafts, beauty). The manual “other” niche is left for the seller to pick; it is not a CLIP label, so a broad “grocery pack” prompt cannot steal snack matches. A match is accepted when the top niche scores at least 0.22, or when the seller’s chosen category is the top label at 0.18 or above. The UI always shows the top three so you can override.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Listing copy:&lt;/strong&gt; templates, not generated text. Each niche has an English title pattern, a Malayalam blurb, a short detail line, and hashtags. CLIP only picks the niche. Price and phone stay blank until the seller fills them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline:&lt;/strong&gt; the model downloads once and is cached. A small service worker caches the app shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; Vite, TypeScript, Vitest. 41 unit tests cover the catalog, classify decision, listing builder, storage, and export. &lt;code&gt;npm run build&lt;/code&gt; is clean. GitHub Actions deploys to Pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof:&lt;/strong&gt; I ran the real model both in Node (transformers.js on the fixtures) and in headless Chrome against the built site. First browser check took about 34 seconds including the download; later checks were a couple of seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why open innovation matters here
&lt;/h2&gt;

&lt;p&gt;The photos that matter to a small seller are inventory, kitchen counters, and half-finished batches. Those should not leave the phone.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; A closed vision API would upload every product shot. Here the image is resized in memory, scored on the device, and thrown away. History stores listing text only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; WhatsApp sellers already live on thin margins. An open-weight model on static hosting means there is no per-image bill and no server for me to keep alive. That is what makes a free or ₹99 tool realistic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline and bad signal.&lt;/strong&gt; Many home kitchens and weekly markets have weak data. Cached weights are the only reason the checker can keep working after the first visit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap and tune.&lt;/strong&gt; The niche prompts and templates are plain code. If someone wants a fish-fry card or a card in Tamil, they can fork it and change the list without waiting on an API vendor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I build Arbor (getarbor.dev), a PR-native security tool that stays on your side of the network. The same instinct applies here: keep the sensitive bit local, keep the open bits readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Show it to one Kochi seller and watch them list five products. I will only claim a first user when that actually happens.&lt;/li&gt;
&lt;li&gt;Let sellers save their own WhatsApp number and default “Made in …” town once.&lt;/li&gt;
&lt;li&gt;Add a couple more niches (seafood fry, flower garlands) if people ask.&lt;/li&gt;
&lt;li&gt;Optional: a tiny fine-tune or prompt pack for Kerala snack packaging, still on-device.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;None. This is an entry for the overall prize.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Grass Bingo: a daily nature card your phone checks offline, with an open-weight model</title>
      <dc:creator>Anand B</dc:creator>
      <pubDate>Sat, 10 Oct 2026 06:44:32 +0000</pubDate>
      <link>https://dev.to/anandb71/grass-bingo-a-daily-nature-card-your-phone-checks-offline-with-an-open-weight-model-1l9g</link>
      <guid>https://dev.to/anandb71/grass-bingo-a-daily-nature-card-your-phone-checks-offline-with-an-open-weight-model-1l9g</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;&lt;strong&gt;Grass Bingo&lt;/strong&gt; is a daily 4x4 bingo card of small things to find outside: a pinecone, a red leaf, moss, a snail, a cloud shaped like something. You tap a square, take a photo, and an open-weight CLIP model running in your browser checks whether the photo matches. Get four in a row and you have a bingo.&lt;/p&gt;

&lt;p&gt;It is for anyone who wants a small reason to go for a walk. Kids, parents, someone on a lunch break, me after a long day of reviewing pull requests. Everyone gets the same card on the same day (it is seeded by the date), so you can compare with a friend without signing up for anything.&lt;/p&gt;

&lt;p&gt;I wanted the screen to be the shortest part. You open the card, look at it once, put the phone away, and take it out again only when you find something. There is no feed or account, and nothing to scroll. A small timer counts how long you've been out, and at the end you can share a text-only summary.&lt;/p&gt;

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

&lt;p&gt;Live: &lt;strong&gt;&lt;a href="https://anandb71.github.io/grass-bingo/" rel="noopener noreferrer"&gt;https://anandb71.github.io/grass-bingo/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;?demo=1&lt;/code&gt; to try it at your desk. Demo mode accepts any photo and skips the model download.&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%2Fewc9s1rv5myj0k8ig1nb.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%2Fewc9s1rv5myj0k8ig1nb.png" alt="Today's card" width="780" height="1910"&gt;&lt;/a&gt;&lt;br&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%2F9md4a4iy9bshmfevjzqu.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%2F9md4a4iy9bshmfevjzqu.png" alt="A diagonal bingo" width="780" height="2268"&gt;&lt;/a&gt;&lt;br&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%2Frp8ltb4qt24szzeiyd24.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%2Frp8ltb4qt24szzeiyd24.png" alt="Text-only share summary" width="780" height="2268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Real model runs, same pinecone photo. Uploading it to "Snail" gets rejected. Uploading it to "Pinecone" is accepted:&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%2F2j37gaom0bpfd5jivyb7.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%2F2j37gaom0bpfd5jivyb7.png" alt="CLIP rejects a pinecone as a snail" width="780" height="1930"&gt;&lt;/a&gt;&lt;br&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%2Fym8iibg9nocbcyazst2o.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%2Fym8iibg9nocbcyazst2o.png" alt="CLIP accepts the pinecone" width="780" height="1884"&gt;&lt;/a&gt;&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/Anandb71" rel="noopener noreferrer"&gt;
        Anandb71
      &lt;/a&gt; / &lt;a href="https://github.com/Anandb71/grass-bingo" rel="noopener noreferrer"&gt;
        grass-bingo
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Daily nature bingo for getting outside. Photos are checked on your phone by an open-weight CLIP model (transformers.js). Nothing is uploaded.
    &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;Grass Bingo&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A daily 4×4 nature bingo card that gets you outside. Tap a square, photograph what you found, and an open-weight CLIP model running in the browser decides whether the photo matches. There is no backend.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why open-weight and on-device&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The checker is &lt;a href="https://huggingface.co/Xenova/clip-vit-base-patch32" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;Xenova/clip-vit-base-patch32&lt;/code&gt;&lt;/a&gt;, an ONNX build of CLIP, loaded with &lt;a href="https://huggingface.co/docs/transformers.js" rel="nofollow noopener noreferrer"&gt;Transformers.js&lt;/a&gt;. The weights are open, so the check does not depend on a proprietary vision API, an account, or a server that can see your pictures.&lt;/p&gt;
&lt;p&gt;The model is downloaded once and cached in the browser. A small service worker then keeps the app shell available, so later visits work offline.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Privacy&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Photos are read locally, resized in memory, scored on the device, and discarded. They are not uploaded, not stored in &lt;code&gt;localStorage&lt;/code&gt;, and not included when you share. The only saved data is which squares you found, when today's card started, and the season…&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/Anandb71/grass-bingo" 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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; &lt;a href="https://huggingface.co/Xenova/clip-vit-base-patch32" rel="noopener noreferrer"&gt;&lt;code&gt;Xenova/clip-vit-base-patch32&lt;/code&gt;&lt;/a&gt;, an ONNX export of OpenAI's open-weight CLIP, run with &lt;a href="https://github.com/huggingface/transformers.js" rel="noopener noreferrer"&gt;transformers.js&lt;/a&gt; (&lt;code&gt;@huggingface/transformers&lt;/code&gt;) on WebAssembly, right in the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How it checks a photo:&lt;/strong&gt; zero-shot image classification. Instead of just asking "is this a pinecone?", I give CLIP the target label plus a few distractor labels taken from other squares on the card. The photo counts if the target is the top label, or if its score is at least 0.35. That logic lives in a tiny pure function sitting behind a classifier interface, so I could unit test it with fake scores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline:&lt;/strong&gt; the model downloads once and the browser caches it. A small service worker caches the app shell, so after the first visit it keeps working where you have no signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rest:&lt;/strong&gt; Vite and plain TypeScript, a seeded card generator (45 outdoor items, filtered by season), bingo line detection, and progress saved in localStorage. No backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests:&lt;/strong&gt; 32 Vitest tests across card generation, bingo lines, the verify decision, storage, and the share summary. All pass, and &lt;code&gt;npm run build&lt;/code&gt; is clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy:&lt;/strong&gt; GitHub Actions builds the app and publishes it to GitHub Pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built it with Cursor's agent and then checked it myself in headless Chromium. On my box, the first real check (which includes downloading and loading the model) took about 25 to 30 seconds. After that, a check took about 2 seconds.&lt;/p&gt;

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

&lt;p&gt;Most of my working life is about not sending things where they don't need to go. With Arbor, the security tool I'm building, the whole point is that the analysis is deterministic and stays on your side. I wanted the same feeling here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your photos stay on your phone.&lt;/strong&gt; Pictures of your backyard, your street, and your kid's hand holding a leaf never reach a server. Everything happens in the browser tab, and then the image is thrown away. With a closed vision API, every square on the card would mean an upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works without signal.&lt;/strong&gt; Parks, trails, and the back of a garden are exactly where reception gets bad. Open weights you can cache are the only reason the offline story works at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; No API key, no per-image bill, and no backend for me to keep alive. Static hosting is enough, so the project can stay free and online for as long as GitHub Pages exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can change it.&lt;/strong&gt; The labels, the threshold, and the model are all in plain code. If you want a birding card or a card in your own language, fork it and change the list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This month I've had PRs merged into flake8-bugbear, scalar, pylint, pyscn, iamb, and wordpress-static-site-exporter. Open code is how I learned most of what I know, so it felt right to build something here that anyone can read, run, and change.&lt;/p&gt;

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

&lt;p&gt;None. This is an entry for the overall prize.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I used AI assistance (Cursor's agent) to build the code and help draft this post. I tested, reviewed, and edited all of it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Still Good — four steps from what is left in the fridge</title>
      <dc:creator>Anand B</dc:creator>
      <pubDate>Sat, 03 Oct 2026 20:13:43 +0000</pubDate>
      <link>https://dev.to/anandb71/still-good-four-steps-from-what-is-left-in-the-fridge-21ib</link>
      <guid>https://dev.to/anandb71/still-good-four-steps-from-what-is-left-in-the-fridge-21ib</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I'm Anand Biju. I built Still Good for my friend Sathguru.&lt;/p&gt;

&lt;p&gt;He lives on his own. He buys groceries with good intentions, then a weeknight shows up, he is short on time, and he ends up staring into the fridge and ordering takeout while the spinach quietly goes bad. He is vegetarian. I wanted one page for that moment: tap what is actually there, star up to three things that should be eaten soon, and get a yellow sticky note. A title, one sentence of why, and four short steps.&lt;/p&gt;

&lt;p&gt;The profile stays in this browser. No account. Diet can be vegan, vegetarian, eggetarian, or eats everything. Allergies are hard stops. A few "won't eat" foods, 15 or 30 minutes, and a skill setting (boil water, follow steps, or improvise) sit beside that. A pantry staple counts only if you tap it. "What's still good?" writes the note. "Another" walks to the next match. Python chooses the dish. A small model on the same machine only writes the words.&lt;/p&gt;

&lt;p&gt;When I showed it to him, he said:&lt;/p&gt;

&lt;p&gt;"lol nice this is great, i think I might just use it everyday thank you so much"&lt;/p&gt;

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

&lt;p&gt;There is no hosted demo. It listens on &lt;code&gt;127.0.0.1:8765&lt;/code&gt; and talks to Ollama on that same machine. The demo profile is set up for Sathguru: vegetarian, 15 minutes, follow steps. It includes a nut allergy so the hard-stop filter is visible. The header says "Four steps for Sathguru will actually cook." His name is in Who is cooking.&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%2F36pb9xdnlnvw23msqelx.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%2F36pb9xdnlnvw23msqelx.png" alt="Friend profile" width="800" height="891"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Empty fridge. The note is still waiting.&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%2Fucum5srhr6hn2edzw6j0.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%2Fucum5srhr6hn2edzw6j0.png" alt="Fridge" width="800" height="891"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rice, oil, garlic, yogurt, spinach, tomato, and lemon are tapped. Spinach and yogurt are starred to use soon.&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%2Fol1yhq12xeluuhwvqsp5.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%2Fol1yhq12xeluuhwvqsp5.png" alt="Sticky note" width="800" height="1164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gemma wrote this note. The badge says From Gemma. The title is "Quick Spinach &amp;amp; Rice," and the why line is "A simple, nourishing meal." The steps rinse the spinach thoroughly and roughly chop it, heat the oil and sauté the minced garlic until fragrant, add the spinach with a pinch of salt and let it wilt slightly, then pour in the yogurt and stir. Spinach and yogurt, the two starred foods, are both used in the steps. The card shows the server-measured time, &lt;code&gt;gemma3:1b · 10.6s&lt;/code&gt;.&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/Anandb71" rel="noopener noreferrer"&gt;
        Anandb71
      &lt;/a&gt; / &lt;a href="https://github.com/Anandb71/still-good" rel="noopener noreferrer"&gt;
        still-good
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Still Good: one dinner from what's left in the fridge, written by Gemma 3 1B running locally via Ollama. Hacktoberfest 2026 DEV weekend challenge.
    &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;Still Good&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Still Good turns what is left in Sathguru's fridge into one dinner he will actually cook tonight: a four-step sticky note that obeys his diet, allergies, time, and skill.&lt;/p&gt;
&lt;p&gt;It is for Sathguru, who shops with good intentions and then stares at the crisper around 8pm. One profile, stored in the browser. No account.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Screenshots&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Friend card: vegetarian, nut allergy, 15 minutes, skill "follow steps."&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Anandb71/still-good/demo/01-friend-profile.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%2FAnandb71%2Fstill-good%2FHEAD%2Fdemo%2F01-friend-profile.png" alt="Friend profile"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Fridge: chips selected, spinach and yogurt starred "use soon."&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Anandb71/still-good/demo/02-fridge.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%2FAnandb71%2Fstill-good%2FHEAD%2Fdemo%2F02-fridge.png" alt="Fridge"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The note after a real Gemma reply on this machine. The card says whether the words came from Gemma or from the fallback, and it shows the model name plus the generation time the server measured.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Anandb71/still-good/demo/03-sticky-note.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%2FAnandb71%2Fstill-good%2FHEAD%2Fdemo%2F03-sticky-note.png" alt="Sticky note"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Install &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;, then pull the small model:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;ollama pull gemma3:1b&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;From this directory, use the existing virtualenv and install the app plus the test tools:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;./.venv/bin/pip install -r requirements.txt -r requirements-dev.txt&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Playwright is listed in &lt;code&gt;requirements-dev.txt&lt;/code&gt;…&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/Anandb71/still-good" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:1b
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
uvicorn app.main:app &lt;span class="nt"&gt;--host&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;--port&lt;/span&gt; 8765
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests never call Ollama. &lt;code&gt;python -m pytest tests -q&lt;/code&gt; runs 29 tests on the filter, the ranker, and the note parser.&lt;/p&gt;

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

&lt;p&gt;Code picks the dish. Gemma only writes the note.&lt;/p&gt;

&lt;p&gt;Gemma 3 1B (&lt;code&gt;gemma3:1b&lt;/code&gt;) runs through Ollama on a CPU-only box. I have 38 handwritten skeletons, each with a fallback note. Python filters by diet, allergies, won't-eat foods, time, skill, and ingredients on hand. Stars change the ranking.&lt;/p&gt;

&lt;p&gt;I send Gemma the chosen skeleton and an allow-list, and ask for a title, a why, and four steps. The call streams at temperature 0.2 with &lt;code&gt;num_predict&lt;/code&gt; 90, and I stop it early. A step that names a disallowed food or an allergen gets dropped. Starred use-soon foods have to appear in the steps. If they do not, I retry once, then show the handwritten note: "Gemma skipped the yogurt; here's the sure version." A malformed reply says "Gemma mumbled; here's the sure version." No answer says "Gemma didn't answer; here's the sure version." The card shows the time the server measured.&lt;/p&gt;

&lt;p&gt;On an idle CPU one note took about 6.7 seconds. Another, while the shared machine was heavily loaded, took 213.3 seconds. I wait five minutes before that fallback, so a slow run can still finish. No build step. The profile lives in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Sathguru's diet stays on the laptop. The app runs offline on localhost, the prompt never leaves the machine, and a note has zero per-request cost. &lt;code&gt;OLLAMA_MODEL&lt;/code&gt; swaps the weight when a bigger local model fits. Gemma 3 1B was small enough for the only box I had.&lt;/p&gt;

&lt;p&gt;The trade-off is right there in the timings. A loaded CPU is slow, and a 1B model sometimes writes a flat sentence, which is why the card shows the real server time and the handwritten note is ready when the wording fails.&lt;/p&gt;

&lt;p&gt;I built the app with the Cursor agent, using Grok 4.7 High as a coding assistant. The app itself runs on open pieces: Gemma 3 1B through Ollama for the note, FastAPI for the server.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 3 1B is the only writer of the sticky note, running locally through Ollama. The prompt, the stream cutoff, the allow-list, and the handwritten fallback exist so a 1B model can title the dish and write four short steps, while Python keeps the diet, the allergy, and the fridge honest.&lt;/p&gt;

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