<?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: Carla</title>
    <description>The latest articles on DEV Community by Carla (@devcarlat800).</description>
    <link>https://dev.to/devcarlat800</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%2F4175266%2Fb828362d-9e59-4071-acdd-ffbf69cc9404.png</url>
      <title>DEV Community: Carla</title>
      <link>https://dev.to/devcarlat800</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devcarlat800"/>
    <language>en</language>
    <item>
      <title>My first month of hackathons: 4 local AI projects, 4 lessons</title>
      <dc:creator>Carla</dc:creator>
      <pubDate>Sun, 08 Nov 2026 17:40:14 +0000</pubDate>
      <link>https://dev.to/devcarlat800/my-first-month-of-hackathons-4-local-ai-projects-4-lessons-2528</link>
      <guid>https://dev.to/devcarlat800/my-first-month-of-hackathons-4-local-ai-projects-4-lessons-2528</guid>
      <description>&lt;p&gt;October 2026 was my first month of hackathons. I wanted a few real projects on GitHub, something outside my work projects, and to learn new things by doing them. I joined Hacktoberfest: two small exercises in MLH Global Hack Week and three weekly DEV challenges.&lt;/p&gt;

&lt;p&gt;The AI in every project is an open Gemma model running on my own computer with Ollama. No paid API. Here is what each project does, what went wrong, and what it taught me.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Search by meaning (Global Hack Week)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Ten short customer-support notes from a small online shop, saved in Postgres with pgvector on Tiger Cloud. EmbeddingGemma turns each note into a list of numbers that represents its meaning, so a search finds notes that mean the same thing, even with no words in common. "Voglio i soldi indietro" ("I want my money back") finds the note about a refund.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong.&lt;/strong&gt; My query always returned 3 notes, even when none of them had anything to do with the search. &lt;code&gt;LIMIT 3&lt;/code&gt; means "give me 3 rows", not "give me the 3 that match".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned.&lt;/strong&gt; Search by meaning needs a minimum score. Relevant notes scored about 0.67–0.70 and unrelated ones about 0.50, so I keep only results above 0.6. Now "no results" is a possible answer. It isn't perfect: the refund note scored only 0.51 for "voglio i soldi indietro", so the minimum score hides it too.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;a href="https://dev.to/devcarlat800/i-built-a-support-copilot-for-one-person-shops-that-runs-gemma-4-on-a-laptop-4bpc"&gt;Support Copilot&lt;/a&gt; (DEV week 2)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Picture a one-person shop that sells handmade ceramics. Every evening the owner answers the same emails: "my mug arrived cracked", "my parcel is late". Support Copilot finds how she solved similar cases before and writes a draft reply that follows her own policies. She reviews it and sends it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong.&lt;/strong&gt; For "My vase arrived broken, I want my money back", the search found two past cases: a broken item and a customer who changed their mind. Gemma sometimes picked the wrong policy: returns accepted if unused, customer pays shipping. Not what you tell someone holding a broken vase. And with a low minimum score, a question about teapots pulled in unrelated cases, and Gemma confidently said the shop had no teapots, something it couldn't know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned.&lt;/strong&gt; What helped: telling the model to follow the most similar case, lowering the temperature to 0.2 so replies stay consistent, and raising the minimum score to 0.6. Now, when nothing relevant is found, the draft says the owner will check. And a person still reads every draft before it is sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;a href="https://dev.to/devcarlat800/i-let-a-local-gemma-4-name-my-screenshots-and-it-caught-my-fake-password-bmb"&gt;Lazy Screenshot Namer&lt;/a&gt; (DEV week 3)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; After week 2 I had a folder of files called "Screenshot 2026-10-11 173802.png". Renaming 7 of them by hand took me 10 to 15 minutes. Lazy Screenshot Namer asks Gemma 4 to look at each screenshot, describe it and suggest a short name, like &lt;code&gt;sql-show-chunks.png&lt;/code&gt;. It also flags screenshots that seem to contain a password. I tested it with a fake password made for the test, and it caught it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong.&lt;/strong&gt; In my first version the model often got stuck in a loop, repeating words inside the file name. One name was 13,706 characters long. It took about 4 minutes per image, and no name was usable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned.&lt;/strong&gt; Small models need limits and simple steps. Capping the output at 40 tokens (&lt;code&gt;num_predict&lt;/code&gt; in Ollama) brought it down to about 9 seconds per image. Removing the example from my prompt stopped the model from copying it: three different screenshots had all been given the example name. And asking it to first describe the image, then name it in a separate step, took useful names from 2 out of 7 to 7 out of 7.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;a href="https://dev.to/devcarlat800/my-family-tree-became-a-404-so-i-built-one-that-lives-on-my-computer-1ad6"&gt;Tell the Tree&lt;/a&gt; (DEV week 4)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; I used to keep my family tree on a genealogy website. The site shut down, and everything I had put there turned into a 404. Tell the Tree is a family tree that lives on my computer. You fill it in with sentences, in Italian or English, like "Allegra è figlia di Claire e di Lord Byron" ("Allegra is the daughter of Claire and Lord Byron"). Gemma copies the names and family words, my code decides what they mean and checks them, and the change appears as a preview. Nothing is saved until I confirm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong.&lt;/strong&gt; I wrote 15 test sentences to build with, and on those Gemma got 14 right. But I had also written 6 secret sentences before the engine existed, in everyday spoken Italian, and I ran them only once. Gemma got 1 out of 6. For example, it kept the title in "Nonna Lucia" (Grandma Lucia) and created a person called "papà" (dad). The secret sentences also found a real bug in my code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned.&lt;/strong&gt; Test with sentences you didn't build on, and publish the result as it is. And keep a person in the loop: those wrong names would show up in the preview, where I can discard them before they are saved.&lt;/p&gt;

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

