DEV Community

Harika Nayudu
Harika Nayudu

Posted on Edited on

Ofrenda: A Remembrance Builder That Never Leaves Your Laptop (clear, and states the open-source angle right away)

What I Built

Every family has a few people whose stories only exist in someone’s head.

A grandmother who hummed while she cooked. An uncle with one phrase he said at every dinner. Those details are the first to fade, and the hardest to write down when you’re staring at a blank page.

The tempting move is to paste the little you remember into a chatbot and ask it to write something beautiful. I didn’t want to do that. The most private words a person writes shouldn’t have to travel to a server they don’t control.

Ofrenda is a local-first virtual ofrenda builder for Día de los Muertos. You tell it who someone was to you: their name, years, favorite foods, favorite songs, and the stories you remember. A Gemma model running on your own machine through Ollama then does two jobs:

Asks you one gentle question at a time (“What did her kitchen smell like?”) to draw out memories you wouldn’t have typed on your own.
Drafts a short remembrance using only the facts you gave it.

When you’re done, you export everything as a single HTML file: marigolds, a framed photo, flickering candles, and your words. It opens in any browser with no server and no internet.

The principle behind the project is simple:

The model asks. You remember. Nothing is invented. The result is yours to keep.

How I Read the Theme

The line between past and present is thin this week. To me, the ghost in the machine isn’t something that goes bump in the night. It’s whatever part of a person survives in the people who remember them. So instead of a haunted terminal, I built a tool whose whole job is to keep that from fading.

Code

GitHub: https://github.com/Harikanayudu18/Ofrenda

ofrenda/
app.py server, storage, Ollama client, fallback, HTML export (stdlib only)
static/index.html the whole UI
tests/test_smoke.py API flow, escaping, path-traversal checks
data/ your ofrendas as JSON, created at runtime

The backend is Python’s standard library only. No pip installs, no framework, no build step.

**How I Built It
**Step 1: Keep the model on a short, honest leash

The risk with using a language model for grief is embellishment. Models love to add a detail that sounds right: a garden that never existed, a sentence the person never said.

So every prompt starts with the same ground rules:

python
GROUND_RULES = (
"You are helping someone build an ofrenda, a Día de los Muertos remembrance, "
"for a person they loved. Be warm, specific and sincere. Never mock, never be "
"spooky or morbid. Use ONLY the facts provided below. Do not invent names, "
"places, dates, quotes or events. If something is not given, leave it out."
)

The facts block is built only from what the user typed. The model never sees anything else.

Step 2: Questions that open memories

The “Ask me a question” button sends the facts so far and asks for exactly one follow-up. It has to be sensory (a smell, a sound, a gesture) or a small story, and it must not repeat what’s already covered. Your answer is appended to the stories, and the next question builds on it. The goal is to make remembering easier, not to write for you.

Step 3: The draft is a draft

“Write remembrance” asks Gemma for 120 to 200 words in the first person, as if placed beside the photo. The text box stays editable on purpose. The model drafts, and the person decides what’s true.

Step 4: One message, no system role

Gemma chat templates have no system role, so the Ollama call sends a single user message:

python
def ollama_chat(prompt, temperature=0.7, timeout=180):
body = json.dumps({
"model": MODEL,
"stream": False,
"options": {"temperature": temperature},
"messages": [{"role": "user", "content": prompt}],
}).encode()
req = urllib.request.Request(
OLLAMA_URL + "/api/chat", data=body,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.load(r)["message"]["content"].strip()

Changing the model is one environment variable: OFRENDA_MODEL=gemma3:12b.

Step 5: It must never block someone

If Ollama isn’t running, the app falls back to a template. It builds a plain remembrance from the person’s own words and rotates through a small set of prompt questions. A badge at the top shows whether Gemma is running or the app is in template mode. I didn’t want a missing model to turn a tender moment into an error screen.

Step 6: Export something that outlives the app

The export is one standalone HTML file with inline CSS, a CSS-animated candle flicker, and the photo embedded as a data URL. Everything the user typed goes through HTML escaping, and a test checks that a name like alert(1) can’t run in the exported file. Photos are resized in the browser before saving, so the exported file stays small.

Step 7: Run it
bash
git clone https://github.com/Harikanayudu18/Ofrenda.git
cd Ofrenda/ofrenda
ollama pull gemma3:4b
python3 app.py # open http://127.0.0.1:8741

Run the tests:

bash
python3 -m unittest tests.test_smoke

The tests start a fake Ollama server, so both the Gemma path and the fallback path are exercised without needing a model. They cover the full save, draft and export flow, HTML escaping, and rejection of path-traversal IDs.

Why Open Innovation Matters

For this project, privacy isn’t a feature. It’s the whole point.

The data stays on the machine. The server binds to 127.0.0.1. The only network call it makes is to your local Ollama instance. No account, no API key, no hosted AI service.
It works with the internet off. Download the weights once, and after that the whole thing runs offline. It works with the lights off, which suits the theme.
It isn’t locked inside a service. Saves are plain JSON files, and the export is a single HTML file you can print, email, or keep for decades. If this project, Ollama, or I disappeared, your ofrenda would still open.
The model is swappable. One setting changes it, without touching the rest of the app.

There are limits to these claims. I haven’t compared Gemma’s writing on this task against a closed model, so I’m not claiming it writes better. I also haven’t audited Ollama’s networking or telemetry behavior, so anyone who needs strict air-gapped operation should verify that separately. And Ofrenda is a single-user local tool with no login, so it should not be exposed to a public network.

What open models gave me is control over where the most personal text a person can write is processed, and a tool that doesn’t depend on a company staying in business.

What It Doesn’t Do Yet
No audio recording or transcription of spoken stories. A local speech model would be a natural next step.
No sharing or multi-user mode, by design.
The automated tests use a stand-in Ollama server, so the quality of the drafts and questions depends on the Gemma model you run and the stories you give it. The editable text box is there for exactly that reason.
Prize Categories

Best Use of Gemma: Ofrenda uses the open-weight Gemma model through Ollama, running locally, to ask follow-up questions and draft remembrances from only the facts a person supplies. The prompts forbid invention, the output is always editable, and the app keeps working in template mode when the model is unavailable.

Thanks for reading. May the candles stay lit.


Top comments (0)