This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
What I Built
Séance is a small web app where you hold a conversation with the ghosts of dead programming languages: COBOL, LISP, BASIC, FORTRAN and ALGOL.
Pick a spirit and ask it anything. COBOL answers in stiff, all-caps mainframe bureaucracy. LISP is smug and speaks in parentheses. BASIC is a nostalgic line-numbered whisper from a blinking cursor. Each voice is an open-weight model wearing a persona.
There is also Haunt my code: paste a snippet and the spirit critiques it in its own voice.
I read the theme literally and a little sideways. "Ghost in the Machine" fits these languages well: they are officially "dead", yet COBOL still moves money, FORTRAN still crunches numbers, and every language since has borrowed from LISP and ALGOL. They haunt the machines we use every day. For Día de los Muertos there is an Ofrenda mode that swaps the bats and lightning for marigold petals and a papel picado banner. It is a small way to remember the languages that came before us.
The idea started from the challenge's own suggestion of an offline séance chatbot that channels a long-dead language. I wanted to see how far I could push that while keeping the stack tiny.
Code
https://github.com/sarasinha1207/Ghost-in-the-Machine
Demo
Live: https://seance-4uj8.onrender.com
How I Built It
I wanted a very light stack, so there is no database, no frontend framework and no build step.
-
Backend: Python and Flask, with
requestsas the only HTTP dependency, served by gunicorn on Render. -
Frontend: plain HTML, CSS and JavaScript. Conversations live in the browser's
localStorage, so the server stores nothing. -
AI: any open-weight model behind an OpenAI-compatible
/chat/completionsendpoint. The defaults point at Ollama runningllama3.2:3b, but the model, endpoint and key are all environment variables.
The whole model integration is one small function. It speaks the standard chat-completions format, which is what Ollama, llama.cpp's server and many hosted providers expose:
def chat(persona, messages):
cfg = _config() # LLM_BASE_URL, LLM_MODEL, LLM_API_KEY, LLM_TIMEOUT
payload = {
"model": cfg["model"],
"messages": [{"role": "system", "content": f"{persona}\n\n{SYSTEM_RULES}"}, *messages],
"max_tokens": 200,
"temperature": 0.9,
}
try:
resp = requests.post(f"{cfg['base_url']}/chat/completions",
json=payload, headers=headers, timeout=cfg["timeout"])
resp.raise_for_status()
text = resp.json()["choices"][0]["message"]["content"].strip()
except (requests.RequestException, KeyError, IndexError, ValueError) as exc:
raise LLMUnavailable(str(exc)) from exc
return text
Each spirit is just a persona prompt plus a few scripted lines. Here is the start of COBOL's:
"persona": (
"You are the ghost of COBOL (1959). You speak in stiff, verbose, "
"bureaucratic ALL-CAPS fragments, like a mainframe report. You are "
"proud that banks still depend on you, and quietly sad that nobody "
"wants to learn you. ..."
),
A few design decisions I'm happy with:
-
Echo mode. If the model can't be reached, the route catches
LLMUnavailableand returns one of the spirit's scripted whispers, taggedmode: "echo". The UI tells the user plainly that they are hearing an echo, not the model. The demo never dies with a stack trace. -
A live status badge. The page shows which open-weight model is answering, or that it is offline, by checking the endpoint's
/modelsroute (cached for 20 seconds). - Small guardrails. Messages are capped at 400 characters, history at 8 turns, snippets at 1200 characters, and each IP gets 20 requests a minute. The system prompt keeps the spirits in character and short.
- Tests. 14 pytest tests cover validation, trimming, rate limiting, the haunt endpoint, and the model-to-echo fallback, with the model mocked.
The atmosphere is pure CSS and a little vanilla JS: a drifting moon and fog, a candlelight glow that follows the cursor, bats at night, a wandering ghost you can click, and a glowing animated border. All of it switches off for visitors who prefer reduced motion.
Why Does Open Innovation Matter?
Because of how this project is built, "open" is a practical feature here, not a slogan.
- It can work with the lights off. Point it at a local model and nothing leaves your machine. The spirits keep talking with no internet.
-
I can swap the model with one variable. Llama, Qwen, Gemma or Mistral: change
LLM_MODELand, if needed,LLM_BASE_URL. The app never cared which model it was talking to, so it can't be stuck with one. - It isn't locked inside a service that might vanish. The app only depends on the open chat-completions format. If a hosted provider changes its pricing or shuts down, the fix is a config change.
- I control how the agent behaves. The personas, guardrails, length limits and temperature are mine. I can tune each ghost's voice without negotiating with anyone.
- Privacy is a choice. Run locally and your questions and code snippets stay on your own machine. On my deployed demo they go to the hosted model provider I configured. The page footer says exactly that: your words go only to the model server that is configured.
It also costs nothing to run locally, which matters for a hackathon project and for anyone who wants to fork it.
The Spirit of It
Languages are a kind of memory. If you ever learned to code with line numbers and a blinking cursor, then a ghost answering ?SYNTAX ERROR IN YOUR LIFE CHOICES. READY. lands somewhere between funny and tender. I wanted the app to feel like lighting a candle for those languages: they shaped everything that came after, and a lot of the world still quietly runs on them.
Try the COBOL spirit and ask why banks still need it. Then hand BASIC a 10 PRINT / 20 GOTO 10 loop and see what it says.
Prize Categories
- Best Use of Render
- Best Use of Gemma

Top comments (0)