&lt;p&gt;Right now I'm using Tell the Tree with my own family, on my computer only, and writing down every sentence it gets wrong. That list will decide what I fix next.&lt;/p&gt;

&lt;p&gt;If you did Hacktoberfest this year too, what did you build?&lt;/p&gt;

</description>
      <category>hacktoberfest</category>
      <category>ai</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My family tree became a 404, so I built one that lives on my computer</title>
      <dc:creator>Carla</dc:creator>
      <pubDate>Sat, 31 Oct 2026 19:12:04 +0000</pubDate>
      <link>https://dev.to/devcarlat800/my-family-tree-became-a-404-so-i-built-one-that-lives-on-my-computer-1ad6</link>
      <guid>https://dev.to/devcarlat800/my-family-tree-became-a-404-so-i-built-one-that-lives-on-my-computer-1ad6</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;I used to keep my family tree on a genealogy website: names, dates, photos. Then the site shut down, and everything I had put there turned into a 404.&lt;/p&gt;

&lt;p&gt;That's my ghost in the machine: a family that still exists somewhere, in a database I can't reach anymore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tell the Tree&lt;/strong&gt; is the tree I wish I'd had. You fill it in by telling it about your family, in plain sentences, in English or Italian:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Allegra è figlia di Claire e di Lord Byron.&lt;br&gt;
&lt;em&gt;(Allegra is the daughter of Claire and Lord Byron.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A small open-weight model, &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt;, runs on my computer through Ollama and reads the sentence. My code checks it and shows the change in the tree as a dashed "new" preview. Nothing is saved until I confirm, and nothing ever leaves my computer.&lt;/p&gt;

&lt;p&gt;It's for anyone who has family stories in their head and doesn't want to hand them to a website that might close one day.&lt;/p&gt;

&lt;p&gt;The demo family has a ghost story of its own: the Godwins, the Shelleys and Lord Byron at Villa Diodati in 1816. That was the rainy summer when, stuck indoors, they held a ghost-story contest and Mary began writing &lt;em&gt;Frankenstein&lt;/em&gt;. It's also a hard family to put in a tree. Mary's father, her half-brother and her son were all called William. Her husband and her son were both Percy. There are stepsisters and half-sisters everywhere.&lt;/p&gt;

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

&lt;p&gt;Tell the Tree runs locally, so there's no live link. Here it is in action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhie4owrjfnenbozrt7q1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhie4owrjfnenbozrt7q1.gif" alt="Tell the Tree demo" width="760" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I type "Elizabeth è la sorella di Percy" (Elizabeth is Percy's sister). The tree has two Percys, so it asks which one I mean, shows Elizabeth as "new", and saves her only when I click "Add to tree". This was the first sentence after starting the app, so Ollama was still loading the model: the 22.7 seconds on screen are real, but I shortened that wait in the GIF.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Besides telling the tree in sentences, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;answer its questions when something is ambiguous ("Which Percy do you mean?")&lt;/li&gt;
&lt;li&gt;add, edit or delete people by hand (the same checks apply)&lt;/li&gt;
&lt;li&gt;fix a relationship&lt;/li&gt;
&lt;li&gt;add a photo for each person, stored inside the tree file&lt;/li&gt;
&lt;li&gt;search, and switch between a horizontal and a vertical tree&lt;/li&gt;
&lt;li&gt;export and import GEDCOM, the standard format genealogy programs use to exchange trees&lt;/li&gt;
&lt;li&gt;print the tree or a PDF register&lt;/li&gt;
&lt;li&gt;switch between two themes: a plain register, and Villa Diodati 1816, with a moon and bats&lt;/li&gt;
&lt;/ul&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/carla800" rel="noopener noreferrer"&gt;
        carla800
      &lt;/a&gt; / &lt;a href="https://github.com/carla800/tell-the-tree" rel="noopener noreferrer"&gt;
        tell-the-tree
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A local family tree you build by telling it, in plain sentences. Gemma 4 E2B + Ollama.
    &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;Tell the Tree&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;A family tree you build by telling it, in plain sentences. It runs entirely on your own computer.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You write sentences like &lt;em&gt;"Mary's father was William Godwin"&lt;/em&gt; or &lt;em&gt;"Allegra è figlia di Claire e di Lord Byron"&lt;/em&gt; (English or Italian). A small local model, &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; running through Ollama, reads the sentence. The code checks it against the tree and shows you a &lt;strong&gt;preview&lt;/strong&gt; of the change. Nothing is saved until you confirm, and your data never leaves your computer.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/carla800/tell-the-tree/docs/images/cover.png"&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%2Fcarla800%2Ftell-the-tree%2FHEAD%2Fdocs%2Fimages%2Fcover.png" alt="Screenshot of Tell the Tree with the demo family"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Built for DEV's "Ghost in the Machine" challenge, October 2026.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why I built it&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;I used to keep my family tree on a genealogy website. The site shut down, and everything I had entered turned into a 404. I wanted a tree that lives on my own computer, in a file I own, that I can export to a standard format (GEDCOM), and that I can…&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/carla800/tell-the-tree" 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;h3&gt;
  
  
  Gemma copies, the code decides
