My submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine.
A piano keeps playing after the pianist lifts her hands. There are footsteps upstairs. Somebody has left an extra cup of tea beside the stove.
I built a small house full of these stories. Then I gave it a search box, a wandering ghost, and five missing keepsakes. 🕯️
Ask “What kept playing after the musician stopped?” and the music room lights up. The result is an original passage about a player mechanism and a brass roll beneath the bench. There is no generated explanation pretending to know more than the archive does.
Ghostlight Archive is a small playable mystery powered by browser-local semantic search. Wisp, a lantern-carrying ghost, has misplaced the house’s memories. Recover five keepsakes by finding the passages that explain its strange noises and little rituals.
The house has six rooms and twelve original fictional passages. Search returns three passages and lights their rooms. Read a source, then Offer this clue to Wisp. The right evidence earns a keepsake and reveals the next mystery. A wrong offer leaves your progress intact.
There is also a ghost to greet, a hint button, room encounters, a friendship badge, an exploration badge, and an ending. Wisp is scripted game fiction; it does not impersonate a real person or generate answers.
The quieter half of the archive is about remembrance: an empty chair, a photograph returned to its place, family stories read aloud. I wanted the theme to have space for those traces too, without a chatbot pretending to be a person who died.
Actual browser screenshot. The room glow follows model retrieval results; it is not a measure of truth.
Open Ghostlight Archive · Hugging Face Space
Choose Enter the house, then Search this mystery. You can rephrase the question in the search box; the model still has to retrieve the actual passage. Offer the source that explains what happened. All five keepsakes lead to the ending, and Restart hunt starts a clean run.
Tap a room to explore its encounter and read its passages directly. These cards say “Room exploration” and show no cosine score. That route does not pretend to be model inference. Visit every room for the House historian badge; greet Wisp three times for its friendship badge. There is no timer or penalty for reading slowly.
Enable sound adds a quiet ambient drone, short room motifs, a ghost greeting and recovery chimes. Everything is synthesized with Web Audio after that explicit tap. Sound starts off; volume and mute are available, and leaving the tab mutes it. ** motion** stops Wisp’s wandering, and the interface respects reduced-motion preferences.
Open “Compare keywords” to see what a small literal word-overlap search finds on the same question. Pin a passage, then export the evidence trail. The Markdown preview preserves the exact source text and the query that led to each pin.
You can also paste your own short archive. Separate passages with blank lines. The limit is 32 passages of 500 characters each, with questions capped at 240 characters. a custom archive exits the authored hunt. Re clears custom text, game progress and the pinned trail.
First load needs a network connection to fetch the runtime and model files. Once the model is ready, inference happens in the browser’s web worker. No question or archive text is sent to a model server. Hosting, font and model-CDN requests still occur; I am not claiming that the page makes no network requests or always works offline.
The app is plain HTML, CSS and JavaScript modules under Apache-2.0. Serve the repository directory over HTTP:
python3 -m http.server 8796
Then open http://localhost:8796. The static app needs no API key or server-side inference process.
The open model is Xenova/all-MiniLM-L6-v2, pinned to revision 751bff37182d3f1213fa05d7196b954e230abad9. Transformers.js 4.3.2 loads the q8 ONNX checkpoint through single-threaded WASM. [1][2]
Each passage becomes a mean-pooled, normalized 384-dimensional vector. The question goes through the same encoder. A dot product ranks the passage vectors against the question vector; because both are normalized, that dot product is cosine similarity.
The model does the language matching. The rest is deliberately ordinary code: rank the passages, retain their IDs, render their exact text, and highlight the rooms those IDs belong to. Embeddings for the current archive stay in worker memory so a new question can reuse them. Changing the archive invalidates that cache.
The game checks an offered passage against the current mystery’s authored source ID. That is ordinary game logic. A cosine score never decides whether a keepsake is correct. The model helps the player find evidence; the player still has to read it.
The room map is a reading aid. It shows where a thought travels through the archive; it does not create an additional model judgment.
One example makes the model useful without requiring a grand benchmark. For the default question about the musician, the visible word-overlap baseline finds no matches after its stop-word filter. The model retrieves the recital passage first. The wording “pianist,” “melody” and “player mechanism” connects to the question without sharing its exact search words.
That is a functional illustration on authored examples, not an accuracy estimate. I ran eight fixed synthetic queries through the actual model on CPU, including an unrelated rocket-engine question, and separately checked the browser WASM route.
The rocket question still retrieves a passage. Nearest-neighbor search always has a nearest neighbor. It does not know that a useful answer is absent.
The common mistake is turning similarity into confidence. Ghostlight displays cosine scores and calls the passages leads. It does not convert a score into a percentage chance that the archive has answered the question.
The project also has checks for input bounds, malformed embeddings, ranking, the Markdown trail, wrong clue offers, duplicate rewards, the ending and clean replay. I completed all five mysteries through the actual deployed browser model and separately checked room exploration, ghost badges, mute and the mobile layout. Its GitHub Actions workflow runs the functional checks, executes the real model fixture, and packages an explicit static-file allowlist.
For this particular app, control over the retrieval path matters more than a long generated answer.
The model weights are downloadable. The revision and runtime are named. The text processing, ranking and export code can be inspected. I can change the passages, replace the model, or alter the interface without rewriting around a proprietary inference API.
Keeping inference in the browser also means custom text can stay in the visitor’s page memory. There is no backend transcript store to build or operate. That is a concrete advantage of this implementation, rather than a claim that a closed model could never do retrieval well.
There are trade-offs. The first download includes roughly 23 MB of weights plus runtime files. A small English sentence encoder can miss nuanced or multilingual relationships. Long passages are truncated to the configured token limit. Similarity cannot establish a causal explanation, resolve contradictory accounts, or replace reading the source.
That is why the original passage gets the largest part of each result card. The interesting moment is the reader noticing what the clue actually says.
Entering Best Use of GitHub Copilot, through the challenge’s explicitly eligible GitHub Actions route. The GitHub Actions workflow runs the game checks, executes the real model fixture, and builds the public static package. The completed run is inspectable here. This entry uses the Actions route; Copilot was not used to write the implementation.
[1] Pinned MiniLM model card, Apache-2.0.
[2] Hugging Face Transformers.js, Apache-2.0.
The interface, map, inline SVG ghost, synthesized sound and fictional passages are original. Fonts are DM Sans and Newsreader, distributed under the SIL Open Font License. The screenshot records the running app. No real family archive, personal conversation or user study is presented.
AI was used to draft the article.