<?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: fatik07</title>
    <description>The latest articles on DEV Community by fatik07 (@fatik07).</description>
    <link>https://dev.to/fatik07</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%2F534182%2Fab0cd111-a5ad-496a-82ee-2d4dbf301098.png</url>
      <title>DEV Community: fatik07</title>
      <link>https://dev.to/fatik07</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fatik07"/>
    <language>en</language>
    <item>
      <title>I let Gemma 4 name 198 screenshots I was never going to rename</title>
      <dc:creator>fatik07</dc:creator>
      <pubDate>Tue, 20 Oct 2026 09:19:19 +0000</pubDate>
      <link>https://dev.to/fatik07/i-let-gemma-4-name-198-screenshots-i-was-never-going-to-rename-n3e</link>
      <guid>https://dev.to/fatik07/i-let-gemma-4-name-198-screenshots-i-was-never-going-to-rename-n3e</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 screenshot folder has 226 files in it. 198 of them are called something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Screenshot from 2022-11-15 10-01-34.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;193 are from my old Ubuntu laptop, moved over to my Mac and never touched again. Every time I need "that Laravel error from last year", I open them one by one. I have never renamed a screenshot by hand, and I'm not going to start now.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;sloth-shots&lt;/strong&gt;, a small command-line tool that looks at each screenshot with an open-weight vision model (Gemma 4) and renames it after what's actually in it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Screenshot from 2022-11-15 10-01-34.png  -&amp;gt;  2022-11-15_laravel-postgresql-role-does-not-exist.png
Screenshot from 2022-12-14 10-33-31.png  -&amp;gt;  2022-12-14_php-undefined-type-pdf-error.png
Screenshot from 2022-03-29 10-09-20.png  -&amp;gt;  2022-03-29_jsfiddle-javascript-matrix-code-snippet.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The date stays at the front, so the folder still sorts by time. The rest of the name is something Spotlight can actually find.&lt;/p&gt;

&lt;p&gt;It has two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tidy&lt;/code&gt; renames the backlog you already have.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;watch&lt;/code&gt; stays running in the background. Take a screenshot, and about a second later it has a real name. You don't do anything.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;This is a real dry run on 20 copies of screenshots from my folder (Gemma 4 31B through OpenRouter). The whole run took 22 seconds:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ sloth-shots tidy ~/demo
Dry run (nothing is renamed; add --apply): 20 screenshot(s)

Screenshot from 2022-08-25 09-20-51.png
  ~&amp;gt; 2022-08-25_vscode-file-explorer-project-structure.png  (1.3s)
Screenshot from 2022-09-13 14-34-26.png
  ~&amp;gt; 2022-09-13_comparison-shared-vs-cloud-hosting.png  (1.2s)
Screenshot from 2022-09-21 14-20-35.png
  ~&amp;gt; 2022-09-21_dicoding-computer-knowledge-quiz.png  (0.9s)
Screenshot from 2022-10-06 10-52-21.png
  ~&amp;gt; 2022-10-06_vscode-css-flexbox-container-settings.png  (0.9s)
Screenshot from 2022-11-15 10-01-34.png
  ~&amp;gt; 2022-11-15_laravel-postgresql-role-does-not-exist.png  (0.9s)
Screenshot from 2022-12-05 09-42-39.png
  ~&amp;gt; 2022-12-05_petanikode-programming-loop-flowchart.png  (0.9s)
Screenshot from 2022-12-14 10-33-31.png
  ~&amp;gt; 2022-12-14_php-undefined-type-pdf-error.png  (2.7s)
Screenshot from 2023-02-04 11-51-35.png
  ~&amp;gt; 2023-02-04_phpmyadmin-rajaongkir-province-table.png  (0.9s)
Screenshot from 2023-05-08 14-31-20.png
  ~&amp;gt; 2023-05-08_vscode-json-api-response-data.png  (1.0s)
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You may notice there are no screenshots in this post, only filenames. That's on purpose. When I checked the results, the Laravel error contained a company database hostname, its IP address and an employee ID, and the JSFiddle one was taken during a Google Meet call with other people's names and faces in it. Screenshots are full of things you'd never post. That's also why I care about the model being open, more on that below.&lt;/p&gt;

