Building a Hallucination-Resistant Etsy Copy Generator with Gemma 4: Read Images, Write Titles

Etsy seller builds listwright on Gemma 4, separating generation from validation with a rule checker to curb AI copy hallucination at low cost.
listwright is a CLI tool built by an Etsy seller on the open-weight model Gemma 4, generating compliant titles, 13 tags, and three-paragraph descriptions from product photos and brief facts. Its core innovation is separating generation from validation: a pure-Python checker classifies results as ready, confirm, or not ready before display, focusing on undeclared numbers and commitment words like personalised and framed, leaving ambiguous calls to humans. In 24 test runs, 17 were ready first time, 7 needed confirmation, 0 failed, at a total cost of $0.0035. The author also shares three checker development stumbles and notes that open-weight models let him switch to a hosted version via an environment variable during a driver failure. The tool's limits: it can't judge tag quality and only supports Etsy rules.
From Handmade Workshop to Copywriting Hell: A Real Etsy Seller's Pain Point
Most people running a handmade side business know the feeling: making the product is a joy, but writing the product page feels like torture. Etsy has a rigid set of rules for listings—titles can't exceed 140 characters and search terms need to come first, tags must be exactly 13 and each no longer than 20 characters, and descriptions have to read like a human wrote them. For a seller managing a shop alone, these constraints stack up enough to burn through a Sunday evening.
Developer casareanderson is exactly this kind of seller, selling moon posters and typographic prints. Instead of paying for a copywriting tool, he built a command-line tool called listwright using the open-weight model Gemma 4: input a product photo and a sentence or two of "real facts," and it outputs a title, 13 tags, and a three-paragraph description.
The interesting part of this project isn't "AI writes your copy for you"—there are already plenty of those tools—but rather how it handles the problem of AI hallucination.

