This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

What I Built

On Día de los Muertos, families build an ofrenda for the people they've lost: their photo, candles, a path of marigold petals so they can find their way home, and the things they loved, like their favourite food, their drink, the song they always sang.

Most of us now have something else they left behind: years of messages. A WhatsApp chat with your grandmother is a strange, precious archive. It's full of the same five phrases repeated a hundred times, the food she was cooking, the song on the radio, the thing she called you. Nobody ever reads it again, because scrolling back through it is unbearable and you don't know what you're looking for.

The obvious "AI" thing to build with that archive is a chatbot that talks like her. I didn't want to build that. A language model trained to sound like your grandmother will eventually say something she never would have, and then the ghost in the machine is just the machine.

Marigold goes the other way. It builds an ofrenda out of their real words, and it has one rule: the AI is never allowed to speak for them.

  • Gather. Drop in a WhatsApp chat export, paste a letter or a card they wrote, and add memories of your own.
  • Their altar. Marigold finds the things they said again and again ("Dios te bendiga", "Did you eat?", "Ponte un suéter"), what they called you, the hour they usually wrote, and their favourite emoji. Then Gemma looks for the offerings: the foods, drinks, songs, places and small loves that come up in their messages. Each one goes on the altar, and you can click it to read the messages it came from.
  • Visit. Light a candle and Marigold shows you something they really wrote, picked from this same day in another year when there is one. Or ask "what did she say about…" and get her actual messages back, word for word, with what you had said just before.
  • Remembrance. Gemma helps you write about them, in your voice, from your memories and the altar. It's labelled as a draft for you to make your own.
  • Keep. Print the altar for the real ofrenda, or save everything as one file to open again next year.

Everything runs inside the browser tab on open models. No account, no upload, no server. After the first visit it works with Wi-Fi off.

Demo

Try it: marigold-o96b.onrender.com in Chrome or Edge on a laptop.

You don't need anyone's real messages: click See Abuela Rosa's. She's a made-up grandmother with two and a half years of WhatsApp messages to her grandson Leo, plus a birthday card she mailed him. The altar, sayings and search appear in a few seconds (after a one-time 34 MB download). Then try:

  • Light a candle (on October 27 it finds the morning she was making pan de muerto)
  • when I failed my exam in the search box
  • Find offerings, which downloads Gemma once (~790 MB) and fills the altar in about a minute and a half
  • Arrange → What Gemma made up, to see every answer Marigold refused to believe

Code

GitHub logo Eternity0207 / marigold

An ofrenda made of their own words: open models in your browser find their sayings and offerings, and never speak for them.

Marigold

An ofrenda made of their own words. Bring the WhatsApp chats, letters and cards someone left you. Marigold finds the things they always said, what they called you, and the foods, songs and places they loved, and builds them an altar for Día de los Muertos. Light a candle to read something they wrote you on this day in another year, or ask what they said about anything.

Open models do all the reading inside your browser tab. There is no server, no account and no upload, and after the first visit it works offline.

Marigold never writes as them. Every word on the altar that is theirs, they really said: sayings are counted, not generated; offerings are kept only if the name appears word for word in their message; search returns their messages, never a summary. The model only helps you find their words, and helps you…

How I Built It

Marigold is static files: Vite and vanilla JavaScript, no framework, the same offline plumbing as my previous three entries (a Web Worker running Transformers.js on ONNX Runtime Web, model files in the Origin Private File System, a service worker for the app shell). What's new this week is how the AI is kept on a short leash.

Three kinds of work, and only one of them is a language model

Job How Model
Read the chat export A parser for WhatsApp's Android and iPhone formats, any date order, 12- or 24-hour clocks none
Sayings, nicknames, emoji, the hour they wrote Counting none
"What did they say about…" Keywords + meaning search gte-small (MIT, 34 MB)
Offerings Meaning search picks the messages, Gemma names the thing, a check throws away anything not in the message Gemma 3 1B (WebGPU), or 270M on CPU
Your remembrance A draft from your memories and the altar Gemma 3

Sayings are counted, not generated

A saying is a phrase of two to six words that appears in many different messages. Three tweaks made the list sound like a person instead of a word cloud:

  1. It has to be theirs, not yours. Phrases you use just as often (good morning, love you) are dropped, so what's left is distinctly them.
  2. Sayings open sentences. "Everything passes." is a sentence on its own, while "this year" lives in the middle of one. Ranking by how often a phrase starts a sentence pulled "Everything passes" into the list and pushed the filler out.
  3. "your abuelo" is a topic, not a saying. Two-word phrases that start with a possessive are skipped.

The nickname finder looks for words in the spot where people address you: after a comma, or after "Buenos días", "Ay", "Dios te bendiga", "Happy birthday". On the sample it finds mijo, 14 times, and nothing else.

Search returns their messages, never a summary

"What did she say about…" mixes BM25 keywords with gte-small meaning vectors, each turned into a z-score across all of their messages so neither signal drowns the other (the same trick as in Hoard last week). I wrote 12 queries for the sample before tuning anything:

Search by Right message first
Keywords only 8 of 12
Meaning only 7 of 12
Both 10 of 12 (11 in the top 3)