&lt;/h3&gt;

&lt;p&gt;Family relationships are full of traps. In Italian, &lt;em&gt;nipote&lt;/em&gt; can mean grandchild, or nephew or niece. A model asked to "work out the relationship" can get this wrong in a quiet, believable way, and a wrong link in a family tree is worse than a missing one.&lt;/p&gt;

&lt;p&gt;So I split the work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gemma copies.&lt;/strong&gt; It reads the sentence and copies out the names and the kinship word exactly as written: "half-brother", "fratellastro", "nipote".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The code decides.&lt;/strong&gt; An Italian/English dictionary decides what that word means.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When a word is ambiguous&lt;/strong&gt;, the code looks at the tree. If the tree isn't enough, it asks me.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then the code runs its checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no cycles: nobody can become their own ancestor&lt;/li&gt;
&lt;li&gt;no impossible dates&lt;/li&gt;
&lt;li&gt;no conflicting parents&lt;/li&gt;
&lt;li&gt;no guessing between people with the same name: it asks "Which Percy do you mean?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every fact keeps the sentence it came from. Finally the change appears in the tree as a dashed preview, and only my confirmation saves it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Gemma 4 E2B (&lt;code&gt;gemma4:e2b-it-qat&lt;/code&gt;), running locally with Ollama&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Python, FastAPI, SQLite (one file per tree, photos included)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React, TypeScript, React Flow and dagre to draw the tree&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests:&lt;/strong&gt; 153. I also checked that the GEDCOM export and import work together: when I export the demo family and import it again, I get back the same tree.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  E2B or E4B?
&lt;/h3&gt;

&lt;p&gt;I tried both on my 15 test sentences:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Gemma alone&lt;/th&gt;
&lt;th&gt;With my code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E2B&lt;/td&gt;
&lt;td&gt;14/15&lt;/td&gt;
&lt;td&gt;15/15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E4B&lt;/td&gt;
&lt;td&gt;12/15 (13/15 after one fix)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;E4B was also 2.5 times slower. Fifteen sentences is a small test, so I don't read too much into it, but it was enough to stay with E2B.&lt;/p&gt;

&lt;h3&gt;
  
  
  The honest part: 6 secret sentences
&lt;/h3&gt;

&lt;p&gt;I wrote those 15 sentences myself while I was building, so they're clean, and a score on them doesn't prove much.&lt;/p&gt;

