<?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: Souptik</title>
    <description>The latest articles on DEV Community by Souptik (@souptik4572).</description>
    <link>https://dev.to/souptik4572</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%2F4153370%2F4ff3535a-cbbe-490c-8d38-c2a66ef9bb7d.jpg</url>
      <title>DEV Community: Souptik</title>
      <link>https://dev.to/souptik4572</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/souptik4572"/>
    <language>en</language>
    <item>
      <title>SlothSpend: a lazy, local-first subscription audit desktop app with Gemma and TabPFN</title>
      <dc:creator>Souptik</dc:creator>
      <pubDate>Thu, 22 Oct 2026 06:41:17 +0000</pubDate>
      <link>https://dev.to/souptik4572/slothspend-a-lazy-local-first-subscription-audit-with-gemma-and-tabpfn-45mm</link>
      <guid>https://dev.to/souptik4572/slothspend-a-lazy-local-first-subscription-audit-with-gemma-and-tabpfn-45mm</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;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%2Ffcktguq93eg5pg4569iw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffcktguq93eg5pg4569iw.jpg" alt="SlothSpend: a cancelled gym membership that charged again, with a refund request already drafted" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;SlothSpend&lt;/strong&gt; is a desktop app that checks your bank statement for subscriptions you forgot about. It runs entirely on your own computer.&lt;/p&gt;

&lt;p&gt;You give it the CSV your bank already lets you download. A few seconds later it tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which charges repeat (and which ones only look like they do),&lt;/li&gt;
&lt;li&gt;which prices went up without you noticing,&lt;/li&gt;
&lt;li&gt;which merchant charged you twice,&lt;/li&gt;
&lt;li&gt;which services overlap (two streaming apps doing the same job),&lt;/li&gt;
&lt;li&gt;how much the next 30 days will probably cost, as a range rather than one made-up number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it does the part I always skip: it &lt;strong&gt;follows up&lt;/strong&gt;. It reminds you a few days before a renewal. If you say "I cancelled it", it waits for your next statement and checks that the charge actually stopped. If the merchant charged you anyway, it tells you so and already has a refund email written, with the exact dates and amounts.&lt;/p&gt;

&lt;p&gt;So who gets to be lazy? Anyone who has ever said "I should really go through my statement" and then didn't. That includes me. Reading 400 rows of &lt;code&gt;ACH DEBIT IRONTRAP FITNESS MEMB 7731&lt;/code&gt; is boring enough that most of us never do it, and that's how a $39.99 gym membership turns into $49.99 without anyone noticing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The sloth rules
&lt;/h3&gt;

&lt;p&gt;I gave the app a personality to fit the theme, and some hard limits that come with it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It informs, it never acts.&lt;/strong&gt; SlothSpend never cancels anything, never logs into anything and never emails a merchant. It writes the draft and you send it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never makes up a link or a phone number.&lt;/strong&gt; Every answer and draft goes through a guard that removes them. A model inventing a "cancel here" URL is a phishing risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never claims to have done something it didn't.&lt;/strong&gt; If the model says "I've set a reminder" and no tool call confirmed it, the reply gets replaced with what actually happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every number says where it came from.&lt;/strong&gt; Each figure is labelled &lt;em&gt;Gemma 4 · local&lt;/em&gt;, &lt;em&gt;TabPFN v2&lt;/em&gt;, or a clearly marked &lt;em&gt;Template&lt;/em&gt; / &lt;em&gt;Heuristic&lt;/em&gt; fallback if a model isn't available.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;🌐 &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://slothspend.netlify.app/" rel="noopener noreferrer"&gt;slothspend.netlify.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The app has a &lt;strong&gt;Demo data → Sample statement&lt;/strong&gt; button that loads 12 months of made-up charges, so you can try everything without using your own bank data. The screenshots below come from that sample. Here's the whole flow, step by step.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Getting ready (one time only)
&lt;/h3&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%2F1odnrdkxi6itb07bx5ye.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1odnrdkxi6itb07bx5ye.jpg" alt="Boot screen showing Engine, Gemma 4 and TabPFN v2 all ready" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On first launch, SlothSpend checks three things: its own local engine, a Gemma model in Ollama, and TabPFN. If you don't have Gemma yet, it offers to download it and shows a progress bar. TabPFN v2's weights (~70 MB) download on their own. After that, nothing leaves your computer.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Import a statement, even an awkward one
&lt;/h3&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%2Fmdyjaktmgfr65m95zf3q.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmdyjaktmgfr65m95zf3q.jpg" alt="Column mapper for an Indian bank CSV with a 13-line preamble, INR detected, day-first dates" width="800" height="915"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every bank exports CSVs differently. This one is from an Indian bank: 13 junk lines before the header, dates written day-first, separate Debit and Credit columns, amounts in ₹. SlothSpend worked out most of that by itself and only asks about the one thing it wasn't sure of, which column holds the money. It remembers the layout for next time.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The report
&lt;/h3&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%2Ffeyz0pjgh9uahvjj6ns2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffeyz0pjgh9uahvjj6ns2.jpg" alt="Audit report: Sloth's summary, monthly recurring, next 30 days with a q10–q90 range, savings, flags, burn chart and findings" width="800" height="1293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pink card at the top is Sloth's summary in plain English. Below it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monthly recurring:&lt;/strong&gt; what you actually pay every month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next 30 days:&lt;/strong&gt; TabPFN's forecast with a likely range (q10–q90), not one fake-precise number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Potential savings:&lt;/strong&gt; how much you'd save by cancelling the "zombie" subscriptions yourself, plus any double charge worth disputing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flags:&lt;/strong&gt; duplicates, price rises, overlaps and zombie suspects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The chart shows 12 months of subscription spend. The pink July bar is the month the gym raised its price &lt;em&gt;and&lt;/em&gt; charged twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. "Why was this flagged?"
&lt;/h3&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%2Fdjdkxmprudjtcmb952us.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjdkxmprudjtcmb952us.jpg" alt="Merchant drawer for Irontrap Fitness showing charges, reasons and a 'Looks like' comparison" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click any merchant to see its full history and the reasons in plain words: &lt;em&gt;"Billed every ~30 days, ±1 day, near the 5th of the month. The same $39.99 eight times in a row, then $49.99 for the last four."&lt;/em&gt; If the model got it wrong, press &lt;strong&gt;Not a subscription&lt;/strong&gt;. TabPFN takes your correction into account and re-scores everything in about 2 seconds, with no retraining.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Ask Sloth, a chat agent that shows its work
&lt;/h3&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%2Fgvp1jevyo01e8n1vgatk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgvp1jevyo01e8n1vgatk.jpg" alt="Ask Sloth answering 'What will subscriptions cost next month if I cancel StreamFlix and WatchNest?' with visible tool calls" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ask Sloth is a local agent: Gemma 4 with 8 typed tools. You can ask things like &lt;em&gt;"What will subscriptions cost next month if I cancel StreamFlix and WatchNest?"&lt;/em&gt; and watch it call &lt;code&gt;get_merchant&lt;/code&gt; twice and then &lt;code&gt;forecast_spend&lt;/code&gt;. That last one runs a &lt;strong&gt;live TabPFN forecast&lt;/strong&gt; with those two merchants removed. Every tool call shows up in the chat, so you can see where each number came from.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. "Cancel it for me" → "No, but here's everything you need"
&lt;/h3&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%2Ffgwss1rhafq75zomgqjc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffgwss1rhafq75zomgqjc.jpg" alt="Ask Sloth declining to cancel, drafting a cancellation email and offering a reminder" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was my favourite thing to test. If you ask &lt;em&gt;"Cancel IronTrap for me"&lt;/em&gt;, Sloth explains that it can't, writes a cancellation email using your real renewal date and amount, and offers to remind you 3 days before the next charge. The email contains no links and no phone numbers, and it ends with &lt;em&gt;"Check the dates and amounts, then send it yourself."&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Reminders that actually follow through
&lt;/h3&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%2F33w2gp1bf9n8wfogx0rf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F33w2gp1bf9n8wfogx0rf.jpg" alt="Reminders tab: Watchnest reminded, Irontrap marked as cancelled and waiting for the next statement" width="800" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each reminder goes through four steps: &lt;strong&gt;Watching → Reminded → Kept / You cancelled → Verified&lt;/strong&gt;. You can keep it, snooze it (up to 3 times), or say you cancelled it. You also get desktop notifications. If the app was closed when a reminder was due, it shows up the next time you open it.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Verified… or still charged
&lt;/h3&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%2Fy7m5hiebdcrnw3rf07vm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy7m5hiebdcrnw3rf07vm.jpg" alt="Irontrap Fitness verified as stopped: 'You're saving $49.99 a month'" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you import the next statement, SlothSpend checks whether a charge appeared after your cancellation date. If none did, the reminder becomes &lt;strong&gt;Verified&lt;/strong&gt; and shows what you're saving.&lt;/p&gt;

