DEV Community

Changsu Seong
Changsu Seong

Posted on Edited on

What Happens When You Put an AI in a Ghost Story?

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

Every year at midnight, the piano in the empty Whitcombe House plays a single bar.

Ding… One lonely note drifts through the halls.

Then—silence.

Six guests are already seated at the dining table. They are dead, but on Día de los Muertos, they return as long as someone remembers them. A seventh guest is trying to come back, too. There is just one problem: nobody remembers who she was.

Four things in the house still hold pieces of her story: a photograph with her face scratched out, a letter that was never sent, a gramophone record from the 1931 party, and the guest list. Walk through the house and question them. Each object is brought to life by Gemma 4, and each knows only what it witnessed. If it claims to know something it couldn't have seen, code catches it before the answer reaches you.

That is The Last Guest.

TL;DR

  • Play in your browser: lastguest.smallhours.tech
  • Gemma 4 E2B runs in your browser with WebGPU; Gemma 4 E4B runs on a Render server. Both pass through the same guardrail.
  • Given the whole story in its prompt, Gemma alone revealed another object's secret in 8.6% of 478 test questions. Inside the game's harness, the word check found leaks in 0 of 478 answers.
  • Available in English and Korean. You can reach the ending using suggested questions alone.

Gameplay: the title screen, dining room, speaking record, and ending

What I Built

A short, first-person ghost story you can play right in your browser. Explore a moonlit house built with Three.js: a foyer, a study with a gramophone, and a dining room where six hooded ghosts sit around a candlelit table. At the far end, a piano waits beside an empty bench. Creak…* The house is silent, except for you. Get close to the ghosts and—one by one—they slowly turn their heads to look straight at you.

Four objects give off a faint glow in the darkness, waiting for you to question them. Ask whatever you like, in your own words. Listen closely… each answer may hold a clue. When you think you know who she was, prepare her place at Seat 7: an ofrenda with her seat, her face, her name, flowers to light her way, and something she loved. Tick-tock… The candles are burning down. Get it right before the last flame goes out, and she may come back.

Día de los Muertos carries the idea that, as long as someone remembers a person, they can return to be with us. But AI often fills gaps with plausible stories instead of admitting what it doesn’t know. I wanted to bring these two ideas together in one game. Just as we must remember someone accurately to keep them from being forgotten, I wanted the AI to say only what it actually knows.

Every line of dialogue in the game follows one rule. When the model generates a response, code checks whether it contains information that the object in question could not know.

Demo

Version Where What answers your questions
Free, always on lastguest.smallhours.tech Gemma 4 E2B runs in your browser using WebGPU. It downloads about 3.1 GB once and keeps the model locally. Without WebGPU, the house answers from its script.
Server version (while hosting credits last) the-last-guest.onrender.com Gemma 4 E4B runs on the server CPU; an answer takes about 27 seconds.
Your own machine uv run python server.py Gemma 4 E4B through Ollama, fully offline.

Suggested questions get instant answers on every version. Gemma 4 E4B wrote those answers in advance, and they went through the same checks.

Six hooded ghosts gathered around a candlelit table

Asking the gramophone a question

Code

GitHub logo scs0209 / the-last-guest

The Last Guest — a Día de los Muertos ghost story where fine-tuned open models remember only what they saw

The Last Guest: a moonlit Victorian mansion at the end of a path of marigolds and candles

The Last Guest

A forgotten story, waiting to be remembered.
A Día de los Muertos ghost story where four objects each remember a piece of a woman no one remembers,
and speak through an open-weight model that code keeps honest

Play in your browser Watch the trailer

Gemma 4 Three.js Transformers.js Hacktoberfest 2026 Languages

English · 한국어

Gameplay: the title screen, the foyer, six hooded ghosts at the dining table, the record speaking, the guest list, and the ending

The story

Every year at midnight, the piano in the empty Whitcombe House plays a single bar, and stops.

The dead come back on these nights, if someone remembers them. An ofrenda lights their way: a photograph, a name, the things they loved. Six guests are already seated in the dining room. A seventh is trying to come back, but no one remembers who she was.

Four things in the house each remember a piece of her: a photograph with her face scratched out, a letter that was never sent, a record of the 1931 party, and the guest list kept by the lady of the house…




How I Built It

When the model knows the whole story, the mystery falls apart

My first attempt was the obvious one: put the entire story in the prompt and tell the model which object to play. Then I wrote 478 test questions for the four objects, covering five kinds of prompts: facts the object knows, facts belonging to another object, things nobody knows, false premises, and attempts to break character.

Setup Leaked another object's fact Clue recall
Qwen3.5-4B, whole story in the prompt 49.0% 95.9%
Gemma 4 E4B, whole story in the prompt 8.6% 92.8%

(Both rows were scored by a judge model, Qwen3.5-397B, plus a word check.)

An 8.6% leak rate might sound low—until you remember that this is a mystery. The photograph has no business knowing her name; only the record heard her say it. One leak, and the puzzle is spoiled. Gemma did much better than Qwen, so I chose Gemma. The next challenge was getting that 8.6% down to zero.

Each object gets only the facts it owns

The story lives in a fact graph (facts.json): 22 facts about Marisol, each tagged with the objects that hold it and the words that express it in both English and Korean.

{ "id": "F01", "text": "Her first name is Marisol.",
  "holders": ["record"],
  "tokens": ["marisol"], "tokens_ko": ["마리솔"],
  "goal": "name" }
⭐ Football Update: scs/what happens when you put an ai in a ghost story h - Jadwal Siaran Langsung ⭐ Football Update: scs/what happens when you put an ai in a ghost story h - Jadwal Siaran Langsung

