<?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: Gabriel Almeida</title>
    <description>The latest articles on DEV Community by Gabriel Almeida (@ghalmeidadev).</description>
    <link>https://dev.to/ghalmeidadev</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%2F587131%2F4701287c-3318-4326-99bc-79c0d938696a.jpeg</url>
      <title>DEV Community: Gabriel Almeida</title>
      <link>https://dev.to/ghalmeidadev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ghalmeidadev"/>
    <language>en</language>
    <item>
      <title>I Haunted My Terminal With a Local Ghost (Gemma 3, No Internet Needed)</title>
      <dc:creator>Gabriel Almeida</dc:creator>
      <pubDate>Mon, 26 Oct 2026 23:30:44 +0000</pubDate>
      <link>https://dev.to/ghalmeidadev/i-haunted-my-terminal-with-a-local-ghost-gemma-3-no-internet-needed-k51</link>
      <guid>https://dev.to/ghalmeidadev/i-haunted-my-terminal-with-a-local-ghost-gemma-3-no-internet-needed-k51</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;&lt;strong&gt;shell-ghost&lt;/strong&gt; is a haunted terminal. After some of the commands you run, a ghost whispers one eerie line about what you just did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ rm -rf node_modules
  👻 Don't worry, it just wanted a little... tidier oblivion.

$ git push --force
  👻 Don't you dare rewrite my carefully crafted digital chaos.

$ sudo make me a sandwich
  👻 It wants fillings… and a very long, slow, digital chew.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default it speaks on about 1 in 3 commands, so it stays a surprise instead of a nuisance.&lt;/p&gt;

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

&lt;p&gt;Repo: &lt;a href="https://github.com/GAlmeidaDev/shell-ghost" rel="noopener noreferrer"&gt;https://github.com/GAlmeidaDev/shell-ghost&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Two tiny files, no dependencies beyond Python 3 and zsh:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;haunt.zsh&lt;/code&gt; hooks into zsh with &lt;code&gt;preexec&lt;/code&gt; (to remember the command) and &lt;code&gt;precmd&lt;/code&gt; (to read its exit code). It rolls a die, and if the ghost is feeling chatty it calls the script.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ghost.py&lt;/code&gt; sends the command and exit code to a local model through Ollama's HTTP API and prints one dim purple line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole prompt is a few lines: &lt;em&gt;you are a ghost trapped inside a computer, reply with one short eerie whisper, spooky and a little funny.&lt;/em&gt; Temperature 0.9 and a 48-token cap keep it short and weird.&lt;/p&gt;

&lt;p&gt;Try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:4b
python3 ghost.py &lt;span class="s2"&gt;"rm -rf node_modules"&lt;/span&gt; &lt;span class="nt"&gt;--code&lt;/span&gt; 1
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'source /path/to/shell-ghost/haunt.zsh'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;GHOST_ODDS=1&lt;/code&gt; and it speaks after every command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Source AI Matters Here
&lt;/h2&gt;

&lt;p&gt;A terminal sees everything: your commands, paths, hostnames, sometimes secrets typed inline. Sending that to a hosted API for a joke would be a bad trade.&lt;/p&gt;

&lt;p&gt;With an open-weight model running locally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing leaves your machine.&lt;/strong&gt; The only network call is to &lt;code&gt;localhost&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works offline.&lt;/strong&gt; Plane, train, or a dead Wi-Fi: the ghost still haunts you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can't break your shell.&lt;/strong&gt; If Ollama isn't running, the call fails silently and the ghost simply stays quiet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You pick the ghost.&lt;/strong&gt; &lt;code&gt;--model&lt;/code&gt; swaps in any model Ollama can run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 4B model is plenty for one spooky sentence, and that's the point: small, open models are good enough to make tools feel alive without a cloud dependency.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Small models follow "ONE line, max 18 words" surprisingly well, but &lt;code&gt;splitlines()[0]&lt;/code&gt; is still a cheap safety net.&lt;/li&gt;
&lt;li&gt;For shell tooling, failing silently is a feature. A decorative tool must never be able to hurt the thing it decorates.&lt;/li&gt;
&lt;li&gt;Randomness (the 1-in-3 odds) matters more than model quality for the "haunted" feel.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  hf26challenge
&lt;/h1&gt;

