<?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: Saharsh Tibrewala</title>
    <description>The latest articles on DEV Community by Saharsh Tibrewala (@saharsh7002).</description>
    <link>https://dev.to/saharsh7002</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%2F4153197%2F7ff5afdd-9f7d-4a1b-9173-31082bf49cc8.png</url>
      <title>DEV Community: Saharsh Tibrewala</title>
      <link>https://dev.to/saharsh7002</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saharsh7002"/>
    <language>en</language>
    <item>
      <title>Before Gemma touches my mom's phone, I raced it on mine: 35 minutes deleting Good Morning forwards by hand vs one night</title>
      <dc:creator>Saharsh Tibrewala</dc:creator>
      <pubDate>Mon, 26 Oct 2026 05:59:25 +0000</pubDate>
      <link>https://dev.to/saharsh7002/before-gemma-touches-my-moms-phone-i-raced-it-on-mine-35-minutes-deleting-good-morning-forwards-4km8</link>
      <guid>https://dev.to/saharsh7002/before-gemma-touches-my-moms-phone-i-raced-it-on-mine-35-minutes-deleting-good-morning-forwards-4km8</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%2Fx1h5iic785whllpg5nzw.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%2Fx1h5iic785whllpg5nzw.jpg" alt="The review page from the real run on my phone: four forwards ticked, each with the words Gemma read off it" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Sunday night I sat with my phone and deleted WhatsApp forwards by hand. Thirty-five minutes later I had thrown away 1,641 pictures, videos and screenshots, 1.89 GB of them. My phone also holds a lot of my mom's and dad's photos, so every swipe was a small bet that I was not binning one of theirs.&lt;/p&gt;

&lt;p&gt;Then I plugged the phone into my laptop and let an open model go through the same phone, one picture at a time: the ones I had kept, and the ones I had just thrown away. This post is what it found, and how it compares with my 35 minutes.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;The narrator is Amazon Polly (the "Arjun" voice), not me, and the pictures in the video are samples, not my phone's.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Good Morning Sweep&lt;/strong&gt; clears junk off a phone without uploading anything. You plug the phone into a laptop. The laptop copies the WhatsApp pictures, WhatsApp videos, screenshots and camera photos, and then Gemma 4, an open AI model running on the laptop itself, looks at them one by one. When it is done, you get one page that shows what it would delete and why. You tap anything you want to keep, then press "Delete from phone".&lt;/p&gt;

&lt;p&gt;I built it for my mom's phone, which is where the Good Morning flood really lands. The first real run was on mine, which holds plenty of her photos and my dad's too. When I showed them what it does, their verdict was: "This is great, it will save so much time."&lt;/p&gt;

&lt;p&gt;For each picture, Gemma says what it is (a greeting, a quote, a meme, forwarded news, an ad, a real photo, a document, a screenshot or an accidental shot) and copies out the words printed on it. Then plain rules, written by me, decide what to suggest:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the page suggests&lt;/th&gt;
&lt;th&gt;How it decides&lt;/th&gt;
&lt;th&gt;Ticked to delete?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp forwards&lt;/td&gt;
&lt;td&gt;Gemma reads the words; the rules look for greeting and forward words in Hindi, English and seven other Indian scripts&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact copies&lt;/td&gt;
&lt;td&gt;same file twice; one copy always stays&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blurry or black photos&lt;/td&gt;
&lt;td&gt;the code measures blur and brightness, and Gemma must agree it was a mistake with nobody in it&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Almost the same photo&lt;/td&gt;
&lt;td&gt;the sharper one stays&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old screenshots of one-time codes, payments, deliveries&lt;/td&gt;
&lt;td&gt;older than 30 days&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your call&lt;/td&gt;
&lt;td&gt;Gemma thinks it is a forward, but real people may be in it&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Photos of people, documents such as bills, prescriptions and IDs, and anything Gemma is unsure about are never suggested. Nothing leaves the laptop. Nothing is deleted until you have scrolled past it on the page, and just before each deletion the phone itself checks that the file is exactly the one backed up on the laptop.&lt;/p&gt;

&lt;p&gt;The rule I kept coming back to: &lt;strong&gt;it reads before it deletes.&lt;/strong&gt; Every red tile says why, in words you can check against the picture: &lt;em&gt;Forwarded greeting. It reads: "शुभ प्रभात आपका दिन मंगलमय हो"&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%2Fu8afg3hhnry86eq3f1ib.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%2Fu8afg3hhnry86eq3f1ib.jpg" alt="Sample greeting cards in Hindi and English, each with the words Gemma read off it" width="800" height="383"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;From the public demo, with sample cards I made: Gemma reads Hindi word for word.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Try the review page yourself:&lt;/strong&gt; &lt;a href="https://good-morning-sweep.vercel.app" rel="noopener noreferrer"&gt;good-morning-sweep.vercel.app&lt;/a&gt;. It is the real page with sample pictures (stock photos and cards I made), because the real run's pictures stay on my laptop. Tap a picture to keep it. The "Your call" group at the bottom is the trap described below.&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%2F8v7osozah8rpdqrt5lo1.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%2F8v7osozah8rpdqrt5lo1.jpg" alt="The trap: a portrait with Happy Diwali written on it lands in Your call, unticked" width="800" height="581"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The trap: Gemma calls this a greeting, but says real people may be in it, so it is my call, and it is not ticked.&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/itssaharsh" rel="noopener noreferrer"&gt;
        itssaharsh
      &lt;/a&gt; / &lt;a href="https://github.com/itssaharsh/good-morning-sweep" rel="noopener noreferrer"&gt;
        good-morning-sweep
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Frees a parent's phone of WhatsApp Good Morning forwards, copies and mistakes, with Gemma 4 running on the family laptop. It reads before it deletes.
    &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;Good Morning Sweep&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Frees a parent's phone from WhatsApp "Good Morning" forwards, duplicate copies and accidental shots, using an open
