<?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: Muaaz Shaikh</title>
    <description>The latest articles on DEV Community by Muaaz Shaikh (@muaazsm).</description>
    <link>https://dev.to/muaazsm</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%2F4155240%2Fa8eff522-7d31-484f-ae36-89fee9442103.png</url>
      <title>DEV Community: Muaaz Shaikh</title>
      <link>https://dev.to/muaazsm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muaazsm"/>
    <language>en</language>
    <item>
      <title>Sair: I built my parents a walk companion that makes them leave the phone at home</title>
      <dc:creator>Muaaz Shaikh</dc:creator>
      <pubDate>Sun, 11 Oct 2026 17:04:25 +0000</pubDate>
      <link>https://dev.to/muaazsm/sair-i-built-my-parents-a-walk-companion-that-makes-them-leave-the-phone-at-home-11dn</link>
      <guid>https://dev.to/muaazsm/sair-i-built-my-parents-a-walk-companion-that-makes-them-leave-the-phone-at-home-11dn</guid>
      <description>&lt;p&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;/p&gt;

&lt;p&gt;My parents walk every evening. Tonight they came back and told me about the sparrows they heard and the mogra flowers they saw. That's pretty much the whole idea behind what I built this week. Give them a small reason to look around, and let the phone stay at home.&lt;/p&gt;

&lt;p&gt;Last week I built them something to protect them from their phones (&lt;a href="https://dev.to/muaazsm/rakshak-i-built-my-parents-a-scam-checker-that-never-leaves-the-house-22ah"&gt;Rakshak&lt;/a&gt;). This week I wanted something that gets them off their phones.&lt;/p&gt;

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

&lt;p&gt;Sair (सैर, it means "a stroll") is a walk companion that barely has a screen.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At the door, Mom taps "Hear today's walk". Sair says one small thing to notice, slowly, twice. Something like "Notice one sound you like on your walk."&lt;/li&gt;
&lt;li&gt;Then the phone stays home. The app actually tells them "Leave me here. I'll be waiting when you're back." There are no maps, no step counts, no streaks and no notifications. It's about 30 seconds of screen before the walk and none during it.&lt;/li&gt;
&lt;li&gt;When they're back, Mom holds one button and just talks for up to a minute.&lt;/li&gt;
&lt;li&gt;A model running on our MacBook turns that voice note into a short diary entry, and the things they noticed show up as little chips, like "gulmohar · near the corner" or "koel · near the temple".&lt;/li&gt;
&lt;li&gt;The next day's mission builds on what they really noticed. For example: "On Tuesday you heard a koel near the temple. Is it there again?"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's built for two people in their fifties and sixties who are fine with WhatsApp and UPI and who switch between English, Hindi and Hinglish in the same sentence.&lt;/p&gt;

&lt;p&gt;The hard part wasn't the app. It was this: hallucinations pile up. If an AI journal makes up a bird, that's a bit annoying. If a walk companion with memory makes up a bird, it sends two people out the next evening to look for a bird that was never there. So Sair has one rule I didn't want to bend: nothing goes into the diary, or into memory, that nobody actually said.&lt;/p&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/DNmmiQKRHs4" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I recorded this in the Android Emulator, talking to the real hub on my MacBook. Same app, same models, same pipeline. The voice note in the video is a text-to-speech stand-in, not my mom.&lt;/p&gt;

&lt;p&gt;In order, you'll see Sair speaking the mission (that's Kokoro, running on the laptop), the "leave me here" screen, hold to talk, "Writing it down…", and then the diary entry with three checked facts and "Mission done".&lt;/p&gt;

&lt;p&gt;One honest note about that last screen. "Mom and Dad went for a walk and did today's mission. They noticed gulmohar, koil and puddles." reads exactly like Sair's plain template, and it isn't a fallback: Sair-Writer wrote that sentence itself, on its first try. It picked up the template's style from its training data (more on that below). "Koil" is how the transcription heard "koel", and it's kept as heard because every fact has to quote the transcript. When it does write properly, it looks like this entry from another test note: "Mom and Dad enjoyed a peaceful walk where they saw kids playing cricket near the gate and a dog under the bench. They think maybe a crow was calling from the tree."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;At the door&lt;/th&gt;
&lt;th&gt;The mission, spoken twice&lt;/th&gt;
&lt;th&gt;Back home: hold to talk&lt;/th&gt;
&lt;th&gt;The diary entry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxp5nfnru772ofagu9j1o.png" alt="Door screen: Hear today's walk" width="540" height="1102"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0fefeeb4exrn3047a83.png" alt="Mission playing: Notice one sound you like on your walk" width="540" height="1102"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4xmpolm9unlci6od0a4w.png" alt="Recording a voice note" width="540" height="1102"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5j08biv1wdegkd1zkbbu.png" alt="Got it. Saved. Entry with three verified fact chips" width="540" height="1102"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The first real walk
&lt;/h3&gt;