</description>
      <category>hf26challenge</category>
      <category>hacktoberfest</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>sloth-sorter: a lazy file tidier that runs Gemma locally</title>
      <dc:creator>Gabriel Almeida</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:15:29 +0000</pubDate>
      <link>https://dev.to/ghalmeidadev/sloth-sorter-a-lazy-file-tidier-that-runs-gemma-locally-21lc</link>
      <guid>https://dev.to/ghalmeidadev/sloth-sorter-a-lazy-file-tidier-that-runs-gemma-locally-21lc</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&lt;/a&gt; — theme: Slow and Steady.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;sloth-sorter&lt;/strong&gt; 🦥 is a file tidier for people who keep putting off cleaning &lt;code&gt;Downloads&lt;/code&gt;. Point it at a folder: a local open-weight model (Gemma 3 4B through Ollama) reads each file, picks a category folder, and gives it a clean kebab-case name. It's a dry run by default, so nothing moves until you pass &lt;code&gt;--apply&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:4b
python3 sloth.py ~/Downloads            &lt;span class="c"&gt;# preview&lt;/span&gt;
python3 sloth.py ~/Downloads &lt;span class="nt"&gt;--apply&lt;/span&gt;    &lt;span class="c"&gt;# move files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It's about 60 lines of stdlib-only Python — no pip install.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;I made a messy folder with five files and ran it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Untitled-3.txt   -&amp;gt;  notes/meeting-notes.txt
doc (1).txt      -&amp;gt;  contracts/service-agreement.txt
export.csv       -&amp;gt;  docs/export-csv.csv
final_final2.py  -&amp;gt;  code/fib.py
scan_0001.txt    -&amp;gt;  invoices/scan-2041.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The model got four of five clearly right. &lt;code&gt;export.csv&lt;/code&gt; (a list of expenses) landed in &lt;code&gt;docs&lt;/code&gt; instead of something better, and its name just repeats the extension. Small models are imperfect, which is exactly why the dry run exists.&lt;/p&gt;
&lt;h2&gt;
  
  
  The lazy payoff
&lt;/h2&gt;

&lt;p&gt;Five files took about 12 seconds on a laptop, with zero decisions from me. Sorting them by hand means opening each one to remember what it is. The saving grows with the folder: you review a list instead of reading files.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why open innovation matters here
&lt;/h2&gt;

&lt;p&gt;Your Downloads folder holds invoices, contracts and personal notes. Sending those to a hosted API just to rename them would be a bad trade. With an open-weight model running through Ollama, file contents never leave the machine, it costs nothing per file, and I can swap the model with &lt;code&gt;--model&lt;/code&gt; for a bigger or smaller one.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;For each file it sends the name plus the first 600 characters (or just size/type for binaries) to Ollama with &lt;code&gt;format: json&lt;/code&gt;, validates the folder against a fixed list, sanitizes the name, and avoids overwriting by appending a counter.&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/GAlmeidaDev" rel="noopener noreferrer"&gt;
        GAlmeidaDev
      &lt;/a&gt; / &lt;a href="https://github.com/GAlmeidaDev/sloth-sorter" rel="noopener noreferrer"&gt;
        sloth-sorter
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;



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

&lt;p&gt;Read PDFs and images (a vision-capable Gemma), and add an undo log.&lt;/p&gt;

