This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
Every family has a ghost like this: a grandmother who named every tomato plant in her garden, an uncle's terrible jokes, a side project that stopped at version 0.3 and has been rotting in a private repo ever since. They are not dramatic stories. They are small, specific, and they disappear the moment the last person who remembers them stops talking.
Memory Séance Lite is a small app for keeping those stories. You write short memories about a person, a pet, a place, or an abandoned software project. They are saved in a SQLite file on your own computer. Then a small open-weight language model, running locally through Ollama, does two things:
"Ghost in the Machine" can mean something spooky, or something that honors the past. I went for the second, and I tried to let both holidays meet in the middle. Halloween gives the season, the dark background, the candlelight. Día de los Muertos gives the spirit: remembrance as something warm, communal and colorful, where the dead are welcomed rather than feared. The interface is deliberately dark but gentle, with marigold and candle tones and a strip of papel-picado-style color at the top.
The "ghost" is not a spirit I pretend to summon. It is the trace a person or a project leaves behind, and the "machine" is a small model that holds that trace for you without making anything up.
A "séance" app could easily slide into pretending to speak as someone who has died. I didn't want to build that. It is unkind to people who are grieving, and small models are terrible at it. So:
A 60-second walkthrough: viewing memories, lighting the candle for an ofrenda with Gemma 3 1B, asking the séance a grounded question with citations, and triggering an honest refusal when memories cannot answer.
To run it yourself (Python 3.10+ and Ollama required):
git clone https://github.com/AbdulKhadhar/memory-seance-lite && cd memory-seance-lite
./scripts/setup.sh # Windows: .\scripts\setup.ps1 (add --low / -Low for qwen3:0.6b)
python -m app.main # or .venv/bin/python -m app.main
Open http://127.0.0.1:8000. After the one-time download of Ollama and the model, the app works with no internet connection at all. There is also python scripts/load_sample.py, which loads fictional sample memories so you can try it without writing your own.
A private, local-first memorial app: save short memories of a person, pet, place, or abandoned project, and let a small open-weight model on your own computer write an ofrenda and answer questions, using only what you wrote.
The séance is a creative memorial conversation. It is not communication with a deceased person. The narrator is a small language model that only retells the memories you wrote.
Grief, nostalgia and half-forgotten projects are some of the most private things we write down, and they are exactly what you should not upload to a hosted AI service. Memory Séance Lite keeps the whole loop on your computer: SQLite for storage, Ollama for inference, and a model small enough for an old laptop.
Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 4: Ghost in the Machine. The "ghost" here…
A quick tour of the repository:
| Path | What it does |
|---|---|
app/routes.py |
The API, and the refusal logic for the séance |
app/retrieval.py |
Tokenizing the question and ranking memories |
app/database.py |
SQLite schema, FTS5 index, full wipe |
app/llm.py |
A thin Ollama client with typed errors, and the ofrenda JSON validator |
app/prompts.py |
Short, grounded system prompts |
static/ ,templates/ |
Vanilla HTML/CSS/JS, no build step, no node_modules |
tests/ |
27 pytest tests; every LLM call is mocked |
The whole backend has three runtime dependencies: fastapi, uvicorn and httpx.
The whole point was that a person with an old laptop could run this. Gemma 3 1B is the default: small enough to download and run on modest hardware, and good enough to retell a handful of short memories in plain language. For machines that are really struggling, there is a one-line switch (in the Settings tab, in .env, or ./scripts/setup.sh --low) to Qwen3 0.6B. The app sends think: false to Qwen3 so it skips its reasoning trace, which saves time on weak hardware, and it strips any stray <think> blocks anyway.
Because the model is "just a name in a setting", you can also point it at any other model you have installed in Ollama. I did not want the project married to one model.
Most "chat with your notes" tutorials reach for an embedding model and a vector database. That is exactly the weight I was trying to avoid. For a few hundred short personal memories, SQLite's built-in FTS5 full-text search with BM25 ranking is plenty:
Only 3 to 5 memories ever go into a prompt, with num_ctx set to 2048. That keeps RAM and latency low.
A small model will happily hallucinate a heartwarming detail that never happened. In a memorial app, that is not a quirk; it is a harm. So I did not rely on the prompt alone. The séance has three separate gates:
Séance answer with retrieved memories expanded and cited.
The first two gates are plain code, not prompting, so the model never gets the chance to improvise when there is nothing to ground it. The third gate handles the sneaky case: a memory matched a keyword but doesn't actually answer the question, and the model is told to reply with a sentinel instead of guessing.
Honest refusal: When asked a question outside the saved memories, the app refuses to hallucinate.
Here is the full séance request as a sequence:
Small models are unreliable at structured output, so the ofrenda has its own safety net:
Generated candle-lit ofrenda card with remembered items and closing line.
The validator tolerates the usual small-model habits (JSON wrapped in prose or code fences, a string where a list should be) and trims to exactly three remembered items. If it still fails after one retry, the user gets a plain explanation, not a stack trace.
Each failure has a typed error and a human message:
| What went wrong | What the user sees |
|---|---|
| Ollama isn't running | "Cannot reach Ollama at ... Start it with ollama serve " (503) |
| Model not pulled | "Model 'gemma3:1b' is not installed. Run: ollama pull gemma3:1b " (409) |
| Model too slow | A timeout message suggesting the smaller Qwen3 model (504) |
| Unusable output | "The model did not return a usable ofrenda" (502) |
| Empty database | An invitation to add the first memory |
| Bad input | Field-level validation messages (422) |
The UI is hand-written HTML, CSS and JavaScript served by the backend: no framework, no build step, no external fonts. It has a skip link, labelled form fields, visible focus rings, live regions for status messages, and respects prefers-reduced-motion. All user text is inserted with textContent, and the server sends a strict Content-Security-Policy, so a memory can never inject markup. The server binds to 127.0.0.1 so only the local machine can reach it.
There are 27 pytest tests, running in about a second, with a temporary SQLite file and a fake LLM in place of every model call. They cover adding memories, FTS retrieval and its fallback, refusing a séance question when no relevant memory exists, ofrenda parsing and retries, Ollama-down and model-missing errors, settings validation, and full database deletion. GitHub Actions runs lint and tests on Python 3.10 and 3.12.
I deliberately do not run a real model in CI: hosted runners don't resemble the modest hardware this targets, down a model on every run is wasteful, and generated text isn't reproducible. A manual workflow produces a Markdown build report instead, and the real demo is recorded locally.
Because the project is designed for modest personal machines, every layer includes deliberate memory caps, disk protections, and graceful fallbacks:
PRAGMA journal_mode=WAL with synchronous=NORMAL: Concurrent reads with zero lock contention and minimal disk I/O stalls while maintaining atomicity.PRAGMA cache_size=-2000: Hard 2 MB upper bound on page cache to avoid memory leaks during long-running sessions.PRAGMA temp_store=MEMORY: Temp tables and sorters reside purely in RAM to eliminate SSD write wear.VACUUM compaction on data deletion to guarantee reclaimed disk space.keep_alive="5m": Instructs Ollama to cleanly unload weights from RAM/VRAM 5 minutes after the last query, returning resources to the host system.num_ctx=2048, num_predict=400, and num_batch=128 cap memory consumption during prompt evaluations.gemma3:1b and tiny models like qwen3:0.6b (with think=False to skip reasoning traces that bottleneck low-RAM CPUs)..github/copilot-instructions.md establishes repository-level hard rules (local-first, grounded answers with citations, and mocked LLM tests).
This is the part of the project where "open" is not decoration. Take the open pieces away and the app stops being possible, or stops being right.
Memories remain private and local. A memorial is about as personal as text gets: a dead parent's habits, a pet's last winter, a failed project you're a little ashamed of. With a closed API, every one of those sentences would be sent to someone else's server, logged under someone else's retention policy, and governed by someone else's terms. Here there is no server to breach because there is no server. The data is one SQLite file you can open, back up, move or delete.
No paid API, no keys, no meter. There are no API keys, no billing, no rate limits. Nobody has to decide whether a grandmother's tomato story is "worth" a few cents of tokens.
It works with the lights off. After the initial downloads, the app works with no internet. Remembrance shouldn't depend on a Wi-Fi signal.
You can inspect, modify and swap models. The model is a setting. When a better tiny model appears, change one line. If you don't like how the narrator sounds, the prompts are about twenty lines in app/prompts.py, and you can edit them.
It isn't locked into a service that might disappear. This one matters most to me. A family's ofrenda should outlive any startup. If a hosted service pivots, raises prices, or shuts down, everything stored there goes with it. A SQLite file, a model on disk and MIT-licensed code do not have an end-of-life date that someone else controls.
It runs on modest hardware. The people with the most to remember are not always the people with the best computers. A 1B model on a CPU-only laptop is slower than a hosted giant, but it is theirs, and that trade is the whole point.
One honest note on terms: Gemma's weights are open but are released under Google's Gemma Terms of Use rather than an OSI-approved license, while Qwen3 is Apache-2.0. The app itself is MIT, and works with either.
I'm using a fictional example here, because I wouldn't put anyone's real private memories in a demo. Imagine a user, Lucía, who saves a few small memories about her grandmother, Abuela Marisol:
Tomatoes in coffee cans. She grew tomatoes in old coffee cans along the back fence and gave every plant a name. The biggest was called Don Ramón.
Sunday pan dulce. Every Sunday she walked to the corner bakery and came back with conchas wrapped in paper. She always saved the pink one for me.
Her radio. A yellow radio on the kitchen windowsill, tuned to old boleros, while she shelled beans.
Here is the exact ofrenda generated by Gemma 3 1B on my machine from those three memories:
Remembrance: Her radio played familiar boleros, a comforting soundtrack to her quiet life. She always brought me a pink concha, a small token of love and a reminder of her garden. She cherished the tomatoes, nurturing them in those coffee cans, and named the biggest one, Don Ramón.
Things Remembered:
- A yellow radio, conchas, tomatoes in coffee cans
- A loving gesture of a pink concha
- A garden of tomatoes named Don Ramón Closing: May the spirits of those who loved you find peace.
Then Lucía asks, "What did she grow in the garden?" and the narrator answers from her memories, with the "Memories used" panel open beside it, showing the tomato memory and its text.
Then she asks something the memories can't answer: "What was her favorite football team?"
The app says it can't answer from the memories saved, and suggests adding one.
That refusal is the moment I care about most. It is not a limitation I'm apologizing for. It's the difference between a tool that helps you hold on to what you actually remember and one that quietly writes fiction about someone you loved. If Lucía later remembers that Abuela cheered for a local team, she can add that memory, and the next answer will have a source.
Your memories stay on your machine.