DEV Community

Cover image for Silent Air: A Radio Mystery Where Gemma Has to Know What It Does Not Know
suraj srivastav
suraj srivastav

Posted on AI-assisted

Silent Air: A Radio Mystery Where Gemma Has to Know What It Does Not Know

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.

A voice keeps repeating from an empty radio station. The interesting question is not whether an AI can invent a ghost. It is whether it can resist inventing the rest of the story.

What I Built

Silent Air is a small, playable radio mystery. You turn a virtual receiver across the FM band, recover five lost transmissions, ask an open-weight Gemma model about the evidence, and reconstruct one night in a fictional town called Bellwether.

The case is The Last Broadcast. A storm is approaching. A volunteer operator keeps the emergency frequency open. Later, the room is empty—but the voice is still there.

I wanted the Halloween theme to be more than a dark background. Here, the “ghost” is information that outlives its speaker. The player has to distinguish what was recorded from what they merely want to believe.

There are three parts to the experience:

  1. The receiver: tune manually or use Find a signal. Recover a transmission, read its transcript, and optionally hear it using a locally installed device voice.
  2. The archive interpreter: ask Gemma a real question about the fragments you have recovered. Inspect its cited sources instead of treating a fluent response as evidence.
  3. The evidence board: arrange the five fragments, explain the repeating broadcast, and choose what to do with a fact that the archive never recorded.

The interface uses a restrained radio aesthetic, reactive waveforms, a moving tuning needle, transmission reveals, and a short first-time guide. Audio starts only when requested, every recording has a transcript, and motion respects the reduced-motion setting.

All characters, places, and archive records are original fiction. This is not a real historical archive, a deceased-person impersonation, or a claim of contact with a spirit.

Demo

Play Silent Air live on Render →

This is the running application, not a recorded-response demo. You can type your own question and receive a fresh Gemma answer. No account or API key is required from the visitor. The free instance may need time to wake up after being idle; the interface reports the model connection separately.

The screenshots below are from the running app, not concept mockups.

The live Silent Air application on Render, tuned to a recovered broadcast

A quick route for a first-time listener:

  • Recover the first signal at 88.4 MHz.
  • Use Find a signal to locate the remaining broadcasts.
  • Open Ask the archive and ask, “Why did the voice keep repeating?”
  • Ask, “What happened to Mara after she left?” Then inspect the evidence behind the answer.
  • Finish the sequence on the Evidence board.

A real hosted Gemma 4 answer in the Render application, with inspectable source links and a privacy notice

The image above shows a real hosted Gemma 4 response captured on the Render site. It is not an answer inserted into the app. New questions trigger new inference requests; if the model is unavailable, the app says so instead of returning a canned answer.

The evidence board, with five recovered transmissions and the timeline puzzle

Local setup and model instructions are in the repository. That version keeps inference on the player's own machine after the dependencies, model, and runtime have been downloaded.

Code

Source code, setup instructions, tests, and license

The project and repository were created specifically for this challenge on October 31, 2026. The application code is MIT-licensed. Third-party models and fonts keep their own licenses.

How I Built It

The frontend is React + Vite. The backend uses Node's built-in HTTP server, with no backend framework dependency. The radio texture comes from procedural Web Audio noise; the waveforms and illustrations are SVG/CSS. Fonts are bundled locally so a Google Fonts request is not needed during local play.

The narrative is deliberately authored, not generated. That gives the model a stable body of evidence and gives me concrete failure cases to test.

The model sees the recovered fragments, not the ending

A question request contains the question and evidence IDs. The backend resolves those IDs against the fictional case, extracts only the selected transcript fields, and sends those to the model. It does not include the case's closing paragraph or answer key in the prompt.

The model is instructed to stay within those transcripts, cite source IDs, and acknowledge missing information. Returned references open the actual source. Short quotations are chosen by lexical matching—not by an independent fact-checking model.

That distinction matters: a valid source ID does not prove that a sentence is true. This is an inspectable grounded-generation workflow, not a hallucination-proof system.

One application, two inference paths

  • Local: Gemma 3n E2B Q4_K_M through LM Studio's API. LM Studio is the host; it is not an open-source product. The open-weight Gemma model is the part that makes self-hosted inference possible.
  • Hosted: the live Render service calls gemma-4-26b-a4b-it through Google's API. The API key stays in Render's secret environment configuration, never in browser code. The interface identifies hosted mode and warns that questions and selected fictional transcripts go to the provider.

Google's pricing documentation currently lists Gemma 4 input and output as free of charge. I created a separate free-tier project without attaching billing. Quotas still apply, and free-tier data may be used to improve Google's products, so the public interface should not receive private information.

What I actually tested

