<?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: Gabby Gill</title>
    <description>The latest articles on DEV Community by Gabby Gill (@gabby_gill_23c85a86800564).</description>
    <link>https://dev.to/gabby_gill_23c85a86800564</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%2F4158423%2Fbb75121b-5250-4f92-97a0-d509611ff8f8.png</url>
      <title>DEV Community: Gabby Gill</title>
      <link>https://dev.to/gabby_gill_23c85a86800564</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gabby_gill_23c85a86800564"/>
    <language>en</language>
    <item>
      <title>Marigold — a private ofrenda that catches the ghosts AI writes into our memories</title>
      <dc:creator>Gabby Gill</dc:creator>
      <pubDate>Sat, 31 Oct 2026 02:14:23 +0000</pubDate>
      <link>https://dev.to/gabby_gill_23c85a86800564/marigold-a-private-ofrenda-that-catches-the-ghosts-ai-writes-into-our-memories-948</link>
      <guid>https://dev.to/gabby_gill_23c85a86800564/marigold-a-private-ofrenda-that-catches-the-ghosts-ai-writes-into-our-memories-948</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;For Día de los Muertos, families build an &lt;strong&gt;ofrenda&lt;/strong&gt;: an altar with photos of the people they've lost, their favourite foods, marigolds to guide them home, and the stories that keep them close.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Marigold&lt;/strong&gt; helps you build one on your own computer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add someone&lt;/strong&gt; and drop in a few old photos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write down what you remember:&lt;/strong&gt; what they always said, what they cooked, the song they hummed on Saturday afternoons.&lt;/li&gt;
&lt;li&gt;A local open-weight model (&lt;strong&gt;Gemma 3 4B via Ollama&lt;/strong&gt;) looks at each photo and suggests a gentle caption. Then it writes a short remembrance &lt;strong&gt;using only what you gave it&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Marigold builds the ofrenda: papel picado, their photos, your words, their favourite things as the offerings, and candles. It's saved as &lt;strong&gt;one HTML file&lt;/strong&gt; you can keep, print or share.
&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%2Fpts8xal71awbg840igh6.jpg" alt="The ofrenda Marigold built in a real Gemma 3 run, using public-domain archive photos and a fictional grandmother" width="800" height="1098"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;The photos are public-domain archive photos and the memories are fictional; with your own family photos, it's their faces on the ofrenda.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My take on "Ghost in the Machine":&lt;/strong&gt; the scariest ghost in AI isn't a spooky one. It's the confident little invention. Ask a model to write about your grandmother and it may "remember" a town she never lived in, a year that's off by one, or a sister who never existed. In most apps that's a bug. &lt;strong&gt;In a remembrance, it's a ghost that rewrites someone's life.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So Marigold has a &lt;strong&gt;ghost check&lt;/strong&gt;. It doesn't just tell the model to stick to the facts; it &lt;em&gt;checks&lt;/em&gt;. Every name, place, number, month and weekday in the written text is compared against everything you wrote and everything the model saw in the photos. Anything that came from nowhere is flagged as a 👻 and highlighted, so you can remove it, or add it to your memories if it's true. Here it is catching a city that was planted in a test:&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%2Frw8puijiyvy3hf0b6eez.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%2Frw8puijiyvy3hf0b6eez.jpg" alt="The ghost check catching an invented city" width="800" height="791"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; download &lt;code&gt;marigold-win-x64.zip&lt;/code&gt; (or the macOS/Linux build) from &lt;a href="https://github.com/GurminderKaur/Marigold/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt;, install &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;, run &lt;code&gt;ollama pull gemma3:4b&lt;/code&gt;, and double-click &lt;code&gt;marigold&lt;/code&gt;. It opens in your browser at &lt;code&gt;127.0.0.1:5180&lt;/code&gt;.&lt;/p&gt;

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


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/GurminderKaur" rel="noopener noreferrer"&gt;
        GurminderKaur
      &lt;/a&gt; / &lt;a href="https://github.com/GurminderKaur/Marigold" rel="noopener noreferrer"&gt;
        Marigold
      &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;Marigold 🌼&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/GurminderKaur/Marigold/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/GurminderKaur/Marigold/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A private, offline ofrenda for the people you remember.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Add a few old photos and write down what you remember: what they always said, what they cooked, the song they hummed. A local open-weight model (&lt;strong&gt;Gemma 3 4B via Ollama&lt;/strong&gt;) looks at each photo and writes a short remembrance &lt;strong&gt;using only what you gave it&lt;/strong&gt;. Marigold then builds an ofrenda page with their photos, your words and their favourite things, saved as &lt;strong&gt;one HTML file&lt;/strong&gt; you can keep, print or share.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/GurminderKaur/Marigold/docs/ofrenda.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FGurminderKaur%2FMarigold%2FHEAD%2Fdocs%2Fofrenda.jpg" alt="The ofrenda Marigold builds, from a real Gemma 3 run on public-domain photos"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The ghost check 👻&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A language model writing about someone you loved will sometimes "remember" things that never happened: a town she never lived in, a year that's wrong, a name nobody had. In a remembrance, that's the worst kind of mistake.&lt;/p&gt;