&lt;p&gt;I checked several of the names against the actual images. Most were spot on. One was wrong: &lt;code&gt;terminal-update-alternatives-command&lt;/code&gt; is really a Telegram chat preview that happens to show a &lt;code&gt;sudo update-alternatives&lt;/code&gt; command. That's a reasonable mistake, and the name is still more findable than &lt;code&gt;Screenshot 2023-06-17 at 14.13.24.png&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/fatik07" rel="noopener noreferrer"&gt;
        fatik07
      &lt;/a&gt; / &lt;a href="https://github.com/fatik07/lazy-screenshoot-renamer" rel="noopener noreferrer"&gt;
        lazy-screenshoot-renamer
      &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-shots 🦥&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Your screenshot folder is full of files called &lt;code&gt;Screenshot 2023-06-17 at 14.11.12.png&lt;/code&gt;. You will never find the one you need.&lt;/p&gt;

&lt;p&gt;sloth-shots looks at each screenshot with an open-weight vision model (Gemma 4) and renames it after what's in it. Real output from my own screenshot folder:&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;Screenshot from 2022-11-15 10-01-34.png  -&amp;gt;  2022-11-15_laravel-postgresql-role-does-not-exist.png
Screenshot from 2022-12-14 10-33-31.png  -&amp;gt;  2022-12-14_php-undefined-type-pdf-error.png
Screenshot from 2022-03-29 10-09-20.png  -&amp;gt;  2022-03-29_jsfiddle-javascript-matrix-code-snippet.png
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Leave &lt;code&gt;watch&lt;/code&gt; running and you never think about it again. Take a screenshot and about a second later it has a name you can search for in Spotlight.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;
&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Get an API key at &lt;a href="https://openrouter.ai/keys" rel="nofollow noopener noreferrer"&gt;https://openrouter.ai/keys&lt;/a&gt;. A few dollars of credit goes a long way: each screenshot costs about $0.00003.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Export it:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;export&lt;/span&gt; OPENROUTER_API_KEY=sk-or-...&lt;/pre&gt;

&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run the tool. It has no dependencies beyond Python 3.10+:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;uvx --from &lt;span class="pl-c1"&gt;.&lt;/span&gt; sloth-shots --help
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or simply&lt;/span&gt;
python3 sloth_shots.py --help&lt;/pre&gt;

&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;If you don't pass a folder, sloth-shots…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/fatik07/lazy-screenshoot-renamer" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;It's one Python file with no third-party dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-or-...
python3 sloth_shots.py tidy &lt;span class="nt"&gt;--limit&lt;/span&gt; 5          &lt;span class="c"&gt;# dry run, renames nothing&lt;/span&gt;
python3 sloth_shots.py tidy &lt;span class="nt"&gt;--limit&lt;/span&gt; 5 &lt;span class="nt"&gt;--apply&lt;/span&gt;  &lt;span class="c"&gt;# apply (reuses the dry-run answers)&lt;/span&gt;
python3 sloth_shots.py watch                   &lt;span class="c"&gt;# rename new screenshots as you take them&lt;/span&gt;
python3 sloth_shots.py stats                   &lt;span class="c"&gt;# how much effort you've saved&lt;/span&gt;
python3 sloth_shots.py undo                    &lt;span class="c"&gt;# put every original name back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The pipeline for each screenshot is short:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find it.&lt;/strong&gt; Only image files whose names start with &lt;code&gt;Screenshot&lt;/code&gt; or &lt;code&gt;Screen Shot&lt;/code&gt; are touched. A renamed file no longer matches, so nothing gets processed twice. Other locales work with &lt;code&gt;--prefix&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shrink it.&lt;/strong&gt; macOS's built-in &lt;code&gt;sips&lt;/code&gt; downscales the image to 1024px before upload. A 965 KB retina PNG becomes about 80 KB. If the original is already smaller, it sends the original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask Gemma.&lt;/strong&gt; The image goes to Gemma 4 with a short prompt: 3 to 6 words, lowercase, hyphenated, name the app first, be specific ("vscode-python-import-error" beats "code-editor").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean the answer.&lt;/strong&gt; Models sometimes get chatty ("Sure! Here's a name: …"), so the tool pulls the hyphenated part out of the reply. If there's nothing usable, the file is skipped rather than renamed to junk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rename safely.&lt;/strong&gt; It never overwrites a file (collisions get &lt;code&gt;-2&lt;/code&gt;), logs every rename to a &lt;code&gt;.jsonl&lt;/code&gt; file so &lt;code&gt;undo&lt;/code&gt; works, and &lt;code&gt;tidy&lt;/code&gt; is a dry run until you add &lt;code&gt;--apply&lt;/code&gt;. Dry-run answers are cached, so applying them doesn't call the model again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;watch&lt;/code&gt; is a plain polling loop: it checks the folder every 2 seconds and waits until a file's size stops changing, because macOS writes screenshots in steps.&lt;/p&gt;

