DEV Community

Harika Nayudu
Harika Nayudu

Posted on

A Ghost with No Imagination: Building a Local Ofrenda Assistant with Gemma

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

*What I Built
*

Language models have a habit that is charming in a poem and painful in a remembrance: they invent.

Ask one to write a tribute to someone’s grandmother and it may give her a garden she never had, a phrase she never said, a lullaby she never sang. It sounds right, so it slips past you. A tribute to a person who has died is the last place for that.

So I set out to build a ghost with no imagination.

Ofrenda is a small local app 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 to draw out memories you wouldn’t have typed on your own.
Drafts a short remembrance using only the facts you supplied.

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

The whole project is an attempt to answer one question: how do you let a model help with grief without letting it make anything up?
**
How I Read the Theme**

An ofrenda is built on the idea that people stay with us through the things we set out for them: their food, their music, their stories. The “ghost in the machine” here is not something scary. It is whatever of a person survives in the people who remember them. I wanted the machine to help hold that, not rewrite it.
**
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.

The Design Problem: A Ghost with No Imagination

I made four decisions to keep the model honest.

  1. The model only sees what you typed

The prompt is assembled from a facts block built entirely from the form fields. There is no other context, no retrieval, and no memory between sessions.

python
def facts_block(o):
lines = [f"Name: {o.get('name', '')}"]
for label, key in (("Years", "years"), ("Relationship to me", "relationship"),
("Favorite foods", "foods"), ("Favorite songs", "songs"),
("Stories I remember", "stories")):
v = (o.get(key) or "").strip()
if v:
lines.append(f"{label}: {v}")
return "\n".join(lines)

Empty fields are left out completely. If you didn’t give a favorite song, the prompt doesn’t contain the word “song”, so the model has nothing to invent a song from.

2. The ground rules say what not to do, and what to do instead
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 last sentence matters most. Telling a model “don’t invent” works better when you also tell it the alternative: when a fact is missing, leave it out. The tone rule (“never spooky or morbid”) exists because a Halloween-week prompt can pull a model toward horror, and this is not that.
**

  1. The questions come from the person’s own words**

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

The model never writes the memory. It only helps you find it.

4. 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 is editable on purpose. The model drafts, and the person decides what is true. A tribute that the family hasn’t read and corrected isn’t finished.

One Message, No System Role

Gemma’s chat template has no system role, so the Ollama call sends everything as 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.

When the Model Isn’t There

If Ollama isn’t running, Ofrenda doesn’t show an error. It falls back to a plain template that rearranges your own words into a remembrance, and rotates through a small set of prompt questions like “What did their kitchen smell like?” A badge at the top shows whether Gemma is running or the app is in template mode.

I made that choice because of who might be using it. Someone sitting down to build an ofrenda may be doing something hard. A missing dependency shouldn’t be what they remember about the evening.

Exporting Something That Outlives the App

The export is one standalone HTML file: inline CSS, a CSS-animated candle flicker, and the photo embedded as a data URL. Photos are resized in the browser before saving, so the file stays small.

Everything the user typed is HTML-escaped. A test saves an ofrenda named alert(1) and checks that it appears in the export as harmless text. Another test checks that a path-traversal ID like ../x is rejected before it touches the filesystem.

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 template fallback are exercised without a model. They cover the full save, draft and export flow, HTML escaping, and path-traversal rejection.

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.
I control the leash. The prompts, the facts block and the fallback are all code I can read and change. Swapping the model is one setting. With a hosted assistant, a silent model update could change how it writes about the dead, and I’d never see it happen.

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 I Haven’t Proven

The prompt rules are a strong nudge, not a guarantee. A small model can still embellish despite “use ONLY the facts provided,” and I haven’t measured how often that happens. The automated tests use a stand-in Ollama server, so they check the plumbing and not the quality of the model’s writing. That is why the draft box is always editable, and why I’d rather the app say “this is a draft” than pretend it can’t be wrong.

The honest version of the project’s promise is not “the model never makes things up.” It is “the model sees only what you gave it, is told not to add to it, and hands the final say back to you.”

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)