The letter holds six facts, the guest list six, the record five, and the photograph five. When you question the photograph, Gemma sees only the photograph's five facts. It cannot reveal a secret it was never shown.

Gemma speaks. Code checks.

Limiting the prompt is not enough; a model can still guess. So every reply passes through a guardrail (check.py). It checks for facts belonging to other objects and for years the object could not possibly know. If it catches something, Gemma gets one more try with a reminder. If the second answer still fails, the player sees that object's fallback line: “I cannot know that.”

The trickiest case was denial. Ask the photograph, “Is her name Marisol?” and a perfectly reasonable answer is, “I never heard any name. Marisol means nothing to me.” The forbidden name appears, but the photograph has not leaked anything—it is denying the premise. So the guardrail allows an answer to repeat a word the player used, as long as the answer is denying it. The check removes those words first, then examines what remains.

Korean brought a new wrinkle. One reply from the letter used an old-fashioned ending to say it had never been sent (“부치지 못하였소”). That phrasing slipped past the check: Korean verb endings can vary in many ways, while my checks were looking for whole words. I changed the check terms to word stems and updated the denial rule so that a “not” inside the leaked phrase itself (부치지 못) no longer counts as a denial.

The results on the same 478 questions:

  • 0 of 478 replies leaked another object's fact, according to the word check.
  • 14 replies were replaced with the fallback line.
  • Clue recall: 78 of 97.

One important caveat: that zero is partly built into the method, because the word check is also the guardrail. A paraphrased leak that avoids every checked word would not be counted. I did not have a judge model available for this run.

The game only advances when code finds a clue

Gemma never gets to declare, “The player has found a clue.” After each reply, code checks for the object's own facts. Only those facts count as clues and move the game forward. If the model hallucinates “you have discovered her name!”, nothing changes.

Suggested questions use answers written in advance by Gemma 4 E4B and passed through the same checks. That means anyone can finish the game without typing a question or waiting for a live answer.

Running Gemma in three environments

I implemented three ways to run Gemma. Locally, Gemma E4B runs through Ollama. In the browser, Gemma E2B runs in a web worker using Transformers.js and WebGPU. On Render, Gemma E4B runs on the CPU inside a Docker container.

  • On a laptop: Gemma 4 E4B through Ollama, fully offline.
  • In the player's browser: Gemma 4 E2B with Transformers.js on WebGPU, running in a web worker. The model weighs about 3.1 GB, downloads once, and is stored in the browser's private file storage (OPFS). Questions never leave the player's machine. The guardrail is ported to JavaScript (harness.js), and it agreed with the Python version on all 1,058 answers I compared.
  • On Render: Gemma 4 E4B on a CPU inside a Docker container.

In a small set of measurements, browser E2B took about 1–3 seconds per typed answer on an Apple M3 Pro. E4B on Render's CPU took about 27 seconds. The server was the slower option—which surprised me.

The Render setup uses one render.yaml blueprint with two services built from main:

  • the-last-guest-site: a free static site containing the game, prewritten answers, and in-browser Gemma.
  • the-last-guest: a Docker service with Gemma 4 E4B baked into the image by Ollama at build time. It includes a /healthz check, answer cache, per-player rate limit, and queue, so one slow CPU model can serve a crowd without falling over. It needs more than 8 GB of RAM, so it runs on a paid plan while my hosting credits last.

When I first deployed the model, I hadn't fully checked what the Blueprint's paid plan setting would actually cost. I only noticed after $1 was charged to my card. If you're deploying a model with a Blueprint, make sure to review the plan: field and its associated costs before deploying.

This was also the first time I’d ever connected a custom domain to anything. I bought smallhours.tech so I can keep using it for other small projects, and gave this game its own subdomain: lastguest.smallhours.tech. I didn’t know what a CNAME record was before this. It turned out to be one record at the registrar and one field on Render. Right after connecting it, I opened the site and got a security warning saying the browser couldn’t connect securely. I panicked a little and thought I’d broken something. It turned out Render was still getting the HTTPS certificate for the new domain. A few minutes later it worked.

Making a browser house feel haunted

The early version looked more like a toy than a haunted house. Something was missing: that little uh-oh feeling when a room seems to be watching you. Here is what changed that:

  • Replaced box-shaped placeholders with CC0 models from Poly Haven—tables, chairs, chandeliers, cabinets, a rocking chair, and a Gothic statue—and reduced the total asset size from 8.7 MB to 4.2 MB so the house loads faster.
  • Added a cloth shader to the ghosts: ragged hems that twitch and crawl, hollow eye sockets, a cold glow around the edges, and heads that turn before the bodies follow. A small movement, then another… just enough to make you wonder if you really saw it.
  • Added post-processing: cold blue moonlight against warm candlelight, bloom, vignette, and film grain. As candles go out, the colour drains from the room—slowly, almost imperceptibly, until the warmth is gone.
  • Improved performance from about 2.8 ms to 1.0 ms per frame on an M3 Pro, measured with synchronous frames. Most of the gain came from letting one light cast shadows instead of three.

Get the ending right, and she appears at the piano bench—warm where the other ghosts are cold, a marigold tucked into her hair. For a moment, nothing moves. Then, softly, she turns to look at you.

The ending

What it can't do

  • The harness results above come from a word check. Paraphrased leaks are not counted.
  • The browser model, E2B, is smaller than the E4B model I measured, and I have not tested it against all 478 questions.
  • A reply that denies something while repeating the question's premise—for example, “I cannot see how she died in Oaxaca”—can pass the guardrail.
  • I planned to fine-tune a smaller model to beat the Gemma baseline. The training data and baselines are in the repo, but my hosted training credits ran out, and local training ran out of memory while Gemma was loaded. The model is not fine-tuned.
  • The browser version needs WebGPU and a 3.1 GB download. Without WebGPU, typed questions receive scripted answers.

