Memory Séance Lite: A Private, Local AI Ofrenda for the Stories We Almost Forget
What I Built
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:
- Writes an ofrenda. Pick who or what to remember, and the model writes a short remembrance paragraph, three "things remembered", and a closing line inspired by Día de los Muertos. Only facts from your saved memories are allowed in.
- Runs a memory séance. Ask a question and the app retrieves the most relevant memories, has the model answer from them alone, and shows you exactly which memories it used. If nothing you saved is relevant, it says it can't answer.
How I Read the Theme
"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.
The Line I Would Not Cross
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:
- The narrator always speaks in the third person. It tells you what your memories say; it never says "I" as the person.
- A disclaimer is on screen at all times: the séance is a creative memorial conversation, not communication with a deceased person.
- Every answer lists the memories that produced it. If something isn't in your memories, the app says so instead of inventing it.
Demo
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.
Code
AbdulKhadhar / memory-seance-lite
Memory Séance Lite 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.
Why This Exists
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.
The "Ghost in the Machine" Theme
Built for the DEV Hacktoberfest Open-Source AI Challenge, Week 4: Ghost in the Machine. The "ghost" here… View on GitHub
A Quick Tour of the Repository
app/routes.py- The API, and the refusal logic for the séanceapp/retrieval.py- Tokenizing the question and ranking memoriesapp/database.py- SQLite schema, FTS5 index, full wipeapp/llm.py- A thin Ollama client with typed errors, and the ofrenda JSON validatorapp/prompts.py- Short, grounded system promptsstatic/,templates/- Vanilla HTML/CSS/JS, no build step, no node_modulestests/- 27 pytest tests; every LLM call is mocked
The whole backend has three runtime dependencies: fastapi, uvicorn and httpx.
How I Built It
The Shape of the System
Open models: Gemma 3 1B, with Qwen3 0.6B as a fallback. 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.
Retrieval Without Embeddings
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:
- Title and person/project matches are weighted more heavily than body text.
- A Porter stemmer means "growing" finds "grew"-adjacent forms like "grow" and "grows" (it is keyword-based, so it won't magically bridge synonyms).
- Common stopwords are dropped, so "What did she do?" doesn't match everything.
If a Python build ships without FTS5, a tiny pure-Python scorer takes over automatically. Only 3 to 5 memories ever go into a prompt, with num_ctx set to 2048. That keeps RAM and latency low.
Making a 1B Model Behave: Grounding by Design
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:
The Ofrenda, and What Happens When the Model Fumbles
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.
Failing Gracefully
Each failure has a typed error and a human message:
- 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)
Frontend, Accessibility, and Safety
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.
Tests and CI
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, downloading 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.
Resource & Disk Optimization Practices and Fallbacks
Because the project is designed for modest personal machines, every layer includes deliberate memory caps, disk protections, and graceful fallbacks:
- SQLite Low-Footprint Tuning:
PRAGMA journal_mode=WALwithsynchronous=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. ImmediateVACUUMcompaction on data deletion to guarantee reclaimed disk space. - Model Lifecycle & RAM Management:
keep_alive="5m": Instructs Ollama to cleanly unload weights from RAM/VRAM 5 minutes after the last query, returning resources to the host system. Bounded prompt evaluation:num_ctx=2048,num_predict=400, andnum_batch=128cap memory consumption during prompt evaluations. - Layered Fallbacks: FTS5 to Pure-Python: BM25 ranking via SQLite FTS5 when present; if the local SQLite build lacks FTS5, it automatically falls back to an in-memory token-matching and stemming algorithm. Tiny Model Support: Seamless compatibility with
gemma3:1band tiny models likeqwen3:0.6b(withthink=Falseto skip reasoning traces that bottleneck low-RAM CPUs). Robust Output Fallback: Multi-pass JSON parser that handles markdown backticks, conversational preambles, and retry attempts before surfacing actionable feedback.
Building It: The Agent and Tooling Side
GitHub Actions and Copilot setup: CI runs lint and tests on multiple Python versions, a manual workflow publishes a build report, Dependabot keeps dependencies and actions fresh, and .github/copilot-instructions.md establishes repository-level hard rules (local-first, grounded answers with citations, and mocked LLM tests).
Why Does Open Innovation Matter?
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.
The Spirit of It
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 al
Comments
No comments yet. Start the discussion.