&lt;p&gt;The hardest part wasn't the code, it was getting a response at all. I started on the free Gemma 4 endpoints, and this week they were swamped. Every request came back &lt;code&gt;429: temporarily rate-limited upstream&lt;/code&gt;, and the few that got through took 70 to 90 seconds. So the tool now tries a list of models in order: Gemma 4 31B, then Gemma 4 26B-A4B, then the free tier. If one is busy or out of credit, it moves to the next instead of sitting in a retry loop.&lt;/p&gt;

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

&lt;p&gt;Gemma 4's weights are public, and that changed how this project turned out in two ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It made it almost free.&lt;/strong&gt; Because anyone can host the weights, about a dozen providers serve Gemma 4 on OpenRouter and compete on price. Naming my 20 test screenshots cost $0.0008. The full backlog of 198 comes to under one cent. When the free pool was overloaded, I could simply switch to a paid provider running the exact same model. With a closed model, I'd have one vendor and one price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It means the model is mine to keep.&lt;/strong&gt; The model naming my screenshots today will behave the same way next year. Nobody can deprecate it or quietly retrain it, which matters for a tool whose whole job is to be consistent across thousands of files.&lt;/p&gt;

&lt;p&gt;And there's the privacy problem I hit in the demo: my screenshots contain hostnames, IDs and other people's faces. Right now they go through OpenRouter. Because the weights are open, the same model can also run fully offline on my own laptop, and a local backend is the next thing I want to add. That's only an option because the model is open.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;198&lt;/strong&gt; screenshots in my folder with no real name.&lt;/li&gt;
&lt;li&gt;Renaming one by hand means opening it, looking at it, thinking of a name and typing it. Call it 20 seconds. That's about &lt;strong&gt;66 minutes&lt;/strong&gt; I was never going to spend.&lt;/li&gt;
&lt;li&gt;sloth-shots did 20 of them in &lt;strong&gt;22 seconds&lt;/strong&gt; for &lt;strong&gt;$0.0008&lt;/strong&gt;, and in &lt;code&gt;watch&lt;/code&gt; mode it takes zero effort from me from now on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part is that I don't have to remember to run it. That's the whole point.&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 (31B and 26B-A4B) is the only model the project uses. It does all the vision work.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>python</category>
    </item>
    <item>
      <title>Strukku: Receipt Bookkeeping for Freelancers, Built on an Open-Weight Model</title>
      <dc:creator>fatik07</dc:creator>
      <pubDate>Thu, 15 Oct 2026 06:05:58 +0000</pubDate>
      <link>https://dev.to/fatik07/strukku-receipt-bookkeeping-for-freelancers-built-on-an-open-weight-model-oa3</link>
      <guid>https://dev.to/fatik07/strukku-receipt-bookkeeping-for-freelancers-built-on-an-open-weight-model-oa3</guid>
      <description>&lt;p&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;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Strukku&lt;/strong&gt;: upload a photo of a receipt, and an open-weight vision model reads it — vendor, date, amount, currency, and expense category — straight into a running bookkeeping ledger you can export as a CSV.&lt;/p&gt;

&lt;p&gt;The target user is a freelancer or small shop owner who currently does one of two things every month: pays a bookkeeper to manually retype a shoebox of receipts, or loses an evening doing it themselves. Strukku turns that into "take a photo, done."&lt;/p&gt;

&lt;p&gt;It's specifically tuned for a case most bookkeeping tools handle badly: &lt;strong&gt;messy, mixed-language, informal local receipts&lt;/strong&gt; — a handwritten note from a small Indonesian &lt;em&gt;warung&lt;/em&gt;, a Gojek/Grab ride receipt, a market vendor's scrawled total. General-purpose OCR and bookkeeping SaaS are trained mostly on tidy, printed, English-first receipts, and fumble exactly this case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who would pay for this
&lt;/h2&gt;

&lt;p&gt;Anyone currently paying a bookkeeper, or burning an afternoon a month doing their own books by hand. A tool that's faster than both and cheap enough to run for free is worth a small monthly fee on its own merits.&lt;/p&gt;

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

&lt;p&gt;Not deployed yet — I built and verified this entirely within the challenge window (typecheck and production build both pass), but haven't yet run it against real photographed receipts with a live Supabase + OpenRouter setup, so I'm not going to claim a polished demo I haven't actually seen work. The honest next step, before I'd trust this with real bookkeeping, is exactly that: clone it, wire up a free Supabase project and OpenRouter key (both free), and feed it a stack of real receipts.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/fatik07/strukku" rel="noopener noreferrer"&gt;github.com/fatik07/strukku&lt;/a&gt; — MIT-style setup instructions in the README, including the SQL to set up Supabase.&lt;/p&gt;

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