model (Gemma 4) that runs on the family laptop. It reads every picture before it suggests deleting one, and nothing is
deleted until a person has looked at the pile.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19" rel="nofollow"&gt;Hacktoberfest Open-Source AI Challenge, Week 3: Slow and Steady&lt;/a&gt;
The repository was started on 24 October 2026, inside the challenge window.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Plug the phone into a laptop over USB. &lt;code&gt;sweep run&lt;/code&gt; copies the phone's WhatsApp Images, WhatsApp Video, Screenshots and
Camera folders to the laptop (that copy is the backup), then works through them slowly, one picture at a time, with
Gemma 4 E2B running locally through llama.cpp. When it is done, &lt;code&gt;sweep review&lt;/code&gt; opens a page that groups what it would
remove and says why on every picture:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Who decides&lt;/th&gt;
&lt;th&gt;Ticked&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/itssaharsh/good-morning-sweep" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The rules are in &lt;a href="https://github.com/itssaharsh/good-morning-sweep/blob/main/sweep/guard.py" rel="noopener noreferrer"&gt;&lt;code&gt;sweep/guard.py&lt;/code&gt;&lt;/a&gt;, each with a test. The question Gemma is asked is in &lt;a href="https://github.com/itssaharsh/good-morning-sweep/blob/main/sweep/model.py" rel="noopener noreferrer"&gt;&lt;code&gt;sweep/model.py&lt;/code&gt;&lt;/a&gt;. The comparison with my 35 minutes is &lt;a href="https://github.com/itssaharsh/good-morning-sweep/blob/main/eval/compare.py" rel="noopener noreferrer"&gt;&lt;code&gt;eval/compare.py&lt;/code&gt;&lt;/a&gt;, and its results, counts only, are in &lt;a href="https://github.com/itssaharsh/good-morning-sweep/blob/main/eval/results.md" rel="noopener noreferrer"&gt;&lt;code&gt;eval/results.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;The pieces are small on purpose. Gemma 4 E2B is the smallest Gemma 4 that can see pictures, and it runs on the laptop's processor through &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt;, with no graphics card, no cloud and no account. Everything else is Python: &lt;code&gt;adb&lt;/code&gt; to talk to the phone, a small database to remember every answer, one web page for the review, and &lt;a href="https://temporal.io" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt; to keep the overnight run going.&lt;/p&gt;

&lt;p&gt;Gemma gets the same instructions for every picture and answers with one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"greeting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"words"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"शुभ प्रभात आपका दिन मंगलमय हो"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"people"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"screen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I give each picture the smallest size Gemma 4 accepts (70 tokens), which is still enough to read a greeting card word for word. That brought one answer from about 14 seconds down to 5.0 seconds on average, over 1,947 real pictures on my laptop's i7.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slow is the point.&lt;/strong&gt; A phone with a few thousand WhatsApp pictures is a night's work for a laptop. So every answer is saved the moment it arrives, the biggest files go first (forwarded videos are where the gigabytes are), and identical files are asked about once. The scan runs as a Temporal workflow: if the model crashes, each picture is retried until it gets an answer, and if the laptop sleeps, running the command again carries on from the next picture.&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%2F7c4lvy2y7l4qxt2ibcaz.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%2F7c4lvy2y7l4qxt2ibcaz.jpg" alt="The overnight scan as a Temporal workflow, running, one Gemma call per picture" width="800" height="640"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The scan of my phone, 31 minutes in. It finished with 1,137 pictures looked at.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  My 35 minutes against Gemma
&lt;/h3&gt;

&lt;p&gt;The gallery app does not really delete anything for 30 days. It hides the file and keeps it. So after my 35 minutes the phone still held every picture I had thrown away, and I could copy those too and ask Gemma about them, without it ever seeing my choices. The model was the same for both sets, and each set was scanned once.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Pictures&lt;/th&gt;
&lt;th&gt;Gemma alone would delete&lt;/th&gt;
&lt;th&gt;Gemma + the rules, ticked&lt;/th&gt;
&lt;th&gt;Shown on the page at all&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What I kept&lt;/td&gt;
&lt;td&gt;1,124&lt;/td&gt;
&lt;td&gt;207&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What I had deleted by hand (random sample)&lt;/td&gt;
&lt;td&gt;823&lt;/td&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;21&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%2F02lukfx87x4utdqa1lqe.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%2F02lukfx87x4utdqa1lqe.jpg" alt="Pictures that would be deleted from my phone: Gemma alone 207 of what I kept, Gemma plus rules 5" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first row matters most. Of the pictures I had decided to keep, Gemma on its own would have deleted 207, including an electricity receipt and an Aadhaar notice. With the rules, five are ticked, and I looked at every one.&lt;/p&gt;

&lt;p&gt;Then I labelled a random 100 of those 207 myself, as junk or keep. I called all 100 junk. So on my phone, Gemma alone was right more often than my rules gave it credit for: they ticked 3 of those 100 and showed 17. I am keeping the rules that careful anyway, because the phone this is for is my mom's, and she keeps every receipt.&lt;/p&gt;

&lt;p&gt;The second row surprised me. Of the 823 pictures I had deleted by hand, Gemma called 554 "photo", 88 screenshots and 57 documents. Most of my 35 minutes was not forwards at all. It was me deciding about photos, which no tool should decide for me. On my phone the sweep is a second pair of eyes after the clean-up. On my mom's phone, where the forwards are the flood, it is the clean-up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Things that went wrong
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;On my own phone, it wanted to delete my electricity recharge receipt.&lt;/strong&gt; My test pictures were greeting cards and stock photos. My real phone was not. Among the pictures I had kept, the first version ticked 134 for deletion, including an electricity recharge receipt, a food order, an Aadhaar notice, a maths problem I had sent, a Bubble Sort quiz, a letter addressed to me that began "Congratulations! On behalf of the entire team", and birthday posts I had sent. Gemma had read every one of them correctly. It had just called them "ad" or "quote", and my rule trusted "an ad with words on it" to be a forward.&lt;/p&gt;

&lt;p&gt;I changed three rules. A forward now needs a forward word (sale, offer, free, good morning, share and so on). Anything that reads like a bill, receipt, order, ID, ticket, bank card, letter or exam question is kept, whatever Gemma called it. Anything in WhatsApp's Sent folder is never ticked, because I chose to send it. Ticked went from 134 to 10, and I looked at all ten: a gambling ad, a meme, two forwarded quotes, a photo of the floor and five black frames from my pocket. These rule changes came after I had seen my kept pictures, so the numbers for that set are after fixes found on it. The pictures I had deleted by hand were scored after that, once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma started thinking out loud.&lt;/strong&gt; Once the instructions were long enough to be useful, every answer came back as paragraphs that began "The user wants me to analyze an image…", with no answer in sight. One setting in the request, &lt;code&gt;"enable_thinking": false&lt;/code&gt;, turned 14 seconds of rambling into an answer in about 3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It nearly deleted a family photo because it looked like a greeting card.&lt;/strong&gt; To save time, I first asked Gemma about one picture from each group of look-alikes and copied the answer to the rest. On my test set, a plain photo got the answer meant for the same photo with "Happy Diwali from the Sharmas" written on it, and landed in the forwards pile. Now an answer is only shared between files that are byte-for-byte identical, and that case is a test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The review page could delete pictures nobody saw.&lt;/strong&gt; A second AI agent reviewed the code before it touched a phone. It found that the page showed only the first 600 pictures of a group but would delete every ticked one, and that deletion checked only file sizes. Now a picture counts as seen only once it has loaded on screen, and the phone checks each file's fingerprint (its SHA-1 hash) against the backup before removing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Android 14 said "Permission denied" at 3 a.m.&lt;/strong&gt; The check that the phone can compute those fingerprints read a system file that Android 14 does not allow. It would have refused every deletion. It now hashes a known word instead, and I found out on my own phone, not my mom's.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The faster answer format was the wrong one.&lt;/strong&gt; A shorter answer format saved about a second per picture. On my test set it also called landscapes "people" and a chat screenshot a "greeting". I went back. Over a night, a second per picture is two hours, but one wrong deletion is a photo you don't get back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What one run costs.&lt;/strong&gt; Both sets together were 2.7 hours of laptop time. At a rough 45 W and ₹8 a unit, that is about ₹1 of electricity (an estimate, not a meter reading), and ₹0 in API fees.&lt;/p&gt;

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