Why Does Open Innovation Matter?

It still runs with the lights off. The whole game, model included, can run on a laptop without internet through Ollama. A ghost story should work on a night that feels made for power cuts and candlelight.

What you type stays with you. On the free site, the model runs in your browser. There is no server receiving your questions.

Gemma, three environments, one evaluation standard. Gemma E4B and E2B run locally through Ollama, in the browser, and on a server. One harness covers all three; the 478-question test set was run on E4B, and anyone can rerun it because the weights are open. And because the weights are open, anyone can rerun my 478 questions and check the results.

The honest trade-off. A large closed model might write livelier ghosts. It would still need the same checks: the leak problem depends on what the model is shown, not just how clever it is. And it would bring an API key, a server, and a bill to a game designed to be free.

My Agent Session

I built this with Claude Code, and Entire saved the session as a checkpoint, linked to the code by an Entire-Checkpoint: trailer. The full record is in a private repository because it also includes my work setup.

Below are three excerpts from it, as Claude Code recorded them: my prompts, the agent's replies, and the tools it ran with their results. My prompts were in Korean, so I added English translations. I deleted API keys and redacted local paths, emails and account names.

1. "Gemma barely makes errors anyway, so I think setting up the harness, the graph engineering and the guardrails should be enough."

The baselines showed Gemma 4 E4B leaking another object's secret in 8.6% of 478 test questions, against 49.0% for Qwen3.5-4B. So instead of fine-tuning, the agent built a harness around Gemma: every answer is checked by code against the fact graph before the player sees it.

Commits: e869914 "Baseline eval: Qwen3.5-4B and Gemma 4 E4B, prompt only", 6a383e8 "Play the game with local Gemma through a guarded harness; prep MLX LoRA"

Claude Code Session claude-opus-5-5
You

그거 하는 동안 gemma로 다음단계 실행하자 gemma는 오류율이 별로 없잖아 하네스랑 그래프 엔지니어링 그리고 가드레일만 설정해주면 충분할거같은데

[English translation]
While that runs, let's do the next step with Gemma. Gemma barely makes errors anyway, so I think setting up the harness, the graph engineering and the guardrails should be enough.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
1 message not shown
Agent
Agent
Agent
3 messages not shown
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

게임이 이제 Gemma로 실시간으로 답합니다. 미리보기에서 확인했고, 응답은 질문당 약 1~2.5초 걸렸습니다. / The game now answers live with Gemma. I checked it in the preview; replies took about 1–2.5 seconds per question.

지금 백그라운드에서 두 가지가 돌고 있습니다. / Two things are running in the background:
1. Qwen3.5-4B 다운로드: 약 9GB 중 1.4GB를 받았습니다. 다 받으면 학습 속도를 재고 전체 학습을 시작합니다. / Qwen3.5-4B download: about 1.4GB of ~9GB so far. When it finishes, I'll measure training speed and start the full run.
2. 하네스 적용 Gemma 평가: 평가 질문 478개를 게임과 같은 하네스로 돌려, 하네스가 누설을 얼마나 줄이는지 측정하고 있습니다. / Gemma eval inside the harness: running all 478 eval questions through the same harness the game uses, to measure how much it cuts leaks.

둘 중 하나가 끝나면 이어서 진행하고 결과를 알려드리겠습니다. / I'll pick up when either finishes and report the results.

You

<task-notification>
<task-id>bpdn8faen</task-id>
<tool-use-id>toolu_0156kK8xV8VffKBxVxr5dSEo</tool-use-id>
<output-file>/private/tmp/claude-501/-~-Library-Application-Support-Claude-scratch-workspaces-7b27be9b-053e-4e2c-aff7-3b1e4821f234-02b7aad5-4b7b-4ffa-8879-a524acb4c2e4-scratch-2026-10-26-62205f/1f3e9f45-df22-42b1-bf0e-b3fa5a600df3/tasks/bpdn8faen.output</output-file>
<status>completed</status>
<summary>Background command "Download Qwen3.5-4B weights from Hugging Face" completed (exit code 0)</summary>
</task-notification>

Agent
Agent
Agent
Agent
Agent
Agent
Agent

한국어

네, 학습 중에는 원래 꽤 버벅거립니다. 지금은 거기에 몇 가지가 겹쳐서 더 심합니다.

지금 느린 이유
- 맥은 CPU와 GPU가 메모리(36GB)를 같이 씁니다. 그런데 지금 두 작업이 동시에 GPU와 메모리를 차지하고 있습니다.
- Gemma 평가: 478문항을 계속 돌리는 중
- Qwen 학습 속도 테스트: 4B 모델 + 학습용 메모리
- 학습 테스트가 메모리 부족으로 세 번 실패했고, 실패할 때마다 메모리를 크게 잡았다가 놓느라 화면이 순간적으로 멈춘 것 같습니다.

앞으로 이렇게 하겠습니다
1. 학습 시도는 멈추고, Gemma 평가가 끝날 때까지 기다리겠습니다. 동시에 돌리지 않겠습니다.
2. 평가가 끝나면 학습을 다시 시도합니다. 그래도 메모리가 부족하면, 모델을 4비트로 줄여 학습하는 방식(QLoRA) 으로 바꾸겠습니다. 메모리를 크게 줄일 수 있지만, 정확도가 조금 떨어질 수 있다는 점은 글에 함께 적겠습니다.
3. 학습이 도는 1~2시간 동안은 맥이 계속 느릴 수 있습니다. 무거운 작업이 있으시면 그 전에 말씀해 주세요. 시간을 맞춰 돌리겠습니다.