&lt;p&gt;If the merchant charged you anyway, you get the screen at the very top of this post: &lt;strong&gt;Still charged&lt;/strong&gt;, plus a refund request that lists both the charge after you cancelled &lt;em&gt;and&lt;/em&gt; the old July double charge.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Settings and privacy
&lt;/h3&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%2Fxuw4xoitmt15bt6zjc2a.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxuw4xoitmt15bt6zjc2a.jpg" alt="Settings: theme, chat model selection, TabPFN v2 with optional Prior Labs key" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here you can pick which Gemma model to chat with, set your default currency and reminder timing, and switch on a demo mode that shrinks every wait to a few seconds. There's also a privacy table listing every possible network call, and a &lt;strong&gt;Delete all local data&lt;/strong&gt; button.&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/souptik4572" rel="noopener noreferrer"&gt;
        souptik4572
      &lt;/a&gt; / &lt;a href="https://github.com/souptik4572/sloth-spend" rel="noopener noreferrer"&gt;
        sloth-spend
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      SlothSpend audits a 12-month bank-statement CSV entirely on the user's machine, finds recurring charges, price creep and duplicate charges, forecasts the next 30 days of subscription spend, lets the user interrogate the audit through a local chatbot.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/souptik4572/sloth-spend/site/icon-192.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%2Fsouptik4572%2Fsloth-spend%2FHEAD%2Fsite%2Ficon-192.png" width="96" alt="SlothSpend logo"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;SlothSpend&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Slow and steady subscription audits — on your own computer.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Find the subscriptions you forgot, the prices that crept up and the charges that came twice
with open models (Gemma + TabPFN) running locally. SlothSpend &lt;strong&gt;informs, never acts&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/33b6efe11626cda11ca81ef1d1e69ae9831ef88a8eb6157adbbf010d739efd23/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f53253230284170706c6525323053696c69636f6e2925323025374325323057696e646f77732532307836342d306230353164"&gt;&lt;img src="https://camo.githubusercontent.com/33b6efe11626cda11ca81ef1d1e69ae9831ef88a8eb6157adbbf010d739efd23/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f53253230284170706c6525323053696c69636f6e2925323025374325323057696e646f77732532307836342d306230353164" alt="Platforms"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/d270fc004f7500a4f85953a05eacfe384b226778d8d218bd0762293cf46de53c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f456c656374726f6e2d34342d343738343866"&gt;&lt;img src="https://camo.githubusercontent.com/d270fc004f7500a4f85953a05eacfe384b226778d8d218bd0762293cf46de53c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f456c656374726f6e2d34342d343738343866" alt="Electron"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/246ea583b3595bc0028bebeabdc7543596a35b3035c641aa74aa1f46f54cac86/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f656e67696e652d507974686f6e253230332e31312d333737366162"&gt;&lt;img src="https://camo.githubusercontent.com/246ea583b3595bc0028bebeabdc7543596a35b3035c641aa74aa1f46f54cac86/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f656e67696e652d507974686f6e253230332e31312d333737366162" alt="Python"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/241d1b37657bdb307ae5410e497cc3f6cedaeff261708db31e10ed50c1b3ae50/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d6f64656c732d47656d6d6125323025324225323054616250464e25323076322d666661386364"&gt;&lt;img src="https://camo.githubusercontent.com/241d1b37657bdb307ae5410e497cc3f6cedaeff261708db31e10ed50c1b3ae50/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d6f64656c732d47656d6d6125323025324225323054616250464e25323076322d666661386364" alt="Models"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e"&gt;&lt;img src="https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e" alt="License"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://slothspend.netlify.app/" rel="nofollow noopener noreferrer"&gt;Website&lt;/a&gt; · &lt;a href="https://github.com/souptik4572/sloth-spend/releases/latest" rel="noopener noreferrer"&gt;Download&lt;/a&gt; ·
&lt;a href="https://github.com/souptik4572/sloth-spend#how-it-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt; · &lt;a href="https://github.com/souptik4572/sloth-spend#privacy-where-your-data-goes" rel="noopener noreferrer"&gt;Privacy&lt;/a&gt; · &lt;a href="https://github.com/souptik4572/sloth-spend#development" rel="noopener noreferrer"&gt;Develop&lt;/a&gt;&lt;/p&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/souptik4572/sloth-spend/docs/screenshots/16-still-charged.jpg"&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%2Fsouptik4572%2Fsloth-spend%2FHEAD%2Fdocs%2Fscreenshots%2F16-still-charged.jpg" alt="SlothSpend: a cancelled subscription that charged again, with a refund request drafted by Ask Sloth" width="900"&gt;&lt;/a&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Contents&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#why-slothspend" rel="noopener noreferrer"&gt;Why SlothSpend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#features" rel="noopener noreferrer"&gt;Features&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#how-it-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#screenshots" rel="noopener noreferrer"&gt;Screenshots&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#install" rel="noopener noreferrer"&gt;Install&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#first-run" rel="noopener noreferrer"&gt;First run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#using-slothspend" rel="noopener noreferrer"&gt;Using SlothSpend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#privacy-where-your-data-goes" rel="noopener noreferrer"&gt;Privacy: where your data goes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#architecture" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#performance" rel="noopener noreferrer"&gt;Performance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#development" rel="noopener noreferrer"&gt;Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#configuration" rel="noopener noreferrer"&gt;Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#engine-api" rel="noopener noreferrer"&gt;Engine API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#troubleshooting" rel="noopener noreferrer"&gt;Troubleshooting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#roadmap" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#hacktoberfest-notes" rel="noopener noreferrer"&gt;Hacktoberfest notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/souptik4572/sloth-spend#license-and-attribution" rel="noopener noreferrer"&gt;License and attribution&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why SlothSpend&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;A year of bank transactions says where you live, what you eat and who you love. Cloud "subscription
trackers" want that data — and often a login to your bank. SlothSpend takes a CSV export, keeps it in a local
database, and runs every model on your own machine. After a one-time model download it makes &lt;strong&gt;no network
calls at all&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The theme is &lt;em&gt;slow&lt;/em&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/souptik4572/sloth-spend" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;To run it from source you need Node 24, Python 3.11, &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; and &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; 0.20+ with any Gemma model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/souptik4572/sloth-spend.git
&lt;span class="nb"&gt;cd &lt;/span&gt;sloth-spend
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run engine:venv   &lt;span class="c"&gt;# builds engine/.venv from the pinned lockfile&lt;/span&gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then click &lt;strong&gt;Demo data → Sample statement&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;SlothSpend has two halves: an &lt;strong&gt;Electron + React&lt;/strong&gt; desktop app and a &lt;strong&gt;Python FastAPI engine&lt;/strong&gt; that runs next to it as a local sidecar process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Electron (React 19, Tailwind v4, zustand)
      │  HTTP + server-sent events, 127.0.0.1 only, per-launch token
      ▼