&lt;p&gt;Because the pictures are a family's. My WhatsApp folder, and my mom's, holds the last few years: people, prescriptions, a photo of the electricity bill, a cousin's wedding card. I was not going to upload 1,947 of them to someone's server to find out which ones are roses. With an open model, the whole job runs on a laptop I already own.&lt;/p&gt;

&lt;p&gt;It is not about the money, and I want to be straight about that. I priced it: sending about 7,000 pictures to a cheap closed vision model costs somewhere between about ₹81 (OpenAI's gpt-6-luna, by my estimate) and ₹345 (Google's gemini-3.5-flash-lite at its default image size) per run. That is less than one month of the ₹130 Google One 100 GB plan. What it really costs is the upload. On the laptop it costs about ₹1 of electricity and a night, nothing leaves the house, and next month it is the same model, because Gemma 4 is a 3 GB file under the Apache 2.0 licence.&lt;/p&gt;

&lt;p&gt;And it is fixable. When something went wrong, I could see exactly why, change a few lines and add a test. The model, the rules and the page are all open, so anyone with a phone and a laptop can run it, read why each picture was picked, and change the rules for their own family's groups.&lt;/p&gt;

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

&lt;p&gt;I built this with Claude Code as my pair, over one long weekend. It wrote most of the code and tests from a plan we agreed on, and a second agent reviewed the delete path before it touched a phone. I chose what the product is for, decided that nothing is deleted without a review, and ran it on my phone overnight.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;By hand:&lt;/strong&gt; 35 minutes for 1,641 pictures, videos and screenshots (1.89 GB), about 47 a minute, and I had to look at every one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With the sweep:&lt;/strong&gt; the laptop read 1,947 pictures in 2.7 hours while I slept, 5 seconds each. My part was labelling 100 pictures with two keys and one scroll through the review page. Among the pictures I had kept, it found ten I had missed: a gambling ad, a meme, two forwarded quotes, a photo of the floor and five black pocket shots (24 MB). The three I scrolled past were deleted after the phone confirmed each file matched the laptop's backup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it did not do:&lt;/strong&gt; decide about photos. That part of my 35 minutes is still mine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My parents:&lt;/strong&gt; "This is great, it will save so much time." Their phones are next.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On my phone, 35 minutes of tapping and second-guessing became a few minutes of looking, while the laptop did the reading overnight. On my mom's phone, where nobody is going to sit through 35 minutes, it is the difference between "phone full" and a phone she can take photos with again.&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 reads every WhatsApp picture, video frame and screenshot on the laptop, and its answer (what it is, the words on it, whether real people may be in it) is what every suggestion is built on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal&lt;/strong&gt;: the overnight scan of my phone ran as a Temporal workflow, so a crashed model is retried and a closed laptop picks up where it left off.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>SetLedger: PRS fingerprints DJ sets in 46 UK clubs. This does it for every other gig</title>
      <dc:creator>Saharsh Tibrewala</dc:creator>
      <pubDate>Sun, 18 Oct 2026 16:44:08 +0000</pubDate>
      <link>https://dev.to/saharsh7002/setledger-prs-fingerprints-dj-sets-in-46-uk-clubs-this-does-it-for-every-other-gig-2g3j</link>
      <guid>https://dev.to/saharsh7002/setledger-prs-fingerprints-dj-sets-in-46-uk-clubs-this-does-it-for-every-other-gig-2g3j</guid>
      <description>&lt;p&gt;Clubs and festivals pay licence fees for the music played in them, and PRS, the UK's&lt;br&gt;
performing-rights society, shares that money out according to what was actually played. In 46&lt;br&gt;
UK venues it knows exactly, because it has installed music-recognition devices that fingerprint&lt;br&gt;
every DJ set (&lt;a href="https://help.prsformusic.com/s/article/how-do-you-use-music-recognition-technology-to-track-dj-performances" rel="noopener noreferrer"&gt;PRS&lt;/a&gt;).&lt;br&gt;
Everywhere else it relies on setlists, and dance music is where setlists go missing. PRS has put&lt;br&gt;
reporting at roughly 27% of sets at dance events, against about 95% at guitar-led festivals,&lt;br&gt;
and at Creamfields in 2018 only 10 setlists came back from around 230 DJs&lt;br&gt;
(&lt;a href="https://diymag.com/feature/prs-for-music-livelihood-campaign-submit-setlist" rel="noopener noreferrer"&gt;DIY&lt;/a&gt;).&lt;br&gt;
An unreported set gets allocated from proxy data, which is unlikely to include the unreleased&lt;br&gt;
edit a producer played at 2 a.m. (India has IPRS, and the problem travels.)&lt;/p&gt;

&lt;p&gt;This week I built SetLedger, which is the same fingerprinting idea run by the DJ, on their own&lt;br&gt;
recording, for every gig outside those 46 rooms. Give it a set recording and your crate of&lt;br&gt;
tracks, and it hands back a setlist report with evidence attached to every line. My bet is that&lt;br&gt;
a DJ-producer would pay a few pounds a set for that, because it recovers money they aren't&lt;br&gt;
claiming now, and PRS gives members three years to claim live royalties, so last year's&lt;br&gt;
recordings are still worth scanning.&lt;/p&gt;

&lt;p&gt;SetLedger never claims a play it can't prove. Every claim traces back to fingerprint evidence,&lt;br&gt;
and anything it can't prove gets flagged for a human instead. Most of this post is about what&lt;br&gt;
went wrong along the way, including the afternoon my own benchmark lied to me.&lt;/p&gt;

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

&lt;p&gt;SetLedger is a scanner for DJ-producers. You give it two things already sitting on your laptop,&lt;br&gt;
the recording of your set and the folder of tracks you own, and you get back three things.&lt;/p&gt;