&lt;p&gt;So Marigold doesn't just ask the model to stick to the facts. It &lt;strong&gt;checks&lt;/strong&gt;. Every name, place, number, month and weekday…&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/GurminderKaur/Marigold" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; .NET 8 (an ASP.NET Core minimal API) serving one offline HTML page, with &lt;strong&gt;Ollama&lt;/strong&gt; running &lt;strong&gt;Gemma 3 4B&lt;/strong&gt;, which reads images as well as text. There are &lt;strong&gt;zero third-party NuGet packages&lt;/strong&gt;, no CDN scripts and no web fonts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two jobs for the model.&lt;/strong&gt; Both go through Ollama's &lt;code&gt;/api/chat&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Looking at a photo:&lt;/strong&gt; a JSON schema in &lt;code&gt;format&lt;/code&gt; makes the model return &lt;code&gt;caption&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt; and &lt;code&gt;era&lt;/code&gt;. The prompt says to describe only what's visible and &lt;em&gt;never&lt;/em&gt; guess a name, age, relationship or city. Captions are suggestions; you can change any of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing the remembrance:&lt;/strong&gt; the prompt lists the facts (name, years, your memories, their favourite things, what's in the photos) under a STRICT RULE: use only these, add no name, place, date, year, age, job, food, song or event. "It is better to be short than to invent."
&lt;strong&gt;A lesson from the real model.&lt;/strong&gt; I first used a JSON schema for the remembrance too. It worked with my test stand-in, but on the real Gemma 3 4B it failed twice in CI: wrapped in forced JSON, the model kept writing until it hit the length limit and never closed the string. Plain text ("title on the first line, then the remembrance") fixed it. Now a cut-off answer is detected and retried once with a different seed. Structured output is great for short fields, but for a few paragraphs of prose, plain text is more reliable on a small model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The ghost check is plain C#, not another model.&lt;/strong&gt; A prompt is a request, not a guarantee, so I check the output with about 120 lines of string matching (&lt;a href="https://github.com/GurminderKaur/Marigold/blob/main/src/Marigold/Ghosts.cs" rel="noopener noreferrer"&gt;&lt;code&gt;Ghosts.cs&lt;/code&gt;&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It builds a bag of words from everything you wrote and everything the model saw.&lt;/li&gt;
&lt;li&gt;It flags capitalized words mid-sentence (names, places), numbers (years, ages), and months and weekdays that aren't in that bag.&lt;/li&gt;
&lt;li&gt;It avoids false alarms: sentence-start words don't count, "&lt;em&gt;May&lt;/em&gt; she rest" isn't a month but "married in &lt;em&gt;May&lt;/em&gt;" is, family words like &lt;em&gt;Abuela&lt;/em&gt; or &lt;em&gt;Nani&lt;/em&gt; are fine, and plurals match ("Halloween costumes" covers "costume").&lt;/li&gt;
&lt;li&gt;It runs again every time you edit, so adding "She was born in Guadalajara" to your memories turns the warning off.
It's predictable, it's explainable, and it's tested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The ofrenda outlives the app.&lt;/strong&gt; The page is a single file with photos embedded as data URIs, no scripts, no links and no fonts to download. It prints to PDF. Photos are resized in the browser before saving (canvas → JPEG), so a 12 MB scan becomes a few hundred KB without any image library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private by construction.&lt;/strong&gt; The server binds to &lt;code&gt;127.0.0.1&lt;/code&gt;, rejects any other host name (blocking DNS-rebinding), and needs a custom header for every change, so another website in your browser can't read or edit anything. Uploaded files are checked by their first bytes (JPEG/PNG only), and photos are only ever served by ID from Marigold's own folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests.&lt;/strong&gt; There are 33 built-in self-tests for the ghost check, photo validation, storage and the ofrenda page, run on Windows, macOS and Linux. Then CI does the real thing: it installs Ollama and &lt;code&gt;gemma3:4b&lt;/code&gt;, downloads a few &lt;strong&gt;public-domain photos&lt;/strong&gt; from Wikimedia Commons (licence checked, credits recorded), and a &lt;strong&gt;Playwright test uses the app like a person would&lt;/strong&gt;. It adds a (fictional) grandmother, uploads the photos, waits for Gemma, writes the remembrance, checks the ghost check, then opens and downloads the ofrenda.&lt;/p&gt;

&lt;p&gt;Here's the real Gemma 3 4B, on a GitHub runner with no GPU:&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;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Photos described&lt;/td&gt;
&lt;td&gt;4 / 4, no names guessed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Era guesses vs. the archive's dates&lt;/td&gt;
&lt;td&gt;1944 sewing photo → "1930s–1950s" ✅, c. 1942 radio photo → "1950s" (close), 1922 homestead → "1930s–1950s" (a bit late)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time per photo / for the remembrance&lt;/td&gt;
&lt;td&gt;~67 s / ~42 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invented details in the real remembrance&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0&lt;/strong&gt; (in both the browser run and the command-line run)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And this is what it wrote, from the family's memories, with a clean ghost check:&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%2F0561xc1abqfvoqblgtwg.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%2F0561xc1abqfvoqblgtwg.jpg" alt="A real remembrance from Gemma 3, with a clean ghost check" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Family photos and memories of someone who has died are about as private as data gets. &lt;strong&gt;To describe a photo, a model has to see it.&lt;/strong&gt; With a closed API, your grandmother's face, her kitchen and your memories of her would be uploaded to a server you don't control, under terms that can change. With an open-weight model, Gemma looks at them on your own laptop. That isn't a promise in a privacy policy; it's how the app is built. Turn off the Wi-Fi and it still works.&lt;/p&gt;

&lt;p&gt;Open also matters for &lt;strong&gt;time&lt;/strong&gt;, and remembrance is about time. An ofrenda should still open in twenty years. A closed service can shut down, change its prices or retire the model. Marigold's output is a plain HTML file, the model weights sit on your disk, and the code is MIT. Nothing here needs anyone's permission to keep working.&lt;/p&gt;

&lt;p&gt;And open let me &lt;strong&gt;build the safety I wanted&lt;/strong&gt; instead of hoping for it. I could pin the exact model, set temperature and seed, choose when to force a JSON schema (and when not to), and put my own check on the output. I can swap in &lt;code&gt;gemma3:12b&lt;/code&gt; or &lt;code&gt;qwen2.5vl:7b&lt;/code&gt; with one flag if a bigger model writes better. With a closed API I'd get whatever the model does that day.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 3 4B runs locally through Ollama and does both halves of the work: its vision input describes the photos, and it writes the remembrance in English, Spanish, French, Portuguese, Punjabi or Hindi. A GitHub Actions job runs the real model end to end in a browser on every push.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>dotnet</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Sloth Sort — a local Gemma 3 tidies my Downloads folder while I do nothing</title>
      <dc:creator>Gabby Gill</dc:creator>
      <pubDate>Thu, 22 Oct 2026 02:19:05 +0000</pubDate>
      <link>https://dev.to/gabby_gill_23c85a86800564/sloth-sort-a-local-gemma-3-tidies-my-downloads-folder-while-i-do-nothing-980</link>
      <guid>https://dev.to/gabby_gill_23c85a86800564/sloth-sort-a-local-gemma-3-tidies-my-downloads-folder-while-i-do-nothing-980</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;My Downloads folder is where files go to be forgotten: &lt;code&gt;document(7).pdf&lt;/code&gt;, &lt;code&gt;download.pdf&lt;/code&gt;, &lt;code&gt;Untitled document.docx&lt;/code&gt;, &lt;code&gt;scan0001.jpg&lt;/code&gt;. Every few months I promise myself I'll sort it. I never do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sloth Sort&lt;/strong&gt; sorts it for me. It reads each file on my own computer, and a local open-weight model (&lt;strong&gt;Gemma 3 4B via Ollama&lt;/strong&gt;) decides what the file actually &lt;em&gt;is&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;document(7).pdf&lt;/code&gt; → &lt;code&gt;Bills &amp;amp; Receipts/2026-10-14 Chinook Mobile Monthly Bill.pdf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Untitled document.docx&lt;/code&gt; → &lt;code&gt;Home &amp;amp; Car/2026-11-01 Rental Lease Agreement.docx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;download.pdf&lt;/code&gt; → &lt;code&gt;Medical/2026-10-02 Bow River Medical Laboratory.pdf&lt;/code&gt; 🔒