Python engine (FastAPI, pandas, SQLite)
  ingest → normalise (Gemma) → detectors → TabPFN score + forecast → summary (Gemma)
  Ask Sloth agent + 8 tools · reminder scheduler
      │  127.0.0.1:11434
      ▼
Ollama → Gemma 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Electron picks a free port and a random token on every launch, then starts the engine. If Electron crashes, the engine notices its stdin closing and shuts itself down, so you never end up with a stray Python process.&lt;/p&gt;

&lt;p&gt;The interesting part is that &lt;strong&gt;two very different open models each do the job they're good at.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma 4 (via Ollama): the language part
&lt;/h3&gt;

&lt;p&gt;Gemma handles everything that involves words:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cleaning up merchant names.&lt;/strong&gt; &lt;code&gt;ACH DEBIT IRONTRAP FITNESS MEMB 7731&lt;/code&gt; becomes &lt;em&gt;IronTrap Fitness&lt;/em&gt; with the category &lt;em&gt;Fitness&lt;/em&gt;. Ollama handles one request at a time, so this runs in priority order under a time budget. Whatever doesn't fit in the budget falls back to rules and gets cleaned up in the background later. If you rename a merchant yourself, your name always wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The summary.&lt;/strong&gt; The &lt;em&gt;code&lt;/em&gt; writes the facts and Gemma only rephrases them into friendly English. The output is then checked: if Gemma drops or changes any number, the code-written version is used instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask Sloth.&lt;/strong&gt; Gemma does tool calling over 8 typed tools (&lt;code&gt;get_merchant&lt;/code&gt;, &lt;code&gt;find_duplicate_charges&lt;/code&gt;, &lt;code&gt;spend_by_category&lt;/code&gt;, &lt;code&gt;forecast_spend&lt;/code&gt;, &lt;code&gt;score_merchant&lt;/code&gt;, &lt;code&gt;run_readonly_sql&lt;/code&gt;, &lt;code&gt;set_renewal_reminder&lt;/code&gt;, &lt;code&gt;get_report_overview&lt;/code&gt;), with up to 4 tool rounds per answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emails.&lt;/strong&gt; Refund, cancellation and negotiation drafts start from a code-written template with the exact facts in it. Gemma makes them read naturally, and then a guard checks the result.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  TabPFN v2: the numbers part
&lt;/h3&gt;

&lt;p&gt;This was the bit that surprised me most. &lt;a href="https://github.com/PriorLabs/TabPFN" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt; is a &lt;em&gt;tabular foundation model&lt;/em&gt;. You show it example rows when you call it (in-context learning) and it predicts straight away, with no training step.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model A (classifier): "Is this a subscription?"&lt;/strong&gt; Every merchant becomes a row of features: median gap between charges, how regular that gap is, how stable the amount is, day-of-month spread, and so on. The context is a set of &lt;em&gt;simulated&lt;/em&gt; billing histories that ship with the app, plus &lt;strong&gt;your own corrections, each repeated 3 times&lt;/strong&gt;. That's why "Not a subscription" changes the score in ~2 seconds: there's no retraining, the new label is just part of the next call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model B (regressor): "What will the next charge be?"&lt;/strong&gt; It predicts the next/last price ratio as 19 quantiles. That's where the q10–q90 range on the forecast card comes from, and it's what Ask Sloth's live what-if forecast runs on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TabPFN refuses more than 1,000 training rows on CPU by default (I found that out the hard way), so the context is capped at 950 rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making it trustworthy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic detectors&lt;/strong&gt; handle duplicates, price creep, overlaps and possible annual plans. I don't want a language model deciding whether $49.99 on Jul 5 and Jul 6 counts as a double charge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reminders&lt;/strong&gt; are rows in SQLite plus a single loop that ticks at startup and every 30 seconds. There's no workflow engine. Every state change is compare-and-set, so a click and a tick can't overwrite each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An egress test.&lt;/strong&gt; &lt;code&gt;engine/tests/test_egress.py&lt;/code&gt; runs the whole app (audit, every chat tool, drafts, a full reminder cycle, &lt;em&gt;Delete all local data&lt;/em&gt;) with &lt;strong&gt;every non-loopback network connection refused&lt;/strong&gt;, and fails if anything tries to reach the internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Numbers
&lt;/h3&gt;