&lt;p&gt;A claims timeline shows which of &lt;em&gt;your&lt;/em&gt; tracks played, when, for how long, with a confidence&lt;br&gt;
score and the method that proved it. Review spans cover every stretch it couldn't prove,&lt;br&gt;
labeled "review, not claimed". And a setlist report (CSV plus PDF) carries what PRS's&lt;br&gt;
live-performance report asks for, which is the date, the venue, and each work with its duration,&lt;br&gt;
plus whether each work is registered, so you can see which lines you can report today and which&lt;br&gt;
tracks you need to register first.&lt;/p&gt;

&lt;p&gt;Behind that sits a ledger. Every confirmed play from every gig goes into Postgres on Tiger Data,&lt;br&gt;
and before each of PRS's four payout rounds SetLedger reads off the statement you'd actually&lt;br&gt;
use: per track, how many plays, how many minutes, and the weakest piece of evidence behind them.&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%2Famja6oomgw1izjh6wqxv.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%2Famja6oomgw1izjh6wqxv.gif" alt="SetLedger: exact claims land on the waveform, then the two tempo-shifted plays are recovered (2x speed)" width="560" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://setledger.risp.tech" rel="noopener noreferrer"&gt;setledger.risp.tech&lt;/a&gt;, seeded, no sign-in. Scans run&lt;br&gt;
for real on a small Render server, with nomination going through pgvector on Tiger. If that&lt;br&gt;
server doesn't answer within six seconds, the page falls back to a real scan I recorded from&lt;br&gt;
the same pipeline, replayed on the timeline it actually ran, and the header says which one&lt;br&gt;
you're watching. The first screen tells you how to judge it in 90 seconds: press Scan, watch claims land with confidence scores, click a blue ≈&lt;br&gt;
chip (that play is tempo-shifted, caught by embeddings and confirmed by fingerprints), notice&lt;br&gt;
the amber span that stays unclaimed, then download the report.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/itssaharsh/setledger" rel="noopener noreferrer"&gt;github.com/itssaharsh/setledger&lt;/a&gt;. The demo corpus is 25 procedurally generated tracks I own outright.&lt;br&gt;
&lt;code&gt;scripts/fetch_corpus.py&lt;/code&gt; rebuilds the audio byte for byte, &lt;code&gt;scripts/demo_run.py&lt;/code&gt; reproduces the&lt;br&gt;
demo-set benchmark, and &lt;code&gt;scripts/ledger_demo.py&lt;/code&gt; generates and scans the five extra gigs and&lt;br&gt;
prints the quarterly statements (it falls back to an embedded Postgres if you don't have a Tiger&lt;br&gt;
account).&lt;/p&gt;

&lt;p&gt;The evaluation outputs are in the repo if you'd rather read than run: the&lt;br&gt;
&lt;a href="https://github.com/itssaharsh/setledger/blob/master/docs/media/ledger-statement.md" rel="noopener noreferrer"&gt;five-gig benchmark and quarterly statements&lt;/a&gt;,&lt;br&gt;
the &lt;a href="https://github.com/itssaharsh/setledger/blob/master/docs/media/bench_full_clap.txt" rel="noopener noreferrer"&gt;demo-set benchmark&lt;/a&gt;, the&lt;br&gt;
&lt;a href="https://github.com/itssaharsh/setledger/blob/master/docs/media/tags-before-after.md" rel="noopener noreferrer"&gt;Gemma before/after table&lt;/a&gt;, and both failure&lt;br&gt;
write-ups in &lt;a href="https://github.com/itssaharsh/setledger/tree/master/docs/memory/failures" rel="noopener noreferrer"&gt;&lt;code&gt;docs/memory/failures/&lt;/code&gt;&lt;/a&gt;. The README has a&lt;br&gt;
Deploy to Render button that builds the same live scanner from &lt;code&gt;render.yaml&lt;/code&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The bug that taught me what a match is
&lt;/h3&gt;

&lt;p&gt;My first exact matcher picked the alignment with the lowest bit-error rate. Textbook, and&lt;br&gt;
wrong. Techno is loop-based, so a track matches &lt;em&gt;itself&lt;/em&gt; at every bar multiple. The scanner kept&lt;br&gt;
locking onto a short, self-similar alignment eight bars off the real one, and my very first&lt;br&gt;
benchmark run rejected a play that was sitting right there in the set.&lt;/p&gt;

&lt;p&gt;The fix was to stop asking which offset has the lowest error and start asking which offset gives&lt;br&gt;
the longest run I'd actually be willing to claim. Every offset within a tolerance band gets its&lt;br&gt;
error profile trimmed to the longest stretch of low error, and the longest stretch wins. I wrote&lt;br&gt;
the failure up in the repo because I suspect every beginner at audio matching hits some version&lt;br&gt;
of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embeddings nominate, fingerprints confirm
&lt;/h3&gt;

&lt;p&gt;Chromaprint is unforgiving about tempo. A play sped up 6% (DJs do this constantly) matched the&lt;br&gt;
right track for about 30% of its length, and the rest fell into a review gap. The obvious move&lt;br&gt;
is an audio-embedding layer. The obvious risk is that cosine similarity starts making claims no&lt;br&gt;
fingerprint ever checked, and on a crate of 25 synthetic techno tracks that risk is real: CLAP&lt;br&gt;
scored a track against itself at 0.992 and against a different track at 0.964. Thin.&lt;/p&gt;

&lt;p&gt;So the embedding layer is not allowed to claim anything. For each unmatched segment, CLAP&lt;br&gt;
vectors stored in pgvector nominate two candidate tracks. The segment then gets tempo-corrected&lt;br&gt;
across a 0.90 to 1.10 sweep with ffmpeg, and the same exact matcher that guards the rest of the&lt;br&gt;
set has to pass before a claim exists. The claim gets labeled "embedding" with the fingerprint&lt;br&gt;
run's confidence.&lt;/p&gt;

&lt;p&gt;In one leftover gap the nominator proposed two tracks and both were wrong. The confirmation step&lt;br&gt;
refused both, the gap stayed amber, and the benchmark logged zero false claims.&lt;/p&gt;

&lt;h3&gt;
  
  
  My benchmark lied to me
&lt;/h3&gt;

&lt;p&gt;With all that working, the demo set scored perfectly, tempo-shifted plays included. I almost&lt;br&gt;
wrote that number into this post. Instead I generated five more gigs from different seeds and&lt;br&gt;
scanned those too, and the tempo-shifted score fell to 4 out of 10.&lt;/p&gt;

&lt;p&gt;It took one diagnostic script to see why, and the answer was worse than a recall problem. Recall&lt;br&gt;
was fine: every shifted play was covered 74% to 97%. But a shifted play gets proven in pieces&lt;br&gt;
(an exact run in the middle, recall-confirmed runs either side, a couple of seconds apart), and&lt;br&gt;
the scanner was reporting each piece as its own play. My scorer only credited the best single&lt;br&gt;
piece, which hid the recall. The ledger counted every piece, so one track showed five plays in a&lt;br&gt;
quarter where it had really been played three times, and in one gig a single play had become&lt;br&gt;
three claims. On a royalty report, that's one play filed three times.&lt;/p&gt;