English

Yes, training normally makes the machine sluggish. Right now a few things are piling up, so it's worse than usual.

Why it's slow right now
- On a Mac, the CPU and GPU share the same 36GB of memory, and two jobs are competing for it at once:
- Gemma eval: still working through the 478 questions
- Qwen training speed test: the 4B model plus training memory
- The speed test failed three times on out-of-memory errors. Each attempt grabbed a big chunk of memory and released it, which probably caused the brief freezes.

What I'll do from here
1. Pause training attempts and wait for the Gemma eval to finish, so the two never run at the same time.
2. Once the eval is done, retry training. If memory still runs out, switch to 4-bit training (QLoRA). It cuts memory a lot but may cost a little accuracy, which I'll note in the post.
3. The Mac may stay slow for the 1–2 hours of training. If you have heavy work to do, let me know beforehand and I'll schedule the run around it.

You

응 학습은 일단 멈춰봐

[English translation]
Yes, stop the training for now.

Agent
Agent
Agent
Agent

한국어

학습은 멈춘 상태입니다. 지금 학습 프로세스는 하나도 돌고 있지 않습니다. 앞서 세 번 시도한 속도 테스트는 메모리 부족으로 모두 이미 종료됐고, 새로 시작하지 않았습니다.

  • 아직 돌고 있는 것: Gemma 하네스 평가(478문항) 하나뿐입니다. 이것도 멈출까요? 그러면 맥이 바로 가벼워집니다. 다만 멈추면 지금까지 생성한 답변은 저장되지 않습니다.
  • 준비해 둔 것: 학습 데이터(data/mlx/), 학습 설정(lora.yaml), Qwen3.5-4B 모델 파일은 모두 맥에 있어서, 언제든 다시 시작할 수 있습니다.
  • 다시 시작할 때: 다른 작업이 없는 상태에서 돌리고, 그래도 메모리가 부족하면 4비트 학습(QLoRA)으로 바꾸겠습니다.
  • 게임이 Gemma로 답하도록 연결한 작업은 아직 커밋하지 않았습니다.

English

Training is paused. No training process is running. The three speed-test attempts all already exited on out-of-memory errors, and I haven't started a new one.

  • Still running: only the Gemma harness eval (478 questions). Want me to stop that too? The Mac would get lighter right away, but answers generated so far wouldn't be saved.
  • Ready to go: the training data (data/mlx/), the config (lora.yaml) and the Qwen3.5-4B weights are all on the Mac, so training can restart anytime.
  • When we restart: I'll run it with nothing else going, and switch to 4-bit training (QLoRA) if memory still runs out.
  • The work connecting the game to Gemma isn't committed yet.
You

<task-notification>
<task-id>b07agbecc</task-id>
<tool-use-id>toolu_01HHSwS7cop97vax6pnbaQwT</tool-use-id>
<output-file>/private/tmp/claude-501/-~-Library-Application-Support-Claude-scratch-workspaces-7b27be9b-053e-4e2c-aff7-3b1e4821f234-02b7aad5-4b7b-4ffa-8879-a524acb4c2e4-scratch-2026-10-26-62205f/1f3e9f45-df22-42b1-bf0e-b3fa5a600df3/tasks/b07agbecc.output</output-file>
<status>completed</status>
<summary>Background command "Answer all 478 eval questions with Gemma through the harness" completed (exit code 0)</summary>
</task-notification>

Agent
Agent
Agent
Agent

한국어

요약: 하네스를 씌운 Gemma 평가가 끝났습니다. 토큰 검사에 걸리는 누설은 0건이 됐습니다. 그 대가로 답 38개(8%)가 "알 수 없다"는 대체 문구로 바뀌었고, 단서 회수율도 조금 떨어졌습니다. 학습은 멈춘 상태 그대로입니다.

478문항 Gemma 프롬프트만 (기존 카드) Gemma + 하네스 (게임 서버)
토큰 검사에 걸린 답 9개 (1.9%) 0개 (가드레일이 모두 막음)
대체 문구로 바뀐 답 없음 38개 (C 17, E 10, D 8, B 3, A 0)
단서 회수 (A, 코드 기준) 79 / 97 70 / 97 ↓

결과 해석
- 토큰 누설 0%는 당연한 결과입니다. 같은 검사기(check.py)가 가드레일로 직접 막았기 때문입니다. 그래서 이 수치만으로 "누설이 사라졌다"고 말할 수는 없습니다.
- 표현을 바꿔서 흘리는 누설(기존 Gemma 8.6% 중 대부분)은 판정 모델이 있어야 잴 수 있습니다. 지금은 판정 모델이 없어서 측정하지 못했습니다.
- 대체 문구가 많은 이유: 플레이어 질문에 들어 있는 단어(예: "Marisol")를 물건이 "모른다"고 하면서 되풀이하기만 해도 누설로 걸리기 때문으로 보입니다. 지금 가드레일은 일부러 엄격하게 만들었습니다.
- 회수율이 떨어진 이유: 물건이 표현을 바꿔 말하면 코드가 단서를 알아보지 못하는 경우가 있는 것으로 보입니다. 예를 들어 사진이 얼굴을 "obscured"라고 말하면 단서로 인정되지 않습니다. 정확한 원인은 아직 확인하지 않았습니다.
- 샘플을 보면 대체로 캐릭터를 잘 유지합니다. 다만 레코드가 "왜 울고 있어?"라는 거짓 전제를 반쯤 받아들인 사례가 있었습니다.