&lt;p&gt;Stack: &lt;strong&gt;TanStack Start&lt;/strong&gt;, TypeScript, &lt;strong&gt;TanStack Router&lt;/strong&gt;, &lt;strong&gt;TanStack Query&lt;/strong&gt;, &lt;strong&gt;TanStack AI&lt;/strong&gt; (&lt;code&gt;@tanstack/ai&lt;/code&gt; + &lt;code&gt;@tanstack/ai-openrouter&lt;/code&gt;), &lt;strong&gt;Supabase&lt;/strong&gt; (Postgres + Storage), &lt;strong&gt;OpenRouter&lt;/strong&gt;, Tailwind CSS.&lt;/p&gt;

&lt;p&gt;The flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receipt photo → base64 → Supabase Storage (saved) → vision model (OpenRouter) → structured JSON → Zod validation → receipts table → ledger + CSV export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI call sends the photo as an image content part alongside a prompt tuned for the messy-receipt case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;imageMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RECEIPT_EXTRACTION_PROMPT&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mimeType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt itself explicitly tells the model what it's looking at:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The receipt may be in Indonesian, English, or a mix of both, and may be a neat printed receipt, a crumpled handwritten note from a small local shop (warung), or a ride-hailing receipt (e.g. Gojek, Grab)."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model's output is validated against a Zod schema before it ever touches the database — if the model doesn't support strict structured output, there's a plain-JSON fallback that re-parses its response, same defensive pattern I'd use for any untrusted model output.&lt;/p&gt;

&lt;p&gt;The entire AI call is isolated in one module (&lt;code&gt;src/server/ai/adapter.ts&lt;/code&gt;), so swapping models — or providers — is a one-line change, not a rewrite.&lt;/p&gt;

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

&lt;p&gt;Three concrete reasons I picked an explicit open-weight model (Google's &lt;strong&gt;Gemma&lt;/strong&gt;, via OpenRouter's free tier — &lt;code&gt;google/gemma-4-31b-it:free&lt;/code&gt;, not the &lt;code&gt;openrouter/free&lt;/code&gt; router, which could silently pick a non-open model for a given request) over a closed API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; The whole pitch is "cheaper than a bookkeeper." An AI step that costs nothing to run is what makes that arithmetic actually work at a small monthly price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in.&lt;/strong&gt; If a different open-weight model turns out to read Indonesian handwritten receipts better, swapping it in is a one-line change in &lt;code&gt;adapter.ts&lt;/code&gt;, not a renegotiated API contract with a single vendor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Financial data privacy.&lt;/strong&gt; Receipts are financial records. Keeping the AI layer open-weight keeps the door open to eventually pointing this at a self-hosted model instead of permanently renting inference from one closed provider — a real option for a tool that handles someone's money.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Not yet — this is fresh code built during the challenge window, not yet handed to a real freelancer. The honest next step is exactly that: get it in front of someone who actually has a shoebox of receipts, and find out where the extraction breaks on their specific mess of paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Disclosure
&lt;/h2&gt;

&lt;p&gt;Built with heavy assistance from an AI coding agent (Claude Code) — architecture, implementation, and this write-up. I reviewed the code and the claims above myself; nothing here claims to be tested beyond what's stated (typecheck + production build pass; live extraction accuracy against real receipts is the next thing to verify, not something I'm claiming already works).&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>What's in My Fridge? — An Open-Source AI Cooking Assistant I Built for a Friend</title>
      <dc:creator>fatik07</dc:creator>
      <pubDate>Mon, 05 Oct 2026 04:59:31 +0000</pubDate>
      <link>https://dev.to/fatik07/whats-in-my-fridge-an-open-source-ai-cooking-assistant-i-built-for-a-friend-12ge</link>
      <guid>https://dev.to/fatik07/whats-in-my-fridge-an-open-source-ai-cooking-assistant-i-built-for-a-friend-12ge</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;A friend of mine has a habit: they open the fridge, stare at a handful of random ingredients — some eggs, half a chicken, a bag of rice — and just... close it again. Not because there's nothing to cook, but because turning "what I have" into "what I could make" takes a kind of mental effort that's easy to skip when you're tired.&lt;/p&gt;

&lt;p&gt;So for this weekend's Hacktoberfest challenge — &lt;strong&gt;Build for a Friend&lt;/strong&gt; — I built them exactly that: &lt;strong&gt;What's in My Fridge?&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tell me what you have. I'll tell you what you can cook.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You add the ingredients sitting in your kitchen, optionally tell it something like "no spicy food" or "only 15 minutes," hit one button, and get back a handful of realistic meal ideas — each one clearly split into what you already have and what you'd still need to buy.&lt;/p&gt;