The two misses are honest ones: playing games with friends doesn't find "Sunday lotería with the señoras", because a small English embedding model doesn't know lotería is a game, and christmas ranks "Feliz Navidad" second. I also tried embedding each message together with what you'd said just before it. That made no difference on these queries, so I kept the simpler version.

Offerings: Gemma names it, then Marigold checks

Asking a 1B model to "read this person's 3,000 messages and list their favourite foods" is a recipe for confident nonsense. So Gemma never sees the whole chat:

  1. Meaning search picks the messages. Each kind of offering (food, drink, music, places, loves) has five example sentences. gte-small scores every message against them, and only the most unusually close ones, up to 18 per kind, go to Gemma. On the sample that's 56 questions about 83 messages and letter paragraphs.
  2. Gemma reads one message at a time and answers with the name, copied from the message, or NONE.
  3. The name must be in the message. The answer is matched against the message text, ignoring case and accents and allowing for plurals. Anything else is thrown away. Generic answers ("dinner", "song", "summer") are thrown away too.

Step 3 is the whole point, and the "What Gemma made up" list shows why. On the sample, Gemma 3 1B suggested:

  • "The Beatles" as a song, for "My heart is tired mijo. But I am happy. I had a good life"
  • "Star Trek", for "I love you more than all the stars"
  • "Blue wine" as a drink, for "Take the mole in the blue container"
  • "Coffee" for "Did you eat breakfast?" (to be fair, her next message was "Coffee is not breakfast")

Without the check, The Beatles would be on Abuela Rosa's altar. With it, every offering links back to something she really wrote.

I measured it against a list I wrote by hand of what a person reading the sample would put on the altar (29 things):

Version Offerings Wrong Thrown away as made up
First: one name per message 17 1 ("summer" as a place) 16
Now: up to three names, time words count as generic, plurals allowed 16 0 8

It finds tamales, pan de muerto, mole, chiles rellenos, tlayudas, champurrado, café de olla, Pedro Infante, Bésame Mucho, Oaxaca, Puerto Escondido, Santo Domingo, her roses and the marigolds: 16 of the 29, with nothing wrong. It misses the lotería, the telenovela, the crossword and the song Amorcito Corazón, mostly because the meaning search never sends those messages to Gemma. I'd rather have an altar with gaps than one with The Beatles on it, and you can add anything in Arrange. The whole pass takes 89 seconds on my laptop's integrated Intel GPU.

The remembrance is yours

The only time Gemma writes prose is the remembrance, and it writes as you about them, never as them. The prompt gives it your memories, the sayings and the offerings, and tells it not to invent anything. It mostly listens: the draft for Leo used "mijo", "Ponte un suéter", the tamales and the papel picado in the shoebox. It also embellished a little (somehow her roses ended up in the kitchen). That's why it lands in an editable text box labelled as a draft, not on the altar.

Small things that mattered

  • The candle looks for this day. Visit picks a message from the same calendar day in another year if there is one, then the same week, then anything. Seeing "27 October 2024 · on this day, 2 years ago: Making the pan de muerto today" is a different feeling from a random quote.
  • Messages keep their context. Messages sent within a few minutes of each other are merged, and each keeps the message it answered. "One exam is not your life" means more under "abuela I failed my chemistry exam".
  • Nothing is assumed. The interface never guesses pronouns. It says "Abuela Rosa called you mijo", not "she".
  • The altar is a night scene in both themes: papel picado in CSS-masked SVG that sways, flickering candles, a marigold petal path, and saying cards in a handwritten font. The animation respects reduced motion, and there's a print layout for putting it on a real ofrenda.

Why Does Open Innovation Matter?

These are the most private messages you have. A chat with someone who has died is full of their health, their money worries, family arguments, and other people's names. They can't consent to a company reading it. With open models running in the tab, nothing is uploaded. There is no server and no upload code. Open DevTools → Network while Marigold reads: the only traffic is the one-time model download, coming in.

It has to outlive the company. "Talk to your loved one again" startups come and go, and when one shuts down, people lose a second time. An ofrenda is something you come back to every November for the rest of your life. Marigold is static files and open weights: it runs offline, saves to one file you own, and anyone can host it. If I stop maintaining it tomorrow, it still works next year.

Open weights let me decide what the model is not allowed to do. With a closed chat API, the easy product is the griefbot, because that's what the API is shaped for. Running Gemma myself, one message at a time with a 24-token answer, meant I could treat it as a narrow tool. It names a food. I check that the food is really there. It never gets the chance to speak for her. Being able to inspect every single answer, keep the ones that are wrong, and show them to the user is only possible because nothing is hidden behind an API.

It costs nothing to keep. You open an ofrenda once a year. A subscription for that would be absurd, and a free tier would be paid for with the messages themselves. On your own laptop the marginal cost is zero, forever.

The Spirit of It

Prize Categories

  • Best Use of Gemma: Gemma 3 1B runs on-device over WebGPU (or 270M on CPU) as a narrow, checked tool: it reads one pre-selected message at a time and names an offering, and every name is checked against the message. On the sample: 16 offerings, 0 wrong, 8 made-up answers caught and shown to the user, in 89 seconds on an integrated GPU. It also drafts the remembrance in the user's voice, never the deceased's.
  • Best Use of Render: hosted on Render as a static site, with a Blueprint that sets the cross-origin isolation headers multithreaded in-browser inference needs.