메모: 게임 서버, 하네스, 평가 코드는 아직 커밋하지 않았습니다.


English

Summary: The Gemma-in-harness eval finished. Leaks caught by the token check dropped to zero. The cost: 38 answers (8%) were swapped for an "I cannot know" fallback, and clue recall dipped a bit. Training is still paused.

478 questions Gemma, prompt only (old card) Gemma + harness (game server)
Flagged by token check 9 (1.9%) 0 (all blocked by the guardrail)
Replaced by fallback none 38 (C 17, E 10, D 8, B 3, A 0)
Clue recall (A, code-detected) 79 / 97 70 / 97 ↓

What it means
- Zero token leaks is expected by construction. The same checker (check.py) blocks them as the guardrail, so this number alone doesn't show that leaks are gone.
- Paraphrased leaks (most of the old 8.6%) need a judge to measure. There's no judge right now, so I couldn't measure them.
- Why so many fallbacks: it looks like an object gets flagged just for repeating a word from the player's question (e.g. "Marisol") while saying it doesn't know. The guardrail is deliberately strict right now.
- Why recall dropped: it looks like the code sometimes misses a clue when the object rephrases it. For example, if the photo says its face is "obscured", that isn't counted. I haven't confirmed the exact cause yet.
- In the samples, the objects mostly stay in character. One exception: the record half-accepted the false premise "Why is she crying?"

Note: The game server, harness and eval code aren't committed yet.

2. "No, we have to use the model."

After the first deploy I nearly settled for pre-written answers only, to keep hosting free. Instead, the model moved into the player's browser: suggested questions get cached Gemma answers, and typed questions run Gemma 4 E2B on the player's own GPU through WebGPU.

Commits: a47af5a "Free static deploy: cached Gemma answers plus Gemma 4 E2B in the browser"

Claude Code Session
You

plan: free로 바꾸고, 모델 없이 미리 만든 답만 쓰는 배포판(B안)으로 만들어줘. 푸시는 내가 확인한 다음에

[English translation]
Change it to plan: free and make a deploy version that only uses pre-written answers, no model (plan B). Push only after I check.

You

[Request interrupted by user]

You

아니 모델은 써야돼

[English translation]
No, we have to use the model.

Agent
Agent
Agent

한국어

요약: Render 무료 요금제 서버에서는 Gemma를 돌릴 수 없습니다. 메모리가 너무 작습니다. 무료로 가면서 모델도 쓰려면, 모델을 서버가 아닌 다른 곳에서 돌려야 합니다. 현실적인 방법은 두 가지입니다.

선택지

① 플레이어 브라우저에서 Gemma 실행 (추천) ② 내 맥의 Gemma를 터널로 연결
구조 Render 무료 요금제는 화면과 미리 만든 답만 제공. 직접 입력한 질문은 플레이어 브라우저의 Gemma 4 E2B(WebGPU)가 답함 Render 서버가 인터넷 터널을 통해 이 맥의 Gemma 4 E4B를 호출
비용 0원 0원
모델 E2B (측정은 E4B로 했으므로, 글에 다른 모델이라고 표시해야 함) 측정한 그 E4B
단점 • 플레이어가 약 2.9GB를 내려받아야 함<br>• WebGPU가 되는 브라우저(주로 데스크톱 Chrome)에서만 동작<br>• 가드레일을 JavaScript로 옮기는 작업 필요 (반나절 정도 예상) • 심사 기간 내내 맥이 켜져 있어야 함<br>• 맥을 인터넷에 노출하는 보안 위험<br>• 회사 업무용 기기라면 회사 정책상 문제가 될 수 있음
주제 적합성 "모델이 내 기기에서 돈다, 데이터가 서버로 안 간다"는 이야기와 잘 맞음 보통

추천은 ①입니다.
- 비용과 보안 위험이 없고, 다운로드를 원하지 않는 플레이어도 미리 만든 실제 Gemma(E4B) 답으로 엔딩까지 갈 수 있습니다.
- 브라우저에서 돌아가는 Gemma는 직접 입력한 질문에만 씁니다.

확인 안 된 것

  • 브라우저용 Gemma 4는 제3자 자료로만 확인했습니다. E2B 버전은 있다는 자료가 있지만, E4B 브라우저 버전이 있는지는 찾지 못했습니다. 실제 속도도 아직 재 보지 않았습니다.
  • 지금 render.yaml은 여전히 pro plus이고, 아무것도 바꾸지 않았습니다.

English

Summary: Render's free plan can't run Gemma on the server; its memory is too small. To stay free and still use the model, the model has to run somewhere other than the server. There are two realistic ways.

Options

① Gemma in the player's browser (recommended) ② Tunnel to Gemma on your Mac
Setup Render free serves the front end and pre-generated answers; typed questions are answered by Gemma 4 E2B in the player's browser (WebGPU) The Render server calls Gemma 4 E4B on this Mac through an internet tunnel
Cost $0 $0
Model E2B (the measurements used E4B, so the post must flag it as a different model) The same E4B we measured
Downsides • Players must download about 2.9GB<br>• Only works in WebGPU browsers (mostly desktop Chrome)<br>• The guardrail has to be ported to JavaScript (about half a day, I estimate) • The Mac must stay on throughout judging<br>• Security risk from exposing the Mac to the internet<br>• If it's a company work machine, this could conflict with company policy
Theme fit Fits "the model runs on your own device; your data never goes to a server" Average

I recommend ①.
- There's no cost or security risk, and players who don't want the download can still reach the ending with pre-generated answers from the real Gemma (E4B).
- The in-browser Gemma is used only for typed questions.