Etsy's listing rules aren't arbitrary. The 140-character title limit stems from its search algorithm's preference for long-tail keywords—the first 40 characters carry the most weight, so sellers are advised to put the most important search terms up front. The 13-tag cap is a hard ceiling the platform set to balance search precision against abuse prevention, and the 20-character limit per tag further squeezes the space for expression. Stacked together, these rules mean manually writing a compliant and effective listing typically takes over 30 minutes. For part-time sellers, this directly erodes already thin profit margins; and paid copywriting tools tend to be monthly subscriptions, which aren't economical for shops selling single digits per month. This is precisely the soil from which lightweight open-source tools emerge.
A Three-Tier Decision Mechanism: Let the Model Speak, Let the Rules Gatekeep
listwright's core design is "separation of generation and validation." The model only writes; a checker written in pure Python (rules.py) classifies results before the user sees them, outputting three states:
- ready: complies with Etsy's hard limits and doesn't state information the user didn't provide
- confirm: contains numbers the user never declared, or commitment-laden words like framed, handmade, or personalised
- not ready: still violates hard rules after 3 retries—too long, wrong tag count, misspelled place names
The key is that middle tier. In the demo, the author uses a children's letter print as an example: the generated title and tags look fine, but the tags include "personalized gift" and "custom name art." This product isn't actually custom—buyers purchase the letters and assemble the name themselves. On Etsy, "personalised" means the buyer expects to send their name to the seller, and once the product doesn't match, it's an awkward after-sales conversation. The checker doesn't take it upon itself to delete it, but flags it as confirm, leaving it to a human to decide.
This trade-off is pragmatic. Information the AI reads from a photo may be true (like the size "A5 to A2" or the year "November 2027"), but if the user didn't say it, it can't be assumed writable into the listing; meanwhile, another shop's mockup frame might be misread as "arrives framed," which is a genuine refund risk.
In Etsy's context, "personalised" and "custom" aren't synonyms but commitments with clear fulfillment implications: the seller must collect the buyer's name, date, and other information after purchase and make the item accordingly. If a listing misuses these words, the buyer will expect custom communication, and if the seller can't deliver, they face refunds, bad reviews, and even platform disputes. Similarly, "framed" means the item includes a frame—if only a print is actually shipped, that's a misdescription. These words are easily added "offhandedly" by the model in automated copywriting because they often co-occur with gift items in training data. So the checker listing them as risk words and triggering human confirmation is essentially a safety gate between generative AI and transactional liability.
Real Test Data: 24 Generations, 0 Failures, Total Cost Under Half a Cent
The author tested with 12 real listings from his own shop, running each twice: once feeding the full description as facts, once giving just one lazy sentence. The result: of 24 generations, 17 were ready on the first try, 7 went to confirm, 0 failed, average time 5.5 seconds, total cost $0.0035.
All 7 confirms were reasonable questions. 5 came from the "lazy" inputs—the model read sizes and dates from the photos, information that was true but not provided by the user, which is overstepping; the other 2 were the "personalised" print mentioned earlier, triggered once each by the two fact inputs.
The cost structure is interesting. 24 drafts cost less than a cent in total, meaning the tool could be a one-time purchase or a near-zero-cost hosted service rather than something that must charge monthly to cover API bills. The author says outright that even a cheap hosted version for sellers who don't want to touch a terminal would be "almost pure profit."
The Checker Is Harder to Write Than the Model: Three Stumbles
The author admits that getting the checker right took longer than calling the model. He documented three specific mistakes:
Spell-check false positives. The first version flagged "includes" as a misspelling of "included" and "covers" as "coders." Real words often differ by just one letter. The final solution only checks place names appearing mid-sentence, because a misspelled small-town name is the error that actually loses you search traffic—this check was prompted by his first test run labeling a poster as "aberdene art" with no mechanism to catch it.
Passing "you didn't give me this number" back to the model as an error. The result was the model simply deleting real information it had read from the photo. The current rule: hard rule violations go back to the model for retry, while factual claims are raised to the user as questions.
Negations misjudged. The sentence "you're getting a digital file rather than a physical print" was flagged as a false claim because it contained "physical print." The checker now skips negation contexts.
There's also a clear boundary: listwright doesn't know whether a tag is a good tag. It once labeled a moon poster "astrology art," completely the wrong shelf. Tag quality still requires human judgment.
The difficulty of spell-checking lies in the fact that natural language is full of real words differing by only one or two letters (includes/included, covers/coders), and generic edit-distance algorithms produce many false positives. listwright's trade-off: abandon whole-sentence spell-checking and do targeted validation only for place names. The reason is that a misspelled place name directly makes that listing invisible in the corresponding geographic search, causing hidden traffic loss, and the model's hallucination rate for place names is high. In implementation, the checker maintains a list of place names or calls a lightweight geographic lexicon, alerting only when a suspected place name appears mid-sentence and isn't in the lexicon. This strategy sacrifices generality but precisely covers the highest-risk error type, embodying the design philosophy that "a rule checker should be narrow and deep."
The Practical Value of Open-Weight Models: The Persuasion of a Driver Failure
The author originally planned to run everything on his own small GPU machine—photos never leave home, no subscription, no one can raise prices. But on the very day he was writing this article, a graphics driver update without a reboot left the GPU completely unused, and Gemma hung locally for ten minutes without responding.
With a closed-source API, he'd have had to just wait or rewrite. Because it's an open-weight model, he changed one environment variable to point at a hosted version of the same Gemma 4 family and continued. The demo data came from the hosted 26B version, and he explicitly states he hasn't rerun all 24 cases on the local 8B, so he won't claim the two perform the same.
This "no exaggeration" attitude isn't common in side-project writing, but it's precisely why it deserves to be taken seriously.
An open-weight model refers to a model whose parameters can be publicly downloaded and that allows local deployment and modification, like the Gemma and Llama series. Compared to closed-source APIs, its core advantage is autonomy and control: users can choose to run locally to protect data privacy, or switch to a hosted service of the same model family when needed without rewriting the calling logic. listwright achieves this portability through an abstraction layer compatible with the OpenAI chat API—by default it points to gemma4 on local Ollama, and switching to a remote endpoint requires only changing an environment variable. This design decouples "model" from "service," letting developers flexibly weigh cost, privacy, and reliability—especially suited to scenarios sensitive to data leaving the country or with unstable networks.
Tech Stack: Three Files and Zero Dependencies
The implementation has only three core files:
model.py: makes a single call to any service compatible with the OpenAI chat API that supports images; defaults to local Ollama'sgemma4, and switching backends takes one environment variabledraft.py: sends photos and facts, receives JSON, hands it to the checker; on hard rule violations, passes it back to the model with a list of problems, retrying up to 3 timesrules.py: a pure rule checker with no AI whatsoever, handling character limits, tag counts, number provenance, a risk-claim word list, and place-name spelling
Installation is pip install git+https://github.com/casareanderson/listwright, with no external dependencies. A typical call looks like:
listwright draft photo1.jpg photo2.jpg --facts "Birmingham moon poster. Instant digital download."
Where the Real Value of This Kind of Tool Lies
What listwright solves isn't "can AI write copy" but "what happens when AI writes things it shouldn't." Splitting generation and validation into two layers, letting deterministic rules guard the factual boundary, and returning ambiguous judgments to humans—this pattern can transfer to many generation tasks in transactional scenarios: product descriptions, real estate listings, job postings—anywhere "one wrong sentence means paying up" applies.
Its limitations are also clear: it doesn't know whether tags are good, only supports Etsy's rules, and has only been used by the author himself. But for a Hacktoberfest side project, finding real problems in a real shop on the first afternoon already surpasses most similar attempts.
Project address: https://github.com/casareanderson/listwright
Related articles

Amphibious Transforming Drone: Beyond 100x Power Savings, What Are the Real Needs?
Is the amphibious transforming drone just a gimmick? Starting from the 100x power difference of the M4 Morphobot, explore real application scenarios and thesis topic ideas, analyzing boundary conditions where pure drones and pure ground robots both fail.

Warhound Raises $200 Million: Genetically Engineered "War Dogs" Paired with Anduril Gear
Warhound raises $200M, backed by Ben Lamm and Palmer Luckey, to genetically engineer dogs for military and law enforcement use and equip them with Anduril gear. This article analyzes its technology direction, funding logic, and ethical controversy.

Claude API Comes to Google Cloud: IAM Integration and SDK Access Guide
Anthropic hosts the full Claude API on Google Cloud with IAM-controlled permissions and unified billing. This guide covers the Vertex AI and Anthropic-hosted entry points, IAM role configuration, and SDK access.