DEV Community

Joe George
Joe George

Posted on

Ofrenda: a ghost that only knows what you tell it

Hacktoberfest Open-Source AI Challenge: Ghost in the Machine Submission 👻🌼

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

What I Built

Ofrenda is a local-first memory altar for Día de los Muertos. You light a candle for someone you miss and write down stories about them: who made the tamales, which song came on while the masa rested. A small open-weight model running on your own computer can then echo those stories back in a chat, or write a letter or a calaverita (a short rhyming poem) for the altar.

The altar: papel picado, candles, a marigold garland, and offerings for Abuela Rosa

I read "ghost in the machine" literally and carefully. A ghost that sounds like your grandmother and quietly invents a husband, a hometown or a favorite color is worse than no ghost at all, because it overwrites the real memory with a plausible fake one. So Ofrenda has exactly one rule: the ghost may only repeat what you wrote down. If it doesn't know, it says so and asks you to teach it.

The inspiration is the ofrenda itself. Every year there is an altar somewhere with a photo, marigolds and a plate of what that person loved to eat, and the stories that go with it usually live in one aunt's memory. I wanted a tool that keeps them.

What it does:

  • Altar: one page per person, with their foods and songs set out as offerings.
  • Memories: write them, or (with an ElevenLabs key) record a spoken story that gets transcribed.
  • Talk: chat with an echo of what you wrote. The "Keeper" mode speaks as the family keeping the memory, not as the person.
  • Letter: a letter or calaverita, in English or Spanish, on paper you can print.
  • Your data: export everything as JSON, or erase it with a typed confirmation.

Code

[Repo: https://github.com/joegeorge022/Week-4] (https://github.com/joegeorge022/Week-4)

ollama pull gemma3:4b
git clone https://github.com/YOUR-USERNAME/ofrenda && cd ofrenda
npm install
cp .env.example .env     # optional keys; none are required
npm run doctor           # checks your setup
npm start                # http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

Tests: npm test (25 passing). Honesty eval against the real model: npm run eval.

How I Built It

The open-source AI:

  • Gemma 3 (open weights) via Ollama generates every reply, letter and poem. The default is gemma3:4b; gemma3:1b is a one-variable swap.
  • Mastra is the agent layer: model config, streaming, and swapping models.

What surprised me: prompting wasn't enough. My first version had careful rules in the system prompt ("use only the memories, never guess"). It mostly worked. Then I wrote a small eval, because "mostly" isn't good enough for this, and it found three problems:

  1. Asked "what was your favorite color?", the model answered with an unrelated real memory about marigolds. That's worse than inventing something, because it looks sourced.
  2. I added a few-shot example to fix the tone. A Spanish question then came back answered with a detail from my example, a different person's memory, in someone's grandmother's voice.
  3. When I told it to say "nobody has told me that yet", it started appending that line even after correct answers. It was copying the phrase from the prompt.

So I stopped trying to persuade the model and enforced the rule in code. The model must answer in two parts:

EVIDENCE: "words copied from ONE memory"   (or NONE)
REPLY: ...
Enter fullscreen mode Exit fullscreen mode

Before any text reaches the screen, the server checks that the quote really exists in a stored memory. Then a second tiny temperature-0 call asks whether that memory is actually about what the user asked. If either check fails, the user gets a fixed refusal and the model's own reply is thrown away. A weaker model fails closed.

The wording of that relevance check mattered more than I expected. "Does this memory directly answer the question?" scored 10/14 on my probe set and rejected "Tell me about your garden", which is a request, not a question. "Is this memory about what the user is asking for?" scored 14/14.

How I measured it. I ran the same 9 cases (5 answerable, 4 unanswerable, one in Spanish) against the real local model:

model recall honesty avg reply
gemma3:4b 15/15 12/12 ~5.7 s
gemma3:1b 6/15 12/12 ~2.2 s

The 1B model is more than twice as fast and refuses far more often, but it never invented an answer to something it couldn't know. That's what "fails closed" looks like in practice.

The rest of the stack:

  • BM25 retrieval, no vector DB. A family has dozens of stories, so almost all of them fit in the context window. The ghost gets everything that fits, ordered by relevance, which also lets a Spanish question reach a story written in English.
  • Plain Node and vanilla JS. No framework, CDN, web fonts or analytics, so it works with the Wi-Fi off. The UI is a hand-built "midnight marigold" design with an SVG marigold, papel picado and flickering candles.
  • Storage is one atomic JSON file by default, or MongoDB Atlas if MONGODB_URI is set.
  • Safety defaults: loopback-only, Host/Origin checks against DNS-rebinding, an optional signed-cookie PIN with rate limiting, and a typed confirmation before erasing anything.
  • Optional, all off by default: ElevenLabs (narration, and recording an oral-history story), Sentry agent tracing (timings only, never the text of a memory), and a Render blueprint.

What this does and doesn't show. I tuned the prompts against these same cases, so it's a regression harness, not a benchmark. Nine cases is small, and the scoring is regexes. The gate checks the quote, not every word of the reply: on the 1B model, one accepted answer added "a warm smile and a good conversation" that no memory contains. Letters and calaveritas are creative output and aren't gated. Even after I tightened their prompt, the 4B model still adds small flourishes ("you loved them so much"), and its Spanish is imperfect: it left "marigolds" in English and coined the word "marigolias".

What I haven't verified. I have not run the ElevenLabs path with a real key, deployed the Render blueprint, run against a real MongoDB Atlas cluster, or confirmed that Sentry events arrive. The code is there and a npm run doctor command checks each key, but I'm not claiming them as working.

The honest cost. Replies take about 6 s in a quiet eval run and 10 to 20 s on my 8 GB MacBook with other apps open. The first answer after the model has been unloaded can take a minute or more while it reloads. A hosted frontier model would answer faster and, on many questions, better. For this project that trade is the point, but it is a trade.

Why Does Open Innovation Matter?

  • The data is a family's private grief. Stories, photos and names never leave the laptop. The server binds to 127.0.0.1, and after the one-time model download I turned Wi-Fi off and it kept working. A closed API would have meant sending a dead grandmother's stories to someone else's server to be answered.
  • I could change the model's behavior, not just its prompt. When I wanted to see how much the model mattered, I changed one environment variable and re-ran the same eval on gemma3:1b. Because I run the model myself, I could stream its output, hold it back, check it against the memories, and stop it midway. That is how the "fails closed" design was possible at all.
  • It can't disappear. There is no service to shut down and no price to change. The memories are one JSON file I can read, export or delete, and the model is a file on disk. An altar is something you keep for decades.

The Spirit of It

The moment I keep coming back to is not an answer, it's a refusal. I asked the altar's ghost for Abuela Rosa's favorite color, something nobody had written down, and it replied:

Nobody has told me that yet. Would you like to share it so it can be kept?

An AI that admits it doesn't know, and asks you to teach it instead of making something up, turns the tool from an imitation of a person into a way of keeping what is actually known about them. The things it does say back (the marigolds she called her little suns, the tamales every December, the boleros while the masa rested) are the ones someone chose to write down.

Prize Categories

  • Best Use of Gemma: Gemma 3 (gemma3:4b and gemma3:1b) runs every reply, letter and poem locally via Ollama, and I compared both sizes with the same eval.
  • Best Use of Mastra: the ghost is a Mastra agent, streamed through fullStream and wrapped by the citation gate.
  • Best Use of Render: Deployed on render (https://ghost-in-the-machine-su1v.onrender.com/)

Open weights. Open source. Your stories stay yours. 🌼

Top comments (0)