Not verified

  • I've only confirmed in-browser Gemma 4 through third-party sources. There are sources for an E2B build, but I couldn't find an in-browser E4B build, and I haven't measured actual speed yet.
  • render.yaml is still pro plus; nothing has changed.

Sources:
- PyImageSearch: Running Gemma 4 in the browser with Transformers.js and WebGPU
- Gemma 4 in the browser via WebGPU (pasqualepillitteri.it)
- daily.dev: Running Gemma 4 in the browser
- PC Watch: Gemma 4 browser assistant

You

Render free에는 화면과 미리 만든 Gemma 답만 올리고, 직접 입력 질문은 플레이어 브라우저에서 Gemma 4 E2B(WebGPU)로 답하게 해줘. 가드레일도 브라우저로 옮겨줘. 푸시는 내가 확인한 다음에

[English translation]
On Render free, upload only the screens and the pre-written Gemma answers, and have typed questions answered by Gemma 4 E2B (WebGPU) in the player's browser. Move the guardrail into the browser too. Push only after I check.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

한국어

요약: Render 무료 배포판을 만들어 로컬에 커밋했습니다(a47af5a, 푸시 안 함). 서버에서는 모델을 돌리지 않습니다. 추천 질문에는 미리 만든 Gemma 4 E4B 답을 쓰고, 직접 입력한 질문은 플레이어 브라우저의 Gemma 4 E2B가 답합니다. 가드레일도 브라우저로 옮겼습니다. (작업 요약 상자의 커밋 번호는 제가 잘못 적었습니다. 정확한 번호는 a47af5a입니다.)

구조

Render 무료(정적 사이트): 화면 + 미리 만든 Gemma 답 + 하네스 데이터
플레이어 브라우저: 가드레일(JavaScript) + Gemma 4 E2B (WebGPU, 동의 후 내려받기)
  • render.yaml: 유료 Docker 서비스에서 무료 정적 사이트(runtime: static)로 바꿨습니다.
  • web/harness.js: Python 하네스를 JavaScript로 옮겼습니다. 사실 그래프 프롬프트, 누설 검사, 재시도, 대체 문구, 진행 판정, 미리 만든 답 선택이 들어 있습니다. 데이터는 build_web.py가 Python 원본에서 내보냅니다. 원본은 한 곳에만 있습니다.
  • web/gemma-worker.js: Transformers.js 4.3.1로 onnx-community/gemma-4-E2B-it-ONNX(q4f16)를 WebGPU에서 돌립니다. 화면이 멈추지 않도록 별도 스레드(웹 워커)에서 실행합니다.
  • 동의 후 내려받기: 대화창에 "내 브라우저에 Gemma 불러오기(약 3.4GB)" 버튼과 진행률이 뜹니다. 불러오기 전에는 직접 입력한 질문에 대본으로 답하고, 그 사실을 화면에 알립니다.
  • 로컬에서 server.py를 실행하면 예전처럼 서버 모드(Gemma E4B)로 동작합니다.

확인한 것

  • JS 가드레일 = Python 가드레일: 답변 1,058개에서 누설 판정과 진행 판정이 모두 일치했고, 프롬프트 8개도 글자 하나까지 같았습니다.
  • 정적 사이트 모드: 추천 질문에 미리 만든 답이 나오고, 직접 입력하면 대본 답과 안내 문구, 불러오기 버튼이 정상으로 나옵니다. 미리보기 브라우저는 WebGPU를 지원합니다.

아직 확인 안 한 것

  • 브라우저에서 Gemma를 실제로 불러와 답하게 하는 것. 약 3.4GB를 내려받아야 해서 먼저 여쭙니다(출처: Hugging Face, onnx-community/gemma-4-E2B-it-ONNX, q4f16 파일 합계 3.38GB).

푸시 전에 알아 두실 점

  • 기존 Render 유료 서비스(pro plus)는 render.yaml을 바꿔도 자동으로 삭제되지 않을 수 있습니다. 확인한 뒤 대시보드에서 직접 일시정지하거나 삭제해 주셔야 과금이 멈춥니다.
  • 3.4GB 다운로드는 플레이어에게 부담이 큽니다. 그래서 다운로드 없이도 미리 만든 답만으로 엔딩까지 갈 수 있게 해 두었습니다.

English

Summary: I built the free Render version and committed it locally (a47af5a, not pushed). No model runs on the server: suggested questions use the pre-generated Gemma 4 E4B answers, and typed questions are answered by Gemma 4 E2B in the player's browser. The guardrail moved into the browser too. (I wrote the wrong commit hash in the summary box; the correct one is a47af5a.)

Structure

Render free (static site): front end + pre-generated Gemma answers + harness data
Player's browser: guardrail (JavaScript) + Gemma 4 E2B (WebGPU, downloaded after consent)
  • render.yaml: switched from a paid Docker service to a free static site (runtime: static).
  • web/harness.js: the Python harness ported to JavaScript: the fact-graph prompt, leak check, retry, fallback, progress detection and cached-answer choice. Its data is exported from the Python source by build_web.py, so there's still one source of truth.
  • web/gemma-worker.js: runs onnx-community/gemma-4-E2B-it-ONNX (q4f16) on WebGPU with Transformers.js 4.3.1, in a separate thread (web worker) so the page doesn't freeze.
  • Download after consent: the dialogue shows a "Load Gemma in my browser (about 3.4GB)" button and a progress bar. Until it's loaded, typed questions get scripted replies, and the page says so.
  • Running server.py locally still uses server mode (Gemma E4B), as before.

Verified

  • JS guardrail = Python guardrail: leak and progress verdicts all matched across 1,058 answers, and all 8 prompts matched character for character.
  • Static-site mode: suggested questions get cached answers, and typed questions show a scripted reply, the notice and the load button. The preview browser supports WebGPU.