&lt;p&gt;So before writing the engine, I wrote 6 more sentences in everyday spoken Italian, about an invented family, and put them aside. When the code was finished, I froze it with a git tag (&lt;a href="https://github.com/carla800/tell-the-tree/tree/holdout-run" rel="noopener noreferrer"&gt;&lt;code&gt;holdout-run&lt;/code&gt;&lt;/a&gt;) and ran the 6 sentences exactly once.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma alone&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1/6&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With my code&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/6&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is what Gemma did with spoken Italian:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It copied "Nonna Lucia" (Grandma Lucia) with the title, as if "Nonna" were part of her name.&lt;/li&gt;
&lt;li&gt;It turned words that aren't names into people: "papà" (dad), "figlio di nonno Carlo" (Grandpa Carlo's son).&lt;/li&gt;
&lt;li&gt;It missed relationships that were implied but never said.&lt;/li&gt;
&lt;li&gt;It invented a marriage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sentence h06 went the other way. Gemma got it right and &lt;strong&gt;my code got it wrong&lt;/strong&gt;: it merged two different Williams into one person. That was exactly the trap I had written into that sentence.&lt;/p&gt;

&lt;p&gt;The 5/6 also needs a warning. It only measures whether the code made the right decision: save, ask, or refuse. In 3 of those 5 cases, it would have saved wrong names, like "Nonna Lucia". The only thing standing between those mistakes and the tree is the preview, where I see the dashed "new" people and can say no.&lt;/p&gt;

&lt;p&gt;These are the real ghosts in my machine: a "papà" who isn't anyone, a wedding that never happened, two Williams who became one. The preview is how they stay out of the family.&lt;/p&gt;

&lt;p&gt;I didn't fix anything before publishing. If I fixed the code until those 6 sentences passed, they would become test sentences like the others, and the number would stop meaning anything. So it stays 1/6 and 5/6.&lt;/p&gt;

&lt;p&gt;What's next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fix the two-Williams bug&lt;/li&gt;
&lt;li&gt;remove kinship titles from names ("Nonna", "Zio")&lt;/li&gt;
&lt;li&gt;when a "name" isn't really a name ("papà"), ask instead of creating a person&lt;/li&gt;
&lt;li&gt;run E4B on the secret sentences&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;For this project, it's the whole point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can't disappear on me.&lt;/strong&gt; My last tree lived inside someone else's service and died with it. Tell the Tree keeps each family in a single file on my computer, exports to GEDCOM, and uses a model whose weights are on my own disk. If I stopped working on this app tomorrow, the tree would still be mine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Family data is private data.&lt;/strong&gt; Names, birth dates and photos of people who are still alive, and who never agreed to be on anyone's server. Here, none of it leaves the computer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model can't change under me.&lt;/strong&gt; I tested one specific model, and because it runs locally, that's the model the app keeps using. With a closed API, the model behind the endpoint can be updated or retired, and my test results would quietly stop being true. A secret test that runs once only means something if the model stays the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trying another model was easy.&lt;/strong&gt; Comparing E2B with E4B meant pulling another model with Ollama and running the same tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing to run.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spirit of It
&lt;/h2&gt;

&lt;p&gt;Día de los Muertos is about remembering the people who came before us. A family tree is a quiet version of the same thing: a place where people who are gone still have a name, a date, a face.&lt;/p&gt;

&lt;p&gt;The part of Tell the Tree I care about most is a small one: every fact keeps the sentence it came from. Not just a date in a box, but the words someone used when they told it.&lt;/p&gt;

