My grandpa took me to wayang every other Sunday. So I taught an open model to be the dalang. A developer built Bayang Eyang, an open-source browser-based wayang kulit shadow-play generator that uses Google's Gemma 4 26B A4B on Cloudflare Workers AI to cast puppets and write scenes from four answers about a loved one, while code renders the puppets, oil lamp, shadows and synthesized gamelan locally. Across 12 test stories the system produced 12 complete plays at roughly 25 seconds and about $0.0006 each with no story text stored or traced, and a $0.50 Tinker fine-tune raised a small Qwen3.5-4B model from 0 to 10 of 12 plays. This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine https://dev.to/challenges/hacktoberfest-week4-2026-10-26 . When I was a child, every second Sunday night belonged to my grandpa. He was Javanese, and he loved two things on a stage: wayang and ludruk. So twice a month we went to the show, and I sat next to him while a dalang, the puppet master, told a whole night of stories with nothing but leather puppets, an oil lamp and a white screen. He is gone now. I still can't hear a gamelan without seeing him. So for "Ghost in the Machine" I didn't want to build a ghost that pretends to be someone. I wanted to build the dalang. TL;DR. Answer four questions about someone you love. Gemma 4 casts the puppets and writes a wayang play; plain code directs and checks it; your browser performs it with an oil lamp, shadows and a gamelan. On 12 test stories: 12 real plays , about 25 seconds to write, about $0.0006 each , and no story text stored or traced. Then Gemma 4 became a teacher: a $0.50 Tinker fine-tune took a small Qwen3.5-4B from 0 to 10 of 12 plays. Bayang Eyang "the grandparent's shadow" turns a story about your grandparent into a wayang kulit shadow play, performed in your browser. You answer four short questions on a card: who they were, what they loved, the hardest thing they faced, and the habit everyone still laughs about. An open-weight model, Google's Gemma 4 26B A4B on Cloudflare Workers AI, takes the dalang's seat. It casts the puppets and writes the scenes, and the stage performs them: puppets on rods, a flickering oil lamp, shadows that swell as they come close to the light, the gunungan the tree-of-life puppet opening and closing the play, and a synthesized gamelan underneath. In wayang, every great trouble takes the shape of a raksasa, a giant. So your grandmother's 1981 flood walks onto the screen as a giant, the clowns Petruk and Bagong tease her about the money she hid in the rice tin, and the play ends with her still standing. The split mirrors real wayang. A dalang doesn't carve the puppets: craftsmen cut them from leather long before the show. The dalang brings them to life: who appears, who fights whom, what the clowns joke about, how the night ends. Here, code is the carver . The puppets, the lamp, the shadows and the gamelan are drawn and synthesized live in your browser, with no image or video model, so they're the same every night and cost nothing to perform. Gemma is the dalang : every word, every casting choice, every scene, every joke. It fits the theme twice. Wayang has always been a ghost in the machine: ancestors and heroes who exist only as shadows thrown by a lamp. And the dalang is the only person who knows the whole story, hidden behind the screen, voicing everyone. This time the dalang is a model. ▶ Watch the whole play with gamelan sound 2 min https://github.com/derbyps/bayang-eyang demo The story in the video is made up: Nenek Sari, the 1981 flood took her rice fields in one night, she hid money in the rice tin and called it her bank. Gemma 4 titled it "The Golden Rice Tin of Nenek Sari." The flood walks on as "the great demon of water," leans toward the lamp until its shadow swallows her, and later Petruk can't help himself: "Hey, did you see the rice tin? It's more full of bills than grain " The half minute while the dalang writes is sped up 4× in the video; the performance plays at real speed. Same story, three nights, three different plays. While making this video I performed Nenek Sari's story three times, twice with the apprentice you'll meet below and once with Gemma 4, and got The Keeper of the Golden Rice , The Golden Rice of Nenek Sari and The Golden Rice Tin of Nenek Sari . The puppets never change. The play does. Try it yourself: open the site, light the lamp, tell it about someone. It writes a play in about half a minute and performs for one to two minutes. To keep it free, each visitor gets 5 plays an hour and the site has a small daily budget; when the dalang rests, it offers a ready-made play instead. github.com/derbyps/bayang-eyang https://github.com/derbyps/bayang-eyang MIT src/stage/ the canvas stage: lamp, screen, depth, shadows src/rigs/ the puppets: rigid bodies, two-joint arms on rods src/choreo/ the motion: ~20 actions, temperaments, seeded variation src/audio/ the synthesized gamelan, in the slendro scale src/dalang/ prompts, schemas, the director, the checks, the pipeline server/ a small Hono server: provider, limits, tracing apprentice/ the Tinker fine-tune: data, training, export eval/ every model I tried, on the same 12 stories My first instinct was to ask the model for the whole performance: positions, timings, every move. That fails in a boring way. A model is good at drama and bad at geometry . So Gemma does the drama, and code does the geometry: story card → Gemma 4: casting ~7 s → Gemma 4: scenes ~18 s → director TypeScript 3.95 ms → checks TypeScript 6.96 ms → problems? back to Gemma 4 with the story, up to 2 rounds → still broken? cut those moves, or perform a ready-made play The model only ever sees a vocabulary generated from the code itself actions, styles, which ones need a target , so the prompt can't drift from what the stage can perform. Why Gemma 4? Its weights are open, it writes Bahasa Indonesia as well as English, and on Workers AI it runs on a free daily allowance with no credit card, which is what lets a stranger try this for free. It does three jobs here: the live dalang, the writer of 169 practice stories, and the teacher of the apprentice below. The first time I pointed this at real Gemma 4, two out of three stories fell back to the ready-made play. Every failure was a lesson: "line": "" and added keys I didn't ask for. My schemas were strict, so a good play died on a technicality. Now empty fields are dropped and unknown keys are ignored. cross walk across the stage the target it required, so a perfectly good move failed validation every time. The model was being blamed for my code. | | Before | After | |---|---|---| | Stories that fell back to the ready-made play | 2 of 3 | 0 of 12 | | Plays right on the first try | – | 75% | | Typical wait to write a play | 45 s | 25 s | | Cost per play | – | ~$0.0006 | The dalang is an agent with two tools, the director and the checks, so I traced it like one: invoke agent Dalang , then chat spans for each Gemma call and execute tool spans for the director and the checks. One play, traced: 24.25 s and 3.2K tokens in total. Casting took 6.68 s, the scenes 17.55 s. The director took 3.95 ms and the checks 6.96 ms . The model takes seconds; the stage's own logic takes milliseconds. These are people's family stories, so no story text, prompt or model output ever leaves for Sentry : only models, token counts, timings and costs. That's checked, not promised. The SDK's automatic capture of AI inputs and outputs is switched off, the model spans are written by hand, and two tests run a real play through the server and fail if any story text or request body shows up in what would be sent to Sentry. Here is the same span with the Input tab open: The app runs as one small, always-on Node service on Render , in Singapore, close to the audience I most want to reach. The whole service is declared in a Blueprint: services: - type: web name: bayang-eyang runtime: node plan: starter region: singapore buildCommand: npm ci && npm run build startCommand: npm start healthCheckPath: /api/health envVars: - key: DALANG MODEL value: "@cf/google/gemma-4-26b-a4b-it" Just under Cloudflare's free 10,000 neurons a day, so visitors see "the dalang is resting" instead of errors. - key: DALANG DAILY BUDGET USD value: "0.10" - key: CF API TOKEN sync: false set in the dashboard, never in the repo The API key stays on the server; the browser never sees it. One real bug from production: my rate limit didn't work. Render sits behind Cloudflare, and I was keying the limit on the last X-Forwarded-For entry, which turned out to be a different Cloudflare server on almost every request. Now it keys on cf-connecting-ip , which Cloudflare sets at the edge and a visitor can't forge. The address is bayang-eyang.buatkamu.tech . Buat kamu means "for you" in Indonesian. In Java, a dalang learns by sitting behind a master for years. I wanted to know: could a small open model learn this craft by watching a big one? First, the honest baseline: the same 12 stories and the same code for every model. | Model | Where | Real plays | First try | Typical wait | |---|---|---|---|---| | gemma3:1b | my laptop, offline | 0 of 12 | 0% | 27 s | | gemma3:4b | my laptop, offline | 4 of 12 | 0% | 4 min 11 s | | Qwen3.5-4B, untrained | Tinker | 0 of 12 | 0% | 10 s | | Qwen3.5-4B apprentice | Tinker | 10 of 12 | 83% | 9 s | | Gemma 4 26B A4B, the teacher | Workers AI | 12 of 12 | 75% | 25 s | Small models fail here. They can write a story, but not one that a strict stage will accept. So Gemma 4 became the teacher: The same base model, on the same Tinker endpoint, went from 0 of 12 to 10 of 12 , every one of them right on the first try. That's more often right first time than its teacher, with a little more variety in its moves 13.8 different moves per play against 12.9 . Twelve stories is a small test, so read that as a strong signal, not a benchmark. What it can't do yet is fix its own mistakes. It only ever watched finished plays, never a repair, so when it slips 2 of 12 , it stays slipped. Gemma 4 can recover. Next time I'd teach it repair rounds too. And a confession: I tried to run all 12 stories on my own 8 GB MacBook, and the Mac froze so hard that macOS restarted itself. The apprentice does run offline: it wrote a cast in 21 seconds with no internet. But a whole play wants more memory than my laptop has, so the 12-story numbers come from Tinker's servers, running the same weights. Where a closed model might be better: I didn't test one, but a frontier model would probably write richer dialogue and need fewer repair rounds. What this stage needs most, though, is structure, and a fifty-cent fine-tune closed most of that gap. Because the material is someone's family. A story about a grandparent can hold a death, an illness, a debt nobody talks about. With open weights, I get to choose where that story goes. On the live site it goes to Gemma 4 on Cloudflare Workers AI which, per Cloudflare's terms, doesn't use prompts to train models , Bayang Eyang itself stores nothing, and the tracing never sees a word of it. If even that is too far, the same play runs against a model on your own machine by changing one setting, and the ghost stays in your machine. Because I could change the model instead of waiting for someone else to. I ran the same 12 stories through five models while building this, by changing one environment variable. When the small ones failed, I didn't file a feature request. I trained one, for fifty cents, and I own the result: the adapter is a 278 MB file on my disk that no one can deprecate. Because wayang should outlive the tool. Wayang kulit has lasted centuries because no one owns it. Every dalang learns from another dalang and changes the story a little. A digital dalang locked behind one company's API would be the opposite of that. Bayang Eyang is MIT-licensed, the teacher's lessons are in the repo, and the apprentice can learn from the master the same way the real ones do. render.yaml Blueprint, with a health check, the model key kept server-side, and a production rate-limit bug fixed behind Cloudflare. chat and execute tool spans, showing seconds of model against milliseconds of code, with no story text ever sent, and tests to prove it. I haven't shown it to my grandma. She doesn't like to be reminded of my grandpa; I think it would make her sad, and I'd rather let her be. But the first time the lamp flickered on and the gunungan swept across the screen, I was a kid again, sitting next to him on a Sunday night. That's why the address says buat kamu : for you. I built this with the help of an AI coding assistant Claude Code . The story, the decisions and the grandpa are mine.