This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
What I Built
Every family has the box. Index cards in three kinds of handwriting, a church cookbook with a cracked spine, a photocopy of a photocopy of someone's mother's pound cake. Everybody agrees it should be "put together someday," and then nobody does, because putting it together means deciding what goes where, who made what, and who even was Aunt Edie.
Recipe Roots is the "someday" part. You give it your public WikiTree ID and a folder of scans. You get back a print-ready 6×9 in cookbook where every recipe is filed under the relative who cooked it, and every cook gets a page that shows where they were born, who their parents were, what was happening in kitchens during their life, and anything you've written about them.
That is my own family: the tree is mine on WikiTree (public profiles of people who have died), plus seven handwritten recipe cards from my relatives, scanned and attached to the person who cooked each one. My mother is in it too: she is living, so she is only there because she agreed to be named, and her page shows her name, her photo, her parents and only the decade she was born in, never a birth date or a place.
What it does
Reads the tree. Ancestors, plus great-aunts and great-uncles and other relatives, from WikiTree's public API. Living people are never included unless you say their family agreed.
Finds the migrations. Born in County Cork, died in Boston: that's a chapter. Plain deterministic code decides this from birth/death/children's places and dates, and it states the move as an honest window ("sometime between 1909 and 1930"), not a guess at the day.
Writes the chapters. An open-weight model (Gemma 3 4B, on my laptop) writes about the food of that place and time. More on why that restriction matters below.
Takes your scans. Photos or PDFs of recipe cards. Each is assigned to a specific person by file name or folder. Gemma's vision reads it into ingredients and directions, or, for handwriting, you can print the scan itself as the page.
Builds the Family Kitchen. A page per cook, oldest first. Their recipes sorted into categories. A contents page with real page numbers and live links to WikiTree profiles. Indices by category and by cook.
Sets the original beside yours. If a public-domain cookbook from their era has the same dish, the book quotes it word for word on the facing page: "How they wrote it, in 1916."
Who would pay
My hypothesis, not a result: people who want a gift (a keepsake for a reunion or a holiday, for parents or grandparents) and family historians who've already done the genealogy and have a box they can't finish. Someone could also run it as a service: scan the box, produce the book, send it to print. The value is the one nobody does well: loose, uncategorized pages become a finished book, with each recipe beside the person who made it. It has the shape of something you'd order several copies of.
Demo
Site: reciperoots.tech
Demo book, built from my own tree (PDF, 52 pages): reciperoots.tech/demo-book.pdf
A fully fictional demo, no real family: reciperoots.tech/demo-fictional.pdf
Code
robinson27225 / recipe-roots
Turn a WikiTree ancestry and scanned family recipes into a print-ready cookbook: a page per cook, recipes by category, and how each dish was written in its own era. Gemma + Postgres/pgvector, runs locally.
Recipe Roots
Turn a public WikiTree ancestry into a print-ready family cookbook (6×9 in PDF): the places your ancestors came from, the food history of their eras, traditional dishes from those kitchens, and your own family's recipes, scanned from the originals.
brew install ollama && ollama pull gemma3:4b # any OpenAI-compatible open-weight model works node src/cli.js Wilder-1 --title "The Wilder Table" --dedication "For Grandma" # -> out/Wilder-1/book.pdf Needs Node 20+ and Chrome/Chromium for the PDF (CHROME_PATH if not found). The only dependency is pg, and only used with a database Demo books (PDFs in docs/): one built from the author's own WikiTree tree, with seven scanned handwritten recipes from relatives (one of them a living relative who agreed to be named) and the profile photos the author uploaded to WikiTree themself (node src/cli.js Robinson-27225 --family-recipes examples/robinson-demo/family-recipes.json --title "The Robinson Table"…
MIT licensed. Node 20+, Chrome for the PDF, and Ollama; the only dependency is pg, and only if you use a database. 125 tests.
brew install ollama && ollama pull gemma3:4b
node src/cli.js ancestors Your-1234 --relatives # find IDs for people to assign recipes to
node src/cli.js scan ./my-scans --tree Your-1234
node src/cli.js review family-recipes.json # proofread the model's reading
node src/cli.js Your-1234 --family-recipes family-recipes.json --family-people people.json
The book above: node src/cli.js Robinson-27225 --family-recipes examples/robinson-demo/family-recipes.json --title "The Robinson Table" --wikitree-photos --me Robinson-27225 --honor-code-signed --confirm-permission (those last flags are my statement that I uploaded those photos). Fictional demo (no real people): node examples/fictional-demo.mjs.
How I Built It
I built Recipe Roots with Claude Code, and the models inside the product are open-weight: Gemma 3 4B writes the chapter prose and restructures old receipts, Gemma 3's vision reads the scans, and EmbeddingGemma (768 dimensions) powers search. All of it runs locally through Ollama.
The model never writes about people
My first version let Gemma write the chapter introductions with everything: names, relationships, dates. It was fluent and confidently wrong. It invented a great-grandfather who "went ahead to join them." A year-checking guard couldn't catch that, because there was no wrong year to catch, just a relationship that did not exist. In a family book that's the worst possible failure: it looks like research and it's fiction.
So I split the job:
Code writes every fact about a person: names, kinship, places, years, straight from the tree. A cook's life sentence is assembled from fields ("Born in 1904 in Belle Plaine, Benton, to Carl Budd and Lovey Jane Hoover. Died in 1966 in Aberdeen, Grays Harbor, aged 62.") and age appears only when both dates are full.
The model only writes about food. It's never shown a name, a relation or a birth date, so it has nothing to build a biography from. It sees the region, the era, a kitchen-history note and the dish.
A guard checks every draft: years not in the supplied facts, kinship words, "would have" speculation, invented motives, meta commentary, any person's name. A failed section gets one retry with the reasons, then falls back to a template individually, and the book's "About" page says honestly which parts the model wrote.
I'd rather ship a plainer paragraph than a lovely invented one. The same rule shapes the family pages: anything about a person that isn't in your own notes comes from the tree, not from the model.
Period receipts: "how they wrote it"
I wanted the book to show a great-grandmother's apple pie next to what an apple pie was in her day. Gutenberg and the Internet Archive have a lot of old American cookbooks, and anything published in the US before 1926 is in the public domain, so the book can quote them: Fannie Farmer (1906), The Virginia Housewife (1860), Eliza Leslie's Philadelphia Directions for Cookery (1840), The Cookery Blue Book of San Francisco (1891), and 15 others, 19 books in all.
The matching is deliberately strict and has no model in it. A receipt counts only if its title is the same dish with at most a few harmless extra words, so "Marrowfat Beans Baked" is not "Boston Baked Beans" and "Johnny Cake" is not "Skillet Cornbread". No match means no quotation. Heritage and era choose the books: a Pennsylvania line is offered the Pennsylvania books, nearest in time first, and a WikiTree heritage sticker on a profile can offer a heritage cookbook where the community has stated one. It is never inferred from a place name.
Old receipts are single paragraphs, so under each quotation the book sets them as ingredients-then-method. Where the receipt already lists ingredients, that's plain code. Where it's prose, Gemma restructures it and then a checker holds it to the original: every quantity and ingredient must be in the text, no temperature may be added, no steps repeated. A receipt that fails just shows the original. On eight real 1907 receipts, five came through and three were correctly withheld. What the original leaves out ("the original gives no oven temperature or baking time") is said in one sentence, not filled in.
Scans, polished
Assign each card to anyone in the tree, including the great-aunt who actually made the thing.
Transcription reads the card twice, the second pass only for numbers, and flags any disagreement, because "tsp" read as "tbsp" ruins a recipe. Until you set "reviewed": true, the page says Transcribed by AI, check amounts.
recipe-roots review writes a sheet with each scan next to what the model read and the doubtful amounts called out.
Handwriting is hard for a 4B model, so there's --page-only: the scan is the page, no model involved. That's usually the right choice for Grandma's own hand.
A small engineering thing I enjoyed: page numbers in a PDF
Chrome's print-to-PDF has no target-counter(), so a contents page can't know page numbers. The fix: put an invisible one-pixel [[marker]] at every section, render, read each marker's page back out of the PDF (PDFKit on macOS, pdftotext elsewhere), render again with real numbers, and repeat until they stop moving. Usually that's two passes. If the PDF can't be read, the contents prints as a plain list, never with wrong numbers.
Tiger Cloud: hybrid search over every receipt
I loaded every receipt from the 19 public-domain cookbooks, 18,971 of them, into a free Tiger Cloud service (PostgreSQL 18.6) with an EmbeddingGemma vector each, a BM25 index from pg_textsearch and pgvector. It is optional: strict title matching works without a database, and the demo books above were built without it. With --similar-period on, one SQL query ranks the receipts by keyword and by meaning, only within the books that fit the cook's era and region, and fuses the two lists with Reciprocal Rank Fusion (Tiger's own hybrid-search recipe). The 56-dish pantry is embedded there too.
What hybrid search is for: strict title matching finds the same dish; it can't tell that "Shoofly Pie" is "SHOO-FLY PIE" or that "Spaetzle" is "SPAETZLEN OR SPATZEN". The vector side finds them, a spelling check accepts them as the same dish, and where there's no same dish a receipt is offered as "A similar dish… not the same dish" only if the keyword and vector rankings both put it near the top and it shares the dish's own words. My first, looser rule offered "Turkey Stuffing (Swedish Style)" for Swedish meatballs, so I tightened it.
The honest numbers: of the 56 pantry dishes, strict matching found 27; hybrid search added 2 spelling variants and 1 similar dish and, correctly, nothing for the other 26 (mostly continental dishes American cookbooks of the time don't print). That is a modest gain, and it's measured, not hoped. A gotcha worth knowing if you use pg_textsearch: inside a CTE or window function the BM25 query fails with "no BM25 index found" unless you name the index with to_bm25query(query, 'index_name'). With 19,000 rows the search is exact, not an ANN index, on purpose. Tiger's certificate comes from a private CA that Node doesn't trust, so I connected with libpq-style TLS (encrypted, not certificate-verified); the README shows how to pin the CA instead.
What doesn't work yet
The recipes in the chapters aren't your family's, and the book says so. They're written from standard published versions and haven't been kitchen-tested. Cook one before you gift it.
Handwriting transcription is a draft, not a result. Typed and handwriting-font cards read correctly. Real handwriting did not. On the first card Gemma 3 4B read ½ cup cocoa as "2 c." and 2 T butter as "2 t", split one card into two recipes and filed ingredients under directions (I fixed the last two in code). On six more cards it kept dropping the "1" before a fraction (2 1/4 became "2/4", 1 1/4 became "1/4", 1/3 became "3/3"), read "Preheat" as "Present", turned "All chopped fine" into "All Chopped Flounder", invented a method ("Mix all ingredients together") for a card that had none, and missed margin notes like "350°". Only the waffles came through almost clean. The number cross-check flagged most of the wrong amounts, which is why every AI transcription is marked until a person reviews it (I corrected these against the scans and proofread them), why review highlights doubtful lines beside the scan, and why --page-only exists: for handwriting you care about, print the scan itself.
The guard can't verify meaning. It blocks invented years, kin and names, but a model can still connect dots loosely. Read the book before it prints.
28 of 56 dishes have a period receipt, and there is no Midwestern cookbook on Gutenberg that I could find.
Permission. WikiTree photos and recipes belong to their contributors, and this book is meant to be sold. Using them is gated and fails closed: it needs your WikiTree ID, a statement that you've signed the Honor Code (Wiki Genealogists have) and an explicit confirmation, and it writes a ledger of whose work is in the book. WikiTree's API doesn't expose membership level, so the Honor Code check is an attestation the tool can't verify. Your own photos and recipes are never gated.
Structuring a receipt into a different layout doesn't, by itself, get around copyright. That's why the sources are public domain.
No print-on-demand specs yet (bleed, spine, cover): the PDF is a 6×9 interior.
What I'd do next: front-and-back recipe cards, a Midwestern and a German period source, a real print-on-demand cover, a WikiTree login so profiles you're trusted on (not just public ones) can be used, and someone other than me using it. If you've got a box, I'd like to hear what's in it.
Why Does Open Innovation Matter?
Private by construction. Scans of a family's recipe cards, and their handwriting, are exactly what many people won't upload to a hosted API. The vision model runs on the laptop; nothing leaves it.
Costs nothing per book. A tree-and-scans book needs a few dozen model calls. At local-inference cost, the margin on a gift book is the price of printing, not the price of tokens. That is what makes a side hustle of it.
Controllable. Because the model is local and swappable I could tune the whole system around its weaknesses: restrict it to food, guard its output, fall back per section. Any OpenAI-compatible endpoint works; --model swaps it. A closed API that changed behaviour under me would have broken a guard I had calibrated to this model's failures.
No lock-in to a pricing change on something I'd want to resell.
Open data under it, too. The people come from WikiTree's open API and the period recipes from public-domain books on Project Gutenberg and the Internet Archive. A product built on open models and open sources can be handed to a family, or run as a service, without anyone's terms of service in the middle.
My Agent Session
I built Recipe Roots with Claude Code. These are the highlights of that session, with secrets and private details removed:
My First User
The first person to use it was me, on my own family, so I can't claim a paying customer. Nobody has paid. What I can report is what happened when someone who actually cares about the people in the book used it.
I took one handwritten recipe card, a chocolate pudding filed under my grandmother, Leona Budd, and ran it through the whole pipeline. It did not go smoothly, and the failures are the useful part. Gemma read "½ cup cocoa" as "2 c.", and "2 T butter" as "2 t". It split the one card into two recipes. It filed some ingredients under the directions. I fixed the split and the misfiled lines in the tool. The cocoa and the butter I caught the way a person would, with review.html open: the scan on one side, the transcription on the other, the doubtful lines highlighted. I corrected them, put the ingredients back into the order on the card, and marked it reviewed. Then I read the page with the facts about her on it, checked them against what I know of her, and they were right. Then I ran six more cards from my relatives through the same steps, including two of my mother's; how that went is under "What doesn't work yet" above.
What I took from it is not about the model. It's what the page does once it's right. In my words:
It is powerful to see a handwritten family recipe and the information about that ancestor together, in a book that can be handed down to nieces and nephews. Seeing snippets of what was happening during that time for that ancestor helps me understand what their everyday life may have been like and what they may have made for their family.
That is the product. Not the chapters of generic regional dishes (those are the backdrop), but the card in her own hand, next to who she was and what her kitchen looked like, in something a niece can hold.
The next test is the one that counts: put a finished book in front of a relative who isn't me and see whether they ask for another copy. I haven't done that yet, and I'll say here what they said when I have.
Prize Categories
Best Use of Gemma. Gemma 3 4B writes the chapter prose and restructures old receipts, Gemma 3's vision reads the scans, and EmbeddingGemma powers recipe search. All three run locally through Ollama, and the product is designed around what a 4B model can and can't be trusted with.
Best Use of Tiger Data. A free Tiger Cloud service holds all 18,971 period receipts with a BM25 index (pg_textsearch) and pgvector, and the book's "similar dish" and spelling-variant matching run as one fused hybrid-search query (details above, including the measured numbers and the BM25 gotcha).
Best Use of a .tech Domain. reciperoots.tech is the project's home: a one-page site on GitHub Pages with both demo books, the code and the screenshots, claimed through the Hacktoberfest offer.