Installers, zips, audio/video and screenshots are filed by simple rules, with no AI needed. Everything else gets a folder, a readable name and the document's real date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Who gets to be lazier?&lt;/strong&gt; Anyone whose Downloads folder is a junk drawer: students, freelancers, parents juggling school forms, and the person who becomes the family's "computer person" every time someone can't find a document.&lt;/p&gt;

&lt;p&gt;The whole effort is &lt;strong&gt;two clicks&lt;/strong&gt;: &lt;em&gt;Look through it&lt;/em&gt;, then &lt;em&gt;Tidy&lt;/em&gt;. In between you can go make tea.&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%2F8t6fcl5oa1qvmt66a948.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%2F8t6fcl5oa1qvmt66a948.jpg" alt="Sloth Sort showing its plan" width="800" height="1053"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Being lazy shouldn't mean being reckless, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing moves until you've seen the plan.&lt;/strong&gt; You can untick a file, rename it or change its folder first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never deletes and never overwrites.&lt;/strong&gt; Name clashes become &lt;code&gt;(2)&lt;/code&gt;, &lt;code&gt;(3)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every tidy-up has an Undo button&lt;/strong&gt; that puts every file back under its original name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It skips files that are still downloading&lt;/strong&gt; (&lt;code&gt;.crdownload&lt;/code&gt;, &lt;code&gt;.part&lt;/code&gt;) and anything changed in the last two minutes.
&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%2F132z8in5p99zzb4yjmgq.jpg" alt="After tidying, with the undo history" width="800" height="1061"&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; download &lt;code&gt;slothsort-win-x64.zip&lt;/code&gt; (or the macOS/Linux build) from &lt;a href="https://github.com/GurminderKaur/SlothSort/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt;, install &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;, run &lt;code&gt;ollama pull gemma3:4b&lt;/code&gt;, and double-click &lt;code&gt;slothsort&lt;/code&gt;. Your browser opens with your Downloads folder already filled in. Click &lt;em&gt;Look through it&lt;/em&gt;; nothing moves until you click &lt;em&gt;Tidy&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch the real model work:&lt;/strong&gt; every push re-runs the messy-folder test with Gemma 3 in &lt;a href="https://github.com/GurminderKaur/SlothSort/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt;. The run summary shows each file, the folder and name Gemma chose, and the score.&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/GurminderKaur" rel="noopener noreferrer"&gt;
        GurminderKaur
      &lt;/a&gt; / &lt;a href="https://github.com/GurminderKaur/SlothSort" rel="noopener noreferrer"&gt;
        SlothSort
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Sloth Sort 🦥&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/GurminderKaur/SlothSort/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/GurminderKaur/SlothSort/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A local AI tidies your messy Downloads folder while you do nothing.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;document(7).pdf&lt;/code&gt; becomes &lt;code&gt;Bills &amp;amp; Receipts/2026-10-14 Chinook Mobile Bill.pdf&lt;/code&gt;. &lt;code&gt;Untitled document.docx&lt;/code&gt; becomes &lt;code&gt;Home &amp;amp; Car/2026-11-01 Rental Lease Agreement.docx&lt;/code&gt;. Installers, zips and screenshots get filed too. It takes &lt;strong&gt;two clicks&lt;/strong&gt;: &lt;em&gt;Look through it&lt;/em&gt;, then &lt;em&gt;Tidy&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/GurminderKaur/SlothSort/docs/screenshot-plan.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FGurminderKaur%2FSlothSort%2FHEAD%2Fdocs%2Fscreenshot-plan.jpg" alt="Sloth Sort planning a tidy-up"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reads your files locally.&lt;/strong&gt; It pulls a short text excerpt from PDFs, Word, Excel, PowerPoint, text and HTML files (and, if you turn it on, looks at photos). A local open-weight model, &lt;strong&gt;Gemma 3 4B via Ollama&lt;/strong&gt;, decides the folder, a human-friendly name and the document's date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules first, AI second.&lt;/strong&gt; Installers, archives, audio/video and screenshots are sorted by rules instantly, with no AI needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shows you the plan before anything moves.&lt;/strong&gt; Untick files, rename them or change their folder, or just click &lt;em&gt;Tidy&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never deletes, never overwrites.&lt;/strong&gt; Every change is a move, recorded in…&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/GurminderKaur/SlothSort" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; .NET 8 (an ASP.NET Core minimal API) serving one offline HTML page, with &lt;strong&gt;Ollama&lt;/strong&gt; running &lt;strong&gt;Gemma 3 4B&lt;/strong&gt;. It uses &lt;strong&gt;zero third-party NuGet packages&lt;/strong&gt;. That matters for a tool that's allowed to move your files around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading files without libraries.&lt;/strong&gt; The model can only sort what it can read, so Sloth Sort pulls a short excerpt (up to 1,500 characters) from each file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDFs:&lt;/strong&gt; inflates the compressed content streams with .NET's built-in &lt;code&gt;ZLibStream&lt;/code&gt; and collects the text-drawing operators. If the result looks like garbage, it's dropped and Sloth Sort falls back to the file name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Word, Excel and PowerPoint files:&lt;/strong&gt; these are zip files of XML, so it reads them with &lt;code&gt;ZipArchive&lt;/code&gt; and a hardened &lt;code&gt;XmlReader&lt;/code&gt; (DTDs off).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text and HTML files:&lt;/strong&gt; read directly, with tags stripped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Photos:&lt;/strong&gt; optional; the image itself goes to Gemma's vision input, so a photographed receipt can land in &lt;em&gt;Bills &amp;amp; Receipts&lt;/em&gt; instead of &lt;em&gt;Photos&lt;/em&gt;.
&lt;strong&gt;Asking the model for a decision, not a conversation.&lt;/strong&gt; Each file goes to Ollama's &lt;code&gt;/api/chat&lt;/code&gt; with a JSON schema in &lt;code&gt;format&lt;/code&gt;. The model must answer with a &lt;code&gt;folder&lt;/code&gt; from a fixed list, a short &lt;code&gt;title&lt;/code&gt; and a &lt;code&gt;date&lt;/code&gt;. Temperature is 0. The prompt says "do not invent": with no clear date, the file's own date is used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing I moved &lt;em&gt;out&lt;/em&gt; of the model: I first asked Gemma to flag "sensitive" files, and it flagged 10 of 11, which makes a warning useless. Now the 🔒 simply comes from the folder (Banking &amp;amp; Taxes, Medical, IDs &amp;amp; Personal). It's predictable, and it's one less thing to ask a 4B model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C# does the risky parts, not the model.&lt;/strong&gt; The model never chooses a path. C# builds the name (&lt;code&gt;yyyy-MM-dd Title.ext&lt;/code&gt;), removes characters Windows doesn't allow, avoids reserved names like &lt;code&gt;CON&lt;/code&gt;, rejects dates that are impossible or more than two years ahead, and adds &lt;code&gt;(2)&lt;/code&gt; instead of overwriting. Moves are recorded in a journal, so undo works even after you restart the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guard rails.&lt;/strong&gt; The app only tidies folders inside your user folder. It refuses your whole user folder, AppData/Library/.ssh, and system folders, and it only touches the top level. The page is served on &lt;code&gt;127.0.0.1&lt;/code&gt;, rejects other host names (to block DNS-rebinding tricks), and needs a custom header for every change, so another website in your browser can't trigger a tidy-up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests.&lt;/strong&gt; There are 31 built-in self-tests covering names, dates, skip rules, PDF and Word reading, the folder guard, never-overwrite, and undo surviving a restart. On every push, GitHub Actions runs them on Windows, macOS and Linux. It then installs Ollama, pulls &lt;code&gt;gemma3:4b&lt;/code&gt;, generates a messy folder of &lt;strong&gt;16 clearly-labelled fictional files&lt;/strong&gt;, and has the real model plan the tidy-up. Each file is scored on "right folder?" and "sensible name?":&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;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Right folder&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15 / 15&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensible new name&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10 / 10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Half-finished &lt;code&gt;.crdownload&lt;/code&gt; skipped&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files the dry run moved&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After (chosen by Gemma 3 4B)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;statement_0923.pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Banking &amp;amp; Taxes/2026-09-01 Prairie Credit Union Chequing Statement.pdf&lt;/code&gt; 🔒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;notes.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;School/2026-11-02 COMP 2510 Assignment 3.txt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;scan0001.jpg&lt;/code&gt; (a receipt photo)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bills &amp;amp; Receipts/2026-10-05 Chinook Coffee House Receipt.jpg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;booking-confirmation.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Travel/2026-11-20 Aurora Air Booking Confirmation.html&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To be fair to the model and to you: sixteen tidy test files are easier than a real Downloads folder. That's why the plan is always shown first, and every tidy-up can be undone.&lt;/p&gt;

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

