This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
What I Built
A side hustle that is live, not a demo: a personalized crossword gift generator with Gemma 3 4B writing the clues on my own laptop, and the Etsy shop that sells it.
You tell it about someone: "Sam proposed in Lisbon. They adopted Biscuit the dog in October. Honeymoon in Tofino, where the kayak flipped." It pulls out the answer words, writes a clue for each, lays out a crossword where every word crosses another, adds a bonus word search from the same words, and renders a print-ready PDF with an answer key and a keepsake page (the solved grid, nothing else) to frame. The buyer prints it, frames it, or folds it into a card.
Who pays: people buying an anniversary, retirement or 60th birthday gift who want something personal for under twenty dollars. Custom crossword listings already sell on Etsy, which is how I know the demand exists; mine is listed at CAD 24. The whole product is a PDF, so the margin is the Etsy fee and fourteen seconds of GPU time.
The problem it solves: the buyer has the memories but not the puzzle skills. Writing fourteen clues that are short, specific and don't contain their own answer is the tedious part. That's the part the model does.
Demo
The shop is live: CrosswordKeepsake on Etsy, opened on October 18 with the anniversary listing; the other occasions follow over the coming weeks. Orders are made to order: the buyer fills in two text boxes at checkout (a title, then their words or memories), and the finished PDF is added to their Etsy order within three business days. The sample puzzles for all ten occasions (anniversary, retirement, new baby, teacher, Christmas and more) are in the repo under samples/, with the rendered PDFs.
The listing images, as buyers see them. A sample puzzle for a fictional couple, solved and framed:
The same product printed big, on an easel at a wedding, which is the second way people use it:
Code
habsology
/
crossword-gift-generator
Personalized crossword gift generator: memories in, printable crossword PDF out. Clues written locally by Gemma via Ollama
Crossword gift generator
Turn a paragraph of memories about someone into a printable, personalized crossword puzzle. The clues are written by a small open-weight model (Gemma 3 4B through Ollama) running on your own machine, so nothing you write about your family leaves your computer.
Two scripts:
-
clues.pyreads the memories, asks the local model for answer words and clues, and validates them (letters only, 3 to 15 letters, no duplicates, no answer hidden in its own clue, and every answer must actually appear in the memories). -
crossword.pylays the entries out on a grid and renders a print-ready PDF: puzzle page (grid and clues), optional word search, answer key, and a keepsake page with the solved grid alone, ready to frame. Deterministic for a given order, so re-runs reproduce the same grid.
Quick try (no model needed)
git clone https://github.com/habsology/crossword-gift-generator
cd crossword-gift-generator
pip install -r requirements.txt
python3 clues.py samples/anniversary-memories.txt…https://github.com/habsology/crossword-gift-generator (MIT)
Two scripts, a few hundred lines of Python, no framework, MIT licensed:
-
clues.py: memories in, validated{word, clue}entries out, via a local model -
crossword.py: deterministic grid layout, numbering, word search, keepsake page, PDF (reportlab)
Plus tests and ten fictional sample orders with their PDFs.
How I Built It
The open piece is the clue writer. It runs Gemma 3 4B (Q4, 3.4 GB) through Ollama on a laptop with an RTX 4060, over Ollama's OpenAI-compatible endpoint, so swapping models is one environment variable:
ENDPOINT = os.environ.get("CLUE_ENDPOINT", "http://localhost:11434/v1/chat/completions")
MODEL = os.environ.get("CLUE_MODEL", "gemma3:4b")
The prompt is small and strict: choose answer words from the memories, one short warm clue each, never put the answer in the clue, return only a JSON array. In three seeded runs on the same fictional couple, 36 of 42 entries were usable as written. The other six are why there is a validator between the model and the grid:
def validate(pairs, n=14, source=None):
out, seen, rejected = [], set(), []
allowed = _source_words(source) # words in the memories, plus adjacent pairs joined
for word, clue in pairs:
w = clean(word) # A-Z only, "New York" -> NEWYORK
if not 3 <= len(w) <= 15: rejected.append((word, "length")); continue
if w not in allowed: rejected.append((word, "not in memories")); continue
if w in seen: rejected.append((word, "duplicate")); continue
if w in clean(clue): rejected.append((word, "answer in clue")); continue
seen.add(w); out.append({"word": w.title(), "clue": clue.rstrip(".")})
if len(out) == n: break
return out, rejected
I ask the model for six more candidates than I need, validate, keep the first fourteen that survive. The grid builder is plain backtracking with a seed, so a re-run for the same order reproduces the same puzzle, which matters when a buyer asks to change one clue.
The "not in memories" line did not exist before the test. Two of the three runs produced PRAMA (not a word; probably PRIYA and TIRAMISU colliding) and padded the list with YEARS and LOVE, which are true of every anniversary and therefore useless as clues. A 4B model will do this; the fix is not a bigger model, it is refusing any answer the buyer did not actually write.
What surprised me: the model is better at choosing answer words than at writing clues. It reliably finds the proper nouns and the one odd detail (the kayak that flipped in Tofino). Clue quality improved most when I stopped asking for "clever" and asked for "specific": the answer should be guessable only by the person who lived it. The first cold call also took almost three minutes and timed out, so the script now sends a one-word warm-up before the real request; after that, 14 seconds per puzzle.
Everything was built with an AI coding assistant driving and me reviewing; the clue prompt was revised against the three recorded runs, kept verbatim in docs/gemma-runs.md.
Why Does Open Innovation Matter?
Three reasons, in order of how much they matter for a side hustle:
- Cost. A gift that sells for twenty dollars cannot carry a per-order API bill that grows with retries, and I retry a lot (ask for twenty candidates, keep fourteen, re-roll bad clues). On a local model the marginal cost of a tenth attempt is zero, so I can afford to be picky.
- Privacy. Buyers send me the most personal things they can think of: how they met, a late parent's nickname, a pregnancy nobody knows about yet. The clue writing happens on the machine that makes the puzzle, not on a third-party API, and the working file is deleted after delivery. I can put that in the shop's privacy policy and mean it.
- Control. The clue style is the product. With an open model I can keep the exact model version forever, and when Gemma 4 ships I can A/B the two on the same sample orders instead of waking up to a changed voice.
Where closed won: drafting the code itself. The open model writes clues; it did not write the grid solver.
Prize Categories
Best Use of Gemma. Gemma 3 4B (gemma3:4b, Q4) is the clue writer: it runs locally through Ollama on an RTX 4060 laptop, and every clue in the ten sample PDFs and in the shop's orders comes from it. The prompt, the validator that catches its failure modes, and three recorded runs with their timings are in the repo. No other partner technology is used, so this is the only category entered.






Top comments (0)