The automated suite currently has 18 passing tests. It covers request bounds, exclusion of hidden source text from prompts, reference handling, origin checks, static-path containment, provider failures, timeouts, concurrency limits, and hosted transport behavior.

I also completed the full case in the browser: all five transmissions, the ordered timeline, the recording explanation, and the unknown-answer choice. That smoke run reported no JavaScript errors. Checks at 375, 768, and 1280 pixels reported no horizontal page overflow.

For local inference, one 356-input-token / 101-output-token run took 42,223 milliseconds on the development laptop. A later browser answer took about 39.5 seconds. These are individual observations, not a benchmark.

A separate Google-hosted Gemma connectivity test returned in about 4.1 seconds. It was also a useful failure: with an overly compressed evidence prompt, the model declined to explain why the voice repeated, even though the prompt mentioned a tape loop. It correctly left the person's later fate unknown, but that did not make the whole answer good.

I repeated the test with fuller evidence. In 3.76 seconds, Gemma 4 responded:

The voice repeated because it was a recording made by Mara that was set to play on the backup battery [E4]. The voice heard by others was not a live operator [E5]. It is unknown what happened to Mara after she left, as there is no later entry for her in the archive [E5].

That run used 240 input tokens and 64 output tokens. It is one inspected result, not proof that the model always gets this right.

Finally, I tested the actual deployed app in a browser with all five recovered fragments. The Render endpoint returned HTTP 200 in 1,927 ms, using 620 input tokens and 67 output tokens. Its answer said:

The voice heard throughout the night was a recording of Mara's message, which was set to repeat on the backup battery [E4, E5]. After Mara left the relay to check the road on foot, her subsequent whereabouts are unknown [E3, E5]. The archive contains no later entries regarding her [E5].

The three source links opened the recovered evidence. The deployed browser run reported no JavaScript errors, and layout checks again found no horizontal overflow at 375, 768 and 1280 px. The response time is one warm request, not an uptime or latency guarantee.

What broke, and what changed

  • “Available” did not mean “loaded.” LM Studio's standard model listing included downloaded models. I changed local readiness checking to its endpoint that explicitly reports loaded state.
  • Grouped citations were different from single citations. A hosted answer used [E4, E5]. The reference parser now accepts grouped IDs instead of silently losing those source links.
  • An empty input is not a reason to fabricate a response. With no recovered evidence, the app asks the player to recover a signal. Provider errors are errors, not fallback fiction.

Why Does Open Innovation Matter?

A closed API could generate a ghost story. That is not the point of this project.

The open-weight approach gives this particular experience three useful properties:

The story can outlive a service. Someone can clone the case, download a supported model, and keep playing locally. The hosted version is convenient, not the only way the archive can exist.

The evidence boundary is visible. The transcripts, system instruction, selection logic, and returned references can all be inspected. A reader can change the model or prompt and test whether it handles unknowns better.

Private play does not require a cloud conversation. The default local setup has no cloud AI API key. That matters for the broader idea of memory tools—even though this release intentionally uses only fictional material.

Open-weight does not mean unrestricted. Gemma has model-specific terms, and those terms are separate from the repository's MIT license. I link the model documentation and preserve font licenses in the repo.

The Spirit of It

The emotional center is a small line in one broadcast:

“Leave your porch light on if you can.”

It is not a threat, a supernatural revelation, or a dramatic generated confession. It is someone trying to help people get home.

The final missing fact stays missing. To me, that is a more interesting use of AI in a story about the past than giving it permission to speak confidently for a person whose record has ended.

Prize Categories

  • Best Use of Gemma: the archive interpreter runs actual open-weight Gemma inference, with a tested local path and a server-side hosted integration. The model is part of the player's investigation, not merely a tool used to write code.
  • Best Use of Render: the public application is a Render Node web service. Render builds the React frontend, serves it alongside the API, and holds the model key in a secret environment variable. The backend applies input limits, request throttling and exact-origin checks before calling Gemma. /healthz reports service health independently of /api/health, which checks the model. I verified that Render deployed GitHub commit f6f6a49, then tested a real model request through the live site. The model runs at Google, not on Render's free CPU; Render is the secure web and API hosting layer.

The entry is also eligible for overall judging. These are category entries, not a claim that one participant can receive multiple prizes.


Limitations and credits

The public game bundle contains the authored case and its solution; this is not a competitive anti-cheat system. Prompt instructions can fail. Source links do not guarantee entailment. CPU inference can be slow, hosted quotas can be exhausted, and Render's free service can cold-start. Browser progress is convenience storage; export gives the player a copy they can keep. This is a prototype, not a full accessibility audit or a secure prompt-injection defense.

Development and writing were AI-assisted with Claude Code. React, Vite, Lucide, Google Gemma, and the font projects are credited in the README. No real-person voice cloning or copyrighted song recording is used.

Top comments (0)