&lt;p&gt;Built with: Gemma 3 (Best Use of Gemma), Ollama, Python.&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>python</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Local Receipts: a receipt organizer for freelancers, powered by local Gemma 3</title>
      <dc:creator>Gabriel Almeida</dc:creator>
      <pubDate>Tue, 13 Oct 2026 00:17:26 +0000</pubDate>
      <link>https://dev.to/ghalmeidadev/local-receipts-a-receipt-organizer-for-freelancers-powered-by-local-gemma-3-41ga</link>
      <guid>https://dev.to/ghalmeidadev/local-receipts-a-receipt-organizer-for-freelancers-powered-by-local-gemma-3-41ga</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;&lt;strong&gt;Local Receipts&lt;/strong&gt; is a receipt organizer for Brazilian freelancers and MEIs (micro-entrepreneurs). You give it the text or a &lt;strong&gt;photo&lt;/strong&gt; of a receipt or invoice, and it returns the vendor, tax ID (CNPJ), date, total and expense category. At the end of the month you get a summary by category and a CSV that opens straight in Brazilian Excel.&lt;/p&gt;

&lt;p&gt;The person who would pay for it is the freelancer who spends the last weekend of every month digging through receipts for their accountant. Those receipts hold client names, tax IDs and amounts, which is exactly what you don't want to upload to someone else's server.&lt;/p&gt;

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

&lt;p&gt;Real output from my machine, with the model running locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST localhost:3000/receipts &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"text":"PADARIA PAO QUENTE LTDA\nCNPJ: 11.222.333/0001-81\nEmissao: 03/10/2026\nTOTAL R$ 21,50"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vendor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PADARIA PAO QUENTE LTDA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cnpj"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"11222333000181"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cnpjValid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuedOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalCents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BRL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alimentacao"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"needsReview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A month of receipts exports to a CSV with &lt;code&gt;;&lt;/code&gt; separators, decimal commas and a UTF-8 BOM, so it opens correctly in Excel without any import wizard:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;fornecedor&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;cnpj&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;valor&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;moeda&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;categoria&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;revisar&lt;/span&gt;
&lt;span class="ld"&gt;2026-10-03&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;PADARIA&lt;/span&gt; &lt;span class="k"&gt;PAO&lt;/span&gt; &lt;span class="k"&gt;QUENTE&lt;/span&gt; &lt;span class="k"&gt;LTDA&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="mf"&gt;11222333000181&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="mf"&gt;21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;50&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;BRL&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;alimentacao&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;n&lt;/span&gt;&lt;span class="err"&gt;ã&lt;/span&gt;&lt;span class="k"&gt;o&lt;/span&gt;
&lt;span class="ld"&gt;2026-10-08&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;Notion&lt;/span&gt; &lt;span class="k"&gt;Labs&lt;/span&gt; &lt;span class="k"&gt;Inc&lt;/span&gt;&lt;span class="err"&gt;.;;&lt;/span&gt;&lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;USD&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;software&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;e&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;assinaturas&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;sim&lt;/span&gt;
&lt;span class="ld"&gt;2026-10-10&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;POSTO&lt;/span&gt; &lt;span class="k"&gt;ESTRELA&lt;/span&gt; &lt;span class="k"&gt;COMBUSTIVEIS&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="mf"&gt;44555666000100&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="mf"&gt;198&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;40&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;BRL&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;transporte&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;&lt;span class="k"&gt;sim&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The last two rows are flagged for review on purpose: the Notion invoice is in dollars, and the gas station's tax ID fails validation (I made it up for the 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/GAlmeidaDev" rel="noopener noreferrer"&gt;
        GAlmeidaDev
      &lt;/a&gt; / &lt;a href="https://github.com/GAlmeidaDev/local-receipt" rel="noopener noreferrer"&gt;
        local-receipt
      &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;🧾 Local Receipts&lt;/h1&gt;