&lt;p&gt;Measured on the packaged macOS build (Apple Silicon, TabPFN v2, &lt;code&gt;gemma4:latest&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;Check&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Launch → window shown&lt;/td&gt;
&lt;td&gt;0.18 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Launch → engine healthy&lt;/td&gt;
&lt;td&gt;7.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full audit of the sample (412 rows, 12 months)&lt;/td&gt;
&lt;td&gt;27.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Re-score after a correction&lt;/td&gt;
&lt;td&gt;2.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine health check p95 &lt;em&gt;during&lt;/em&gt; an audit&lt;/td&gt;
&lt;td&gt;19 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One real bug from the week: launch first took 22.5 s. It turned out that reading the saved Prior Labs key &lt;em&gt;synchronously&lt;/em&gt; from the macOS keychain made the OS verify the whole app bundle (921 MB including the Python engine) before handing the key over, and it froze the window while doing so. Switching to &lt;code&gt;safeStorage.decryptStringAsync&lt;/code&gt; brought the window to 0.18 s and the engine to 7.1 s.&lt;/p&gt;

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

&lt;p&gt;For this app, open models aren't a nice extra. Without them the app doesn't really work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A year of bank transactions shows where you live, what you eat, who you see and what you're struggling with.&lt;/strong&gt; Most "subscription tracker" apps want that data on their servers, and many want your bank login too. I didn't want to build another one of those, and honestly I wouldn't use one.&lt;/p&gt;

&lt;p&gt;Open weights changed what I could build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing leaves the machine.&lt;/strong&gt; Gemma runs through Ollama on &lt;code&gt;127.0.0.1&lt;/code&gt; and TabPFN runs on local PyTorch. After the one-time model download, SlothSpend makes &lt;strong&gt;no network calls&lt;/strong&gt;, and there's a test that proves it. A closed API would mean sending every transaction to someone else's server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; No API key, no per-token bill, no usage limit. An app whose job is saving you money shouldn't charge you every time it reads your statement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swapping models is easy.&lt;/strong&gt; Any Gemma you already have in Ollama works, and Settings lets you pick one. TabPFN goes v2 (open weights, the default) → 3.5 (if you add a free Prior Labs key) → a labelled heuristic (if no model is available). Being able to fall back like this only works because I control the model layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN made the "learns from you" part possible.&lt;/strong&gt; Because it learns in context, a user correction becomes part of the prompt rather than a training job, so the app adapts to you, on your own device, in two seconds. I couldn't have built that loop with a hosted black box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's auditable.&lt;/strong&gt; The engine is open source, the models are open, and the privacy table in Settings lists every byte that could ever leave your computer. You don't have to take my word for any of it.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;On the 12-month sample statement, a single 28-second audit found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$87.46/month&lt;/strong&gt; of potential savings across 4 "zombie" subscriptions, about &lt;strong&gt;$1,050 a year&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;$49.99 double charge&lt;/strong&gt; worth disputing,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;two price rises&lt;/strong&gt;: a gym at +25% and a music app at +9.1%,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;two streaming services&lt;/strong&gt; doing the same job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finding that by hand means reading 412 rows, matching messy merchant names across 12 months, and noticing a $10 price change in the middle of the year. For me that's an evening I'd keep putting off. Then there's the follow-up, which is where most of the money actually gets lost: remembering a renewal date, checking next month's statement, and writing the refund email. SlothSpend turns all of that into &lt;strong&gt;one import, one click on "I cancelled it", and a second import a month later.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The sloth does the slow and steady part. You just stay lazy.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 4 runs locally through Ollama and does every language task in the app: merchant normalisation, the summary (with a number-preservation check), the tool-calling Ask Sloth agent, and refund/cancellation/negotiation drafts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of TabPFN:&lt;/strong&gt; TabPFN v2 is the scoring and forecasting core. The classifier decides what recurs, using simulated priors plus user corrections as in-context examples, and the quantile regressor powers the q10–q90 forecast and the live "what if I cancel X" tool. Built with PriorLabs-TabPFN.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;AI disclosure: I built SlothSpend with help from Claude Code (Claude Opus) as a coding assistant, and this post was drafted and edited by me. The product decisions, the "inform, never act" rules and the testing are mine.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SlothSpend is MIT-licensed. TabPFN v2 weights are under the Prior Labs License 1.1; Gemma is used under Google's Gemma terms.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>TuitionSetu: a tutor's Sunday of WhatsApp updates, written by Gemma on her own laptop</title>
      <dc:creator>Souptik</dc:creator>
      <pubDate>Sat, 17 Oct 2026 06:25:12 +0000</pubDate>
      <link>https://dev.to/souptik4572/tuitionsetu-a-tutors-sunday-of-whatsapp-updates-written-by-gemma-on-her-own-laptop-1gg</link>
      <guid>https://dev.to/souptik4572/tuitionsetu-a-tutors-sunday-of-whatsapp-updates-written-by-gemma-on-her-own-laptop-1gg</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hf26"&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;Mita teaches Maths to 30 children from home. After every batch she jots something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Riya ch5 test 14/20 careless in algebra, missed HW twice; Arjun absent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Parents expect a WhatsApp update every week, in Bengali, Hindi or English. So on Sunday evening she turns a week of those scribbles into 30 polite, specific messages. It takes about two hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TuitionSetu does the Sunday part.&lt;/strong&gt; She types her notes the way she always has, on her phone or laptop. A local model works out who each line is about and every score. On report day it writes each parent's update in the parent's language. Code checks every number against her notes, and then she reads, approves and taps &lt;strong&gt;Send on WhatsApp&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who pays, and why.&lt;/strong&gt; Home tuition is a common side income for teachers in India, and regular updates to parents are part of keeping students. A tutor with 30 students would pay a small monthly fee (I'm thinking ₹99, about a dollar) to get Sunday evening back and to send updates that sound like her and quote the right numbers. That price only works because there is no per-message AI bill: the model runs on the laptop she already owns.&lt;/p&gt;
&lt;h3&gt;
  
  
  A week with TuitionSetu
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;After each class.&lt;/strong&gt; She types the note on her phone, exactly as she'd scribble it. It's saved the moment she taps Save, and the laptop reads it in the background. The note then shows who it was about and what was found: scores, homework, attendance and topics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When something isn't clear.&lt;/strong&gt; TuitionSetu never guesses. Anything it isn't sure of becomes one short question in &lt;strong&gt;Needs you&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Who is 'Riya'?&lt;/em&gt; Maybe Riya Sen (there are two Riyas)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Riya 14 — out of how many?&lt;/em&gt; The total is missing&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Arjun 18/15 — is that right?&lt;/em&gt; The score is more than the total&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Mark fees pending for Kabir?&lt;/em&gt; Your note mentions fees&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When she picks the right student for a name, that spelling is saved as a nickname, so she's asked once. Names it matched on its own are listed under &lt;strong&gt;Matched for you&lt;/strong&gt;, with Undo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On report day.&lt;/strong&gt; She opens the week's review and taps &lt;strong&gt;Write updates&lt;/strong&gt;. Each parent's update streams onto the screen in that parent's language and is checked the moment it's finished. It's either ready, or flagged with the exact words highlighted. She can edit any update (the check runs again on her words), dismiss a warning she knows is fine, or &lt;strong&gt;Skip this week&lt;/strong&gt; for a student. Then &lt;strong&gt;Send on WhatsApp&lt;/strong&gt; opens WhatsApp with the message filled in, and she presses Send herself.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbv0udu9oettu9egr127j.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbv0udu9oettu9egr127j.gif" alt="A typed note about five students becomes checked WhatsApp updates: Bengali streaming, a wrong number flagged, approved and sent" width="720" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the GIF: Mita types one note about five students. Gemma reads it in about 17 seconds and writes Riya's update in Bengali. A score changed by mistake (80% → 90%) is flagged before anyone sees it. She approves, WhatsApp opens with the message ready, and she presses Send herself. Everything runs on one laptop, with no internet needed.&lt;/p&gt;

&lt;p&gt;There's no hosted demo, on purpose: the point is that children's names, marks and parents' phone numbers never leave the tutor's laptop. Trying it takes two commands:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjbjywn00f5wrpj2idz1h.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjbjywn00f5wrpj2idz1h.gif" alt="./setup.sh on a fresh clone: checks Node and Ollama, installs, downloads the model, builds, in 17 seconds when the model is already there" width="800" height="498"&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/souptik4572" rel="noopener noreferrer"&gt;
        souptik4572
      &lt;/a&gt; / &lt;a href="https://github.com/souptik4572/tuition-setu" rel="noopener noreferrer"&gt;
        tuition-setu
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Turn 5 minutes of rough tutor shorthand into 35 personalized parent WhatsApp updates. 100% local AI, zero cloud costs, total student privacy.
    &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;TuitionSetu&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Turn a private tutor's class notes into WhatsApp updates for parents — written by an open-weight model on the tutor's own laptop, and checked by code before anything is sent.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/souptik4572/tuition-setu/docs/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fsouptik4572%2Ftuition-setu%2FHEAD%2Fdocs%2Fdemo.gif" alt="A typed note about five students becomes WhatsApp updates: Riya's in Bengali streams in, a wrong number is flagged, and the tutor approves and sends"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mita teaches Maths to 30 children from home. After each batch she jots shorthand like &lt;code&gt;Riya ch5 test 14/20 careless in algebra, missed HW twice; Arjun absent&lt;/code&gt;. On Sunday evening she spends two hours turning a week of those notes into a message for every parent, in English, Bengali or Hindi.&lt;/p&gt;

&lt;p&gt;TuitionSetu does the Sunday part:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;She types the note as she always does&lt;/strong&gt;, on her phone or laptop, right after class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The laptop reads it&lt;/strong&gt; with a local model: who each line is about, scores, homework, attendance. Anything it isn't sure of (a name it doesn't know, a score without a total) waits in &lt;strong&gt;Needs you&lt;/strong&gt;; it never guesses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On report day it writes each&lt;/strong&gt;…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/souptik4572/tuition-setu" 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;git clone https://github.com/souptik4572/tuition-setu.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;tuition-setu &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./setup.sh
./start.sh            &lt;span class="c"&gt;# or ./start.sh --lan to use it from a phone on the same Wi-Fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You need Node 22+ and &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;. The first run downloads the model (about 6.6 GB). On my laptop, with the model already downloaded, setup took 17 seconds.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The model writes; code checks.&lt;/strong&gt; Only two steps use the model: reading messy shorthand, and writing a warm paragraph in Bengali. Everything a parent could act on is plain code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Type a note.&lt;/strong&gt; It's saved instantly, even if the AI engine is off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract (Gemma).&lt;/strong&gt; The model returns JSON: who, topics, scores, homework, attendance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate (code).&lt;/strong&gt; Every number the model returns must appear in the line it came from, or it's removed. A score with no total, or a name it doesn't know, goes to &lt;strong&gt;Needs you&lt;/strong&gt; instead of being guessed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolve names (code).&lt;/strong&gt; Exact name, saved nickname ("Riyu"), or close spelling ("Payal" → Payel). Anything unsure is asked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facts (code).&lt;/strong&gt; This week's scores, percentages and the trend, worked out from the confirmed notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft (Gemma).&lt;/strong&gt; Only the body, in the parent's language, from those facts. It streams on screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard (code).&lt;/strong&gt; Every number in the draft must be in the facts, including Bengali and Devanagari digits. No other child may be named, and the language must be right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assemble (code).&lt;/strong&gt; The title, greeting, fee line and sign-off come from templates, never from the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tutor approves&lt;/strong&gt;, and a &lt;code&gt;wa.me&lt;/code&gt; link opens WhatsApp with the text. TuitionSetu never sends anything on its own.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Reading the note, and what code won't take on trust
&lt;/h3&gt;