&lt;p&gt;Tonight was my parents' first walk with Sair. I set up both phones, and in the evening they went out.&lt;/p&gt;

&lt;p&gt;When they came back, they told me about the sparrows they heard and the mogra flowers they saw. That's exactly the kind of small thing I hoped Sair would make room for. Not steps, not a route, just what they noticed.&lt;/p&gt;

&lt;p&gt;It's one walk, so I'm not drawing conclusions from it. What I'm watching over the next few evenings is simple: do they actually leave the phone at home, does the voice note feel natural to them, and does the diary entry sound like what they said. The Hinglish problem below is the part I'm most nervous about, because that's how they talk. I'll add real numbers once there are enough real walks to count.&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%2Fjj6pmlm0mcs8xwmxcnkl.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%2Fjj6pmlm0mcs8xwmxcnkl.jpg" alt="My parents' first walk with Sair" width="800" height="1067"&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%2Fj8jvtuq7s716r9rbmonx.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%2Fj8jvtuq7s716r9rbmonx.jpg" alt="On the first walk" width="800" height="1067"&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/MuaazSM" rel="noopener noreferrer"&gt;
        MuaazSM
      &lt;/a&gt; / &lt;a href="https://github.com/MuaazSM/sair" rel="noopener noreferrer"&gt;
        sair
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Sair (सैर)&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A screenless evening-walk companion for two parents. Before the walk, Sair speaks one small &lt;em&gt;noticing&lt;/em&gt; mission at the door, twice. The phone stays home. Afterwards, a voice note of up to a minute becomes a short family diary entry, and the next mission builds only on what was actually said.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"On Tuesday you heard a koel near the temple. Is it there again?"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Everything runs on a MacBook at home. No inference, and no content or timing of any real walk, leaves the house.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why it's built this way&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Hallucinations compound. If a diary invents a bird, tomorrow's mission sends two people looking for a bird that was never there. Sair keeps the diary faithful with three pieces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a small open model fine-tuned to write grounded entries (Qwen3.5-4B + LoRA, served by &lt;code&gt;llama-server&lt;/code&gt; with a JSON grammar);&lt;/li&gt;
&lt;li&gt;a grounding check in code: every fact must quote the transcript…&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/MuaazSM/sair" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;A quick map of the repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hub/&lt;/code&gt; is the FastAPI and LangGraph hub (a journal graph and a mission graph) with SQLite. It only listens on loopback.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hub/grounding.py&lt;/code&gt; has the seven grounding checks, written as plain code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hub/safety.py&lt;/code&gt; keeps missions safe with a verb allowlist, a blocklist and a check on where every noun came from.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;training/&lt;/code&gt; covers data generation, teacher labelling, LoRA training on Tinker and the export to GGUF.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eval/&lt;/code&gt; is the evaluation harness, with bootstrap confidence intervals and the memory contamination chains.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;web/&lt;/code&gt; is the React PWA, matched pixel by pixel against the design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are 1,136 tests (unit, integration and privacy) and they all pass in CI (&lt;a href="https://github.com/MuaazSM/sair/actions/runs/38158275182" rel="noopener noreferrer"&gt;latest run&lt;/a&gt;).&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Everything runs on one MacBook
&lt;/h3&gt;

