{
    "componentChunkName": "component---src-templates-blog-post-js",
    "path": "/two-liars-one-dungeon-making-gpt-3-and-stable-diffusion-agree-on-a-world/",
    "result": {"data":{"site":{"siteMetadata":{"title":"catch { snail }","author":"Andrew"}},"markdownRemark":{"id":"e042059b-da99-5fba-850a-37d01d857390","excerpt":"A month ago Stability released the Stable Diffusion weights — actually released them, a real checkpoint you download and run, no waitlist, no API meter. After…","html":"<p>A month ago Stability released the Stable Diffusion weights — actually released them, a real checkpoint you download and run, no waitlist, no API meter. After two years of renting intelligence by the token, having a model <em>on my own GPU</em> feels almost transgressive. It draws anything, in about seven seconds, for the price of electricity.</p>\n<p>So naturally I tried to build a game with it. A tiny dungeon crawler: GPT-3 writes each room, Stable Diffusion illustrates it, you click exits and descend. Text model as dungeon master, image model as court painter. Three weekends later I have something playable, and a hard-won thesis:</p>\n<p><strong>Generation was never the problem. Agreement is the problem.</strong> These two models have never met. They share no world. One of them says “a rusted iron door, slightly ajar” and the other paints three pristine wooden doors and a chandelier. Each is a fluent improviser; together they’re two liars who haven’t compared notes. The entire engineering effort — like, 95% of it — went into building the room where they’re forced to agree.</p>\n<h2 id=\"the-setup\" style=\"position:relative;\"><a href=\"#the-setup\" aria-label=\"the setup permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The setup</h2>\n<p>Local: a 10GB consumer card, fp16 weights, the <code class=\"language-text\">diffusers</code> pipeline. 512×512, 30 steps, ~7 seconds a room. Meanwhile davinci writes the dungeon over the API. The naive pipeline is four lines:</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\">room_text <span class=\"token operator\">=</span> gpt<span class=\"token punctuation\">(</span><span class=\"token string\">\"Describe the next dungeon room.\"</span><span class=\"token punctuation\">)</span>\nimage <span class=\"token operator\">=</span> sd_pipeline<span class=\"token punctuation\">(</span>prompt<span class=\"token operator\">=</span>room_text<span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>images<span class=\"token punctuation\">[</span><span class=\"token number\">0</span><span class=\"token punctuation\">]</span></code></pre></div>\n<p>And the naive pipeline is garbage, for three separate reasons worth dissecting, because they’re the actual lessons.</p>\n<h2 id=\"failure-1-prose-is-a-terrible-interface\" style=\"position:relative;\"><a href=\"#failure-1-prose-is-a-terrible-interface\" aria-label=\"failure 1 prose is a terrible interface permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Failure 1: prose is a terrible interface</h2>\n<p>GPT-3’s lovely paragraph — moss, dread, a distant dripping — is exactly what SD does <em>not</em> want. Diffusion prompts aren’t sentences; they’re bags of weighted concepts. Feeding narration into the image model gives you mush.</p>\n<p>Fix: never let the models talk to each other directly. GPT-3’s real job is to emit a <strong>structured scene</strong>, and prose and prompt are both <em>renderings</em> of it:</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\">schema <span class=\"token operator\">=</span> <span class=\"token triple-quoted-string string\">\"\"\"{\n  \"name\": \"...\", \"mood\": \"...\",\n  \"key_objects\": [\"...\", \"...\"],     # max 3, concrete nouns\n  \"exits\": [{\"direction\": \"...\", \"description\": \"...\"}],\n  \"palette\": \"...\", \"light_source\": \"...\"\n}\"\"\"</span>\n\nscene <span class=\"token operator\">=</span> json<span class=\"token punctuation\">.</span>loads<span class=\"token punctuation\">(</span>gpt<span class=\"token punctuation\">(</span><span class=\"token string-interpolation\"><span class=\"token string\">f\"Continue the dungeon. Reply with ONLY this JSON:\\n</span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>schema<span class=\"token punctuation\">}</span></span><span class=\"token string\">\"</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span>\n\nprose  <span class=\"token operator\">=</span> gpt<span class=\"token punctuation\">(</span><span class=\"token string-interpolation\"><span class=\"token string\">f\"Narrate this room in 2 atmospheric sentences: </span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>scene<span class=\"token punctuation\">}</span></span><span class=\"token string\">\"</span></span><span class=\"token punctuation\">)</span>\nprompt <span class=\"token operator\">=</span> <span class=\"token string-interpolation\"><span class=\"token string\">f\"</span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>scene<span class=\"token punctuation\">[</span><span class=\"token string\">'name'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">}</span></span><span class=\"token string\">, </span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span><span class=\"token string\">', '</span><span class=\"token punctuation\">.</span>join<span class=\"token punctuation\">(</span>scene<span class=\"token punctuation\">[</span><span class=\"token string\">'key_objects'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">}</span></span><span class=\"token string\">, \"</span></span> \\\n         <span class=\"token string-interpolation\"><span class=\"token string\">f\"</span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>scene<span class=\"token punctuation\">[</span><span class=\"token string\">'light_source'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">}</span></span><span class=\"token string\">, </span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>scene<span class=\"token punctuation\">[</span><span class=\"token string\">'palette'</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">}</span></span><span class=\"token string\">, \"</span></span> \\\n         <span class=\"token string\">\"dungeon interior, dark fantasy concept art, highly detailed\"</span></code></pre></div>\n<p>(Yes, <code class=\"language-text\">json.loads</code> fails sometimes — davinci decorates its JSON with friendly commentary like a labrador bringing you a stick with the tree attached. One retry with the error message pasted back fixes most of it. Ask-for-JSON-and-pray is apparently a load-bearing pattern of our era.)</p>\n<p>The JSON is the <em>contract</em>. The text model may only invent within the schema; the image prompt is assembled deterministically from it; the game logic (exits! items!) reads the same object. One source of truth, three renderings. The moment I did this, doors started appearing where doors were described.</p>\n<h2 id=\"failure-2-the-world-redrew-itself\" style=\"position:relative;\"><a href=\"#failure-2-the-world-redrew-itself\" aria-label=\"failure 2 the world redrew itself permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Failure 2: the world redrew itself</h2>\n<p>Second visit to the same room: completely different room. Obvious in hindsight — sampling is stochastic, and a game world that won’t hold still isn’t a world, it’s a fever.</p>\n<p>Fix, and this is my favorite line of the project: <strong>make every room a pure function of the world seed.</strong></p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\">room_seed <span class=\"token operator\">=</span> <span class=\"token builtin\">int</span><span class=\"token punctuation\">(</span>hashlib<span class=\"token punctuation\">.</span>sha256<span class=\"token punctuation\">(</span><span class=\"token string-interpolation\"><span class=\"token string\">f\"</span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>world_seed<span class=\"token punctuation\">}</span></span><span class=\"token string\">:</span><span class=\"token interpolation\"><span class=\"token punctuation\">{</span>room_id<span class=\"token punctuation\">}</span></span><span class=\"token string\">\"</span></span><span class=\"token punctuation\">.</span>encode<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>hexdigest<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span> <span class=\"token number\">16</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">%</span> <span class=\"token number\">2</span><span class=\"token operator\">**</span><span class=\"token number\">32</span>\nimage <span class=\"token operator\">=</span> pipe<span class=\"token punctuation\">(</span>prompt<span class=\"token punctuation\">,</span> generator<span class=\"token operator\">=</span>torch<span class=\"token punctuation\">.</span>Generator<span class=\"token punctuation\">(</span><span class=\"token string\">\"cuda\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>manual_seed<span class=\"token punctuation\">(</span>room_seed<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n             guidance_scale<span class=\"token operator\">=</span><span class=\"token number\">7.5</span><span class=\"token punctuation\">,</span> num_inference_steps<span class=\"token operator\">=</span><span class=\"token number\">30</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>images<span class=\"token punctuation\">[</span><span class=\"token number\">0</span><span class=\"token punctuation\">]</span></code></pre></div>\n<p>Same seed, same prompt, same sampler → bit-identical image, forever. Room 14 of world <code class=\"language-text\">snail-7</code> looks the same on your machine as mine. Determinism isn’t a compromise with these models — it’s <em>available</em>, one <code class=\"language-text\">manual_seed</code> away, and suddenly you have shareable worlds, cacheable renders (hash the prompt+seed, never draw a room twice), and reproducible bug reports for a generative game. Old-fashioned engineering values, smuggled into the diffusion era.</p>\n<h2 id=\"failure-3-the-hero-had-forty-faces\" style=\"position:relative;\"><a href=\"#failure-3-the-hero-had-forty-faces\" aria-label=\"failure 3 the hero had forty faces permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Failure 3: the hero had forty faces</h2>\n<p>The player character appeared in room illustrations — as a different person every single time. Same prompt words, different guy. Descriptive text simply cannot pin down an identity; “a pale knight with a copper lantern” is a <em>category</em>, and SD samples a fresh member of it each call.</p>\n<p>The fix is the newest tool in the box: <strong>textual inversion</strong>. You train a new embedding — a made-up token like <code class=\"language-text\">&lt;snail-knight></code> — against a handful of images of the character you want, and the concept itself becomes a word the model knows. Twenty minutes of training on my card, and now:</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">prompt = \"&lt;snail-knight> standing in \" + room_prompt</code></pre></div>\n<p>…produces <em>the same knight</em>, room after room. This one floors me more than the base model does. The open weights mean the vocabulary is writable — identity as a token, consistency as a training artifact. A month ago the frontier was “what can it draw”; the actual frontier is “what can it draw <em>twice</em>.”</p>\n<h2 id=\"what-the-toy-taught-me\" style=\"position:relative;\"><a href=\"#what-the-toy-taught-me\" aria-label=\"what the toy taught me permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>What the toy taught me</h2>\n<p>Standing back, every fix was the same fix wearing different hats: <strong>don’t let the models hold the state.</strong> The world lives in a JSON graph my code owns; the seed pins the visuals; the embedding pins the character. The models are renderers — spectacular, alien renderers — at the edges of a boring deterministic core. Creativity at the edges, truth in the middle. I keep arriving at this architecture from different directions, which either means it’s right or I only have one idea.</p>\n<p>And it reframes what “multimodal” is going to require. Everyone’s excited that we have a text mind and an image mind; the unglamorous news is that they don’t share a world <em>model</em>, so someone has to build the world and make both of them serve it. The prompt is an API between two minds that have never met — and like every API, the schema matters more than the eloquence.</p>\n<p>The dungeon itself? Honestly, it’s a slideshow with delusions. But it’s a slideshow where every room exists twice — once as words, once as light — and they finally match. My daydream post in July wanted NPCs with a mouth on a leash; turns out worlds need the same leash for their <em>eyes</em>. The cage keeps growing new rooms.</p>\n<p>Total cost: one warm GPU, three weekends, and whatever davinci charged me for all that JSON it wrapped in apologies.</p>","frontmatter":{"title":"Two liars, one dungeon: making GPT-3 and Stable Diffusion agree on a world","date":"September 24, 2022","description":"The weights are public, my GPU is warm, and I spent three weekends on a tiny game where one model writes the rooms and another draws them. The hard part isn't generation. It's agreement."}}},"pageContext":{"slug":"/two-liars-one-dungeon-making-gpt-3-and-stable-diffusion-agree-on-a-world/","previous":{"fields":{"slug":"/the-games-ill-build-when-the-npcs-can-think/"},"frontmatter":{"title":"The games I'll build when the NPCs can think"}},"next":{"fields":{"slug":"/the-harness-ate-the-model/"},"frontmatter":{"title":"The harness ate the model"}}}},
    "staticQueryHashes": ["3435731857","426816048","63159454"]}