&lt;p&gt;Gemma reads the note through Ollama's structured outputs, so its reply has to fit a JSON schema, and Zod checks it again on arrival. If the reply isn't valid JSON or doesn't fit, the app asks once more and says what was wrong. Thinking is turned off: these jobs don't need it, and it makes a small model slow.&lt;/p&gt;

&lt;p&gt;Then the validator checks the reading against the note itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The part of the note each entry came from must really be in the note. If it isn't, the entry waits for the tutor.&lt;/li&gt;
&lt;li&gt;Every number must appear in the line it came from. One that doesn't is removed, not trusted.&lt;/li&gt;
&lt;li&gt;A score with no total ("Riya 14"), or one bigger than its total, becomes a question, not a fact.&lt;/li&gt;
&lt;li&gt;Two entries about the same child in one note are merged.&lt;/li&gt;
&lt;li&gt;A line about no one becomes a question. If it isn't in the note at all, it's dropped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Names.&lt;/strong&gt; Each name is checked in order: the exact name, then a saved nickname, then a close spelling. "Close" means one letter off for names of five letters or fewer, two for longer ones. Only one clear answer is ever taken. Two students called Riya, or two names that are equally close, go to the tutor with up to three suggestions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Facts.&lt;/strong&gt; Code works out everything an update may say: this week's scores and percentages, homework missed, attendance, topics, strengths and concerns. The trend compares the latest score with the average of up to three earlier ones, and needs at least two. Eight points or more either way is "improving" or "declining"; less is "steady". Code rounds the average before Gemma sees it, so the percentage Gemma quotes is exactly the one the guard allows.&lt;/p&gt;

&lt;h3&gt;
  
  
  The guard
&lt;/h3&gt;

&lt;p&gt;Every update goes through seven checks before the tutor sees it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Numbers&lt;/td&gt;
&lt;td&gt;Any number not in the facts. Scores are checked as pairs (14/20), percentages against the ones code worked out. Bengali (১৪) and Devanagari (१४) digits count, and so do "২০ এর মধ্যে ১৪" and "20 में से 14", which put the total first.&lt;/td&gt;
&lt;td&gt;Flagged, number highlighted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;This child&lt;/td&gt;
&lt;td&gt;The update never names the student&lt;/td&gt;
&lt;td&gt;Flagged in English. A warning in Bengali and Hindi, where the name is written in another script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other children&lt;/td&gt;
&lt;td&gt;Any other student's name, including Bengali forms like তন্ময়ের ("Tanmoy's")&lt;/td&gt;
&lt;td&gt;Flagged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Under 60% of the letters in Bengali or Devanagari script (90% for English)&lt;/td&gt;
&lt;td&gt;Flagged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Length&lt;/td&gt;
&lt;td&gt;Under 30 or over 160 words&lt;/td&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fees&lt;/td&gt;
&lt;td&gt;Any mention of fees or money&lt;/td&gt;
&lt;td&gt;Flagged: the fee line only ever comes from a template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leftovers&lt;/td&gt;
&lt;td&gt;Braces, "JSON", "as an AI" and the like&lt;/td&gt;
&lt;td&gt;Flagged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A flagged update can't be approved. The tutor can edit it, and the guard runs again on her words. An out-of-date update is checked against this week's current facts, so an old number can't slip back in. Or, if the flagged part is right, she can dismiss the warning. The dismissal is written to the audit log, lasts only until the text changes, and can be undone.&lt;/p&gt;

&lt;p&gt;The guard caught Gemma for real while I was recording the demo. Its advice said "practicing 2-3 short word problems together each evening", and "2" and "3" aren't in Mita's notes, so the update was flagged. Here it's harmless advice, so the tutor can dismiss the warning (the dismissal is recorded) or edit the text. What she can't do is approve a number she hasn't seen.&lt;/p&gt;

&lt;p&gt;The guard has a limit, the same one &lt;a href="https://dev.to/nodefiend"&gt;@nodefiend&lt;/a&gt; spells out in &lt;a href="https://dev.to/nodefiend/trust-the-server-not-the-llm-a-deterministic-approach-to-llm-accuracy-20ag"&gt;Trust the Server, Not the LLM: A Deterministic Approach to LLM Accuracy&lt;/a&gt;: it checks that every number exists in the facts, not that it sits next to the right label. If Riya scored 14/20 in algebra and 18/20 in geometry, an update that swapped them would pass. That's one reason a person reads and approves every message.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing the model by measurement
&lt;/h3&gt;

&lt;p&gt;I wrote an eval harness (&lt;code&gt;npm run eval&lt;/code&gt;): 10 real-style notes covering Hinglish, Bengali in English letters, one-liners, nicknames, a date that looks like a score, typos in names and an unknown student. It runs the actual extract, validate and resolve code and scores the result:&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;Version&lt;/th&gt;
&lt;th&gt;Students&lt;/th&gt;
&lt;th&gt;Scores&lt;/th&gt;
&lt;th&gt;Extra questions&lt;/th&gt;
&lt;th&gt;Names as written&lt;/th&gt;
&lt;th&gt;Median time&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gemma4 (E4B)&lt;/td&gt;
&lt;td&gt;v1&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;56.5%&lt;/td&gt;
&lt;td&gt;7.4 s&lt;/td&gt;
&lt;td&gt;11.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;gemma4 (E4B)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;v2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;91.3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.8 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13.7 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.5:9b&lt;/td&gt;
&lt;td&gt;v1&lt;/td&gt;
&lt;td&gt;95.7%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;77.3%&lt;/td&gt;
&lt;td&gt;23.6 s&lt;/td&gt;
&lt;td&gt;40.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.5:9b&lt;/td&gt;
&lt;td&gt;v2&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;82.6%&lt;/td&gt;
&lt;td&gt;23.7 s&lt;/td&gt;
&lt;td&gt;39.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every run returned valid JSON on the first try, and every run left the unknown student for the tutor instead of guessing.&lt;/p&gt;