&lt;p&gt;A Downloads folder is the most private place on a computer. Mine has bank statements, a lease with my address, lab results, my ID scan and my résumé. &lt;strong&gt;To sort those files, a model has to read them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With a closed API, every one of those files would leave my laptop to be sorted. With an open-weight model, Gemma reads them on my machine, and the files never go anywhere. That isn't a policy promise; it's how the app is built. You can unplug the network cable and it still works.&lt;/p&gt;

&lt;p&gt;Open weights also changed what was &lt;em&gt;worth&lt;/em&gt; building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; a cloud API charges per file. Sloth Sort tidying 2,000 files costs the same as tidying 20: nothing. That makes "tidy my Downloads every night" a reasonable thing to schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running it unattended:&lt;/strong&gt; &lt;code&gt;slothsort tidy ~/Downloads --yes&lt;/code&gt; can run from Task Scheduler. There's no API key to expire and no rate limit to hit at 3 a.m.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yours to change:&lt;/strong&gt; swap in &lt;code&gt;gemma3:12b&lt;/code&gt; or &lt;code&gt;qwen2.5vl:7b&lt;/code&gt; with one flag, edit the folder list or the prompt, or add rules. The code is MIT; the model's terms are Gemma's.
Open source also means you can check that a tool which moves your files around does what it says. Every move, and every undo, happens in one short C# file (&lt;code&gt;Engine.cs&lt;/code&gt;) that anyone can read.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This challenge rewards laziness, so here's the honest accounting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My effort: 2 clicks.&lt;/strong&gt; &lt;em&gt;Look through it&lt;/em&gt;, then &lt;em&gt;Tidy&lt;/em&gt;. That's zero decisions if I trust the plan, or a few seconds per file if I want to fix one. On a schedule (&lt;code&gt;slothsort tidy ~/Downloads --yes&lt;/code&gt;), it's zero clicks. I ran it on my Downloads folder this week: 143 files, and I changed 9 suggestions before clicking Tidy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The machine's effort: slow, and that's fine.&lt;/strong&gt; On a CPU-only GitHub runner with no GPU, Gemma 3 4B takes about &lt;strong&gt;31 seconds per file&lt;/strong&gt; it has to read. The 4 rule-sorted files are instant. So the sample folder took roughly 6 minutes. That's slower than me sorting a single file. But I'm not the one waiting: I'm making tea. &lt;strong&gt;Slow and steady, and none of it on my time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By hand, it's the part I never actually do.&lt;/strong&gt; Each file means opening it, working out what it is, finding the date, typing a name and dragging it somewhere. At a rough 30 seconds a file, that's about 8 minutes for the 16-file test folder, and a whole afternoon for a real one with hundreds of files. That's why my Downloads folder has never been sorted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The payoff that matters:&lt;/strong&gt; a year from now, when I need last October's phone bill, I can search &lt;code&gt;Chinook&lt;/code&gt; in &lt;code&gt;Bills &amp;amp; Receipts&lt;/code&gt; instead of opening &lt;code&gt;document(1).pdf&lt;/code&gt; through &lt;code&gt;document(7).pdf&lt;/code&gt; one by one.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 3 4B runs locally through Ollama and does all of the understanding: it reads text excerpts and, optionally, photos through its vision input. A GitHub Actions job runs the real model against a test folder on every push.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>dotnet</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Shoebox — a private receipt &amp; GST sorter for Alberta freelancers, powered by a local Gemma 3</title>
      <dc:creator>Gabby Gill</dc:creator>
      <pubDate>Sat, 17 Oct 2026 23:39:52 +0000</pubDate>
      <link>https://dev.to/gabby_gill_23c85a86800564/shoebox-a-private-receipt-gst-sorter-for-alberta-freelancers-powered-by-a-local-gemma-3-2oi1</link>
      <guid>https://dev.to/gabby_gill_23c85a86800564/shoebox-a-private-receipt-gst-sorter-for-alberta-freelancers-powered-by-a-local-gemma-3-2oi1</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Every April or end of financial year, self-employed people across Alberta sit down with a shoebox of faded receipts and a spreadsheet. I wanted the boring part, reading and sorting, to happen in minutes without handing a year of financial data to a cloud service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shoebox&lt;/strong&gt; is a small app for self-employed people in Alberta. You drop in photos of your receipts and get back a year of expenses sorted the way CRA's T2125 form wants them, with the GST worked out. It runs entirely on your own computer.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Drop in receipt photos.&lt;/strong&gt; A local open-weight vision model, &lt;strong&gt;Google's Gemma 3 (4B)&lt;/strong&gt; running through Ollama, reads the vendor, date, subtotal, GST, tip and total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shoebox checks the AI's work with plain arithmetic&lt;/strong&gt;, not more AI. It flags any receipt where subtotal + tax + tip doesn't equal the total, where the tax isn't Alberta's 5% GST (a BC receipt with PST, or Ontario HST), where the date is in the future, or where it looks like a duplicate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each expense goes to a T2125 line.&lt;/strong&gt; Gas goes to 9281 Motor vehicle, coffee with a client to 8523 Meals (counted at 50%), printer ink to 8811 Office supplies. Business-use percentages for your vehicle and phone are applied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GST is handled both ways.&lt;/strong&gt; If you're GST-registered, Shoebox deducts the pre-GST cost and totals your &lt;strong&gt;input tax credits&lt;/strong&gt;. If you're not, the GST is part of your expense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export a CSV for your accountant&lt;/strong&gt;, or for yourself at tax time.&lt;/li&gt;