&lt;p&gt;It's an M3 Pro with 18 GB that sits on a shelf at home. The hub only binds to &lt;code&gt;127.0.0.1&lt;/code&gt;, and the phones reach it over a private Tailscale network using &lt;code&gt;tailscale serve&lt;/code&gt;, with Funnel turned off.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Open model or tool&lt;/th&gt;
&lt;th&gt;Runs on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transcribe the voice note&lt;/td&gt;
&lt;td&gt;Gemma 4 E2B through Ollama, straight from audio, temperature 0&lt;/td&gt;
&lt;td&gt;MacBook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan tomorrow's mission&lt;/td&gt;
&lt;td&gt;Gemma 4 E2B&lt;/td&gt;
&lt;td&gt;MacBook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write the diary entry&lt;/td&gt;
&lt;td&gt;Sair-Writer: Qwen3.5-4B with a LoRA I trained on Tinker, as a Q8_0 GGUF on llama.cpp with a GBNF grammar&lt;/td&gt;
&lt;td&gt;MacBook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speak the mission&lt;/td&gt;
&lt;td&gt;Kokoro-82M, in its own process&lt;/td&gt;
&lt;td&gt;MacBook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glue&lt;/td&gt;
&lt;td&gt;LangGraph (no persistent checkpointer), FastAPI and SQLite&lt;/td&gt;
&lt;td&gt;MacBook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tell me a walk happened&lt;/td&gt;
&lt;td&gt;self-hosted ntfy, counts only, never any text&lt;/td&gt;
&lt;td&gt;MacBook&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fb36za56r8xqlm4f0913k.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%2Fb36za56r8xqlm4f0913k.png" alt="How Sair works: phones reach the MacBook over Tailscale; Gemma 4, Sair-Writer, the grounding checks, Kokoro and SQLite all run on it" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The fact checking is code, not a prompt
&lt;/h3&gt;

&lt;p&gt;Politely asking a model not to make things up doesn't work. So every entry goes through seven checks written in ordinary Python, in &lt;code&gt;hub/grounding.py&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every fact has to quote the transcript exactly.&lt;/li&gt;
&lt;li&gt;The thing they saw, and where, has to be inside that quote.&lt;/li&gt;
&lt;li&gt;Hedges survive. "I think it was a koel" stays a maybe.&lt;/li&gt;
&lt;li&gt;Every concrete detail in the entry has to be backed by one of those facts.&lt;/li&gt;
&lt;li&gt;No new numbers, times or names.&lt;/li&gt;
&lt;li&gt;The entry has to look like an entry: 5 to 70 words, says who walked, no questions, no links.&lt;/li&gt;
&lt;li&gt;If it says the mission was done, that needs a quote too.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If an entry fails, the writer gets one more try. If it fails again, Sair writes a plain entry from a template that code builds. I'd rather have a boring true sentence than a lovely false one.&lt;/p&gt;

&lt;p&gt;Memory only holds facts that passed the first three checks. The mission planner never sees the diary text, only those checked facts and the last five missions. Missions get checked too. Each one has to be a single task, at most 30 words, use an allowed verb, avoid anything risky, speak to them as "you", never mention a time of day, and only use nouns that came from a checked fact or a short list of safe starters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fine-tuning a small writer on Tinker
&lt;/h3&gt;

&lt;p&gt;The stock Qwen3.5-4B, even with good examples in the prompt, put at least one made-up detail into 42% of its entries. I wanted something small enough to run next to Gemma on a laptop, and a lot more honest.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data: synthetic voice-note transcripts made from generic seeds like parks, lanes, temples and markets. Nothing from my family went in. I split by seed, so the test seeds were never seen during training.&lt;/li&gt;
&lt;li&gt;Teacher: Qwen3.5-397B on Tinker wrote the gold entries, and each one had to pass the same seven checks or it got replaced by the code template. That gave me 1,114 training rows.&lt;/li&gt;
&lt;li&gt;Training: LoRA rank 32 on Qwen3.5-4B. I ran a small learning-rate sweep and then the full run, and picked epoch 2 because it had the lowest made-up-detail rate on the dev set.&lt;/li&gt;
&lt;li&gt;Export: I merged the LoRA into the base weights in f32 and converted to GGUF. Q4_K_M failed my export check (its template fallback rate moved by 2.33 points and my limit was 2), so Sair ships Q8_0, which matched the Tinker checkpoint on every metric I gate on.&lt;/li&gt;
&lt;li&gt;Cost: about $6.65 on the Tinker usage page. That's $4.95 for the 397B teacher and roughly $1.70 for the 4B sweeps, training and dev sampling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;These numbers come from a held-out synthetic test set: 130 voice-note transcripts from 8 seeds the model never saw in training, scored by the same code checker. The confidence intervals are 95%, from a bootstrap that resamples whole seeds (10,000 times).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Writer&lt;/th&gt;
&lt;th&gt;Entries with a made-up detail&lt;/th&gt;
&lt;th&gt;Facts kept (recall)&lt;/th&gt;
&lt;th&gt;Fell back to the template&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E2B, zero-shot&lt;/td&gt;
&lt;td&gt;20.0%&lt;/td&gt;
&lt;td&gt;0.864&lt;/td&gt;
&lt;td&gt;no safety net&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B base, few-shot&lt;/td&gt;
&lt;td&gt;42.3%&lt;/td&gt;
&lt;td&gt;0.743&lt;/td&gt;
&lt;td&gt;no safety net&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sair-Writer (fine-tuned)&lt;/td&gt;
&lt;td&gt;6.2%&lt;/td&gt;
&lt;td&gt;0.939&lt;/td&gt;
&lt;td&gt;no safety net&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma with the grounding check&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0.862&lt;/td&gt;
&lt;td&gt;55.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen base with the grounding check&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0.732&lt;/td&gt;
&lt;td&gt;24.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sair-Writer with the grounding check&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0.937&lt;/td&gt;
&lt;td&gt;6.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Fine-tuning took made-up details from 42.3% down to 6.2% compared with its own base. That's 36.2 points lower, with a 95% interval of 22.9 to 49.3 points.&lt;/li&gt;
&lt;li&gt;It didn't get there by saying less. Recall went up from 0.743 to 0.939 (19.7 points, interval 13.4 to 27.7), and hedges survived more often too, from 0.849 to 0.949.&lt;/li&gt;
&lt;li&gt;With the grounding check switched on, every writer gets to 0% made-up details, because that's what the check enforces. The real difference is how often the safety net has to catch them. Sair-Writer falls back to the plain template on 6.2% of notes. Gemma falls back on 55.4%.&lt;/li&gt;
&lt;li&gt;There's a catch I only found while making the demo. Sair-Writer learned the template's style from its training data, so sometimes it writes the template sentence itself. Counting those, 16.2% of its final entries read word for word like the template (8 real fallbacks plus 13 it wrote itself), against 55.4% for Gemma and 24.6% for the base model. Still far fewer, but not the 6.2% the fallback number alone suggests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftvgmsvbwzwln39pq150j.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%2Ftvgmsvbwzwln39pq150j.png" alt="Sair-Writer vs its base model and Gemma zero-shot: made-up details 42.3% vs 6.2%, facts kept 74.3% vs 93.9%, template fallback 24.6% vs 6.2%" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What didn't work
&lt;/h3&gt;