&lt;p&gt;Two things came out of it. First, Gemma kept "helpfully" correcting names ("Payal" → "Payal Dutta"), which broke the check that the model's quote really appears in the note. One change to the reading step, keeping every name exactly as the tutor spelled it, took names-as-written from 56% to 91%. Second, after that change Gemma sometimes returned the class date as a line about no one. That would have become a pointless question for the tutor, so the validator now drops any such line that isn't in the note. Gemma matched Qwen on accuracy and was three times faster, so it does both jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  When things go wrong
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The AI engine is off.&lt;/strong&gt; The note still saves (in 106 ms in my QA run) and is read automatically once the engine is back (19 seconds later, in the same run).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The engine stops halfway through an update.&lt;/strong&gt; The screen says "The AI engine stopped partway through. Your note is safe and nothing was sent.", and the previous version stays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A note can't be read.&lt;/strong&gt; It waits in Needs you, with &lt;strong&gt;Try the other model&lt;/strong&gt; when another model is installed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Keeping it on the laptop
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The server listens only on &lt;code&gt;127.0.0.1&lt;/code&gt; unless it's started with &lt;code&gt;--lan&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every page needs the PIN. It's stored as a scrypt hash, and five wrong tries in ten minutes pause sign-in for ten minutes.&lt;/li&gt;
&lt;li&gt;The AI log records timings and token counts only. The logging function has no parameter for note or message text, so there's nowhere for it to go.&lt;/li&gt;
&lt;li&gt;If the AI engine is ever pointed at another machine, the home screen shows a red warning.&lt;/li&gt;
&lt;li&gt;Fonts are bundled at build time, so the app makes no internet requests at runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Testing it like a tutor would
&lt;/h3&gt;

&lt;p&gt;I did a QA pass from a fresh &lt;code&gt;git clone&lt;/code&gt; on a MacBook with an M4 and 16 GB, driving the app in headless Chrome against the real model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;41 seconds&lt;/strong&gt; from the fresh clone to the first draft, with the model already downloaded and the setup wizard scripted.&lt;/li&gt;
&lt;li&gt;One note about five students was read in &lt;strong&gt;16 seconds&lt;/strong&gt;, with every score right and nothing left in Needs you.&lt;/li&gt;
&lt;li&gt;Five updates in &lt;strong&gt;21 seconds&lt;/strong&gt;: three in Bengali, one in Hindi, one in English, all checked.&lt;/li&gt;
&lt;li&gt;A sampler listed every connection the server opened during the run. All of them were on &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It found four bugs, now fixed: a vague error when the engine died mid-update, a score shown as "unknown 18/25" because Gemma named an unnamed test "unknown", a sent update still labelled "Preview · not sent", and 12 lines of npm warnings in &lt;code&gt;./setup.sh&lt;/code&gt; that look like errors to someone who isn't a developer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rest of the stack:&lt;/strong&gt; Next.js 16 and React 19, SQLite through Drizzle, Zod for every model reply, and Ollama for the models. There's a PIN-protected local sign-in, a &lt;code&gt;--lan&lt;/code&gt; mode so the tutor's phone can use the laptop, and 243 tests across 33 files. Models are swapped with one line in &lt;code&gt;.env.local&lt;/code&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Children's data stays home.&lt;/strong&gt; The notes, marks and parents' numbers live in a SQLite file on the tutor's laptop. In my QA run, the app's only network connections were to itself and to Ollama on 127.0.0.1. A closed API would mean sending every child's marks to a third party.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bengali quality is something I can measure and swap.&lt;/strong&gt; If a better open model for Bengali appears tomorrow, it's one line in &lt;code&gt;.env.local&lt;/code&gt;, and the eval tells me whether it really is better before any parent sees it. Gemma was a good place to start: in &lt;a href="https://dev.to/devsaquib/i-tested-gemma-4-and-gpt-4o-mini-on-indian-language-tasks-the-results-surprised-me-19g8"&gt;I Tested Gemma 4 and GPT-4o-mini on Indian Language Tasks&lt;/a&gt;, &lt;a href="https://dev.to/devsaquib"&gt;@devsaquib&lt;/a&gt; found that Gemma 4 (31B) wrote proper Bengali and Devanagari, where GPT-4o-mini answered a formal Hindi request in Roman script and sent the Bengali prompt back instead of answering it. I run the much smaller E4B, so my own eval is what decides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in.&lt;/strong&gt; It's Apache-2.0, the data is a file she owns, and once it's set up it needs no internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest limits: a tutor needs a laptop with at least 8 GB of memory, and 16 GB is comfortable. The Bengali and Hindi templates for greetings and sign-offs still need a native speaker's review.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Photos of the paper register. Gemma 4 reads images, and the step is designed, but it was cut from this MVP.&lt;/li&gt;
&lt;li&gt;A settings page, and setup scripts for Windows.&lt;/li&gt;
&lt;li&gt;A hosted version for tutors without a capable laptop. It's designed in the spec, with Postgres and row-level security keeping each tutor's data apart, and the code already carries a workspace on every table so the switch is small.&lt;/li&gt;
&lt;li&gt;Later, an Android app with a small on-device model, so the phone works without the laptop.&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 4 (E4B, &lt;code&gt;gemma4:latest&lt;/code&gt; in Ollama) does both model jobs: reading the tutor's shorthand into structured JSON, and writing each parent's update in Bengali, Hindi or English. I chose it over Qwen 3.5 9B by measurement (table above): just as accurate on my fixtures, three times faster, and small enough for a tutor's 16 GB laptop. It reads notes through Ollama's JSON-schema structured outputs with thinking off, streams each update as it writes, and stays loaded for 15 minutes between notes so the next one starts quickly. A tutor with more memory can switch to a bigger Gemma, such as &lt;code&gt;gemma4:12b&lt;/code&gt;, with one line in &lt;code&gt;.env.local&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I built TuitionSetu with Claude Code as my pair programmer. Every number above comes from the project's tests, evals and my QA run.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Plink: a patient xylophone practice partner for my friend's daughter</title>
      <dc:creator>Souptik</dc:creator>
      <pubDate>Sat, 03 Oct 2026 05:19:29 +0000</pubDate>
      <link>https://dev.to/souptik4572/plink-a-patient-xylophone-practice-partner-for-my-friends-daughter-6m9</link>
      <guid>https://dev.to/souptik4572/plink-a-patient-xylophone-practice-partner-for-my-friends-daughter-6m9</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;My friend's daughter has just started her first instrument: a toy xylophone with eight coloured bars. She reads a little, and practice works best when a grown-up sits beside her, points at the next bar, and waits. Plink is that patient partner.&lt;/p&gt;

&lt;p&gt;Plink is a web app that runs on the family laptop. It listens through the microphone, learns the sound of &lt;em&gt;her&lt;/em&gt; bars, lights up the bar to hit next and waits, with no buzzer, until she finds it. Between parts, two open models help: TabPFN learns from every note she plays and picks the part she'll get right about 80% of the time, and Gemma 4 turns any tune into a short lesson and writes a two-line note for her parent. Nothing she plays leaves the laptop, and it works with the Wi-Fi off.&lt;/p&gt;