&lt;p&gt;My old website lost everything at once. This time, the people, the photos and the words all stay in a file on my own computer.&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 4 E2B runs locally through Ollama and reads every sentence. The app is designed around what a small local model is good at (copying words out of a sentence) and what I'd rather not trust it with (deciding what a relationship means), and I measured both, including where it fails.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Villa Diodati photo: Robertgrassi, public domain, via Wikimedia Commons&lt;/li&gt;
&lt;li&gt;Fonts: Atkinson Hyperlegible and IM Fell English, SIL Open Font License&lt;/li&gt;
&lt;li&gt;Gemma by Google DeepMind, run with Ollama; FastAPI, React Flow and dagre&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full story, mistakes included, is in my &lt;a href="https://github.com/carla800/tell-the-tree/blob/main/DEVLOG.md" rel="noopener noreferrer"&gt;DEVLOG&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for reading. And if your family history also lives on a website you don't control, maybe export it today.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I let a local Gemma 4 name my screenshots (and it caught my fake password)</title>
      <dc:creator>Carla</dc:creator>
      <pubDate>Sun, 25 Oct 2026 12:18:13 +0000</pubDate>
      <link>https://dev.to/devcarlat800/i-let-a-local-gemma-4-name-my-screenshots-and-it-caught-my-fake-password-bmb</link>
      <guid>https://dev.to/devcarlat800/i-let-a-local-gemma-4-name-my-screenshots-and-it-caught-my-fake-password-bmb</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;Last week, for my &lt;a href="https://dev.to/devcarlat800/i-built-a-support-copilot-for-one-person-shops-that-runs-gemma-4-on-a-laptop-4bpc"&gt;Week 2 project&lt;/a&gt;, I took a lot of screenshots. Then I spent 10 to 15 minutes doing the most sloth-unfriendly job in the world: opening each &lt;code&gt;Screenshot 2026-10-11 173802.png&lt;/code&gt;, figuring out what was in it, pressing F2 and typing a name like &lt;code&gt;ghw-hypertable-show-chunks.png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lazy Screenshot Namer&lt;/strong&gt; does that for me. It looks at every screenshot in a folder with a local Gemma 4 model, describes it, and suggests a short name that says what's inside. It also does one thing I would never bother to do by hand: it flags screenshots that seem to contain a password or a key, before they end up in a repo or a blog post.&lt;/p&gt;

&lt;p&gt;Who gets to be lazier: anyone who documents their work with screenshots. Developers writing READMEs and bug reports, students taking notes, anyone with a Screenshots folder full of timestamps.&lt;/p&gt;

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

&lt;p&gt;Before: eight screenshots with the names Windows gave them.&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%2Fuzlf5z8x32hqzt2vyfo5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuzlf5z8x32hqzt2vyfo5.png" alt="Screenshots before renaming" width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dry run shows what Gemma 4 sees in each image and the name it suggests. Nothing is renamed yet:&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%2Fwsew1xwmqi6me0e5qm7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwsew1xwmqi6me0e5qm7n.png" alt="Dry run with descriptions" width="800" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After &lt;code&gt;--apply&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpym175zac0059osjncec.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpym175zac0059osjncec.png" alt="Screenshots after renaming" width="800" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How close did it get to the names I chose by hand last week?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Suggested by Gemma 4&lt;/th&gt;
&lt;th&gt;My own name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sql-show-chunks.png&lt;/td&gt;
&lt;td&gt;ghw-hypertable-show-chunks.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sql-time-bucket-devices.png&lt;/td&gt;
&lt;td&gt;ghw-hypertable-time-bucket.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;search-results-customer-orders.png&lt;/td&gt;
&lt;td&gt;ghw-pgvector-ricerca.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sql-note-execution-result.png&lt;/td&gt;
&lt;td&gt;ghw-pgvector-tabella.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;teapot-inquiry-draft.png&lt;/td&gt;
&lt;td&gt;demo-teapots.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;customer-service-vase-replacement.png&lt;/td&gt;
&lt;td&gt;demo-damaged.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;customer-service-package-delay.png&lt;/td&gt;
&lt;td&gt;demo-late.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sensitive-config-db-connection-vars.png ⚠️&lt;/td&gt;
&lt;td&gt;a fake &lt;code&gt;DB_PASSWORD&lt;/code&gt;, made for the test&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not identical, but every name tells you what's inside, and it even read &lt;code&gt;show_chunks&lt;/code&gt; and &lt;code&gt;time_bucket&lt;/code&gt; straight from the SQL on screen. The one miss: it called my customer notes "customer orders".&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/carla800" rel="noopener noreferrer"&gt;
        carla800
      &lt;/a&gt; / &lt;a href="https://github.com/carla800/lazy-screenshot-namer" rel="noopener noreferrer"&gt;
        lazy-screenshot-namer
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Rename screenshots with a local Gemma 4 model that describes each image, and flag the ones that seem to contain secrets
    &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;Lazy Screenshot Namer&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Turns &lt;code&gt;Screenshot 2026-10-11 173802.png&lt;/code&gt; into &lt;code&gt;sql-show-chunks.png&lt;/code&gt;. A local Gemma 4 model looks at each screenshot, describes it and suggests a name that says what's inside. It also flags screenshots that seem to contain a password or a key, before they end up in a repo or a blog post.&lt;/p&gt;