&lt;/ol&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%2Fgcrjp5104xld0jfqhmux.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%2Fgcrjp5104xld0jfqhmux.jpg" alt="Shoebox main screen" width="800" height="762"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every flagged receipt opens side by side with its photo, so fixing a misread takes seconds:&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%2Fwdnes1h68sf2zxuq6xvg.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%2Fwdnes1h68sf2zxuq6xvg.jpg" alt="Checking a receipt" width="799" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who would pay for it?&lt;/strong&gt; Ride-share and delivery drivers, realtors, hairstylists, trades and consultants. These are people with a few hundred receipts a year who either pay a bookkeeper by the hour or lose deductions because the shoebox never gets sorted. Because it runs locally, there's no per-receipt cost to pass on. That means a &lt;strong&gt;one-time price&lt;/strong&gt; works, instead of yet another monthly subscription.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; download for Windows, macOS or Linux from the &lt;a href="https://github.com/GurminderKaur/ShoeBox/releases/tag/Shoebox0.1.0" rel="noopener noreferrer"&gt;Shoebox 0.1.0 release&lt;/a&gt;. Every push also runs Gemma 3 on the sample receipts: &lt;a href="https://github.com/GurminderKaur/ShoeBox/actions" rel="noopener noreferrer"&gt;see the CI runs&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/GurminderKaur" rel="noopener noreferrer"&gt;
        GurminderKaur
      &lt;/a&gt; / &lt;a href="https://github.com/GurminderKaur/ShoeBox" rel="noopener noreferrer"&gt;
        ShoeBox
      &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;Shoebox 🧾&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/GurminderKaur/Shoebox/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/GurminderKaur/Shoebox/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sort a year of receipts for tax time, privately, on your own computer.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You drop in photos of your receipts. A local open-weight AI model (Google's &lt;strong&gt;Gemma 3&lt;/strong&gt;, run through &lt;strong&gt;Ollama&lt;/strong&gt;) reads the vendor, date, subtotal, GST and total. Shoebox then:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;checks the arithmetic itself&lt;/strong&gt;, flagging any receipt where the numbers don't add up, the tax isn't Alberta's 5% GST, or the date looks wrong;&lt;/li&gt;
&lt;li&gt;sorts each expense into its &lt;strong&gt;CRA T2125 line&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;totals your &lt;strong&gt;deductible expenses and GST input tax credits&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;exports a CSV for you or your accountant.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's built for Alberta freelancers and gig workers: drivers, realtors, stylists, trades, consultants.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/GurminderKaur/ShoeBox/docs/screenshot-main.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FGurminderKaur%2FShoeBox%2FHEAD%2Fdocs%2Fscreenshot-main.jpg" alt="Shoebox main screen"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nothing leaves your computer.&lt;/strong&gt; The web page is served from &lt;code&gt;127.0.0.1&lt;/code&gt; only. The AI model runs on your own machine. Receipts are stored in a normal folder (&lt;code&gt;~/Shoebox&lt;/code&gt;) that you can back up or delete.&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;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/GurminderKaur/ShoeBox" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; .NET 8 (an ASP.NET Core minimal API) serving one self-contained HTML page, with &lt;strong&gt;Ollama&lt;/strong&gt; running &lt;strong&gt;Gemma 3 4B&lt;/strong&gt; for vision. There are &lt;strong&gt;zero third-party NuGet packages&lt;/strong&gt; and no CDN scripts or web fonts, so the whole thing works offline and there's very little supply chain to trust with financial data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading receipts.&lt;/strong&gt; Each photo goes to Ollama's &lt;code&gt;/api/chat&lt;/code&gt; with a JSON schema in the &lt;code&gt;format&lt;/code&gt; field (Ollama's structured outputs). The model &lt;em&gt;must&lt;/em&gt; answer with &lt;code&gt;vendor&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;subtotal&lt;/code&gt;, &lt;code&gt;gst&lt;/code&gt;, &lt;code&gt;other_tax&lt;/code&gt;, &lt;code&gt;tip&lt;/code&gt;, &lt;code&gt;total&lt;/code&gt;, &lt;code&gt;items&lt;/code&gt; and a &lt;code&gt;category&lt;/code&gt; drawn from a fixed list of T2125 codes. Temperature is 0. The parser is still lenient: it accepts "$12.50" as a string, month names in dates, and stray chatter around the JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not trusting the model.&lt;/strong&gt; A 4B model &lt;em&gt;will&lt;/em&gt; misread a digit sometimes. Rather than pretend otherwise, Shoebox sorts its findings into two kinds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problems&lt;/strong&gt; keep a receipt flagged until it's fixed: the numbers don't add up, there's no total, or the date is invalid or in the future.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cautions&lt;/strong&gt; clear once you've looked and saved: PST on an out-of-province receipt, a GST rate under 5% (some items may be exempt), or a possible duplicate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A misread total becomes a highlighted row to check, not a silent error in your tax numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Categories: rules first, AI second.&lt;/strong&gt; Keyword rules handle the obvious cases (Petro-Canada, Tim Hortons, Canada Post, Telus to name a few and many more) predictably and explainably. Gemma's suggestion is only used when no rule matches, and the UI shows whether a category came from a &lt;em&gt;rule&lt;/em&gt;, the &lt;em&gt;AI&lt;/em&gt; or &lt;em&gt;you&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tax maths in C#, not in the model.&lt;/strong&gt; Meals are counted at 50%. Vehicle and phone use your business-use %. GST registrants get &lt;code&gt;deductible = (total − GST) × share&lt;/code&gt; and &lt;code&gt;ITC = GST × share&lt;/code&gt;. Non-registrants deduct &lt;code&gt;total × share&lt;/code&gt;. There are 23 built-in self-tests covering this maths, the checks, category rules and CSV safety: text read off a receipt like &lt;code&gt;=HYPERLINK(...)&lt;/code&gt; is neutralized so it can't run as an Excel formula.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy by construction.&lt;/strong&gt; The server binds to &lt;code&gt;127.0.0.1&lt;/code&gt; only, rejects requests for any other host name (to block DNS-rebinding tricks), and requires a custom header on every change, so a website open in another tab can't upload, edit or delete anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing with a real model.&lt;/strong&gt; On every push, GitHub Actions builds and self-tests on Windows, macOS and Linux, then installs Ollama, pulls &lt;code&gt;gemma3:4b&lt;/code&gt;, and has it read six clearly-labelled &lt;strong&gt;sample receipts from fictional businesses&lt;/strong&gt;. Each field is scored against the known values and the results are posted in the run summary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Subtotal&lt;/th&gt;
&lt;th&gt;GST&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Correct (of 6)&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each receipt took about &lt;strong&gt;80 seconds&lt;/strong&gt; on GitHub's CPU-only runner. A laptop with a decent GPU is much faster.&lt;/p&gt;