&lt;p&gt;A session goes like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plink's turn:&lt;/strong&gt; Plink plays the part while the bars light up, a little slower than normal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Her turn:&lt;/strong&gt; the next bar glows and Plink waits. A miss just makes the right bar wiggle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Help that fades:&lt;/strong&gt; lots of help (only the right bar sounds, and Plink says its colour), some, or little (she plays from memory).&lt;/li&gt;
&lt;li&gt;Every note right first time is a &lt;strong&gt;star&lt;/strong&gt;, and every 15 stars unlock a &lt;strong&gt;sticker&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;After every three parts of her song, Plink slips in a short part on &lt;strong&gt;a jump she finds hard&lt;/strong&gt;. The last part is always one she's likely to get right, so she &lt;strong&gt;ends on a win&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app is loud and colourful everywhere except during a part. Then the page calms down, the other bars dim, and only the target glows, in its real colour, because Plink's hints name the colours.&lt;/p&gt;

&lt;p&gt;There's also an on-screen xylophone (mouse, touch, or the &lt;code&gt;A S D F J K L ;&lt;/code&gt; keys) for when the real one isn't to hand. A grown-ups area behind a simple sum lets her parent add songs by playing them in, typing letters, singing them, or &lt;strong&gt;taking a photo of a song card&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handing it over
&lt;/h3&gt;

&lt;h2&gt;
  
  
  Hosted 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://plink-tt30.onrender.com/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;plink-tt30.onrender.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&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/souptik4572" rel="noopener noreferrer"&gt;
        souptik4572
      &lt;/a&gt; / &lt;a href="https://github.com/souptik4572/plink-xylophone-practice-app" rel="noopener noreferrer"&gt;
        plink-xylophone-practice-app
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Plink is a local-first web app built for children playing their first xylophone, and for the parent who sits beside her. It has a built-in on-screen xylophone, playable by mouse, touch or the home-row keys, so she can play and practise even without the real instrument.
    &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;Plink&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A patient xylophone practice partner, built for one child and the parent beside her. Hacktoberfest 2026 Weekend Challenge, "Build for a Friend". Full design in &lt;a href="https://github.com/souptik4572/plink-xylophone-practice-app/spec.md" rel="noopener noreferrer"&gt;spec.md&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run&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;make setup   &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; uv sync, pnpm install, ollama pull&lt;/span&gt;
make dev     &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; backend on 127.0.0.1:8000, app on http://localhost:5173&lt;/span&gt;
make &lt;span class="pl-c1"&gt;test&lt;/span&gt;
make &lt;span class="pl-c1"&gt;eval&lt;/span&gt;    &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; TabPFN drill-picker benchmark&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The first time, make an account in the app (see &lt;a href="https://github.com/souptik4572/plink-xylophone-practice-app#accounts" rel="noopener noreferrer"&gt;Accounts&lt;/a&gt;).&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Accounts&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A grown-up signs up with an email and a password (8 characters or more), on the laptop and on the demo alike. Everything Plink keeps (her songs and their lessons, practice, settings and calibration) belongs to that account, and no account can see another's.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Passwords are stored only as Argon2id hashes.&lt;/li&gt;
&lt;li&gt;The avatar at the top right opens the grown-up's &lt;strong&gt;Account&lt;/strong&gt; page and &lt;strong&gt;Log out&lt;/strong&gt;, both behind the grown-up sum, so she can't log herself out. The page holds your name…&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/souptik4572/plink-xylophone-practice-app" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;code&gt;make setup &amp;amp;&amp;amp; make dev&lt;/code&gt; runs it locally: FastAPI and SQLite on the back end, React, TypeScript and Vite on the front, and Gemma through Ollama. There are 215 backend and 114 frontend tests, plus headless-Chrome runs that play whole sessions.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;her xylophone ── mic ──▶ browser: AudioWorklet → strike gate → which bar?
on-screen xylophone ──┘   (plain signal processing; audio never leaves the tab)
                                  │ only bar numbers and timings
                                  ▼
         FastAPI ── SQLite on the laptop (Postgres on the Render demo)
            │  every note she plays = one row in her practice log
            ├── TabPFN: what to practise next, when to help, her trickiest jumps
            └── Gemma 4: lessons, praise, the parent note, song cards, Ask Plink
singing ──▶ Basic Pitch (in the browser) → notes → fitted to her eight bars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hearing her bars is not a model's job
&lt;/h3&gt;

&lt;p&gt;Detection is plain signal processing, on purpose. It has to answer the moment she strikes, the audio must stay in the browser, and it has to fail predictably.&lt;/p&gt;

&lt;p&gt;Calibration has her strike each bar three times. Plink stores what each bar sounds like on a musical scale (quarter-semitone bands), so a calibration made on one laptop still works on another with a different sample rate. A sound only counts as a strike if it peaks at the mallet and then only decays, as a few clear partials. Claps, knocks, key clicks and voices don't behave like that, so Plink ignores them, and it re-measures the room's noise as she plays.&lt;/p&gt;

&lt;p&gt;On synthetic wooden and metal bars it accepted all 96 test strikes and rejected every clap, knock, click and voice. In the browser, fed a recording through Chrome's fake microphone, the Mic check scored 40 of 40. &lt;strong&gt;[TODO: her real xylophone's Mic check score from Sunday.]&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  TabPFN: seven small decisions, from her first session
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/PriorLabs/TabPFN" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt; is a tabular foundation model: you hand it a small table and ask for predictions, with no training loop. That fits a child's practice log well. Every note she plays is one row (the bar, the jump from the last one, the help level, how many times she's seen the part…) plus whether she got it right first time.&lt;/p&gt;

&lt;p&gt;From about 20 rows, which is her first session, TabPFN:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;chooses the next part&lt;/strong&gt; at the level she'll get right about 80% of the time. It predicts every candidate part "as if she played it next" and picks the one closest to 0.8;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;times the help&lt;/strong&gt; for each note: where it expects a miss, the glow comes sooner and Plink says the colour;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;advises&lt;/strong&gt; more, less or the same help, but only for levels it has seen her play. It can't know what it hasn't seen, so otherwise it suggests trying that level once;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rates each song&lt;/strong&gt; for her and suggests one on the home screen;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;finds her trickiest jumps&lt;/strong&gt; between bars;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;turns those jumps into practice&lt;/strong&gt;: three short parts per jump, easiest first ("Stepping stones", "One big hop", "Hop and back"), slotted in after every three song parts at her sweet spot;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ends the session on a win&lt;/strong&gt;: when the next part probably won't fit in the time left, Plink says "Last one!" and plays the part TabPFN is surest she'll get right.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A fit and prediction takes 0.8 s warm on a laptop, so the next pick is computed in the background while she's celebrating the last one. On the 36 notes I played while testing, TabPFN edged logistic regression (ROC AUC 0.858 against 0.819). That's far too little data to call a win, and it isn't the point. The point is that TabPFN works on the tiny table you actually have in week one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma 4: six jobs, and code that owns the notes
&lt;/h3&gt;