&lt;p&gt;Everything runs on your laptop with Ollama: your screenshots never leave your machine.&lt;/p&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19" rel="nofollow"&gt;Hacktoberfest 2026 Open-Source AI Challenge, Week 3: Slow and Steady&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Before and after&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/carla800/lazy-screenshot-namer/docs/before.png"&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%2Fcarla800%2Flazy-screenshot-namer%2FHEAD%2Fdocs%2Fbefore.png" alt="Screenshots before"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/carla800/lazy-screenshot-namer/docs/after.png"&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%2Fcarla800%2Flazy-screenshot-namer%2FHEAD%2Fdocs%2Fafter.png" alt="Screenshots after"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Windows name&lt;/th&gt;
&lt;th&gt;Suggested by Gemma 4&lt;/th&gt;
&lt;th&gt;My own name, chosen by hand&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Screenshot 2026-10-11 173802.png&lt;/td&gt;
&lt;td&gt;sql-show-chunks.png&lt;/td&gt;
&lt;td&gt;ghw-hypertable-show-chunks.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshot 2026-10-11 174415.png&lt;/td&gt;
&lt;td&gt;sql-time-bucket-devices.png&lt;/td&gt;
&lt;td&gt;ghw-hypertable-time-bucket.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshot 2026-10-15 140512.png&lt;/td&gt;
&lt;td&gt;search-results-customer-orders.png&lt;/td&gt;
&lt;td&gt;ghw-pgvector-ricerca.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshot 2026-10-15 140748.png&lt;/td&gt;
&lt;td&gt;sql-note-execution-result.png&lt;/td&gt;
&lt;td&gt;ghw-pgvector-tabella.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshot 2026-10-18 223001.png&lt;/td&gt;
&lt;td&gt;teapot-inquiry-draft.png&lt;/td&gt;
&lt;td&gt;demo-teapots.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshot 2026-10-18 223612.png&lt;/td&gt;
&lt;td&gt;customer-service-vase-replacement.png&lt;/td&gt;
&lt;td&gt;demo-damaged.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshot 2026-10-18 223740.png&lt;/td&gt;
&lt;td&gt;customer-service-package-delay.png&lt;/td&gt;
&lt;td&gt;demo-late.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshot 2026-10-25 122211.png&lt;/td&gt;
&lt;td&gt;sensitive-config-db-connection-vars.png&lt;/td&gt;
&lt;td&gt;(a fake password, made for testing)&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;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;For each image in a folder:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Describe&lt;/strong&gt;…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/carla800/lazy-screenshot-namer" 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;It's a single Python script around &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; (&lt;code&gt;gemma4:e2b-it-qat&lt;/code&gt;, about 4 GB), Google's open-weight model, running locally with &lt;strong&gt;Ollama&lt;/strong&gt; on a laptop without a GPU. Gemma 4 can read images, so for each screenshot:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Describe&lt;/strong&gt; (with the image): one sentence about what the screenshot shows, plus a line &lt;code&gt;sensitive: yes/no&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt; (text only, fast): turn that sentence into 2 to 5 hyphenated words.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rename safely&lt;/strong&gt;: dry run by default, &lt;code&gt;--apply&lt;/code&gt; to rename, never overwrite a file, and log every change to a CSV so it can be undone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Getting there took three wrong turns, and they taught me more than the final code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 13,706-character file name.&lt;/strong&gt; My first version asked for a JSON answer. On most screenshots the model got stuck in a loop, repeating words inside the name (&lt;code&gt;...show-chunks-table-data-preview-success-connected-status...&lt;/code&gt;) and never closing the JSON. Four minutes per image, zero usable names. Capping the output at 40 tokens fixed both problems at once: about 9 seconds per image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model copied my example.&lt;/strong&gt; My prompt said "for example: sql-editor-query-results", and three different SQL screenshots all got exactly that name. Removing the example and asking for "the most specific thing you can read on screen" made them distinct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Describe first, then name.&lt;/strong&gt; On terminal screenshots, the model latched onto the command (&lt;code&gt;python copilot.py&lt;/code&gt;) and ignored the actual content. Asking it to describe the screenshot first, then naming it in a separate call, took me from 2 useful names out of 7 to 7 out of 7. Splitting a task into two simpler steps really helps a small model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also learned not to trust the output blindly: the code keeps at most 5 words, strips anything that isn't a letter, a digit or a hyphen, and asks for the &lt;code&gt;sensitive&lt;/code&gt; line &lt;em&gt;before&lt;/em&gt; the name, so a truncated answer never loses the privacy flag.&lt;/p&gt;

&lt;p&gt;As last week, I paired with an AI assistant to move fast, but the testing, the wrong turns and the fixes were all part of the fun.&lt;/p&gt;

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