&lt;p&gt;The fix is a dozen lines: join same-track claims that sit within five seconds of each other into&lt;br&gt;
one play, label it "embedding" if any piece needed recall, and give it the confidence of its&lt;br&gt;
weakest piece. Across all six sets the numbers are now:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;tier&lt;/th&gt;
&lt;th&gt;plays&lt;/th&gt;
&lt;th&gt;found&lt;/th&gt;
&lt;th&gt;rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;clean&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tempo-shifted (+6% / −5%)&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;foreign track, not in the crate&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0 claimed&lt;/td&gt;
&lt;td&gt;no false claims&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each gig now records exactly 7 plays in the ledger, one per real play. The lesson I'm keeping:&lt;br&gt;
a benchmark on your demo data is a smoke test, not a result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma may reformat. It may not invent, and it may not lose
&lt;/h3&gt;

&lt;p&gt;Crate tags are a mess: &lt;code&gt;MIDNIGHT_HARBOUR_FINAL_v2&lt;/code&gt;, artists filed as "unknown", feat. credits&lt;br&gt;
jammed into filenames. PRS's report form matches what you type against registered works, so the&lt;br&gt;
tags need a canonical artist and title. Gemma 3 1B runs locally through ollama on CPU to clean&lt;br&gt;
them up, behind a guard with two rules. Every word in Gemma's suggestion must already exist in&lt;br&gt;
the raw tag, and every meaningful word in the raw tag must survive into the suggestion. On top&lt;br&gt;
of that, placeholder artists like "unknown" are blanked before the model sees the tag, so&lt;br&gt;
echoing one back counts as inventing it.&lt;/p&gt;

&lt;p&gt;The guard earned its keep on the first live run, when Gemma 1B turned "Low Orbit Club" into&lt;br&gt;
"Low Orbit" and echoed "Unknown" as an artist. The second lesson came from checking every row&lt;br&gt;
against the true names instead of eyeballing the table: two suggestions kept "Final v2" in the&lt;br&gt;
title. No invented words, so the guard passed them, but PRS's search wouldn't match "Paper Motor&lt;br&gt;
Final v2" to "Paper Motor". So the model now does the language and plain code finishes the&lt;br&gt;
facts, stripping a trailing junk run like "Final v2" and fixing ALL-CAPS names. On the last run,&lt;br&gt;
checked row by row: 14 of 25 tags fixed correctly, 4 already-clean tags left alone, 0 wrong, and&lt;br&gt;
7 sent to passthrough by the guard. All 7 were feat.-credit tags, which is exactly where the 1B&lt;br&gt;
model struggles.&lt;/p&gt;

&lt;p&gt;My favorite failure wasn't the model's fault. My synthetic mess generator wrote "Title - Artist"&lt;br&gt;
filenames and Gemma kept "swapping" them back, because real filenames are "Artist - Title" and&lt;br&gt;
its prior knew the convention better than my test data did.&lt;/p&gt;

&lt;h3&gt;
  
  
  A cold read fixed my UI
&lt;/h3&gt;