&lt;p&gt;I'd rather tell you than have you find it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My contamination experiment came back null. I ran 7-day chains comparing memory built from diary text against memory built from checked facts, expecting the diary-text memory to leak made-up details into later missions. It didn't, for any writer. Diary-text memory gave 0 contaminated chains out of 20, and fact memory gave at most 1 out of 20, which was a number word like "one". It turns out Gemma's missions are generic enough that they rarely reuse details at all. I still think the rule is right, but this run doesn't prove it.&lt;/li&gt;
&lt;li&gt;Hinglish voice notes currently come back from Gemma in Devanagari script, and then the writer falls back to the template. English works well. This is the first thing I'm fixing.&lt;/li&gt;
&lt;li&gt;Live audio is harder than clean transcripts. All the numbers above are on text. Tonight I ran 12 synthetic voice notes end to end through the real app and hub, and only 3 came out as a properly written entry. 5 fell back to the template and 4 were Sair-Writer copying the template's wording. Small transcription slips, like "koel" heard as "koil", make the checker reject facts, and the same file doesn't always come out the same way. This is the thing I most want to fix next, together with the training data that taught it the template's style.&lt;/li&gt;
&lt;li&gt;Speed: in my demo runs, an 18 to 20 second note was ready in about 20 to 25 seconds (I was checking every 5 seconds). I haven't measured it properly on real notes yet.&lt;/li&gt;
&lt;li&gt;Every number above is synthetic. Real-walk numbers will come after real walks, and I'm not going to publish any before they exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Privacy: nothing about a walk leaves the house
&lt;/h3&gt;