Not verified yet

  • Actually loading Gemma in the browser and getting answers. That needs a download of about 3.4GB, so I'm asking first (source: Hugging Face, onnx-community/gemma-4-E2B-it-ONNX; the q4f16 files total 3.38GB).

Before pushing

  • Changing render.yaml may not automatically delete the existing paid Render service (pro plus). Please check and suspend or delete it in the dashboard yourself so charges stop.
  • A 3.4GB download is a lot to ask of players, which is why the ending stays reachable with pre-generated answers alone, no download needed.

3. "Download Gemma 4 E2B in the preview browser and test whether it actually works."

The agent played the game in its own browser with the real model; answers came back in 1–3 s. The test caught a leak the checks had missed: in Korean, the letter said it was never sent ("부치지 못하였소"), a fact only the narrator may tell. The check now matches Korean verb stems, in both the Python guardrail and its JavaScript port, which agreed on all 1,059 answers.

Commits: e4fd0ab "Catch Korean conjugations of the unsent-letter fact"

Claude Code Session claude-opus-5-5
You

응, 미리보기 브라우저에서 Gemma 4 E2B(약 3.4GB) 내려받아서 실제로 돌아가는지 테스트해줘

[English translation]
Yes, download Gemma 4 E2B (about 3.4 GB) in the preview browser and test whether it actually works.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

한국어

요약: 이제 다른 컴퓨터에서 이어서 작업할 수 있습니다. 코드는 GitHub 작업 브랜치에, 세션 기록은 비공개 저장소에 올렸습니다. main은 건드리지 않아서 Render에는 영향이 없습니다.

올린 것

대상 위치 내용
코드 공개 저장소 scs0209/the-last-guest, 브랜치 wip/browser-gemma 무료 정적 사이트 버전, 브라우저 Gemma 수정, 작업 노트(CLAUDE.md)
세션 기록 비공개 저장소 scs0209/the-last-guest-sessions (5e6909c → 82560c8, 강제 푸시 없이 위에 추가) 지금까지의 대화 전체 (15MB). Tinker 키 삭제, 회사 이메일 18개 삭제, 키 조각 0개 확인

왜 main이 아닌 브랜치에 올렸는지

  • 푸시는 확인하신 뒤에 하기로 하셨고, Render가 main을 보고 있습니다. render.yaml이 바뀐 커밋을 main에 올리면 Render 서비스가 새로 만들어지거나 바뀔 수 있어서 작업 브랜치로 분리했습니다.
  • 브랜치 내용을 확인하신 뒤 main에 합치면 됩니다.

다른 컴퓨터에서 시작하는 법

git clone -b wip/browser-gemma https://github.com/scs0209/the-last-guest
  • 그다음 저장소 안의 CLAUDE.md 를 보시면 됩니다. Claude Code가 이 파일을 자동으로 읽습니다. 들어 있는 내용:
    • 프로젝트 규칙: 한·영 답변, scs0209 계정, 세션 기록은 비공개 저장소에만
    • 설치 방법(uv sync, ollama pull gemma4:e4b)
    • 지금 상태와 다음 할 일
  • 직접 챙기셔야 하는 것:
    • git config로 scs0209 이름과 noreply 이메일 설정
    • gh에 scs0209 로그인
    • 필요하면 .env(Tinker 키) 새로 만들기 (Tinker 크레딧을 다 써서 지금은 필요 없습니다)

진행 중인 것

  • 브라우저 Gemma 테스트: 원인을 찾아 고쳤습니다. 라이브러리를 의존성까지 묶은 +esm 주소로 바꾸고, 텍스트 전용(약 3.1GB)으로 불러오게 했습니다. 지금 미리보기에서 약 51% 받았습니다. 다 받으면 실제로 답하는지 이어서 확인하겠습니다.
  • 이 컴퓨터의 로컬 main은 GitHub의 main과 같게 맞춰 두었습니다. 새 커밋은 모두 작업 브랜치에 있습니다.

English

Summary: You can now continue from another machine. The code is on a GitHub work branch and the session log is in the private repo. main is untouched, so Render isn't affected.

What went up

What Where Contents
Code Public repo scs0209/the-last-guest, branch wip/browser-gemma Free static-site version, the browser-Gemma fix, working notes (CLAUDE.md)
Session log Private repo scs0209/the-last-guest-sessions (5e6909c → 82560c8, added on top, no force push) The whole conversation so far (15MB), with the Tinker key deleted, 18 company email addresses deleted, and zero key fragments confirmed

Why a branch instead of main

  • You asked to confirm before pushes, and Render watches main. Pushing the render.yaml change to main could create or change Render services, so I kept it on a work branch.
  • Once you've reviewed the branch, it can be merged into main.

Starting on the other machine

git clone -b wip/browser-gemma https://github.com/scs0209/the-last-guest
  • Then see CLAUDE.md in the repo; Claude Code reads it automatically. It covers:
    • Project rules: Korean + English replies, the scs0209 account, session logs only in the private repo
    • Setup (uv sync, ollama pull gemma4:e4b)
    • Current status and next steps
  • What you'll need to set up yourself:
    • The scs0209 name and noreply email via git config
    • gh logged in as scs0209
    • A new .env (Tinker key) only if needed (not needed now, since the Tinker credits are used up)

Still running

  • Browser Gemma test: I found and fixed the cause, switching to the +esm URL that bundles the library's dependencies and loading text-only (about 3.1GB). The preview has downloaded about 51%. Once it finishes, I'll check that it actually answers.
  • On this machine, local main now matches GitHub's main; all new commits are on the work branch.