&lt;p&gt;Gemma 4 (&lt;code&gt;gemma4:e4b&lt;/code&gt;, through Ollama) does the language and vision work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lessons:&lt;/strong&gt; it splits any tune into short named parts, with a tip for each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Praise and the parent note:&lt;/strong&gt; the note is two plain lines on what to practise next, with TabPFN's trickiest jumps fed in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Song cards from a photo:&lt;/strong&gt; beginner songbooks often print songs as coloured circles or letters. Gemma counts the notes first (with thinking on), then lists them (thinking off), and code maps each one to her bars. That two-pass approach read 14 of 14 notes on test cards printed with letters, numbers and colours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask Plink:&lt;/strong&gt; a grown-up can ask "Is she ready for Jingle Bells?" Gemma calls read-only tools over her practice data and TabPFN, and answers with the numbers ("87% first try"). TabPFN decides; Gemma explains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The line Plink says before each jump practice part,&lt;/strong&gt; naming the colours: "Go orange and turquoise back and forth, yay!"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule everywhere is that &lt;strong&gt;code owns the notes&lt;/strong&gt;. Gemma names and explains, but every reply goes through a validator, and when one fails, Plink falls back to plain parts and plain text. A child never sees a broken lesson.&lt;/p&gt;

&lt;p&gt;Two surprises along the way. Gemma 4 thinks by default: a lesson took 44 s with thinking and 10 s without, so thinking is only on for counting the notes on a song card. And with no name set, Gemma called her "Lily", in the praise and in the note. The prompts now forbid inventing names.&lt;/p&gt;

&lt;h3&gt;
  
  
  What didn't work: asking Gemma twice
&lt;/h3&gt;

&lt;p&gt;The usual advice for structured output is a bounded retry-repair loop: when the JSON fails validation, send the error back and let the model fix it. &lt;a href="https://dev.to/devshakib/structured-output-from-llms-a-retry-repair-loop-your-parser-never-sees-through-3b0b"&gt;Structured Output From LLMs: A Retry-Repair Loop Your Parser Never Sees Through&lt;/a&gt; by &lt;a href="https://dev.to/devshakib"&gt;@devshakib&lt;/a&gt; lays the pattern out well. I added one (simplified from &lt;code&gt;backend/app/coach.py&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;REPAIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;That answer can&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t be used: {problem}. Reply again with only the corrected JSON, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nothing before or after it, keeping every rule in the first message.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GEMMA_REPAIRS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                     &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;REPAIR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It barely helped. The repair fixed &lt;strong&gt;2 of 10&lt;/strong&gt; broken lessons; the rest repeated the mistake or made a new one. The mistakes were arithmetic. I'd asked for each part's first and last note, and Gemma wrote things like "phrase 4 starts at note 2" when it had to start at note 21.&lt;/p&gt;

&lt;p&gt;So I changed the question. Gemma now says how many notes each part holds (&lt;code&gt;{"notes": 4, "nickname": "…", "tip": "…"}&lt;/code&gt;), and code places the parts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lessons on the Gemini API (gemma-4-26b-a4b-it)&lt;/th&gt;
&lt;th&gt;Valid first try&lt;/th&gt;
&lt;th&gt;Repairs that worked&lt;/th&gt;
&lt;th&gt;Valid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First and last note, with repair (10 runs)&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1 of 4&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ complaints that say how to fix it (20 runs)&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;1 of 6&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part sizes, placed by code (20 runs)&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;1 of 1&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every lesson that came back was valid. The other 8 hit the API's 60 s timeout, and those songs keep plain parts until a grown-up asks again. On the laptop's e4b, 5 of 5 were valid.&lt;/p&gt;

&lt;p&gt;The jump practice taught me the same thing. Asked to name six nearly identical practice parts, Gemma came back with junk half the time ("nickname-2", "=", "pattern-pattern"), and the repair fixed none of it. Now each kind of part has a fixed name, which is also easier for a child to learn, and Gemma only writes the spoken line, which it got right in 11 runs of 12. A line that names a colour the part doesn't use goes back for repair, because she finds her bars by colour.&lt;/p&gt;

&lt;p&gt;The lesson: &lt;strong&gt;a retry loop can't fix a question the model finds hard. Change the question, and let code do the counting.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Render: the public demo
&lt;/h3&gt;

&lt;p&gt;Her copy stays on the laptop. For everyone else there's a public demo on &lt;a href="https://render.com" rel="noopener noreferrer"&gt;Render&lt;/a&gt;, defined in one &lt;a href="https://github.com/souptik4572/plink-xylophone-practice-app/blob/main/render.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;render.yaml&lt;/code&gt;&lt;/a&gt; Blueprint: a web service that serves the app and the API from one Docker image, plus a free Render Postgres database for logins and each account's practice. Render generates the login-signing secret, and each new demo account is seeded with simulated practice (6 sessions, 373 notes), so TabPFN is live on the first visit. Render has no GPUs, so the demo runs Gemma 4 through Google's Gemini API (&lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt;), which built a lesson in about 6 s.&lt;/p&gt;

&lt;p&gt;Two things I learned getting there. PyTorch in a 1-CPU container saw all 10 of the host's CPUs, started 10 threads, and was throttled for 486 s until I set &lt;code&gt;OMP_NUM_THREADS&lt;/code&gt;. And leaving uv's download cache in the image made it 3.47 GB instead of 2.06 GB.&lt;/p&gt;

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

&lt;p&gt;Because of who it's for. &lt;a href="https://dev.to/sanskarin/building-local-first-ai-apps-what-changes-when-the-data-stays-on-the-device-15mi"&gt;Building Local-First AI Apps: What Changes When the Data Stays on the Device&lt;/a&gt; by &lt;a href="https://dev.to/sanskarin"&gt;@sanskarin&lt;/a&gt; covers the general case. For a child's practice, it isn't a nice-to-have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Her audio never leaves the room.&lt;/strong&gt; Detection runs in the browser, and only bar numbers and timings are saved. There is no recording of her to leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works with no internet.&lt;/strong&gt; Gemma runs through Ollama on the laptop and TabPFN runs locally, so practice doesn't depend on a connection or a subscription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It fits her.&lt;/strong&gt; It's calibrated to her bars, Plink's words can be in her family's language, and a smaller or bigger Gemma is one environment variable (&lt;code&gt;GEMMA_MODEL&lt;/code&gt;) away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing per session&lt;/strong&gt;, so she can play every day.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A closed API could write lessons too. But every song, every practice note and every question about a child would go to someone else's server, and practice would stop when the Wi-Fi does.&lt;/p&gt;

&lt;p&gt;The honest limits: Gemma 4 and Basic Pitch are Apache 2.0, but TabPFN's weights are open under a non-commercial licence. That's fine for a gift, not for selling it. And the public demo trades some of this away for convenience: there, song-card photos, the numbers behind a parent note and Ask Plink questions go to Google's Gemini API. Her laptop copy sends nothing.&lt;/p&gt;

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

&lt;p&gt;I built Plink with help from Claude Code. This session is day two: logins (JWT access and refresh tokens), the calibration rework, moving the demo's Gemma to the Gemini API, and the Gemma and TabPFN upgrades above, measurements included.&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; six jobs across text, vision and tool calling (lessons, praise, the parent note, song cards from a photo, Ask Plink, and the lines for jump practice), with validators, a repair step and fallbacks, so a child never sees a bad answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of TabPFN:&lt;/strong&gt; seven live decisions from her own practice log, starting in her first session: what to practise next, when to help, which help level to try, how hard each song is for her, her trickiest jumps, practice on those jumps, and a last part she'll get right.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Plink's on-screen xylophone was inspired by my earlier project &lt;a href="https://github.com/souptik4572/xylophone" rel="noopener noreferrer"&gt;souptik4572/xylophone&lt;/a&gt; and rebuilt from scratch here. Thanks to Prior Labs (TabPFN), Google DeepMind (Gemma), Spotify (Basic Pitch) and Ollama. I built Plink with help from Claude Code.&lt;/em&gt;&lt;/p&gt;

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