&lt;p&gt;A record of when two older people leave home and how long they're out is sensitive. So it isn't only what they say that stays at home. The timing stays home too.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The hub refuses to start on anything other than &lt;code&gt;127.0.0.1&lt;/code&gt;. Phones reach it through &lt;code&gt;tailscale serve&lt;/code&gt;, and Funnel is off.&lt;/li&gt;
&lt;li&gt;The only outbound connections are to the model servers on the same machine. Models are downloaded ahead of time and Hugging Face runs in offline mode.&lt;/li&gt;
&lt;li&gt;Raw audio is deleted right after transcription. No audio or text ever goes into a log, a trace or a notification. When something fails, the reason is a short code.&lt;/li&gt;
&lt;li&gt;A canary test pushes a marker phrase through the whole pipeline, and CI fails if that phrase shows up in any log, trace, notification or model-server log.&lt;/li&gt;
&lt;li&gt;There's no telemetry in production. Sentry only runs in dev and eval on synthetic data, and the settings won't turn it on if any data path points at the real data folder.&lt;/li&gt;
&lt;li&gt;No location, no streaks, no gamification. The app never asks for location, and CI scans the built app for geolocation, beacons and analytics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Training wasn't fully local: synthetic transcripts went to Tinker for teacher labelling and LoRA training. No family voice note, transcript or entry was ever part of it. At runtime, nothing about the walks leaves the house.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;It can live in our house. With a closed API, every voice note, plus a timestamped record of when my parents go out, would sit on someone else's server. With open weights, inference never leaves the MacBook.&lt;/li&gt;
&lt;li&gt;I could make it honest. I fine-tuned a 4B model for this one job and cut made-up details from 42% to 6%. I couldn't do that with a closed model this cheaply, and I definitely couldn't run the result offline.&lt;/li&gt;
&lt;li&gt;I can check it. Open weights and a GBNF grammar let me force valid JSON and compare the export byte for byte. 105 of the 129 dev outputs were identical between the Tinker checkpoint and the Q8_0 GGUF.&lt;/li&gt;
&lt;li&gt;It's free to run. About $6.65 to train, and then nothing per walk, on a laptop we already own.&lt;/li&gt;
&lt;li&gt;It keeps working when the internet doesn't. I unplugged the router's internet connection and Sair kept working.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Best Use of Tinker (Thinking Machines): a LoRA fine-tune of Qwen3.5-4B on Tinker with a measured gain over its own base. Made-up details went from 42.3% to 6.2%, recall from 0.743 to 0.939, and template fallback with the safety net from 24.6% to 6.2%.&lt;/li&gt;
&lt;li&gt;Best Use of Gemma: Gemma 4 E2B runs locally through Ollama and does two jobs on every walk. It transcribes the voice note straight from the audio, with no separate speech model, and it plans the next mission from checked facts. It's also the zero-shot baseline I measured the fine-tune against.&lt;/li&gt;
&lt;li&gt;Best Use of GitHub Copilot (GitHub Actions): every push runs the 1,136 unit, integration and privacy tests, scans the built app for outbound URLs, geolocation and analytics, and replays the cached evaluation against a committed baseline. If a change makes the diary less faithful, the build fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built solo by &lt;a href="https://dev.to/muaazsm"&gt;@muaazsm&lt;/a&gt;. My parents get the credit for the walking.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Rakshak: I built my parents a scam-checker that never leaves the house</title>
      <dc:creator>Muaaz Shaikh</dc:creator>
      <pubDate>Sun, 04 Oct 2026 21:09:36 +0000</pubDate>
      <link>https://dev.to/muaazsm/rakshak-i-built-my-parents-a-scam-checker-that-never-leaves-the-house-22ah</link>
      <guid>https://dev.to/muaazsm/rakshak-i-built-my-parents-a-scam-checker-that-never-leaves-the-house-22ah</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 parents have never lost money to a scam. I'd like to keep it that way.&lt;/p&gt;

&lt;p&gt;My mom lives on WhatsApp. My dad pays for everything with UPI. Their phones get a steady drip of messages written to make someone panic: a KYC deadline "today", a parcel "held at customs", the electricity "disconnected tonight", a "police officer" on a video call. They've been careful so far. But these messages are designed for the one moment you're tired, alone or scared. And they only need to work once.&lt;/p&gt;

&lt;p&gt;So I built them &lt;strong&gt;Rakshak&lt;/strong&gt; ("protector"): a second opinion that lives on a laptop in our house. You hand it a suspicious message, a screenshot, or a description of a phone call, and within seconds a calm voice tells you in plain English whether it's real and what to do.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mom (Android)&lt;/strong&gt; shares a message to it straight from her share sheet. A big card comes up: red with a raised hand for &lt;em&gt;This is a scam&lt;/em&gt;, amber for &lt;em&gt;Be careful&lt;/em&gt;, green for &lt;em&gt;This looks like a normal message&lt;/em&gt;, grey for &lt;em&gt;Couldn't check right now&lt;/em&gt;. The explanation is read aloud. For a suspicious call, she holds a button and describes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dad (iPhone)&lt;/strong&gt; uses two Shortcuts: "Rakshak" in the share sheet, and "Rakshak Bolo" (bolo means "speak") to describe a call out loud. Same card, same voice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; on a scam verdict my phone gets a push with which parent, what kind of scam, and what time. Never the message.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every explanation is three short sentences at most: the verdict, the one or two strongest warning signs quoted &lt;em&gt;exactly&lt;/em&gt; from the message, and one thing to do. Don't tap. Don't pay. Never share the OTP. Call me.&lt;/p&gt;