&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Organize os recibos do seu MEI ou freela sem mandar um centavo (nem um CNPJ) para a nuvem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Você envia o texto ou a &lt;strong&gt;foto&lt;/strong&gt; de um recibo ou nota fiscal. Um modelo &lt;strong&gt;open-weight rodando na sua máquina&lt;/strong&gt; (Gemma 3 4B via Ollama) lê o documento e extrai fornecedor, CNPJ, data, valor e categoria. O código &lt;strong&gt;confere o CNPJ&lt;/strong&gt; (dígitos verificadores) e marca para revisão tudo que não consegue confirmar. No fim do mês, você tem o resumo por categoria e um CSV que abre direto no Excel brasileiro.&lt;/p&gt;

&lt;p&gt;Feito para o Hacktoberfest 2026 Open-Source AI Challenge, tema &lt;em&gt;Side Hustle&lt;/em&gt;.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Por que não confiar só no modelo&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Um modelo de 4B parâmetros lê bem, mas erra. Por isso o modelo só &lt;strong&gt;sugere&lt;/strong&gt; e o código &lt;strong&gt;decide&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CNPJ:&lt;/strong&gt; validado com os dígitos verificadores. Inválido ou ausente → &lt;code&gt;needsReview&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moeda:&lt;/strong&gt; o valor nunca é convertido. Documento…&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/GAlmeidaDev/local-receipt" 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;The open-weight model.&lt;/strong&gt; Gemma 3 4B, served by Ollama on my own machine. It reads both text and images, so one model handles pasted text and photos of receipts. Ollama's &lt;code&gt;format&lt;/code&gt; field takes a JSON schema (generated from a zod schema), so the model has to answer in a fixed shape. The reply is validated with the same schema, and an invalid one becomes a clean &lt;code&gt;503&lt;/code&gt; instead of garbage in the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model suggests, the code decides.&lt;/strong&gt; A 4B model reads well but is wrong sometimes, and it is bad news when it is wrong about money. So everything checkable is checked outside the model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CNPJ is validated with its two check digits. If it is missing or invalid, the receipt is flagged.&lt;/li&gt;
&lt;li&gt;Amounts are stored as integer cents.&lt;/li&gt;
&lt;li&gt;The same document is never entered twice (content hash).&lt;/li&gt;
&lt;li&gt;If the date, tax ID or total can't be confirmed, the receipt gets &lt;code&gt;needsReview: true&lt;/code&gt;, and the model is told to answer &lt;code&gt;null&lt;/code&gt; rather than guess.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Two bugs my own tests found.&lt;/strong&gt; Mocked tests passed, so I ran real receipts through Gemma and found two problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A gas station receipt came back as &lt;strong&gt;food&lt;/strong&gt; (&lt;code&gt;alimentacao&lt;/code&gt;). Fix: I added a short category guide to the prompt, and it now comes back as &lt;code&gt;transporte&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;US$ 10.00&lt;/strong&gt; invoice was saved as &lt;strong&gt;R$ 10,00&lt;/strong&gt; with no warning. That is the kind of silent error that costs real money. Fix: the model now extracts the currency, the amount is never converted, and any non-BRL document goes to review and stays out of the BRL total.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both are the kind of failure a happy-path demo never shows. The final suite has 9 tests with the model mocked, and the real model was run against the cases above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Node, TypeScript, Fastify, zod, SQLite (better-sqlite3), Ollama, Gemma 3 4B.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy is the product.&lt;/strong&gt; A receipt has a CNPJ, an amount and sometimes a client's name. With a local model, none of it leaves the laptop, and that is a feature I can honestly put on the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unit economics work.&lt;/strong&gt; Processing 10 or 1,000 receipts costs the same: zero marginal cost. There is no per-token bill that forces a subscription price up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It works offline.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in.&lt;/strong&gt; &lt;code&gt;OLLAMA_MODEL&lt;/code&gt; swaps the model, and open weights mean a model could later be fine-tuned on Brazilian invoices, which a generic API handles unevenly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I didn't compare against a closed API, so I won't claim it would extract worse. The point is where the data goes and what each document costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;My photo test used a clean image I generated myself. A crooked, shadowed phone photo will fail more often, and I haven't measured by how much.&lt;/li&gt;
&lt;li&gt;No direct PDF support yet (convert to an image or send the text).&lt;/li&gt;
&lt;li&gt;Categories are the model's suggestion, so review what comes flagged.&lt;/li&gt;
&lt;li&gt;No login: it is designed as a single-user local tool.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A tiny upload page, PDF support, and fine-tuning on Brazilian invoice formats.&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 the extraction, for both text and photos.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>TouchGrass API: outdoor missions from local Gemma 3 and the weather</title>
      <dc:creator>Gabriel Almeida</dc:creator>
      <pubDate>Tue, 06 Oct 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/ghalmeidadev/touchgrass-api-outdoor-missions-from-local-gemma-3-and-the-weather-575d</link>
      <guid>https://dev.to/ghalmeidadev/touchgrass-api-outdoor-missions-from-local-gemma-3-and-the-weather-575d</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;&lt;strong&gt;TouchGrass API&lt;/strong&gt; is a small backend that answers one question: &lt;em&gt;"I have 20 free minutes. What should I do outside?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You send it a city and the minutes you have. It looks up the current weather, hands that to a local open-weight model, and returns one short, concrete outdoor mission that fits both the sky and your schedule. Drizzle gets a "stand under an awning and listen to the rain" mission. A clear afternoon gets a walk with a destination. Do the mission, check in, and the API tracks your streak of consecutive days.&lt;/p&gt;