&lt;p&gt;The two misreads are exactly why the checks exist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Courier receipt:&lt;/strong&gt; Gemma read the GST as &lt;strong&gt;$25.00&lt;/strong&gt;, copying the subtotal line, instead of $1.25. Shoebox flagged it twice: &lt;em&gt;"Amounts don't add up (off by $23.75)"&lt;/em&gt; and &lt;em&gt;"GST is 100% of the subtotal."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grocery receipt:&lt;/strong&gt; the receipt clearly printed &lt;strong&gt;$0.00 GST&lt;/strong&gt; (basic groceries are zero-rated). Gemma "helpfully" filled in &lt;strong&gt;$1.87&lt;/strong&gt;, which is exactly 5%. The model &lt;em&gt;assumed&lt;/em&gt; Alberta GST instead of reading it. Shoebox flagged it: &lt;em&gt;"off by $1.87."&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Both wrong amounts were caught; neither slipped into the totals.&lt;/strong&gt; The one thing no check can catch is judgement: Gemma filed the groceries under "Other" instead of "Personal". That's why every category shows whether it came from a rule, the AI or you.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The business model only works with an open model.&lt;/strong&gt; A cloud vision API charges per image. That's fine for a demo, but it turns a sorting tool into a subscription that has to cover someone else's inference bill. With Gemma running locally, reading the 500th receipt costs the same as the first, which is nothing. That's what makes a &lt;strong&gt;one-time-price tool for a freelancer&lt;/strong&gt; viable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data is the most sensitive kind.&lt;/strong&gt; A year of receipts shows where you drive, what you buy, who your clients are and roughly what you earn. Shoebox's answer to "where does my data go?" is "a folder on your laptop". That isn't a privacy policy; it's how the software is built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I can tune it for a niche general tools handle poorly.&lt;/strong&gt; Alberta has GST but no PST, CRA has its own T2125 lines, and meals are counted at 50%. Owning the whole pipeline means the prompt, the categories and the checks all speak Canadian tax. I can swap in a bigger model (&lt;code&gt;--model gemma3:12b&lt;/code&gt;) for someone with a faster computer, or fine-tune later on corrected receipts, without asking anyone's permission or changing pricing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No lock-in.&lt;/strong&gt; If Ollama, Gemma or I disappear tomorrow, the user still has a CSV and a folder of their own photos.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Honest limits:&lt;/em&gt; Shoebox gives estimates, not tax advice. It doesn't handle capital cost allowance, home-office expenses or receipts that mix personal and business items, and a small model on a laptop is slower than a cloud API (about 80 seconds per receipt on a CPU-only machine in my tests; faster with a GPU). For a once-a-month sorting session, that trade-off is worth it.&lt;/p&gt;

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