&lt;p&gt;Nothing leaves the house. That's not a slogan, it's the architecture.&lt;/p&gt;

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

&lt;p&gt;There's no public link, and that's on purpose. Rakshak runs on a MacBook at home and is reachable only over our private Tailscale network, because the whole point is that my parents' messages never touch someone else's server. Here it is running for real:&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%2F1txe1db3976pe7zs3x2h.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%2F1txe1db3976pe7zs3x2h.png" alt="Home screen: Check a message, Describe a call" width="780" height="1688"&gt;&lt;/a&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%2Fi3sqcvpfqmcucp7bz0mt.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%2Fi3sqcvpfqmcucp7bz0mt.png" alt="Checking on the home computer: Reading, Checking, Explaining" width="780" height="1688"&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%2Fq43j2t0b8ngx9ni4d4a8.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%2Fq43j2t0b8ngx9ni4d4a8.png" alt="An app-install scam: red card, the dangerous link quoted, and " width="800" height="1201"&gt;&lt;/a&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%2F80rixerit338576tc7ha.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%2F80rixerit338576tc7ha.png" alt="A real bank OTP: green card, " width="780" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Left to right: the home screen; the check in progress; an app-install scam caught with the dangerous link quoted, plus&lt;/em&gt; Listen again &lt;em&gt;for the spoken explanation; a real bank OTP correctly left alone.&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%2Fvdtc1371orusf2txvad3.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%2Fvdtc1371orusf2txvad3.png" alt="The hub status page: model health, latency, per-parent counts, feedback queue, and no message text anywhere" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The hub's status page for me: three local models, latency against targets, today's verdicts per parent, and a "was this right?" queue. Metadata only. It never shows a message.&lt;/em&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/MuaazSM" rel="noopener noreferrer"&gt;
        MuaazSM
      &lt;/a&gt; / &lt;a href="https://github.com/MuaazSM/rakshak" rel="noopener noreferrer"&gt;
        rakshak
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Rakshak&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A scam checker for my parents that runs on a MacBook at home.&lt;/p&gt;
&lt;p&gt;My mom uses an Android phone and my dad uses an iPhone. Both get a steady stream of messages written to make a parent panic: fake KYC deadlines, parcels "held at customs", electricity cut off "tonight", fake police calls. Rakshak gives them a second opinion. Mom shares a message or screenshot to the Rakshak web app from her share sheet; Dad shares it to an iOS Shortcut. A few seconds later they see a large verdict card (scam, be careful, looks normal, or couldn't check) and hear a short spoken explanation in plain English. If it's a scam, my phone gets a notification that says which parent and what kind of scam, never the message itself. Every model runs on the Mac. The messages never leave the house.&lt;/p&gt;
&lt;p&gt;The verdict comes from a small model fine-tuned…&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/MuaazSM/rakshak" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Everything is in the repo: the rules engine, the LangGraph pipeline, training, export and evaluation scripts, the web app, and the privacy tests. The dataset is regenerated locally and the model weights are built by the export script rather than checked in, but nothing about them is secret. They're trained only on synthetic and public data.&lt;/p&gt;

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

&lt;p&gt;A MacBook Pro (M3 Pro, 18 GB) is the hub. It binds to localhost only; phones reach it over a private tailnet. Each check runs through a small &lt;strong&gt;LangGraph&lt;/strong&gt; graph: normalize, perceive the image or audio, rules, detect, fuse, explain, then notify and store. SQLite keeps the records. Every model runs locally through &lt;strong&gt;llama.cpp&lt;/strong&gt; and &lt;strong&gt;Ollama&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two open models, three jobs
&lt;/h3&gt;

&lt;p&gt;I wanted the verdict to come from a model I'd fine-tuned and measured, not a prompt. Tinker doesn't offer Gemma, which made the split natural. It also gave Gemma a second, less obvious job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma 4 E2B wrote the training set.&lt;/strong&gt; I had no scam dataset, and I refused to build one from my parents' private messages. So Gemma generated it: from about 60 short seed patterns across every scam and genuine category, it produced around 1,300 realistic messages in English, Hinglish and Devanagari, varied across channel and the tricks scammers use: misspellings, odd spacing, look-alike characters, shortened links. Every quoted red flag had to appear exactly in the generated text or the item was dropped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tinker trained the judge.&lt;/strong&gt; &lt;em&gt;Rakshak-Detector&lt;/em&gt; is &lt;strong&gt;Qwen3.5-4B&lt;/strong&gt; with a LoRA adapter trained on &lt;strong&gt;Tinker&lt;/strong&gt; on that data, then merged, converted to GGUF and quantized to Q4_K_M (2.9 GB) for &lt;code&gt;llama-server&lt;/code&gt;. It returns only JSON: verdict, category, red-flag quotes. I read the scam probability from the &lt;strong&gt;log-probabilities of the verdict token&lt;/strong&gt;, not from the text, so the fusion step has a real number to work with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma explains the verdict.&lt;/strong&gt; It transcribes screenshots and voice notes on-device, and turns the verdict into a short explanation in plain words. It never decides. It receives the verdict as input, and a post-check rejects any explanation that drops the verdict word, &lt;em&gt;negates&lt;/em&gt; it, adds a link or number that wasn't in the message, or runs long. On failure a fixed template takes over.&lt;/p&gt;

&lt;p&gt;Under all of it sits a &lt;strong&gt;rules engine&lt;/strong&gt; of plain, unit-tested functions. An &lt;code&gt;.apk&lt;/code&gt; link, a look-alike of a bank's domain, or "enter your UPI PIN to receive money" is a scam whatever the model says. Softer signals push toward &lt;em&gt;Be careful&lt;/em&gt;: a bank claiming to text from a plain mobile number, a link shortener, arrest threats, urgency plus a payment ask. The word lists cover English, Hinglish and Devanagari, because that's how scams actually arrive.&lt;/p&gt;

&lt;p&gt;Two safety rules live in code, not prompts: red flags shown to my parents must be &lt;strong&gt;exact substrings&lt;/strong&gt; of their message, or they're dropped; and if the detector is down the answer is &lt;em&gt;Couldn't check&lt;/em&gt;, never &lt;em&gt;looks normal&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Training
&lt;/h3&gt;

&lt;p&gt;LoRA rank 32, loss on the assistant's JSON only, batch 32, 3 epochs, thinking mode off, learning rate 4.91e-4 from &lt;code&gt;tinker_cookbook&lt;/code&gt;'s &lt;code&gt;get_lr&lt;/code&gt;. In 20% of training examples I blanked the rule signals so the model can't just copy the rules. I ran a smoke run, a learning-rate sweep, and then a retrain, because two independent review passes caught the quiet failure of synthetic data: the first model had learned shortcuts like &lt;em&gt;a link means scam&lt;/em&gt; and &lt;em&gt;an unknown sender means scam&lt;/em&gt;, which would have meant false alarms on real bank SMS. I fixed the generator to balance those cues and trained again. Total: &lt;strong&gt;about $3.80 of the $10 Tinker credit.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Results, on data it never saw
&lt;/h3&gt;

&lt;p&gt;I did &lt;strong&gt;not&lt;/strong&gt; dataset my parents' messages, so there is no family test set. Instead, the evaluation is &lt;strong&gt;100 public fraud reports&lt;/strong&gt;: scams and genuine messages documented by RBI, PIB, India's cybercrime advisories, news outlets, fact-checkers and court records, each with a source URL and verified verbatim on its page (65 scam, 35 genuine). The model never trains on them. 95% bootstrap CIs; scam recall counts any warning on a real scam.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Scam recall&lt;/th&gt;
&lt;th&gt;False alarms on genuine&lt;/th&gt;
&lt;th&gt;On alarming-but-genuine&lt;/th&gt;
&lt;th&gt;Macro-F1&lt;/th&gt;
&lt;th&gt;Valid JSON&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rules only&lt;/td&gt;
&lt;td&gt;0.43&lt;/td&gt;
&lt;td&gt;9%&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;td&gt;0.05&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E2B zero-shot&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;0.38&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B base, few-shot&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;43%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen3.5-4B + Tinker LoRA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.92 [0.86–0.98]&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.59&lt;/strong&gt; [0.55–0.63]&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full Rakshak (rules + fusion)&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;0.58&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The interesting number isn't the F1. It's the &lt;strong&gt;false alarms&lt;/strong&gt;. The base Qwen model with few-shot prompting catches every scam, but only by crying wolf. It flags 43% of genuine messages and &lt;strong&gt;89% of genuine-but-alarming ones&lt;/strong&gt; (real OTPs, real debit alerts), and only a third of its replies are even valid JSON. Fine-tuning the &lt;em&gt;same&lt;/em&gt; model on this one narrow task cuts false alarms on genuine messages to &lt;strong&gt;9%&lt;/strong&gt; and makes the output &lt;strong&gt;100% machine-usable&lt;/strong&gt;, while keeping recall at 0.92, on data it never trained on.&lt;/p&gt;

&lt;p&gt;That's the whole project in one row. Not missing scams is easy. Not terrifying your mom about a real OTP is the hard part, and that's exactly what a small, fine-tuned open model fixed.&lt;/p&gt;

&lt;p&gt;(Adding the rules on top nudges recall to 0.94 but raises genuine false alarms, because a few real government advisories trip the threat lexicon. I can see that trade-off in the numbers, which is the point of measuring.)&lt;/p&gt;

&lt;p&gt;These are small-sample numbers on public data, not a benchmark claim. The synthetic dev set is near-saturated and I don't report it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed on a laptop
&lt;/h3&gt;

&lt;p&gt;All three model servers run together on 18 GB. End to end:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Genuine&lt;/strong&gt; (detector only): p50 0.8 s, p95 0.9 s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scam&lt;/strong&gt; (detector + spoken explanation): p50 4.1 s, p95 11.1 s&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The detector dominates the scam path; the explainer adds about 1.6 s. The scam path is over my 6-second goal. Shorter red-flag output, constrained JSON on the first attempt, and streaming the verdict before the explanation are next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tracing without reading anyone's messages
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Sentry&lt;/strong&gt; gets one transaction per check and one span per graph node, so I can see where the seconds go and which model answered, but never the content. Span attributes come from an allowlist (node, model, token counts, latency, verdict, category, &lt;code&gt;p_scam&lt;/code&gt;, rule-signal names, retries, error type), and a scrubbing hook deletes any text, quote, explanation, image or audio field at any depth, including inside exception messages. Request bodies and local variables are never captured.&lt;/p&gt;

&lt;p&gt;I don't just trust that. A &lt;strong&gt;canary test&lt;/strong&gt; pushes a fake message with a unique marker through every input path of the real app and fails if the marker appears in any Sentry payload, ntfy request, log line or console output, or if the hub talks to anything but the local model servers and ntfy. I broke the scrubber on purpose three ways; the test caught all three.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Privacy that's literally true.&lt;/strong&gt; The messages my parents get are about their bank, their pension, their family. Asking a hosted API "is this a scam?" means one more stranger reads them. With open weights, the model comes to the data, and checks keep working with the Mac's internet off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A small specialist beats a big generalist here.&lt;/strong&gt; "Is this a scam, and which words prove it?" is narrow and structured. Open weights let me fine-tune a 4B model on exactly that and measure it against its own base: false alarms went from 43% to 9%, valid output from 33% to 100%. You can't do that with a model you can't fine-tune or run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open models can make the data, too.&lt;/strong&gt; Gemma generated the whole training set locally. No scraping, no labeling service, no one's private messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero marginal cost.&lt;/strong&gt; Each check costs electricity, so there's no reason to ration them. My parents can share every message that feels off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest caveat.&lt;/strong&gt; Training wasn't fully local: synthetic and redacted public examples went to Tinker's servers to train the adapter. No family message was ever part of it. At runtime, nothing leaves the house.&lt;/p&gt;

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

&lt;p&gt;I built Rakshak with Claude Code, working from a written requirements document that fixed the schemas, prompts, thresholds and privacy rules before any code existed. The agent did the heavy lifting in parallel (rules engine, hub pipeline, training and export, web app), and the most valuable thing it did was review its own work. Two independent review passes caught the detector throwing away &lt;em&gt;suspicious&lt;/em&gt; probability mass, and the synthetic-data shortcuts that would have caused false alarms on real messages. The judgment calls I kept for myself were the ones about honesty: never letting public reports become a "test set", and insisting the evaluation say plainly what data it actually ran on.&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 E2B generated the training set, reads screenshots and voice notes on-device, and writes every warning my parents hear, with a post-check that keeps it from ever changing the verdict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Tinker.&lt;/strong&gt; The verdict comes from Qwen3.5-4B fine-tuned with LoRA on Tinker, exported to a quantized GGUF, and measured against its own base model and Gemma on public data it never trained on, where fine-tuning cut genuine false alarms from 43% to 9% and raised valid structured output from 33% to 100%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Sentry Agent Tracing.&lt;/strong&gt; One trace per check and one span per LangGraph node, with a canary test proving no message text ever reaches Sentry.&lt;/li&gt;
&lt;/ul&gt;

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