{"slug": "my-grandpa-took-me-to-wayang-every-other-sunday-so-i-taught-an-open-model-to-be", "title": "My grandpa took me to wayang every other Sunday. So I taught an open model to be the dalang.", "summary": "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.", "body_md": "*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).*\n\nWhen 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.\n\nHe is gone now. I still can't hear a gamelan without seeing him.\n\nSo for \"Ghost in the Machine\" I didn't want to build a ghost that pretends to be someone. I wanted to build the dalang.\n\n**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.\n\n**Bayang Eyang** (\"the grandparent's shadow\") turns a story about your grandparent into a wayang kulit shadow play, performed in your browser.\n\nYou 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.\n\nIn 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.\n\nThe 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.\n\nIt 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.\n\n▶ [Watch the whole play with gamelan sound (2 min)](https://github.com/derbyps/bayang-eyang#demo)\n\nThe 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.)\n\n**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.\n\n**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.\n\n**[github.com/derbyps/bayang-eyang](https://github.com/derbyps/bayang-eyang)** (MIT)\n\n```\nsrc/stage/      the canvas stage: lamp, screen, depth, shadows\nsrc/rigs/       the puppets: rigid bodies, two-joint arms on rods\nsrc/choreo/     the motion: ~20 actions, temperaments, seeded variation\nsrc/audio/      the synthesized gamelan, in the slendro scale\nsrc/dalang/     prompts, schemas, the director, the checks, the pipeline\nserver/         a small Hono server: provider, limits, tracing\napprentice/     the Tinker fine-tune: data, training, export\neval/           every model I tried, on the same 12 stories\n```\n\nMy 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:\n\n```\nstory card\n  → Gemma 4: casting               ~7 s\n  → Gemma 4: scenes                ~18 s\n  → director (TypeScript)          3.95 ms\n  → checks (TypeScript)            6.96 ms\n  → problems? back to Gemma 4 with the story, up to 2 rounds\n  → still broken? cut those moves, or perform a ready-made play\n```\n\nThe 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.\n\nWhy 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.\n\nThe 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:\n\n`\"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.\n|  | Before | After | \n|---|---|---|\n| Stories that fell back to the ready-made play | 2 of 3 | **0 of 12** | \n| Plays right on the first try | – | **75%** | \n| Typical wait to write a play | 45 s | **25 s** | \n| Cost per play | – | **~$0.0006** | \n\nThe 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.\n\nOne 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.\n\nThese 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:\n\nThe 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:\n\n```\nservices:\n  - type: web\n    name: bayang-eyang\n    runtime: node\n    plan: starter\n    region: singapore\n    buildCommand: npm ci && npm run build\n    startCommand: npm start\n    healthCheckPath: /api/health\n    envVars:\n      - key: DALANG_MODEL\n        value: \"@cf/google/gemma-4-26b-a4b-it\"\n      # Just under Cloudflare's free 10,000 neurons a day, so visitors\n      # see \"the dalang is resting\" instead of errors.\n      - key: DALANG_DAILY_BUDGET_USD\n        value: \"0.10\"\n      - key: CF_API_TOKEN\n        sync: false   # set in the dashboard, never in the repo\n```\n\nThe API key stays on the server; the browser never sees it.\n\nOne 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.\n\nThe address is **bayang-eyang.buatkamu.tech**. *Buat kamu* means \"for you\" in Indonesian.\n\nIn 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?\n\nFirst, the honest baseline: the same 12 stories and the same code for every model.\n\n| Model | Where | Real plays | First try | Typical wait | \n|---|---|---|---|---|\n| gemma3:1b | my laptop, offline | 0 of 12 | 0% | 27 s | \n| gemma3:4b | my laptop, offline | 4 of 12 | 0% | 4 min 11 s | \n| Qwen3.5-4B, untrained | Tinker | 0 of 12 | 0% | 10 s | \n| **Qwen3.5-4B apprentice** | **Tinker** | **10 of 12** | **83%** | **9 s** | \n| Gemma 4 26B A4B, the teacher | Workers AI | 12 of 12 | 75% | 25 s | \n\nSmall models fail here. They can write a story, but not one that a strict stage will accept.\n\nSo Gemma 4 became the teacher:\n\nThe 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.\n\nWhat 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.\n\nAnd 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.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n`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.\nI 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.\n\n*I built this with the help of an AI coding assistant (Claude Code). The story, the decisions and the grandpa are mine.*", "url": "https://wpnews.pro/news/my-grandpa-took-me-to-wayang-every-other-sunday-so-i-taught-an-open-model-to-be", "canonical_source": "https://dev.to/derbyps/my-grandpa-took-me-to-wayang-every-other-sunday-so-i-taught-an-open-model-to-be-the-dalang-38io", "published_at": "2026-10-31 15:11:05+00:00", "updated_at": "2026-10-31 15:22:33.643317+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "generative-ai", "ai-tools"], "entities": ["Gemma 4", "Google", "Cloudflare Workers AI", "Qwen3.5-4B", "Tinker", "Bayang Eyang", "Hacktoberfest"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/my-grandpa-took-me-to-wayang-every-other-sunday-so-i-taught-an-open-model-to-be", "markdown": "https://wpnews.pro/news/my-grandpa-took-me-to-wayang-every-other-sunday-so-i-taught-an-open-model-to-be.md", "text": "https://wpnews.pro/news/my-grandpa-took-me-to-wayang-every-other-sunday-so-i-taught-an-open-model-to-be.txt", "jsonld": "https://wpnews.pro/news/my-grandpa-took-me-to-wayang-every-other-sunday-so-i-taught-an-open-model-to-be.jsonld"}}