&lt;p&gt;It is three endpoints on purpose, so the screen stays the shortest part of the experience: ask, read one paragraph, go outside.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /mission&lt;/code&gt; takes &lt;code&gt;{ city, minutes }&lt;/code&gt; and returns a mission.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /checkin&lt;/code&gt; marks a mission as done. It is idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /streak&lt;/code&gt; returns &lt;code&gt;{ current, longest, completedToday }&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is built for anyone who wants a nudge off the screen without installing another app that wants their data. Because it is an API, it can sit behind a CLI, a shortcut, a widget or a bot.&lt;/p&gt;

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

&lt;p&gt;This is a real response from my machine, with the model running locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST localhost:3000/mission &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"city":"Curitiba","minutes":20}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Observação da Garoa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Saia e observe a garoa que cobre Curitiba. Concentre-se nos sons da chuva, nas gotas que escorrem pelas folhas e na sensação do ar úmido no rosto. [...]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"weather"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Curitiba"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"temperatureC"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;14.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"precipitationMm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"windKmh"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isDay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"garoa"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Curitiba was drizzling at 14 °C when I called it, and the model proposed a rain-listening mission instead of a jog. That is the point: the weather changes the mission.&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/GAlmeidaDev" rel="noopener noreferrer"&gt;
        GAlmeidaDev
      &lt;/a&gt; / &lt;a href="https://github.com/GAlmeidaDev/TouchGrass-API" rel="noopener noreferrer"&gt;
        TouchGrass-API
      &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;🌱 TouchGrass API&lt;/h1&gt;