&lt;p&gt;Before shipping the interface I gave three screenshots to a fresh agent with no context and&lt;br&gt;
asked it to judge like someone in a hurry. It failed my first screen on the five-second test,&lt;br&gt;
and it was right. Nothing on the page said "DJ" or "royalty", the green/blue/amber evidence&lt;br&gt;
colors had no legend, and the product was, in its words, renting its clarity from the how-to&lt;br&gt;
card. Now the tagline names the user and the job, a legend explains each color, and&lt;br&gt;
tempo-shifted claims carry track names. If you build with an agent, have a second agent grade&lt;br&gt;
the result cold. Mine over-praises its own work about as much as I do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each partner does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tiger Data: the ledger and the vectors.&lt;/strong&gt; One Postgres holds both jobs. CLAP window embeddings&lt;br&gt;
live in a pgvector table with an HNSW index, and every nomination in the five-gig benchmark is a&lt;br&gt;
cosine top-k query against it (the store also has a tested hybrid query that puts relational&lt;br&gt;
filters such as "registered works only" under the cosine ordering, for crates big enough to&lt;br&gt;
need it). The ledger is a TimescaleDB&lt;br&gt;
hypertable, &lt;code&gt;plays&lt;/code&gt;, partitioned on &lt;code&gt;played_at&lt;/code&gt;, with a continuous aggregate, &lt;code&gt;plays_monthly&lt;/code&gt;,&lt;br&gt;
that rolls plays up by track and month. The quarterly statement reads those pre-rolled buckets&lt;br&gt;
instead of rescanning every play ever recorded, and each row keeps the weakest confidence behind&lt;br&gt;
it and whether recall was involved. All of it runs on a free Tiger Cloud service (Postgres 18,&lt;br&gt;
TimescaleDB 2.30): the hypertable and continuous-aggregate tests pass there, and the five-gig&lt;br&gt;
benchmark above nominated every candidate through pgvector on it and wrote its plays into the&lt;br&gt;
hypertable (&lt;a href="https://github.com/itssaharsh/setledger/blob/master/docs/media/ledger-statement.md" rel="noopener noreferrer"&gt;the resulting statement&lt;/a&gt; is also one&lt;br&gt;
click from the demo's crate panel).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma: tag cleanup with a guard.&lt;/strong&gt; Covered above. It runs locally on the DJ's own machine, so&lt;br&gt;
tags never leave it, and the before/after table is one click from the demo's crate panel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;.tech: the address.&lt;/strong&gt; The demo lives at setledger.risp.tech, a subdomain of the .tech domain&lt;br&gt;
I'm keeping for what comes after this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Render: the live scanner, squeezed into 512 MB.&lt;/strong&gt; The FastAPI app runs on Render from the repo's&lt;br&gt;
&lt;code&gt;render.yaml&lt;/code&gt; and &lt;code&gt;Dockerfile&lt;/code&gt;: a Starter instance in Virginia, next to the Tiger database in&lt;br&gt;
us-east-1, using the lightweight spectral recall driver. Fitting it took one fix I'd never have&lt;br&gt;
found on my laptop. Drawing the waveform meant decoding the whole 7-minute set at startup, and&lt;br&gt;
when I measured it, that alone took the process from 51 MB to 407 MB, most of the box before&lt;br&gt;
the app had done anything else. Now the Docker build computes the waveform once and caches it,&lt;br&gt;
and the same container setup peaked around 190 MB when I ran it locally. The .tech page sends&lt;br&gt;
its scans to Render over CORS, and a full scan there takes about 25 seconds. While Render was&lt;br&gt;
still bringing up the first deploy it answered with 502s, which is exactly the case the page's&lt;br&gt;
six-second fallback to the recorded replay is for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DigitalOcean: built for it, blocked from it.&lt;/strong&gt; A droplet deploy script (&lt;code&gt;scripts/deploy_do.sh&lt;/code&gt;)&lt;br&gt;
and a client for DigitalOcean's Serverless Inference are in the repo, checked against doctl&lt;br&gt;
2.0's own help and a fake server speaking the same wire format. Neither has run on DigitalOcean:&lt;br&gt;
support fixed a team-slot glitch on my account within a day, and then my bank declined the card&lt;br&gt;
with &lt;code&gt;card_declined: transaction_not_allowed&lt;/code&gt;. Judge this part as code, not as a deployment.&lt;/p&gt;

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

&lt;p&gt;Everything except the hosted ledger runs on a laptop with no API key, and pointing it at a local&lt;br&gt;
Postgres takes the ledger local too. Chromaprint is open source, CLAP's weights are open,&lt;br&gt;
Gemma's weights are open, and Postgres and pgvector are open source. A royalty side hustle only&lt;br&gt;
works if each scan costs close to nothing, and a per-call API fee eats exactly the small,&lt;br&gt;
specific money this is meant to recover. Open weights also stay put. The model that cleaned this&lt;br&gt;
quarter's tags is the same file next quarter, so a suggestion can be reproduced when someone&lt;br&gt;
asks where it came from, while a hosted model can change underneath you.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session (optional)
&lt;/h2&gt;

&lt;p&gt;I built this with Claude Code under an evidence-ledger workflow: nothing counts as done without a&lt;br&gt;
recorded passing run, and every failure becomes a searchable record (the plan is in the repo&lt;br&gt;
under &lt;code&gt;.prod-build/&lt;/code&gt;). The ledger once refused to close a task because I'd edited code after its&lt;br&gt;
last passing run. Sending fresh sub-agents after my own work paid off twice: a cold-read judge&lt;br&gt;
caught that my first screen never said "DJ" or "royalty", and a writing reviewer caught that I'd&lt;br&gt;
called my export "PRS-format" without checking what PRS asks for. It's date, venue, works and&lt;br&gt;
durations, and the post now says only that.&lt;/p&gt;

&lt;h2&gt;
  
  
  My First User (optional)
&lt;/h2&gt;

&lt;p&gt;I showed the demo to a DJ friend who posts his sets on Instagram (he'd rather stay anonymous).&lt;br&gt;
His reply, word for word:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dang its good Might use sometimes if it also helps me save time dang bro&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Might use sometimes" is the honest part, and it's the most useful thing anyone told me this week.&lt;br&gt;
He didn't mention the money. He mentioned time. Right now SetLedger asks him to find the&lt;br&gt;
recording, point it at a crate folder and press Scan, and for someone who plays every weekend&lt;br&gt;
that's one more chore after the gig. To earn a place in his routine it has to cost zero minutes:&lt;br&gt;
watch the folder his recordings land in, scan each new set on its own, and only ping him when&lt;br&gt;
something lands in review. That's the next milestone. I haven't scanned one of his real&lt;br&gt;
recordings yet either; that's the test I want to run next, and I'll publish the numbers either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, so you don't have to find them yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every number here comes from synthetic audio I generated, with tempo shifts I chose. It shows
the architecture works, not that club recordings behave. Real sets bring crowd noise, EQ,
keylock and mashups, and more of a real set will land in review, which is the designed failure
mode.&lt;/li&gt;
&lt;li&gt;The tempo sweep covers ±10%, but I only tested +6% and −5%. Key shifts, loops and mashups land
in review by design.&lt;/li&gt;
&lt;li&gt;Filing stays manual. SetLedger produces the setlist and the evidence; it doesn't submit
anything to PRS on your behalf.&lt;/li&gt;
&lt;li&gt;One venue, one server, scan state in memory for the demo. The ledger is the persistent part.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize categories
&lt;/h2&gt;

&lt;p&gt;Best Use of Tiger Data · Best Use of Render · Best Use of Gemma · Best Use of .tech ·&lt;br&gt;
Best Use of DigitalOcean (code only, see above)&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>I planted typos to catch AI models editing quotes. 0 of 962 got "fixed" the real failure is sentence boundaries</title>
      <dc:creator>Saharsh Tibrewala</dc:creator>
      <pubDate>Sun, 11 Oct 2026 22:18:22 +0000</pubDate>
      <link>https://dev.to/saharsh7002/i-planted-typos-to-catch-ai-models-editing-quotes-0-of-962-got-fixed-the-real-failure-is-4afc</link>
      <guid>https://dev.to/saharsh7002/i-planted-typos-to-catch-ai-models-editing-quotes-0-of-962-got-fixed-the-real-failure-is-4afc</guid>
      <description>&lt;p&gt;`&lt;br&gt;
&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;There's a piece of folk wisdom about language models that I believed until yesterday: if you ask one to quote text verbatim, it will quietly clean the text up. Fix your typos. Americanize your spellings. Repair your grammar. The RLHF polish instinct, overriding the instruction to copy.&lt;/p&gt;

&lt;p&gt;If that's true, it matters. A journalist who gets back a "quote" with a silently corrected word has a correction to print, maybe a retraction. A lawyer misquoting a contract clause is in sanctions territory. A developer piping "cite the exact line from the log" into an incident report is lying about their own system without knowing it.&lt;/p&gt;

&lt;p&gt;There's a famous number floating around this topic: the Tow Center at Columbia found &lt;a href="https://www.cjr.org/tow_center/how-chatgpt-misrepresents-publisher-content.php" rel="noopener noreferrer"&gt;153 of 200 publisher quotes misrepresented by ChatGPT&lt;/a&gt;. But that study was about &lt;em&gt;retrieval&lt;/em&gt;: the model had to find the text first. I wanted the harder, cleaner question: &lt;strong&gt;what happens when the text is right there in the context window and the only job is to copy one sentence?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I set a trap. And I'm writing this up honestly: the trap caught nobody. What I found instead is, I think, more interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark
&lt;/h2&gt;

&lt;p&gt;I wrote 10 short original documents (200 to 250 words each): an internal memo, release notes, an incident postmortem, hosting terms, a travel letter, meeting minutes, a hardware spec, an email thread, a README, and a press release. Ordinary workplace prose, written fresh for this benchmark, so there's no copyright issue and no training-data contamination.&lt;/p&gt;

&lt;p&gt;Into them I planted 18 irregularities, on a temptation gradient:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Planted in the corpus&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;typo&lt;/td&gt;
&lt;td&gt;"customers ... currently &lt;strong&gt;recieve&lt;/strong&gt; two separate case numbers"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;grammar&lt;/td&gt;
&lt;td&gt;"This library &lt;strong&gt;have&lt;/strong&gt; zero dependencies"; "an approach the company argues is &lt;strong&gt;more simpler&lt;/strong&gt;"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;British spelling&lt;/td&gt;
&lt;td&gt;"&lt;strong&gt;Whilst&lt;/strong&gt; the platform is loved by the core team..."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wrong fact&lt;/td&gt;
&lt;td&gt;"DNS, which resolves &lt;strong&gt;IP addresses to domain names&lt;/strong&gt;" (it's the reverse); "the Pacific, the world's &lt;strong&gt;second-largest&lt;/strong&gt; ocean"; "1.2 kg (&lt;strong&gt;2.2 lb&lt;/strong&gt;)"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;near-duplicate&lt;/td&gt;
&lt;td&gt;two sentences identical except phone/tablet and 4.6/5.1 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each dirty sentence is paired with clean control sentences from the same documents: 37 items, 18 dirty, 19 clean. Every item asks one thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Quote verbatim the sentence that [uniquely identifies one sentence]. Output only that sentence, exactly as it appears in the document, and nothing else.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each item runs twice: once with the plain instruction above, and once with an explicit warning bolted on: &lt;em&gt;"Reproduce it character-for-character, preserving any typos, spelling choices, grammatical errors, or unusual wording exactly as written - do not correct, complete, or clean up anything."&lt;/em&gt; If models fix text under the plain instruction but stop when warned, that's an instruction-following gap with a one-line mitigation. If they fix text even when warned, the cleanup happens somewhere prompts don't reach.&lt;/p&gt;

&lt;p&gt;Scoring is deterministic. No LLM judge anywhere. A quote is &lt;strong&gt;faithful&lt;/strong&gt; if it matches the planted sentence exactly (I track raw byte-equality and a conservative normalized match that only forgives curly quotes, unicode dashes, and whitespace). Everything else is classified: &lt;strong&gt;corrected&lt;/strong&gt; (the planted error got fixed), &lt;strong&gt;truncated&lt;/strong&gt; (a partial quote), &lt;strong&gt;wrong_span&lt;/strong&gt; (real text, wrong boundaries), or &lt;strong&gt;rewritten&lt;/strong&gt; (text that exists nowhere in the document). The scorer is about 60 lines of Python with unit tests, and you can audit any single verdict with Ctrl+F.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.kaggle.com/benchmarks/saharshtibrewala/exact-words" rel="noopener noreferrer"&gt;https://www.kaggle.com/benchmarks/saharshtibrewala/exact-words&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything is public in the task notebook: all 10 documents, all 37 items, the scorer, and every one of the 962 model responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Models I Tested and Why
&lt;/h2&gt;

&lt;p&gt;13 models, 74 prompts each, all run on Kaggle Benchmarks: open-weight models because it's Hacktoberfest and self-hosters deserve to know (Gemma 4 31B and 26B-A4B, GLM-5, Qwen3-Next-80B, DeepSeek-R1, GPT-OSS-20B and 120B), and closed models as reference points (Claude Haiku 5.5 and Sonnet 5.5, GPT-5.4-mini and GPT-6-sol, Gemini 3.5 Flash-Lite and 3.7 Flash). I deliberately included small/large pairs within three families, because my prior was that more polish means more silent correction, and family pairs isolate that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Found (And What Surprised Me)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Finding 1: the trap caught nobody. 0 silent corrections in 962 quotes.
&lt;/h3&gt;

&lt;p&gt;My headline hypothesis is dead, and I have receipts. Across 13 models, two instruction arms, and 962 scored responses, &lt;strong&gt;not one model fixed a planted error inside a quote&lt;/strong&gt;. "Recieve" survived 26 encounters. So did "more simpler", "This library have", the reversed DNS definition, and "the Pacific, the world's second-largest ocean", which I was certain someone would quietly promote back to first place.&lt;/p&gt;

&lt;p&gt;It's stronger than that. Of 911 faithful quotes, &lt;strong&gt;every single one was byte-identical&lt;/strong&gt; to the source. Not one needed even my gentle normalization. When a 2026 model copies, it copies exactly.&lt;/p&gt;

&lt;p&gt;And the twelve responses that produced text existing nowhere in the document? Five of them contained a planted error, and &lt;strong&gt;all five preserved it&lt;/strong&gt;. Even while mangling a quote's boundaries, no model touched the errors. Whatever typo-fixing machinery lives inside these models (there's published mechanistic work on &lt;a href="https://aclanthology.org/2025.emnlp-main.313/" rel="noopener noreferrer"&gt;dedicated typo-correction structure in LLMs&lt;/a&gt;), the quoting context switches it off. Folk wisdom: falsified, at least for short documents in 2026.&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%2Ff1gmq88p7p2rsgmw55e3.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%2Ff1gmq88p7p2rsgmw55e3.png" alt=" " width="800" height="732"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding 2: what actually fails is the sentence boundary.
&lt;/h3&gt;

&lt;p&gt;51 of 962 quotes failed, and the taxonomy is lopsided: 26 truncated, 13 wrong-span, 12 rewritten, 0 corrected. Nearly every failure is a disagreement about &lt;strong&gt;where a sentence starts and ends&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The colon problem.&lt;/strong&gt; The hardest item in the whole benchmark (13 failures out of 26 attempts) was: &lt;em&gt;"But a part of it is our own doing: customers who contact us twice about the same order currently recieve two separate case numbers, and about a tenth of those end up double-refunded."&lt;/em&gt; Models split on the colon. Gemini 3.5 Flash-Lite started the "sentence" at "customers" and capitalized it to "Customers", producing a sentence that exists nowhere in the memo (typo intact!). Claude Sonnet 5.5 went the other way and glued on the &lt;em&gt;previous&lt;/em&gt; sentence. GPT-OSS-120B stopped mid-clause. One sentence, three different theories of its edges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The neighbor-sentence problem.&lt;/strong&gt; Asked for "the final sentence of the letter", models failed 9 times out of 26: Sonnet 5.5 and Qwen3 returned the final &lt;em&gt;two&lt;/em&gt; sentences ("Some trips end with a landmark. This one ended with..."), and Flash-Lite once returned only "Some trips end with a landmark.", which is the second-to-last sentence, alone. The instruction said one sentence; they knew where the text was and disagreed about which one it was.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Section labels.&lt;/strong&gt; GPT-OSS-20B prepended "4.3" (and elsewhere "4.2") to terms-of-service quotes, faithfully copying labels that aren't part of the sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fabricated punctuation.&lt;/strong&gt; DeepSeek-R1 cut a long sentence after its first clause and appended a period that isn't in the source, with the reasoning trace visibly talking itself into it: &lt;em&gt;"The period after 'water' is part of the original sentence structure."&lt;/em&gt; It isn't; the source continues with a comma. The one true paraphrase of the whole run also lives here: Flash-Lite, in the strict arm, restructured "The extended battery delivers a full two-shift day, defined as..." into "A full two-shift day is defined as..." — the only failure in 962 that reads like the folk-wisdom rewrite, and it's 1 case, not a pattern.&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%2Fksz4xgukr03edghfb64x.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%2Fksz4xgukr03edghfb64x.png" alt=" " width="800" height="732"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding 3: telling it not to correct does nothing (because there's nothing to prevent).
&lt;/h3&gt;

&lt;p&gt;Plain arm: 95.0% faithful. Strict "do not correct anything" arm: 94.4%. A null result, but a clean one: the explicit warning neither rescued the boundary failures nor prevented corrections that were never going to happen. If you've been adding "preserve all typos exactly" to your prompts, on this evidence it's a lucky charm.&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%2Fnm4y81rxdfy4owmhtkqy.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%2Fnm4y81rxdfy4owmhtkqy.png" alt=" " width="800" height="732"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding 4: bigger is worse, twice out of three.
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GPT-OSS-&lt;strong&gt;120B&lt;/strong&gt;: 82.4%. GPT-OSS-&lt;strong&gt;20B&lt;/strong&gt;: 89.2%. The 6x larger sibling is 7 points worse at copying, mostly through truncation. Both siblings share a family tic: swapping plain hyphens for U+2011 non-breaking hyphens inside otherwise perfect quotes ("day‑old", "hot‑swappable"), a single invisible character that makes the quote no longer exist in the source.&lt;/li&gt;
&lt;li&gt;Claude &lt;strong&gt;Sonnet 5.5&lt;/strong&gt;: 93.2%. Claude &lt;strong&gt;Haiku 5.5&lt;/strong&gt;: 94.6%. The smaller model wins again.&lt;/li&gt;
&lt;li&gt;Gemma 4 &lt;strong&gt;31B&lt;/strong&gt; and &lt;strong&gt;26B-A4B&lt;/strong&gt;: both a perfect 100%, every byte, both arms, alongside Gemini 3.7 Flash, GPT-6-sol, and GLM-5.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Five models walked through my trap untouched, and two of them are open-weight models you can run yourself. The scaling intuition ("the big model is better, use it for precision work") points exactly the wrong way on this task.&lt;/p&gt;

&lt;h2&gt;
  
  
  The grader is always the first suspect
&lt;/h2&gt;

&lt;p&gt;Two confessions, because this genre earns its keep through self-audit.&lt;/p&gt;

&lt;p&gt;First: my initial leaderboard showed DeepSeek-R1 at &lt;strong&gt;0.0%&lt;/strong&gt;, with 100% of its quotes "not in the source". Alarming headline, completely false. R1 wraps its answers in &lt;code&gt;&amp;lt;think&amp;gt;...&amp;lt;/think&amp;gt;&lt;/code&gt; reasoning, my scorer was grading the thinking instead of the answer, and the real answer was sitting right after the closing tag. One line of stripping later, R1 scores 91.9%. If I had shipped the first number, the first commenter who knows R1 would have ended this post. (Its remaining failures are real: truncating clauses and inventing terminal periods, reasoning aloud about punctuation the whole time.)&lt;/p&gt;

&lt;p&gt;Second: this isn't even the benchmark I started building yesterday. The first one asked models to compute cron firing times across daylight-saving transitions. I killed it after discovering that cron implementations &lt;em&gt;themselves&lt;/em&gt; disagree about DST semantics (Vixie cron skips the nonexistent hour, Quartz handles it differently), which means my "ground truth" would have been one dialect's opinion. And while building that grader I inverted PEP 495 fold semantics and spent twenty minutes confidently marking correct answers wrong. Both experiences set this benchmark's design rule: the correct answer must be a string I planted myself, findable with Ctrl+F, with no dialects and no judge model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations (read before quoting my numbers)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;37 items, 13 models.&lt;/strong&gt; Big enough that 0/962 corrections is a real signal; small enough that I hand-verified every item. Per-class splits (4 typos, 3 wrong facts) are directional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short documents (~250 words).&lt;/strong&gt; Copy fidelity at 200 words deep tells you nothing about 50,000 tokens deep. I built a combined-document variant (same questions, all ten documents fused into one 2,225-word distractor field) that has not run yet; it is my first follow-up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run-to-run variance is real.&lt;/strong&gt; I ran the full suite more than once at provider-default sampling; per-model scores wobble by 1–4 points between runs, while the headline findings held every time: zero corrections, the same five models at 100%, byte-identical faithful quotes. Numbers in this post are from one complete 962-call run; per-item CSVs are public in the notebook output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;English workplace prose only.&lt;/strong&gt; Code, poetry, non-Latin scripts: open questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact-match is strict on purpose.&lt;/strong&gt; Several failures differ from the source by a single character, like a non-breaking hyphen or an invented terminal period. I publish the full taxonomy so you can draw the line elsewhere; the CSVs have every response. A handful of "rewritten" verdicts are models adding a lead-in like "The sentence is:" despite "output only that sentence", which I count as failures of the task as instructed.&lt;/li&gt;
&lt;li&gt;My sentence boundaries follow period-to-period convention; the colon item shows models hold other theories. I'd call that a finding, but you're free to call it item ambiguity, and the item-level data lets you recompute without it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd Measure Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Depth stress:&lt;/strong&gt; the combined-document variant above, then real long-context distances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; quote the exact line from a stack trace or diff, where an invisible "fix" is maximally dangerous.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The colon census:&lt;/strong&gt; a benchmark made entirely of sentences with mid-sentence colons, semicolons, and conjunctions, since that's where models' sentence-boundary theories diverge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trailing-punctuation fidelity&lt;/strong&gt; as its own tracked class; it may explain a chunk of "near-miss" exact-match failures in other benchmarks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning-model output hygiene:&lt;/strong&gt; R1's failures all happened inside otherwise-correct answers; how much benchmark noise across the industry is really answer-extraction noise?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honesty section
&lt;/h2&gt;

&lt;p&gt;The documents, planted errors, benchmark design, and analysis are mine; I used AI coding tools (Claude Code) to accelerate the harness, scorer tests, and charts, as the challenge rules permit. Every item was hand-verified against its document, and every number in this post comes from the public run on Kaggle. The project was built during the contest window.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Benchmark: &lt;a href="https://www.kaggle.com/benchmarks/saharshtibrewala/exact-words" rel="noopener noreferrer"&gt;https://www.kaggle.com/benchmarks/saharshtibrewala/exact-words&lt;/a&gt;. All code, documents, and per-item results are public. If you find a scoring bug, comment; as established above, the grader is always the first suspect.&lt;/em&gt;&lt;br&gt;
`&lt;/p&gt;

</description>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