&lt;p&gt;I haven't landed one yet: I'm new to Calgary's freelance scene and didn't have contacts to ask this week. So here's an open invitation. If you're self-employed in Canada, download the &lt;a href="https://github.com/GurminderKaur/ShoeBox/releases/tag/Shoebox0.1.0" rel="noopener noreferrer"&gt;release&lt;/a&gt;, try it on a few receipts, and tell me in the comments what it got wrong and whether you'd pay for it. I'll update this post with what I hear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why someone would pay for this
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The customer:&lt;/strong&gt; an Alberta ride-share driver, realtor, stylist or contractor with a few hundred receipts a year. Today they either pay a bookkeeper by the hour to type them in, or they don't sort them at all and miss deductions. That's real money: meals count at 50%, gas at your business-use %, and GST input tax credits are free money for registrants who track them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What they're paying for:&lt;/strong&gt; an evening of sorting instead of a weekend, a CSV their accountant can use as is, and a check that catches the misread numbers a tired human (or an AI) would miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a one-time price works:&lt;/strong&gt; because Gemma runs on their own laptop, my cost per receipt is zero. I don't have a cloud bill to pass on, so I can sell it once (I'm thinking [$29] as side hustle) instead of adding another monthly subscription to a freelancer's expenses. And "your receipts never leave your laptop" is a selling point no cloud tool can match.&lt;/p&gt;

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

&lt;p&gt;I built Shoebox with an AI coding agent (Claude), working from the CRA rules down to the arithmetic checks, a security pass on the local server, browser testing on desktop and mobile, and a CI run against a real local Gemma model.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma:&lt;/strong&gt; Gemma 3 (4B, vision) runs locally through Ollama and reads every receipt. It's the app's default model, and CI tests it on every push.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 3&lt;/a&gt; by Google DeepMind (Gemma Terms of Use), served by &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dotnet.microsoft.com" rel="noopener noreferrer"&gt;.NET 8&lt;/a&gt; / ASP.NET Core.&lt;/li&gt;
&lt;li&gt;CRA guidance on T2125 expenses and GST/HST registration. Shoebox gives estimates only, not tax advice.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>dotnet</category>
      <category>opensource</category>
    </item>
    <item>
      <title>TrailBirder — offline bird-song ID and a local-LLM field journal for Alberta trails</title>
      <dc:creator>Gabby Gill</dc:creator>
      <pubDate>Sun, 11 Oct 2026 18:36:18 +0000</pubDate>
      <link>https://dev.to/gabby_gill_23c85a86800564/trailbirder-offline-bird-song-id-and-a-local-llm-field-journal-for-alberta-trails-g16</link>
      <guid>https://dev.to/gabby_gill_23c85a86800564/trailbirder-offline-bird-song-id-and-a-local-llm-field-journal-for-alberta-trails-g16</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Hearing birds on Calgary walks and not knowing what they were.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TrailBirder&lt;/strong&gt; is a small C# / .NET 8 app that listens to birds on the trail, names them, and writes you a short field-journal entry. It runs entirely on your own device, with no signal, account or cloud.&lt;/p&gt;

&lt;p&gt;The idea is that &lt;strong&gt;the screen should be the shortest part of the walk&lt;/strong&gt;. You start it and put the device away. This is what live mode looks like (example output from BirdNET's sample recording):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;▸ Listening · Inglewood Bird Sanctuary · Oct 10 — press Ctrl+C to stop
  11:18:17  Black-capped Chickadee       0.81 likely    ★ new today
  11:18:26  House Finch                  0.64 probable  ★ new today
  11:18:58  Dark-eyed Junco              0.74 likely    ★ new today
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It's built for Alberta. It has presets for places like Inglewood Bird Sanctuary, Fish Creek, Frank Lake, Kananaskis, Elk Island and Waterton. You can also ask it before you leave:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trailbirder species --where fish-creek
▸ Likely around Fish Creek Provincial Park the week of Oct 10
   88 %  Black-billed Magpie
   81 %  Black-capped Chickadee
   77 %  Common Raven
   ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It's for anyone who wants to learn the birds they keep hearing, from a first-time birder to someone keeping a season list. It works three ways: a phone voice memo you analyze later, a laptop at a viewpoint, or a Raspberry Pi with a USB mic left by a marsh.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;&lt;a href="https://github.com/GurminderKaur/TrailBirder/actions" rel="noopener noreferrer"&gt;CI run&lt;/a&gt;&lt;/strong&gt; is a working demo anyone can inspect. On every push, GitHub Actions does two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It builds the app on Windows, macOS and Linux and runs the bird models on each.&lt;/li&gt;
&lt;li&gt;On Ubuntu, it installs Ollama, pulls &lt;strong&gt;Gemma 3 (1B)&lt;/strong&gt;, and analyzes BirdNET's example recording end to end. The journal it writes appears in the run summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also run it yourself in about 20 minutes. The README has a step-by-step Windows setup, and BirdNET's sample recording makes a quick first test.&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/GurminderKaur" rel="noopener noreferrer"&gt;
        GurminderKaur
      &lt;/a&gt; / &lt;a href="https://github.com/GurminderKaur/TrailBirder" rel="noopener noreferrer"&gt;
        TrailBirder
      &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;TrailBirder 🐦&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/GurminderKaur/TrailBirder/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/GurminderKaur/TrailBirder/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Models are for non-commercial use only.&lt;/strong&gt; The BirdNET models in &lt;code&gt;models/&lt;/code&gt; are licensed CC BY-NC-SA 4.0 (see &lt;a href="https://github.com/GurminderKaur/TrailBirder/models/LICENSE.md" rel="noopener noreferrer"&gt;models/LICENSE.md&lt;/a&gt;). The TrailBirder source code is MIT licensed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Offline bird-sound ID and field journal for Alberta trails, built in C# / .NET 8.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Leave your phone in your pocket, record or listen on the trail, and get a species list plus a short field-journal entry. Everything runs on your own device. No signal, account, or cloud is needed.&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;▸ Listening · Inglewood Bird Sanctuary · Oct 10 — press Ctrl+C to stop
  11:18:17  Black-capped Chickadee       0.81 likely    ★ new today
  11:18:26  House Finch                  0.64 probable  ★ new today
  11:18:58  Dark-eyed Junco              0.74 likely    ★ new today
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;Step&lt;/th&gt;

&lt;th&gt;Open piece&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;Hear&lt;/td&gt;

&lt;td&gt;Your mic (NAudio on Windows, &lt;code&gt;arecord&lt;/code&gt; on Linux/Pi, ffmpeg on macOS) or recordings from your phone&lt;/td&gt;

&lt;td&gt;Device&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;Identify&lt;/td&gt;

&lt;td&gt;
&lt;br&gt;
&lt;strong&gt;BirdNET v2.4&lt;/strong&gt; (open-weight model from the&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;…&lt;p&gt;&lt;/p&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/GurminderKaur/TrailBirder" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


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

&lt;p&gt;There are three open pieces, and all of them run locally:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Open piece&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Identify&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;BirdNET v2.4&lt;/strong&gt; (Cornell Lab of Ornithology and TU Chemnitz), run with &lt;strong&gt;ONNX Runtime&lt;/strong&gt; in .NET&lt;/td&gt;
&lt;td&gt;Turns 3 s of audio into scores for 6,522 sound classes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter&lt;/td&gt;
&lt;td&gt;BirdNET's &lt;strong&gt;range model&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Turns latitude, longitude and week into which species are plausible here right now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Gemma 3&lt;/strong&gt; through &lt;strong&gt;Ollama&lt;/strong&gt; (any Ollama model works via &lt;code&gt;--model&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Turns the detection table into a grounded journal entry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Getting BirdNET into C# was the interesting part.&lt;/strong&gt; BirdNET ships as TensorFlow Lite, and .NET has no good TFLite runtime, so I converted it to ONNX. The standard converter (&lt;code&gt;tf2onnx&lt;/code&gt;) failed on BirdNET's built-in spectrogram layer. That layer takes an FFT and then casts the complex result to float, which keeps only the &lt;em&gt;real part&lt;/em&gt;. That's an unusual pattern the converter doesn't support.&lt;/p&gt;

&lt;p&gt;Because the weights are open, I could fix it myself. The real part of an FFT is just a matrix multiply by a cosine table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Re(RFFT(x))[k] = Σₙ x[n] · cos(2πkn/N)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I wrote a small graph rewriter that swaps that step for a &lt;code&gt;MatMul&lt;/code&gt;, then checked the result against the original model on real audio. The largest difference in any bird's probability was &lt;strong&gt;0.00005&lt;/strong&gt;, so the converted model is effectively identical. The converter script is in the repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The range filter does more work than you'd expect.&lt;/strong&gt; On the test recording, the raw classifier also suggested a &lt;em&gt;Chestnut-winged Cuckoo&lt;/em&gt; (Southeast Asia) and a &lt;em&gt;Hawfinch&lt;/em&gt; (Europe). The range model scores both at 0.000 for Calgary in October, so they never reach the user. It also reflects the season: mid-October around Calgary means magpies, ravens, chickadees, Bohemian waxwings and late waterfowl, and that's what it expects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The LLM is fenced in.&lt;/strong&gt; Small models will happily invent a Great Horned Owl. The journal prompt gives the model only two lists, both computed on the device: what BirdNET detected and what the range model expects nearby. It is told to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;never mention species outside those lists,&lt;/li&gt;
&lt;li&gt;flag IDs below 0.5 confidence as uncertain and point you to the clip,&lt;/li&gt;
&lt;li&gt;remember that "4 clips" doesn't mean 4 birds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Ollama isn't running, nothing breaks. Detections and clips are still saved, and &lt;code&gt;trailbirder journal&lt;/code&gt; writes the entry later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Other details:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microphone input uses NAudio on Windows, &lt;code&gt;arecord&lt;/code&gt; on Linux/Pi and ffmpeg on macOS.&lt;/li&gt;
&lt;li&gt;There's a built-in resampler for when ffmpeg isn't installed.&lt;/li&gt;
&lt;li&gt;The models are split into pieces under 25 MB for GitHub and reassembled on first run, with a SHA-256 check.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Why Does Open Innovation
&lt;/h2&gt;

&lt;p&gt;It costs nothing to run. On a CPU with no GPU, two minutes of audio is analyzed in about two seconds. A full morning of recordings costs nothing, and nobody needs an API key.&lt;/p&gt;
&lt;h2&gt;
  
  
  Taking It Outside
&lt;/h2&gt;

&lt;p&gt;On Sunday, October 11, I went to Inglewood Bird Santuary around 10:00 am. It was -1 °C with snow/wind, an early taste of Alberta winter.&lt;/p&gt;

&lt;p&gt;I kept my phone in my pocket and only took my glove off to start voice memos of about  minutes each. The screen was out for less than a minute in total.&lt;/p&gt;

&lt;p&gt;At home, TrailBirder analyzed 1 minutes of audio in 25 seconds on my laptop, fully offline.&lt;/p&gt;

&lt;p&gt;Black-capped chickadee clips 1 best 0.81 likely&lt;br&gt;
Black eyed junco clips 4 best 0.74 likely&lt;br&gt;
House finch clips 4 best 0.64 probable&lt;br&gt;
American Goldfinch clips 3 best 0.48 possible&lt;br&gt;
Blue Jay clips 1 best 0.44 possible&lt;/p&gt;
&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this with an AI coding agent (Claude). We worked through choosing the stack and the model conversion, then testing against the original model, CI on three operating systems, and an end-to-end run with a real Ollama model.&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Claude Code"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building TrailBirder: BirdNET to ONNX in C#, a local Gemma field journal, and CI on 3 operating systems&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        0 of 0 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma.&lt;/strong&gt; Gemma 3 runs locally through Ollama to write the field journal. It's the default model (&lt;code&gt;gemma3:4b&lt;/code&gt;), and CI tests it end to end with &lt;code&gt;gemma3:1b&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BirdNET v2.4&lt;/strong&gt; models © K. Lisa Yang Center for Conservation Bioacoustics (Cornell Lab of Ornithology) and Chemnitz University of Technology, licensed CC BY-NC-SA 4.0. Kahl et al. (2021), &lt;em&gt;Ecological Informatics&lt;/em&gt; 61, 101236.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/microsoft/onnxruntime" rel="noopener noreferrer"&gt;ONNX Runtime&lt;/a&gt;, &lt;a href="https://github.com/onnx/tensorflow-onnx" rel="noopener noreferrer"&gt;tf2onnx&lt;/a&gt;, &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;, &lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma&lt;/a&gt;, &lt;a href="https://github.com/naudio/NAudio" rel="noopener noreferrer"&gt;NAudio&lt;/a&gt;, &lt;a href="https://ffmpeg.org" rel="noopener noreferrer"&gt;ffmpeg&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;TrailBirder code: MIT.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>dotnet</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