&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Fecha o notebook. Abre a porta. &lt;strong&gt;Vai pegar sol.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;API em Node/TypeScript que olha o clima da sua cidade e pede a um modelo de IA &lt;strong&gt;open-weight rodando na sua máquina&lt;/strong&gt; uma missão curta ao ar livre, do tamanho do tempo que você tem. Feita para o desafio &lt;em&gt;Touch Grass&lt;/em&gt; do Hacktoberfest 2026 Open-Source AI Challenge (DEV).&lt;/p&gt;

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

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;POST /mission {city, minutes}
   │
   ├─► Open-Meteo (geocoding + clima atual, sem chave de API)
   └─► Gemma 3 4B via Ollama (local) ─► missão em JSON validado
         │
         └─► SQLite (histórico de missões)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;O que faz&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /mission&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Gera uma missão para a cidade e o tempo livre informados&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /checkin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Marca a missão como cumprida (&lt;code&gt;{"missionId": 1}&lt;/code&gt;); idempotente&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /streak&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Dias seguidos com missões cumpridas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Health check&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Como rodar&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Requisitos: Node 20+ e &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;.&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;ollama pull&lt;/pre&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/GAlmeidaDev/TouchGrass-API" 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;The open-weight model.&lt;/strong&gt; Missions come from &lt;strong&gt;Gemma 3 4B&lt;/strong&gt; served by &lt;strong&gt;Ollama&lt;/strong&gt;, running on my own machine. It is about 3.3 GB and fast enough on a regular laptop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured output instead of prompt-and-pray.&lt;/strong&gt; A 4B model is happy to ramble. I pass a JSON schema (generated from a zod schema) in Ollama's &lt;code&gt;format&lt;/code&gt; field, so the model has to answer &lt;code&gt;{ "title", "description" }&lt;/code&gt;. The reply is then parsed with the same zod schema, and anything invalid becomes a clean &lt;code&gt;503&lt;/code&gt; rather than garbage in the database. The system prompt asks for pt-BR, a mission that fits the time including the walk back, no equipment, and a safe alternative when there is rain, a storm, extreme cold or darkness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weather without an API key.&lt;/strong&gt; &lt;a href="https://open-meteo.com" rel="noopener noreferrer"&gt;Open-Meteo&lt;/a&gt; handles geocoding and current conditions, so there is nothing to sign up for. I map the WMO weather code to a readable condition, and the model sees temperature, feels-like, rain, wind and day or night.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaks.&lt;/strong&gt; Missions live in SQLite (&lt;code&gt;better-sqlite3&lt;/code&gt;). Check-ins store the local day, and the streak is computed from distinct completed days. It stays alive if your last completed day was yesterday, so you have until the end of today to keep it, and it resets after a skipped day while preserving your longest run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testable by design.&lt;/strong&gt; The weather provider, the mission generator and the clock are injected into the Fastify app. The 12 tests run with no network and no Ollama, and the clock injection let me test consecutive days and a month rollover without waiting.&lt;/p&gt;

&lt;p&gt;Stack: Node, TypeScript, Fastify, zod, SQLite, Ollama, Gemma 3 4B, Open-Meteo.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; The model runs on your machine. Where you live and how often you go outside is not sent to an AI provider. The only outbound request is the weather lookup, which carries a city name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline-capable.&lt;/strong&gt; Inference needs no internet. Only the forecast does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero cost.&lt;/strong&gt; No API key, quota or per-token bill, so a habit app can call the model every day for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swappable.&lt;/strong&gt; Open weights mean one environment variable (&lt;code&gt;OLLAMA_MODEL&lt;/code&gt;) swaps the model. A closed API can change or retire a model under you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I haven't tried the closed-API version of this, so I won't claim it is worse. For a tool about personal routine, keeping the data local was the deciding factor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing It
&lt;/h2&gt;

&lt;p&gt;I ran the full flow against live weather and the local model. In Curitiba (heavy drizzle, 14 °C, 15 minutes) it proposed a rain-watching mission. In Lisbon at night (20 °C, 45 minutes) it switched to a night walk by the river instead of a daytime route, so the weather and the time of day do change the output. A check-in moved the streak to 1, and repeating the same check-in changed nothing. An unknown city returned a clean 404.&lt;/p&gt;

&lt;p&gt;The honest limitation: with a 4B model the missions lean toward "observe and reflect", and the minutes barely change their scope. The next thing I'd try is a richer prompt (varied mission types per duration) or a bigger Gemma through the same &lt;code&gt;OLLAMA_MODEL&lt;/code&gt; switch.&lt;/p&gt;

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

&lt;p&gt;This is the backend only. Next I want a tiny client (CLI or phone shortcut) and per-user time zones. Right now days follow the server's time zone, which fits the local, single-user setup.&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 generates every mission.&lt;/p&gt;

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