&lt;p&gt;Screenshots are some of the most private files on a computer. In the last two weeks alone, mine included a database dashboard, query results and, at one point, a real connection string with a password. Sending them to a cloud API just to get a nicer file name would be a terrible trade.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Screenshots never leave the laptop.&lt;/strong&gt; Gemma 4 runs locally with Ollama: no upload, no third-party AI provider seeing your screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works offline and costs nothing.&lt;/strong&gt; No API key, no per-image bill. You can point it at a folder of hundreds of screenshots and let it run, which is the whole point of being lazy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small open model runs on normal hardware.&lt;/strong&gt; Gemma 4 E2B reads screenshots well enough on a laptop without a GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could fix what broke.&lt;/strong&gt; The endless names, the copied example and the ignored content were all fixed by changing things I fully control: the output limit, the prompt and the number of steps. With a closed model I could still change the prompt, but not run it on my own machine for free.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;By hand, last week:&lt;/strong&gt; about 10 to 15 minutes for 7 screenshots (look, think of a name, F2, type).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With the namer:&lt;/strong&gt; 8 screenshots in 25 seconds (from 12:26:49 to 12:27:14 in the rename log), plus about a minute to read the dry run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bonus I would never have done by hand:&lt;/strong&gt; checking every screenshot for secrets. It flagged the fake password on the first try, with no false alarms on the other seven.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Best Use of Gemma&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>I built a support copilot for one-person shops that runs Gemma 4 on a laptop</title>
      <dc:creator>Carla</dc:creator>
      <pubDate>Sun, 18 Oct 2026 21:08:10 +0000</pubDate>
      <link>https://dev.to/devcarlat800/i-built-a-support-copilot-for-one-person-shops-that-runs-gemma-4-on-a-laptop-4bpc</link>
      <guid>https://dev.to/devcarlat800/i-built-a-support-copilot-for-one-person-shops-that-runs-gemma-4-on-a-laptop-4bpc</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;Picture a one-person online shop selling handmade ceramics. The owner makes the mugs, photographs them, packs them... and every evening answers the same emails: "my mug arrived cracked", "my parcel is late", "can I get a refund?". She already knows the answers. She just has to write them again, consistently, every single time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Little Kiln Support Copilot&lt;/strong&gt; drafts those replies for her. You paste a customer message; it finds how &lt;em&gt;you&lt;/em&gt; solved similar cases before and writes a draft that follows your own policies. You review it and send it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who would pay:&lt;/strong&gt; solo makers and small shops on Etsy, Shopify or their own site, for whom support eats hours but a full helpdesk subscription is overkill. Because everything runs on open models on the owner's laptop, there is no per-message AI bill, so a flat few euros a month (or a one-time setup fee) would be a sustainable price.&lt;/p&gt;

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