&lt;p&gt;No chat window. No meal-planning dashboard. Just: ingredients in, meal ideas out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo &amp;amp; code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/fatik07/my-frigde" rel="noopener noreferrer"&gt;github.com/fatik07/my-frigde&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; not deployed yet — this was built and tested locally during the challenge window. The README has full setup instructions if you want to run it yourself.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The AI model is the core feature of this app — not a bolt-on. It runs through &lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt;, defaulting to &lt;code&gt;openrouter/free&lt;/code&gt;, OpenRouter's router that automatically picks a free, open model for each request.&lt;/p&gt;

&lt;p&gt;Why that choice specifically, and not a single proprietary model locked behind one vendor's API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; A friend using this shouldn't need me to keep a paid API key topped up. &lt;code&gt;openrouter/free&lt;/code&gt; means the running cost is zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It isn't locked to one model.&lt;/strong&gt; The entire AI call is isolated behind one small module (&lt;code&gt;src/server/ai/adapter.ts&lt;/code&gt;). Swapping to a different open model — or a different provider entirely — is a one-line change, not a rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rest of the app doesn't know or care which model answered.&lt;/strong&gt; Everything downstream just sees validated, typed JSON.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be precise about what "open" means in this build: inference doesn't run locally on my friend's laptop — it still happens on OpenRouter's infrastructure. What open buys here is &lt;em&gt;choice and cost&lt;/em&gt;, not offline/local inference. I think that's a legitimate and underrated version of "open matters": it's the difference between a project my friend can actually keep running for free, versus one that quietly stops working the day my API credits run out.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Stack: &lt;strong&gt;TanStack Start&lt;/strong&gt;, TypeScript, &lt;strong&gt;TanStack Router&lt;/strong&gt;, &lt;strong&gt;TanStack Query&lt;/strong&gt;, &lt;strong&gt;TanStack AI&lt;/strong&gt; (&lt;code&gt;@tanstack/ai&lt;/code&gt; + &lt;code&gt;@tanstack/ai-openrouter&lt;/code&gt;), &lt;strong&gt;Supabase&lt;/strong&gt; (Postgres), &lt;strong&gt;OpenRouter&lt;/strong&gt;, Tailwind CSS.&lt;/p&gt;

&lt;p&gt;The flow is deliberately boring and linear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ingredients (Supabase) → AI call (server-only) → OpenRouter → structured JSON → reconciled against pantry → rendered meal cards
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few pieces worth calling out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The AI call never touches the browser.&lt;/strong&gt; It's a TanStack Start server function, and the OpenRouter key only ever lives in &lt;code&gt;process.env&lt;/code&gt; on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getMealSuggestionAdapter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;OpenRouterTextAdapter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENROUTER_API_KEY&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPENROUTER_API_KEY is not configured&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AI_MODEL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openrouter/free&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createOpenRouterText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. The response is schema-validated, not trusted prose.&lt;/strong&gt; &lt;code&gt;chat()&lt;/code&gt; takes a Zod &lt;code&gt;outputSchema&lt;/code&gt;, so the model either returns a typed object or the call fails loudly — with a plain-JSON fallback for models that don't support strict structured output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;outputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AiSuggestionsResponseSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. The model's claims get fact-checked against the real pantry.&lt;/strong&gt; This was the part I cared about most: an AI that says "uses: chicken" when you don't actually have chicken is worse than useless. So every suggestion gets reconciled server-side — anything the model claims as "used" that isn't genuinely in your pantry gets demoted to "missing" before it's ever rendered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcileWithPantry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MealSuggestion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pantryNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// anything claimed as "used" that isn't actually in the pantry&lt;/span&gt;
  &lt;span class="c1"&gt;// gets moved to "missing" instead — the model can't fake what you have&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard stuff rounds it out: empty-pantry state, a loading state ("Thinking of something delicious…"), and a retry-able error state that never leaks a raw API error to the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Given more time, the next step is exactly what the theme asks for: hand it to my friend and see what breaks. Right now it's been built and exercised locally, but not yet used by the person it's actually for — that's the real test, and I'll be reporting back on what they said.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI disclosure
&lt;/h2&gt;

&lt;p&gt;This project and this post were built with heavy assistance from an AI coding agent (Claude Code) — architecture, implementation, debugging version mismatches in the TanStack Start/Router packages, and writing this post. I reviewed and verified the code and claims above myself.&lt;/p&gt;




&lt;p&gt;Built solo for the &lt;strong&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/strong&gt; (&lt;code&gt;#hf26challenge&lt;/code&gt;).&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
