DEV Community

Cover image for Etsy listing writer with Gemma 4: it reads your photos and won't make stuff up
Christian Anderson
Christian Anderson

Posted on

Etsy listing writer with Gemma 4: it reads your photos and won't make stuff up

Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle Submission 💸

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle

What I Built

I run a tiny Etsy shop on the side. Moon posters, word prints, that sort of thing. The making bit is fun. Writing the listing is not. Every product wants a title that fits in 140 characters with the search words first, exactly 13 tags of 20 characters or less, and a description that doesn't sound like a robot wrote it.

So I built listwright. You give it your product photos and a line or two about what's true, and it gives you back the title, the 13 tags and the description, using Gemma 4.

The bit I actually care about is what happens next. Before you see anything, a dead simple Python checker reads the listing and sorts it into one of three:

  • ready: inside Etsy's limits and says nothing you didn't tell it
  • confirm: it's said something you never did, like a number, or a word like framed, handmade or personalised
  • not ready: still breaks a hard rule after 3 goes

Who'd pay for it? Anyone with more than a handful of listings and better things to do on a Sunday. It costs a fraction of a penny per listing to run, or nothing at all on your own box, so even a cheap hosted version for sellers who don't want to touch a terminal would be nearly all profit.

Demo

Here's a real run on one of my listings, with the kind of one-line facts you'd actually type when you're knackered:

listwright output for a kids alphabet print, flagging personalised and custom

Decent title, all 13 tags, sensible description. But it's slipped "personalized gift" and "custom name art" into the tags. That product isn't personalised. You buy the letters and spell the name yourself. On Etsy, "personalised" means the buyer expects to send you a name, so that's an awkward message waiting to happen. The checker didn't fix it. It asked me.

Then I ran it on 12 real listings from the shop, each one twice. Once with the shop's full description as the facts, once with a lazy one-liner:

Results: 17 of 24 ready first time, 7 sent to confirm, 0 failed, 5.5 seconds average, $0.0035 total

The 7 "confirm" ones were all fair questions. Five were the lazy runs picking up things like "A5 to A2" and "November 2027" from the photos. True, as it happens, but I never said them, and on another shop that's a mockup frame turning into "arrives framed". The other two were that "personalised" print above, once with each set of facts.

Code

GitHub logo casareanderson / listwright

Etsy listings from your product photos, written by an open-weight model you run yourself (Gemma 4), checked so it cannot make things up.

listwright

Etsy listings from your product photos, written by an open-weight model you can run yourself and checked so it can't sell something you don't make.

listwright draft photo1.jpg photo2.jpg --facts "Birmingham moon poster. Instant digital download."

listwright flagging "personalized" and "custom" on a print that isn't personalised

You get a title (140 chars max, search words first), exactly 13 tags (20 chars max each) and a three-paragraph description. Then a plain Python checker reads it before you do:

Result Means
✓ ready inside Etsy's limits and says nothing you didn't tell it
? confirm it states something you didn't say: a number, or a claim like framed, handmade, personalised. Usually it read it off your photo and it's true. Sometimes it's a mockup frame and a refund waiting to happen. You decide.
✗ not ready still breaks a hard rule after 3 tries (too long, wrong tag count, a misspelt place name)

Only hard failures go back…

Python, no dependencies. pip install git+https://github.com/casareanderson/listwright and then:

listwright draft photo1.jpg photo2.jpg --facts "Birmingham moon poster. Instant digital download."
Enter fullscreen mode Exit fullscreen mode

How I Built It

Three small files that matter:

  • model.py makes one call to anything that speaks the OpenAI chat API with images. The default is Ollama on your own machine with gemma4. Point one environment variable somewhere else and nothing else changes.
  • draft.py sends the photos and your facts, gets JSON back, and checks it. If it breaks a hard rule it goes back to the model with the list of what's wrong, up to 3 tries.
  • rules.py is the checker. No AI in it at all. Character limits, tag counts, every number has to be in your facts, a list of risky claim words, and misspelt place names.

Getting the checker right took longer than the model bit, honestly. Some things I got wrong first:

  • My first go at catching typos flagged "includes" as a misspelling of "included" and "covers" as "coders". Real words sit one letter apart all the time. It now only checks names you used mid-sentence, because a misspelt town is the typo that costs you the search. The whole check exists because my very first test run tagged a poster "aberdene art" and nothing stopped it.
  • I first sent "you said a number I didn't give you" back to the model as an error. Bad idea. It just deleted true stuff it had read off the photo. Now hard rules go back to the model and claims come to you as a question.
  • "You get a digital file rather than a physical print" got flagged for claiming "physical print". Saying it isn't something is fine, so it skips negatives now.

What it doesn't do: it doesn't know if a tag is a good tag. It tagged one moon poster "astrology art". Wrong shelf completely. Read your tags.

Why Does Open Innovation Matter?

Two reasons, and one of them happened to me today.

The plan was to run all of this on my own little GPU box with Gemma 4 locally. Photos never leave the house, no subscription, nobody can put the price up on you. This morning that box had other ideas. A graphics driver update had gone in without a reboot, so the card wasn't being used at all, and Gemma sat there for ten minutes without answering.

With a closed API I'd have been stuck or rewriting. Because it's an open model, I changed one setting and pointed it at a hosted copy of the same Gemma 4 family, and carried on. The demo numbers above are from that hosted 26B version. I haven't re-run all 24 on the local 8B yet, so I'm not going to claim it does just as well.

Update, same morning: rebooted the box, GPU back. First local run on the 8B Gemma 4 failed in a new way: it "thinks" before answering, and on Ollama's default 4,096-token window two photos plus a long description left no room, so the JSON got cut off mid-sentence. Thinking is now off by default. With that, the local 8B passed the full-description listing on its second try. With the lazy one-liner it kept writing descriptions that were too short and the checker wouldn't let it through. The bigger hosted model passed both first time. So, honestly: local works, but give the small model proper facts.

The second reason is cost. All 24 drafts cost $0.0035 in total. That's the difference between a tool that has to charge you every month to pay its AI bill and one that basically costs nothing to run.

My First User

My own shop, if I'm honest. Nobody's paid for it. But it found real problems in my real listings on its first afternoon, and that's more than most of my side projects manage.

Prize Categories

Best Use of Gemma: Gemma 4 does all the writing, reads the product photos, and runs locally through Ollama by default.


🤖 Drafted with AI assistance from my own notes and test runs, then reviewed and edited before publishing.

Top comments (0)