&lt;p&gt;Three real runs from the command line, for a sample shop called "Little Kiln Ceramics".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A broken vase.&lt;/strong&gt; The customer asks for their money back; the copilot recognizes a damaged item and applies the shop's damaged-item policy: free replacement within 2 days, no need to return the broken piece, a photo of the damage.&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%2Fxfsbrp4c6avnlr4qdbqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxfsbrp4c6avnlr4qdbqx.png" alt="Draft reply for a damaged item" width="800" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A late parcel.&lt;/strong&gt; It promises to check the tracking and offers the 10% discount code (SORRY10) the shop used in a similar case. Notice the "In the past, we have offered..." wording: a small model sometimes leaks internal phrasing, which is exactly why a human reviews every draft.&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%2F3vbqie1j1gjxt5nc46pp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3vbqie1j1gjxt5nc46pp.png" alt="Draft reply for a late parcel" width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A question nobody asked before.&lt;/strong&gt; No past case is similar enough, so instead of inventing an answer, the draft says the owner will 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%2Fn5fzq0hg6sc7ztr7v0hx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn5fzq0hg6sc7ztr7v0hx.png" alt="Draft reply for an uncovered question" width="800" height="151"&gt;&lt;/a&gt;&lt;/p&gt;

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


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/carla800" rel="noopener noreferrer"&gt;
        carla800
      &lt;/a&gt; / &lt;a href="https://github.com/carla800/support-copilot" rel="noopener noreferrer"&gt;
        support-copilot
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Draft customer-support replies for one-person shops with local open models (Gemma 4, EmbeddingGemma) and pgvector
    &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;Little Kiln Support Copilot&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Draft replies to customer messages for one-person online shops, based on how &lt;em&gt;you&lt;/em&gt; solved similar cases before. The AI runs on open models on your own laptop, so customer messages are never sent to a third-party AI API and every draft costs nothing to generate.&lt;/p&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12" rel="nofollow"&gt;Hacktoberfest 2026 Open-Source AI Challenge, Week 2: Side Hustle&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The problem&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;If you sell handmade products online on your own, a big part of your evenings goes into answering the same questions again and again: a parcel arrived broken, a delivery is late, someone wants a refund. You already know the answers. You just have to write them again, every time, in a consistent way.&lt;/p&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;The sample shop is &lt;strong&gt;Little Kiln Ceramics&lt;/strong&gt;, a fictional one-person shop selling handmade ceramics.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your past cases&lt;/strong&gt; (customer message + how you resolved it) are stored in Postgres…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/carla800/support-copilot" 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;The copilot is a small retrieval-augmented generation (RAG) pipeline in Python, built around two open-weight Google models running locally with Ollama:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Past cases become vectors.&lt;/strong&gt; Each past case (customer message + how it was resolved) is embedded with &lt;strong&gt;EmbeddingGemma&lt;/strong&gt; (300M parameters, 768-dimensional vectors) and stored in Postgres with &lt;strong&gt;pgvector&lt;/strong&gt; on the &lt;strong&gt;Tiger Cloud&lt;/strong&gt; free plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieve.&lt;/strong&gt; A new message is embedded the same way, and a single SQL query ranks past cases by cosine similarity (&lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt;) and keeps only those above a threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate.&lt;/strong&gt; &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; (the quantization-aware &lt;code&gt;e2b-it-qat&lt;/code&gt; build, about 4 GB, runs on a laptop with no GPU) receives the message plus the retrieved cases, with instructions to use only those policies and never invent prices or deadlines.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three things I learned while testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The first drafts applied the wrong policy.&lt;/strong&gt; "My vase arrived broken, I want my money back" retrieved both the damaged-item case and a change-of-mind refund case, and Gemma sometimes picked the refund policy: returns accepted if unused, customer pays shipping. Not what you want to tell someone holding a broken vase! Two fixes made it correct and consistent: telling the model to apply the resolution of the &lt;em&gt;most similar&lt;/em&gt; case, and lowering the temperature to 0.2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A low similarity threshold invites invented answers.&lt;/strong&gt; At 0.5, the teapot question pulled in unrelated cases and Gemma confidently said the shop had no teapots, something it could not know. Relevant cases scored above 0.63 and noise below 0.58, so I raised the threshold to 0.6: with nothing relevant to read, the model now honestly defers to the owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval alone isn't enough.&lt;/strong&gt; Earlier this week I built only the search half: "I want my money back" could not find a note about defective shoes, because embeddings capture what a text is &lt;em&gt;about&lt;/em&gt;, not what &lt;em&gt;follows&lt;/em&gt; from it. Adding the generation step is what connects "broken" to "here's what we do about it".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is my first hackathon. I work a lot with AI tools, so I paired with an AI assistant to move fast on the parts that were new to me: pgvector and local models.&lt;/p&gt;

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

&lt;p&gt;For a side hustle, the economics decide everything, and open models change them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No per-message cost.&lt;/strong&gt; Every draft is generated on the owner's laptop. A tiny shop answering 20 emails a day pays nothing per reply, so a cheap flat price is sustainable for both sides. With a paid API, the cost would grow with every customer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customer data stays out of third-party AI APIs.&lt;/strong&gt; Names, orders and complaints are never sent to an AI provider. (The past cases live in a Postgres database I control, here on Tiger Cloud's free plan; for full privacy, the same code runs against a local Postgres with pgvector.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small enough for a normal laptop.&lt;/strong&gt; Gemma 4 E2B runs without a GPU, which is the hardware a solo maker actually has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multilingual for free.&lt;/strong&gt; EmbeddingGemma handles 100+ languages; earlier this week I tested the same setup with Italian notes. A shop answering customers in their own language doesn't need a different product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I can swap and tune everything.&lt;/strong&gt; Model, temperature, threshold and prompt are mine to change, and they were exactly the knobs that fixed the wrong-policy and invented-answer problems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is honest: a bigger closed model might write smoother prose. But for drafts that a human reviews anyway, a free, private, local model is the better deal.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Best Use of Gemma&lt;/li&gt;
&lt;li&gt;Best Use of Tiger Data&lt;/li&gt;
&lt;/ul&gt;

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