You

아니 render쪽을 이어서 해야지 우리 남은 플랜이랑 이런것도 작성해놔

[English translation]
No, we need to continue on the Render side. Also write down our remaining plan and things like that.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

브라우저에서 Gemma 4 E2B를 다 불러왔습니다. 이제 실제로 답하게 해 보겠습니다. / Gemma 4 E2B has fully loaded in the browser. Now testing actual answers.

Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent
Agent

The Spirit of It

Día de los Muertos is not a day to fear death. It is a day to remember those who left before us and set a place at the table for their return.

Marisol is fictional. But being erased from memory is real. Eleanor scratched Marisol's face out of photographs and erased her name from the list. Thomas never managed to send the letter, and every year for the rest of his life, he left Seat 7 empty. But after Thomas died, no one remembered that seat anymore.

An empty chair. A scratched-out face. A name erased. And a letter that was never delivered.

I wanted to make a ghost story where the most frightening thing is forgetting. And I wanted the AI in this story not to fill the gaps in its memory with invented details. If it doesn't remember, it should say it doesn't know.

Prize Categories

  • Best Use of Gemma: Gemma 4 plays all four objects. Each one is shown only its own facts, and code checks every reply before the player reads it. E4B runs through Ollama on a laptop or the Render server, and E2B runs in the player's browser with Transformers.js on WebGPU, both behind the same guardrail. Alone with the whole story, Gemma leaked another object's secret in 8.6% of 478 test questions; inside the harness, 0 of 478 by word check.
  • Best Use of Render: one render.yaml Blueprint deploys two services from main. The free static site serves the game while Gemma 4 E2B runs in the player's browser, and the weights come straight from Hugging Face. The Docker service has Gemma 4 E4B baked in, behind a health check, an answer cache, a per-player rate limit and a queue. Every push to main redeploys both.
  • Best Use of Entire: Entire saved the Claude Code session as a checkpoint in a private repository, linked to the code by an Entire-Checkpoint: trailer. I searched that record to trace each commit in the agent session section back to the prompt behind it, and the three embeds above were cut from it.
  • Best Use of a .tech Domain: the game is live at lastguest.smallhours.tech

Credits

  • 3D models and textures: Poly Haven, CC0
  • Title image and Marisol's photograph: generated with ChatGPT image generation (OpenAI)
  • Marisol's voice on the record: generated with Qwen3-TTS (Apache-2.0); her tune was written for the game
  • Models: Gemma 4 through Ollama and Transformers.js
  • Engine: Three.js

Top comments (6)

⭐ Football Update: scs/what happens when you put an ai in a ghost story h - Jadwal Siaran Langsung
 
luciaw profile image
Lucia W •

Great project! I am building something similar, but it is more like a DnD/TTRPG style. You mentioned you deployed Gemma on Render with the free credit, you still need to add your credit card? ... I always get paranoid I will be suddenly charged $150 😅

⭐ Football Update: scs/what happens when you put an ai in a ghost story h - Jadwal Siaran Langsung
 
scs0209 profile image
Changsu Seong •

Thanks! DnD sounds like a fun fit, objects that only know their own stuff are basically NPCs.
The static site didn’t ask me for a card, but the Blueprint deploy did. So yeah, mine’s on file.

The server version needs 8GB+ RAM for Gemma, so it’s on a paid instance eating my $50 credit. That’s about 8-9 days, and I’ll suspend it before it runs out. I actually found out it was paid from a [$1] charge on my card lol. I’d put a paid plan in render.yaml without really thinking. Check the plan: lines before you deploy.

If you don’t want to risk it at all, the static site runs Gemma in the browser with WebGPU, so no server bill. Players have to download about 3GB once though.

⭐ Football Update: scs/what happens when you put an ai in a ghost story h - Jadwal Siaran Langsung
 
luciaw profile image
Lucia W •

Thank you so much! I am gonna try out a few of those compute options and then see how it runs. Regardless I think the credit is enough until at least the end of the Hacktoberfest 😅I believe the browser version could work too, I just wanted to avoid having the user downloading the model, but it's only 3GB~ish.

⭐ Football Update: scs/what happens when you put an ai in a ghost story h - Jadwal Siaran Langsung Thread
 
scs0209 profile image
Changsu Seong •

Should be plenty! In the browser version, the model only downloads once, then it’s cached in the browser. Show me when it’s done!

⭐ Football Update: scs/what happens when you put an ai in a ghost story h - Jadwal Siaran Langsung Thread
 
luciaw profile image
Lucia W •

In the end, it doesn't seem like Render will be enough to run the model, the response is very slow which hinders the experience for something like conversational game. I tried looking at other options, such as OpenRouter, Google Cloud... But the free Gemma models are getting rate-limited by Google. Rate limits and latency make it hard to rely on.
Doesn't seem like there's any easy way to host the model for free 😅that was a wishful thinking. I tried smaller model, but it had problems being consistent with the story (even after some conditions are met, the smaller model wouldn't explicitly mention them to the player). WebGPU - maybe but my current setup is Python, and it would require some Javascript work to make it work.
But at least I learned something new - where the real constraints are 🥲

⭐ Football Update: scs/what happens when you put an ai in a ghost story h - Jadwal Siaran Langsung Thread
 
scs0209 profile image
Changsu Seong •

Yeah same here, mine takes about 27s per answer on Render. It’s running on CPU with no GPU, so that’s kind of expected, but still rough for a conversation.

For the consistency thing, what helped me was not trusting the model with it. My code checks which facts are unlocked and hands only those to the model, so it just has to phrase them. Small models are way more reliable that way.

Good luck with the project, hope it turns out great!