DEV Community

Avishi Shreya
Avishi Shreya

Posted on

lantern: a candle for the notes you never finished

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

Every notes folder eventually turns into a graveyard. Half-finished side projects, journal entries written at 2am, a list of things you'd do if you had a free Tuesday that has never once been acted on.

Most of us can't reread that folder, and can't delete it either.

So I built lantern. You point it at a folder of old .md and .txt files. An open-weight model reads them and names the ghosts still living in them โ€” the motifs you kept circling, the ideas you abandoned but never quite buried. For each one it writes an epitaph and quotes the line of your own writing it came from. Then it renders a single HTML altar with a candle burning for each ghost.

lantern's output: an ofrenda with five lit candles, each holding a name, an epitaph, and the original line it came from

That screenshot is real output, unedited โ€” the altar line, the five ghost names, everything. The notes are the sample archive shipped in the repo.

On the theme: Dรญa de los Muertos is not a mourning ritual. It's a hosting ritual. You build the ofrenda, you put out the food, you say the names out loud, and then the dead are guests rather than absences. I wanted that for writing. An abandoned draft doesn't need a backup strategy. It needs to be looked at once, honestly, named, and then left alone.

The whole project is one Python file, no dependencies, 227 lines.

Demo

git clone https://github.com/shreya-oeo/lantern
cd lantern

ollama serve                 # any model you already have
python lantern.py notes/     # a folder, or a single file
Enter fullscreen mode Exit fullscreen mode

It prints what it found and writes ofrenda.html. Open it in a browser. No server, no build step, no account.

reading 456 words from notes ...

A collection of attempts to capture things that cannot be held.

  the free Tuesday: A four year list of possibilities that never found a calendar date.
  the grease stain: A thumbprint of fat obscuring the definition of enough.
  the wrong map: A geography of memory where the data model failed the feeling.
  the fridge film: Two rolls of silver and shadow, waiting for a light that never came.
  the taste of house: A soup that mimics a building rather than a person.
Enter fullscreen mode Exit fullscreen mode

You can also see the generated altar without running anything: docs/sample-ofrenda.html.

Code

Repository: https://github.com/shreya-oeo/lantern

flowchart LR
    A[notes/*.md] --> B[gather<br/>flatten, cap 6000 chars]
    B --> C[one /api/generate<br/>format json, temp 0.9]
    C --> D[unfence<br/>JSON repair]
    D --> E[field coercion<br/>+ quote strip]
    E --> F[render<br/>single HTML file]
    F --> G[ofrenda.html]

Three functions do all the work: gather, invoke, render. Everything else is one prompt string and one HTML template.

How I Built It

lantern speaks to Ollama over plain urllib โ€” one POST to /api/generate, format: "json", temperature 0.9. I used Gemma through Ollama. Any open-weight model you have pulled works, and --model swaps it.

Three things I'd flag for anyone pointing a model at personal writing:

1. Ask for an archivist, not a sage. The obvious prompt โ€” what is this archive really about? โ€” invites greeting cards: a journey of growth and self-discovery. The prompt that actually ships asks a narrow, almost clerical question instead: which sentence does this person keep returning to? Then it demands the literal quote. A model that must cite has much less room to drift into horoscopes. That single constraint did more for output quality than temperature or model size.

2. Models fence their JSON even when you set format: json. Mine came back as json โ€ฆ on the first run, and the program exited. Now unfence() strips the fence and slices from the first { to the last } before parsing. Nine lines of paranoia around a structured-output mode that is supposed to be safe.

3. Escape everything. Names, epitaphs and quotes are model output going straight into HTML. Every field is html.escaped at render time. It's a decorative page, but it's a decorative page built from text I didn't write, and escaping is cheap.

The design constraint I'm happiest with is that the quote is the receipt. Every epitaph sits above a phrase copied near-verbatim from the archive. If the model invents a memory, you can see it immediately, because there's no line under it that you actually wrote. It converts hallucination from a hidden failure into a visible one โ€” which for a project about memory felt non-negotiable.

On the "local" claim

I ran this with gemma4:cloud, which Ollama routes off-machine. I want to be straight about that rather than wave at the word "local."

So the footer of every altar states where inference happened. With a :cloud tag it reads this model is cloud-routed via Ollama; with a pulled local tag it reads local inference, nothing left this machine. lantern makes exactly one network call โ€” to OLLAMA_HOST โ€” and nothing else. Same code, same output, different honesty in the footer depending on the tag.

Why Does Open Innovation Matter?

A tool for reading your most private writing is the worst possible candidate for a closed API. Not because the vendor is untrustworthy, but because the entire premise โ€” look at this once, name it, and then leave it alone โ€” depends on the words never having left the room.

Concretely, open innovation bought three things:

  • The data can stay put. A pulled model means the archive never crosses the network boundary. That's not a policy promise, it's an absence of code โ€” there is no upload path to audit because there's no upload.
  • The swap is one flag. The model is a poor fit for some archives and a good fit for others. --model changes it with no key, no quota, no pricing page โ€” try three tags in an afternoon for free.
  • It will still run later. The output is a static HTML file with inline CSS and no CDN, no fonts, no analytics, no external requests. In ten years it opens. That matters for a tool whose whole job is honoring the past โ€” a project that archives your memories into a format that expires is a joke with a bad punchline.

The open part isn't decoration on this project. It is the feature: the reason you'd trust a machine with your old notebooks is that you can read every line of what it does with them. There are 227 of them.

My Agent Session

Built with an AI agent in one sitting. Session transcript: (link to follow)

The Spirit of It

The grease stain.

One of the sample notes is about a grandmother's recipe card, and the line in it is that there's a thumb-shaped grease stain over the word enough. The model named that ghost "the grease stain" and gave it the epitaph a thumbprint of fat obscuring the definition of enough.

That's not a clever line. That's the sentence the note was too close to write. The stain sat in the archive as a detail about a card; the altar read it as a detail about inheritance, and it took a machine with no memory of that grandmother to say so in eleven words.

That's the ghost in the machine for me โ€” not a haunting, but the moment an archive reads you back.

Prize Categories

  • Best Use of Gemma โ€” Gemma is the whole intelligence layer: it identifies the recurring motifs, writes the epitaphs, and pulls the receipts from the source text.

Code: https://github.com/shreya-oeo/lantern ยท MIT ยท one file, no dependencies

Top comments (0)