Showing Posts From
Automation
-
Ahmed Arat - 28 Feb, 2026
- 22 Mins read
Fuck Prompt Engineering. Sincerely, Ranty Dev.
Part One: The Anatomy of Slop Let me paint you a picture. You've just spent six hours writing a 3,000-word article about a niche technical topic, packed with code snippets, architectural diagrams in your head, and enough spite to make Ramsay Bolton blush, and now you need a hero image for the dev server preview. You're a dev-blogger; this is a normal Sunday evening for you. So you boot up your custom 900-line Node.js script to auto-gen that bitch up, right? No of course not, you're a lazy bastard like I am. (Until last week anyways). What you actually do is what every other lazy bastard does: you copy-paste the entire fucking article into ChatGPT or Nano Banana or whatever's trendy this week, add the magic words "minimalist, flat vector, no text, clean editorial style," and hit enter. What you get back is frankly what you deserve. Neon-soaked cyberpunk hacker hunched over a mechanical keyboard like it's 1999. Glowing green rain. Matrix code dripping down a monitor that looks like it was designed by someone who's only seen computers in films. And, my personal favourite, a screen filled with gibberish text that says something like "JVSCRPT DMMR" in a font that looks like it was assembled by a drunk Victorian typesetter.You said no text. It drew text anyway. Why? Tempting it may be to call the AI stupid. And it may very well be, but reality is often disappointing, is it not? The Transformer architecture is fundamentally an issue of Lady Whistledown, where every token talks to every other token, and you just fed it a novel-length scream. The failure mode is mathematical, predictable, and completely fucking avoidable if you know the thing I'm about to show you. Self-Attention: The Toxic Gossip Train I've garnered a bit of a reputation for referencing Attention Is All You Need. You know, that paper that kicked off this entire circus? In that very paper, Self-Attention was introduced. Every token looks at every other token simultaneously to figure out what the hell is going on. Mathematically, for $(n)$ tokens, you're doing: $$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$ Where $(Q)$, $(K)$, and $(V)$ are your Query, Key, and Value matrices. At this point, softmax is not as important as complexity. Because every token chats with every other token, your memory and compute cost scale as $(O(n^2))$ with sequence length.FlashAttention is $(O(n^2))$ in compute/FLOPs but achieves $(O(n))$ memory through tiling and recomputation. It lowers the VRAM/SRAM footprint but computational complexity stays quadratic, which means the main constraint (at least for SOTA models running on enterprise hardware and data centres) is still compute. More important for our purposes: as the input gets longer, the model's "attention" gets spread thinner than Marmite on toast. It has to pay attention to everything. Your frontmatter YAML, your inline code blocks, your hyperlinks, your prose, your headers, your existential dread... until the actual visual signal you care about drowns in the fucking noise. Now, to be precise: when you feed a long prompt to a text-to-image pipeline, it doesn't hit the generation model raw. It goes through a text encoder first; CLIP, T5, whatever the flavour of the month is, which then produces fixed-size embeddings regardless of input length. The $(O(n^2))$ dilution happens there, in the encoder, as it struggles to compress your 3,000-word article into a meaningful embedding vector. The result is what I call "encoder saturation": a mushy, low-signal blob that the generation model then has to work from. The framing is the same. The specific culprit is the encoder stage. There's also more here because CLIP has a hard token limit of 77 including the start/end tokens. It makes the problem worse because depending on your inference provider's internal harness, they either throw away the rest of the prompt, or pass it through a shitty "distillation model" to make sense of your content just like I do, except said model is EXTRA SHITTY. They have traffic constraints to deal with, remember? When you feed a 3,000-word Markdown blog post into an image generator, you're not giving it a rich input. I made this point before when I talked about AGENTS.md files. The wisdom behind giving your agent rich context to work with is technically correct but prone to being overdone. You need only enough to prevent wrong assumptions or else you're giving it a flooded basement and asking it to find a specific teacup. The attention weights distribute across every token, the signal-to-noise ratio becomes total dogshit, and the output is chaotic because the input was a fucking mess. Cross-Attention: LALALA CAN'T HEAR YOU Self-attention explains why the output is noisy. Cross-Attention explains why it spells words like a drunk toddler. In text-to-image diffusion, your prompt gets chewed up by a text encoder (usually CLIP or a language model), producing embeddings. These embeddings then get fed into the image generation via Cross-Attention layers, where the image generation blocks "look at" the text to decide what to draw. Here's the problem: in the latent space of a model trained on the entire internet (including, regrettably, Twitter a.k.a X), the concept of "a blog post about JavaScript" is mathematically adjacent to the visual concept of "text on a screen." These ideas have high cosine similarity because that's how they appear in the training data. Articles about code are overwhelmingly represented as... text on screens. When you say "draw a hero image for this JavaScript article," the Cross-Attention layers pull on embeddings that are entangled with visual representations of... text. The model cannot easily disentangle "the topic of JavaScript" from "the visual of JavaScript" which is, in the training data, invariably a glowing IDE, a mechanical keyboard, and neon text. And oh God, the garbled text. That's the model generating "text-like pixel patterns" because it learned that "screen with content" correlates strongly with "shapes that look vaguely like letters." It doesn't know what an 'A' looks like. It knows what the idea of text looks like, and that's close enough for its purposes, even if it's nonsense. In data science, we call this Compositional Generalisation. If a model has Lexical Compositionality, it doesn't see a "man-with-a-hat" as one big blob of pixels instead it understands the relationship between them. When a model fails at it, we call it Attribute Binding failure (e.g., you ask for a "red hat and a blue shirt" and get a "blue hat and a red shirt"). This is the reason why when you tell the model "no text" in the negative, it helps, but is like asking a shark not to swim. That is, you're fighting against the gravitational pull of the entire training distribution by asking it to draw a concept while actively suppressing the most common visual representation of that concept. Circa 2022-2023, it was a dumpster fire with simple, short prompts including negative prompts. The "Pink Elephant" issue comes to mind. Nowadays, with larger corpora of data containing more metadata/descriptions of images and antonymic comparisons, we can safely insert negative prompts to some degree. Six-Fingers: Three Solutions This brings us to the elephant in the room. Or rather, the six-fingered hand in the room. If you were using AI image generators circa 2022–2023, you know the pain. Hands were fucked. Too many fingers. Too few fingers. Fingers that looked like they were trying to escape the hand. It became a cultural shorthand for AI-generated slop. "Look, six fingers" was the easiest way to spot the fraud. Or strange ways of consuming spaghetti. Hands are high-frequency, spatially complex, and appear in the training data in every possible pose, lighting condition, and occlusion state. Without the hands being contextualised properly in the training set's metadata, the model's learned representation of "hand" was a blurry average of all those cases, and when forced to render one at high resolution, it hedged its bets and grew extra digits. Interestingly, the six-finger problem has been largely "fixed" in modern models. The question is: how? The first mechanism is intrinsic behavioural change through training data and RLHF. The big labs identified the problem, weighted more anatomically correct hand data, and used Reinforcement Learning from Human Feedback to penalise deformed outputs. The fix is baked into the weights and the model doesn't follow a rule that says "draw five fingers." It has learned, at a parameter level, that five-fingered hands have higher reward signal than six-fingered ones. This is intrinsic because it lives inside the model. The second mechanism is a harness. External constraints applied at inference time, without touching the weights. A negative prompt is the simplest example. A pipeline of structured prompts is a more sophisticated one. The harness doesn't change what the model knows intrinsically. It changes the conditions under which the model generates, nudging it away from low-quality attractors. This is extrinsic and lives outside the model. The third mechanism is external multimodal simulation. Instead of asking one model to do everything, you decompose the task across multiple models, each handling a specific subtask it's genuinely suited for, and integrate the outputs. This is a harness taken to its logical conclusion. Instead of constraining a single model, we replicate the specialist-model decomposition that happens internally in a proper multimodal architecture. We do it externally, with independently swappable components. A human or an orchestrator script acts as the integrator. These three mechanisms are frequently discussed as though they were interchangeable, but they are not. They have different failure modes, different scopes, and they can interfere with each other in ways that are instructive. Which brings me to the hero image for this very article. The image at the top depicts a six-fingered hand transforming to a five-fingered hand. It's the visual metaphor for this entire argument. It's also, I can tell you from direct personal experience, an image that the current generation of production-grade models fucking refused to generate. The prompt was wholly unambiguous. The pipeline produced a clean, structured brief. The Art Director stage correctly identified the concept. The image generation stage received a precise, constrained prompt asking for a six-fingered hand as the input to a machine which is a fairly deliberate visual element, the entire fucking point of the illustration. And yet every modern model I tried (Nano Banana Pro, Imagen 4, SeeDream v4.5, Flux 2 Flex) ignored the six-finger instruction and drew a perfectly normal hand. The intrinsic RLHF fix baked into the weights was so aggressively applied that it overrode an explicit creative instruction. The model couldn't be told to draw six fingers, even as a deliberate metaphor, because its learned parameters treat "six fingers" as an error condition to be suppressed regardless of context. This is an overfitting problem. RLHF is extraordinarily effective at pushing model behaviour toward high-reward outputs. It is much less effective at teaching the model when a particular behaviour is appropriate. The model learned "six fingers = bad, suppress it." It did not learn "six fingers = bad in a realistic portrait, intentional in a satirical illustration." We call this "reward hacking." The model does not understand the true underlying quality it is being penalised or rewarded for leading to literal interpretations of the human feedback. The solution, then, was to lean into the third mechanism, external multimodal simulation at a component level. I found an older, less aggressively fine-tuned model that still had the original glitchy-hand failure mode intact. I used it to generate only the deformed hand. I then took the pipeline's clean, correct output i.e. the machine, the setting, the style, and then manually composited the glitchy hand from the legacy model into it in Photoshop. This is where the external simulation approach reveals its defining advantage over any unified architecture: composability. Because the pipeline is a collection of independently addressable components rather than a fused set of weights, I could swap one component for a version with different intrinsic behaviour, get precisely the output I needed from it, and slot it back in. A proper multimodal model gives you no such lever. The sub-models are fused. The weights are the weights. You get what the training run decided, and if the RLHF pass decided six-fingered hands are always wrong, you have no appeal. Talk about a dictator, eh? The hero image you see is a Frankenstein's monster. The competence is from the pipeline. The deliberate incompetence is from a model that was not yet "fixed." The stitch is human. And the ability to do any of that is a direct consequence of simulating the multimodal architecture externally rather than relying on one model to do everything. I am telling you this not as a charming anecdote, but because it is a near-perfect empirical demonstration of everything I am about to describe. The intrinsic RLHF fix worked so well that it blocked a naïve single-model approach. The external simulation approach routed around it by treating model versions as interchangeable components. That is the entire fucking argument, in one image. Right. Back to the maths. If you understand the architecture, the solution becomes obvious:You cannot give the image model a noisy, long-form input. The $(O(n^2))$ attention dilution in the encoder will cause it to lose focus. You need to compress the input to a dense, visually relevant signal before it reaches the image model. You cannot let the image model decide what "visual" means for a given concept. Its Cross-Attention will default to the most statistically probable visual representation in the training data, which for technical content is almost always "text on a glowing screen." You need to manually decouple the semantic concept from its default visual representation. Negative prompts are not sufficient. They shift the generation away from certain vector clusters in the latent space, but they cannot override strong associative priors and as we have just seen, they cannot override intrinsic RLHF behaviour at all. You need to provide a positively constrained, narrow target, not just a set of exclusions.This is why a single-prompt approach is a dead end when it comes to long-form data. You need a fucking pipeline. Specifically, you need to simulate, externally and explicitly, the kind of specialist decomposition that a proper multimodal architecture would do internally and you want to do it externally precisely because that gives you the ability to swap components when the model's intrinsic behaviour gets in your way. Part Two: Building a Digital Sweatshop Let's be precise about what we are building before a single line of code appears. The pipeline I am about to describe is an external multimodal simulation. It replicates, using independently addressable components wired together by a harness, the specialist-model decomposition that happens inside a proper multimodal architecture... except we're doing it on the outside, where we can see it, control it, and swap individual pieces out when they misbehave. A real multimodal model has sub-models fused together by training. You get what the training run produced, full stop. Our simulation has sub-models that are independently versioned, independently replaceable, and independently constrainable. The six-finger compositing in Part One is not a workaround. It is this property in action. The implementation mechanism is a harness: a system of external constraints applied at inference time that does not touch any model's weights, does not retrain anything, and produces no intrinsic behavioural change. The harness structures the conditions under which each specialist model is asked to generate, compensating for architectural limitations we cannot fix from the outside. This is both its greatest strength and its hard ceiling. The harness is model-agnostic, flexible, and deployable without access to training infrastructure. It is also, as we have already seen with the six-finger debacle, completely powerless against intrinsic behaviour baked into the weights by RLHF. When the harness meets a strong enough trained prior, the prior wins. Every fucking time. The answer, when that happens, is not to try harder with the harness. It is to swap the component. Keep that in mind as we go through each stage. Each one is doing a specific job, compensating for a specific architectural failure mode. For each one, I will be explicit about where the harness ends and where the model's intrinsic behaviour begins. Here is the full generate-hero-images.mjs pipeline at a high level before we get into each stage: const DISTILL_MODEL = "gemini-3-flash-preview"; // The cheap speed-reader. Speed = 100 TPS, Comprehension=NULL const BRIEF_MODEL = "gemini-2.5-pro"; // The pedantic art director who never made it onto Broadway const IMAGE_MODEL = "gemini-3.1-flash-image-preview"; // The volatile artist. Unlikely to start a war because we won't reject itThree models. Three distinct jobs: one external multimodal simulation.Stage One: The Speed-Reader (gemini-3-flash-preview) The first problem to solve is context dilution. A blog post is not a prompt. It is a document chock-full of structural noise, tangential prose, code snippets that are visually irrelevant, and MDX component tags that mean absolutely fuck-all to an image generator. This is the $(O(n^2))$ problem made practical. Feeding a 3,000-word article to an image generation pipeline is giving it a flooded input. The encoder saturates, the signal-to-noise ratio collapses, and the visual output is chaotic because the embedding was a mushy average of everything you wrote. The longer the sequence, the worse this gets. The simulation solves this by intercepting the input before it reaches the sensitive part of the pipeline. Stage One is a cheap, high-context model whose singular job is to read the entire article and compress it into a dense paragraph of raw visual facts without metaphors or interpretation. async function distillArticle(ai, content) { const cleanText = stripMdxNoise(content).slice(0, 25000); const prompt = [ "Analyze the following blog post text.", "Your goal: Extract the 'Visual Context' for a technical illustrator.", "Please identify:", "1. The Core Subject (What is explicitly happening?).", "2. Concrete Objects (List physical/UI things mentioned).", "3. The Vibe (e.g. 'Frustrated rant', 'Clean tutorial', 'Theoretical deep-dive').", "", "Output a dense paragraph of text summarizing these visual ingredients.", "Do not interpret. Do not use metaphors. Just facts.", "", `TEXT:\n${cleanText}`, ].join("\n"); const res = await ai.models.generateContent({ model: DISTILL_MODEL, contents: prompt, config: { temperature: 0.1 }, }); // ... }Several deliberate choices are worth calling out because I'm not just throwing shit at the wall here. The stripMdxNoise() function is pre-processing that happens before we touch the API at all. It strips MDX component syntax, replaces code blocks with [CODE BLOCK] placeholders, removes image tags, and discards link URLs. The model does not need to know what my <Callout> component looks like. It needs to know that the article is about bypassing a DRM by hooking the browser's MSE API. We clean the signal before it even enters the noisy channel. function stripMdxNoise(mdx) { return mdx .replace(/```[\s\S]*?```/g, " [CODE BLOCK] ") .replace(/!\[[\s\S]*?\]\([\s\S]*?\)/g, " [IMAGE] ") .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1") .replace(/<[^>]+>/g, " ") .replace(/[`*_~]/g, " ") .trim(); }The 25,000-character cap is a practical concession to the very problem Stage One is solving. Even with a model that has a massive context window, a focused input produces a more focused output. This is the harness compensating for an architectural limitation it cannot fix. We cannot make the attention mechanism cheaper, but we can give it less to attend to. The temperature: 0.1 setting is the creativity knob turned almost all the way down. At 0.1, the model is nearly deterministic. We do not want creativity here. We want the model to read the article like a filing clerk on amphetamines and produce the same output reliably, every time, for the same input. For reference, Gemini models tend to do well at around 0.7-0.8 for coding, 0.8-0.9 for conversation, and 1.0 for nearly everything else. The output of Stage One for the DRM article looks something like this:A developer examines and defeats a JavaScript-based DRM system on an NSFW audio streaming platform. Key objects: browser developer tools, JavaScript source code, audio waveform, a web browser, a hex editor. Concrete actions: intercepting API calls, hooking prototype methods, reassembling audio chunks. Vibe: technical rant, cat-and-mouse engineering challenge.That is your article, compressed from 5,000 words of snark, code, and philosophy into a single paragraph of actionable visual information. The context dilution problem is solved at the harness level. We pass this into Stage Two. Stage Two: The Art Director (gemini-2.5-pro) We now have a dense, clean summary. But we still have a problem that compression alone cannot solve: if we feed even this clean summary to an image model with a vague instruction like "draw a minimalist hero image," the Cross-Attention layers will default to the most statistically probable visual representation of the concept in the training data. "Developer, browser, code" → glowing screen, keyboard, neon aesthetic. The intrinsic prior is still there, and a clean summary does not override it. The harness needs to go further. We need to manually decouple the semantic concepts from their default visual representations and do, by hand, the kind of structured decomposition that the model's Multi-Head Attention attempts but cannot reliably perform under freeform prompting. Recall from Part One: $$\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h)W^O$$ $$\text{where } \text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)$$ Different attention heads are supposed to learn to attend to different types of relationships: syntax, coreference, semantic similarity, all in parallel. The intuition is sound. The practice, under freeform prompting, is that the heads compete for salience, and the strongest trained prior tends to dominate. The harness solves this by doing the decomposition manually, upstream of the model, separating each visual dimension into a discrete, structured channel before it ever reaches the image generator. This is the job of Stage Two. We impose a constraint that is absolute: the model is only allowed to respond in strict JSON. It is not allowed to draw anything. It is not allowed to write prose. It is only allowed to fill out a fucking form. const schema = [ "Respond ONLY with valid JSON. No markdown fences, no prose.", "{", ' "primaryConcept": "6–12 words describing what to literally draw",', ' "motifs": ["0–2 supporting concrete objects max"],', ' "setting": "simple setting/background idea, 1 line",', ' "composition": "one line layout instruction",', ' "palette": ["3–5 colour names"],', ' "styleNotes": ["2–4 short style constraints"],', ' "negative": ["things to avoid"]', "}", ].join("\n");Each key in the schema is a manually separated attention dimension. Colour goes in palette. Layout goes in composition. Subject goes in primaryConcept. By forcing the semantic extraction into these isolated channels, we are doing the work that Multi-Head Attention would theoretically do automatically but doing it reliably, explicitly, and in a form that the image generator will receive as a pre-organised signal rather than a tangled mess. This is the crux of what external simulation can do that intrinsic training, at the current level of model capability, does not do consistently under freeform input. The model has the capacity to reason about colour, composition, and subject simultaneously. It does not consistently separate those concerns into clean, non-interfering outputs when given a freeform prompt. The simulation enforces the separation extrinsically, and because each stage is its own addressable component, we can tune or replace the Art Director independently of everything else. Here is the full prompt construction for Stage Two: async function generateVisualBrief(ai, distilledContent) { const prompt = [ "You are an art director for a minimalist technical blog.", "Based on this article summary, create a visual brief for a flat-vector hero image.", "", "ARTICLE SUMMARY:", distilledContent, "", "RULES:", "- The image must be 16:9, flat vector, 2D only. No 3D, no photorealism.", "- NO text, logos, letters, or numbers of any kind in the image.", "- Use clean geometric shapes, icons, abstract representations.", "- Colour palette should be editorial and muted, not neon or garish.", "- The image must be immediately legible as a thumbnail at 400px wide.", "", schema, ].join("\n"); const res = await ai.models.generateContent({ model: BRIEF_MODEL, contents: prompt, config: { temperature: 0.4 }, }); return extractJsonFromText(res.text); }temperature: 0.4 makes it slightly more creative than Stage One, because we do want the Art Director to make interesting choices about palette and composition. Not so creative that it goes completely off-piste and decides a unicorn would look nice. The extractJsonFromText() function is a defensive measure. Models, even when told explicitly "no markdown fences," sometimes wrap their JSON in triple backticks anyway. They are pattern-completion machines, and the pattern of "JSON in a response" is overwhelmingly represented with fences in the training data. This is, incidentally, a perfect small example of intrinsic behaviour a harness cannot fully override. We can tell it not to do this, but it will still do it some percentage of the time. So we strip defensively instead of letting the bastard fuck us over: function extractJsonFromText(text) { const start = text.indexOf("{"); const end = text.lastIndexOf("}"); if (start === -1 || end === -1) return null; try { return JSON.parse(text.slice(start, end + 1)); } catch { return null; } }For the DRM article, Stage Two might produce something like: { "primaryConcept": "A padlock made of source code being dissolved by a key", "motifs": ["audio waveform", "browser window outline"], "setting": "Abstract dark background with subtle geometric grid", "composition": "Central padlock, key approaching from left, waveform as supporting element below", "palette": ["slate grey", "electric indigo", "off-white", "deep charcoal"], "styleNotes": [ "flat vector 2D only", "clean geometric shapes", "no gradients heavier than 20% opacity shift", "editorial minimalism" ], "negative": [ "no text", "no letters", "no numbers", "no photorealism", "no 3D render", "no neon", "no hacker cliches", "no keyboards", "no glowing screens" ] }This is infinitely better input for an image generator than the raw article. The concepts are clean, separated, and constrained. The Art Director has done its job. Now we just need to stop the final model from fucking it up.Stage Three: The Restricted Artist (gemini-3.1-flash-image-preview) We now have a fully formed visual brief in structured JSON. The final step is to assemble this into a prompt and hand it to the image generation model. This is where the harness meets its ceiling and where we must be honest about what the simulation can and cannot do. The negative array, the style constraints, the composition instruction: all of these are harness-level controls. They work by pushing the generation away from low-quality attractors in the latent space. They are effective. They are not infallible. If the model has a strong enough intrinsic prior for a given concept like we saw with the six-finger brief then the harness loses. When the harness loses, you do not write a better prompt. You swap the misbehaving component like you wish Krampus did with your stubborn middle child. That is the entire point of externalising the simulation. buildImagePrompt() takes the JSON brief and reconstructs it into a tightly controlled natural language prompt: function buildImagePrompt(brief) { const enforced = [ "no text", "no letters", "no numbers", "no watermarks", "no signatures", "no UI chrome", "no photorealism", "no 3D", ]; const allNegatives = [ ...enforced, ...(brief.negative ?? []), ]; return [ `Create a 16:9 flat vector illustration.`, ``, `SUBJECT: ${brief.primaryConcept}`, brief.motifs?.length ? `SUPPORTING ELEMENTS: ${brief.motifs.join(", ")}` : "", `SETTING: ${brief.setting}`, `COMPOSITION: ${brief.composition}`, `COLOUR PALETTE: ${brief.palette?.join(", ")}`, ``, `STYLE REQUIREMENTS:`, ...(brief.styleNotes ?? []).map((n) => `- ${n}`), ``, `ABSOLUTELY AVOID:`, ...allNegatives.map((n) => `- ${n}`), ] .filter(Boolean) .join("\n"); }The all-caps headers (SUBJECT:, COMPOSITION:, ABSOLUTELY AVOID:) are not arbitrary stylistic choices. It is theorised that headers in the training data are overwhelmingly associated with high-importance, structurally significant information. The model's attention layers allegedly weight these anchors more heavily. This is the harness exploiting an intrinsic attention pattern i.e. using the model's learned formatting heuristics as a leverage point, rather than fighting against them. I couldn't find any papers corroborating this hypothesis, but there are no known disadvantages to including them. The enforced array is hardcoded and cannot be overridden by the JSON brief. I do not care what the Art Director said. "No text" is always in the negatives. Always. This is a lesson learned from early testing, where the Art Director occasionally decided that a floating letter or a subtle icon would look "charming." It would not. Broadway still won't call. But note the limitation being acknowledged here: these are harness-level constraints. They are instructions layered on top of the model's weights, not baked into them. Against a weak-to-moderate trained prior, they work reliably. Against a strong intrinsic prior e.g. "hands have five fingers, suppress six" they will fail. The harness can push but it cannot override. When it can't override, swap the fucking component. The actual image generation call: async function generateImage(ai, prompt, outputPath) { const res = await ai.models.generateContent({ model: IMAGE_MODEL, contents: prompt, config: { responseModalities: ["IMAGE", "TEXT"], }, }); for (const part of res.candidates[0].content.parts) { if (part.inlineData) { const imgBuffer = Buffer.from( part.inlineData.data, "base64" ); await fs.writeFile(outputPath, imgBuffer); return true; } } return false; }We iterate over parts rather than assuming the response is a simple single blob. Gemini's multimodal response format can include both text commentary and image data as separate parts. We are only interested in the inlineData, which are the actual image bytes. The output is saved as a PNG to the .ai-images directory, keyed to the article slug. The pipeline also carries a cache layer that checks for an existing image before making any API calls because burning API quota to regenerate an image that already exists is exactly the kind of thing that will make you question every life decision you have made. Have you seen how absurdly expensive Nano Banana Pro is? const cacheFile = path.join(CACHE_DIR, `${slug}.json`); if (!FORCE && readJsonIfExists(cacheFile)) { console.log(`[cache hit] ${slug} — skipping.`); return; }The --force CLI flag bypasses the cache, for when you actually want to regenerate. The --slug flag targets a single article. The --brief-only flag runs only Stages One and Two and prints the JSON brief without touching the image model, which is useful for debugging the Art Director's output before committing to an API call or for generating off-platform with models you have no API access to. Part Three: Wrapping it Up Let me walk through a complete end-to-end execution. The full picture is more satisfying than the individual parts. You run: node generate-hero-images.mjs --slug javascript-drm-bypassLoad environment. The script reads the .env file and extracts the Google GenAI API key.Resolve the article. It reads the frontmatter from src/content/posts/javascript-drm-bypass.mdx using gray-matter, extracts the title and body, and passes the raw MDX through stripMdxNoise().Cache check. It looks for .ai-images/cache/javascript-drm-bypass.json. If it exists and --force wasn't passed, it exits early with a smug [cache hit] message.Stage One: Distill. (Simulation compensating for $(O(n^2))$ encoder saturation.) The clean text, capped at 25k characters, is sent to gemini-3-flash-preview at temperature: 0.1. The response is a dense, factual visual summary, stripped of everything the image generator does not need.Stage Two: Brief. (Simulation manually separating semantic dimensions the model cannot reliably decompose under freeform prompting.) The visual summary is sent to gemini-2.5-pro with the JSON schema constraint. The response is parsed, JSON is extracted defensively, and the brief object is validated.Stage Three: Generate. (Harness meets intrinsic behaviour. Harness-level constraints apply; intrinsic priors apply harder, in cases of conflict. If a component fails, swap it.) The brief is assembled into a structured image prompt via buildImagePrompt() and sent to gemini-3.1-flash-image-preview. The response is parsed for inlineData parts, and the image bytes are written to .ai-images/javascript-drm-bypass.png.Cache write. The brief JSON is written to .ai-images/cache/javascript-drm-bypass.json.Total cost for the DRM article: three API calls, roughly 45 seconds of wall clock time. The hero image lands in .ai-images. Et voilà, a perfectly suitable hero image for your dev server. Totally not over-engineered, amirite? Conclusion: Three Mechanisms. Know Which One You're Using. We have established that there are three distinct mechanisms for shaping the behaviour of a generative model, and they are not interchangeable: Intrinsic behavioural change through training data and RLHF is the only mechanism that actually changes what the model is. It lives in the weights. It cannot be prompted around. It cannot be overridden by a harness when the trained signal is strong enough. It is the most powerful mechanism available, and the most blunt precisely because a reward signal that successfully penalises deformed hands also, it turns out, penalises intentionally deformed hands in a satirical illustration, because the model has no mechanism to distinguish the two. A harness is an external system of constraints applied at inference time. It does not touch the weights. It compensates for architectural limitations i.e. context dilution, freeform semantic blending, probabilistic defaulting to high-frequency training patterns by structuring the conditions under which the model is asked to generate. It is flexible, model-agnostic, and deployable without access to training infrastructure. Against a weak-to-moderate intrinsic prior, it works reliably. Against a strong one, not so much. External multimodal simulation is a harness taken to its logical conclusion: decomposing a task across multiple specialist models, each constrained to the subtask it handles reliably, wired together with the integrating logic that a unified multimodal architecture would bake into its training. It is not smarter AI as much as it is smarter system design. And it has a property that neither of the other mechanisms can offer: composability. Because the simulation is external, each component is independently addressable. You can version them separately. You can upgrade the distillation stage without touching the Art Director. You can pin the image model to a specific checkpoint. And most importantly, the six-finger debacle demonstrated that you can reach for a different model when the current one's intrinsic behaviour is precisely your problem. A unified multimodal model gives you no such lever. The sub-models are fused. The RLHF pass that fixed five-fingered hands affects every generation, in every context, with no appeal mechanism. Externalising the simulation is what makes component-level surgery possible. The hero image at the top of this page is the empirical proof of all three. The pipeline produced a clean, correct illustration of a machine. The harness could not override the intrinsic RLHF fix that has effectively banned six-fingered hands from modern production models. So I swapped the component: I found an older model that predated the fix, generated the deformed hand from it, and composited it manually in Photoshop. Three mechanisms, one image. The competence is from the simulation. The deliberate incompetence is from a model whose intrinsic behaviour hadn't yet been "corrected." The stitch is a human exercising the composability that only an external architecture affords. In truth, you do not even need Photoshop. If you chained an older model to Nano Banana (the image editing model), you could have done the same thing. That model is capable of editing in addition to generation. The pipeline would look more like Flash + Pro 2.5 + DALL·E + Nano Banana. If you want to use this pipeline yourself, the full code is on GitHub. The architecture itself is model-agnostic but some blocks of code may need rewriting since the API shapes for your inference provider may be different. Now, if you'll excuse me, I have a Pixeldrain proxy to write about. Until next time :)1. [Attention Is All You Need — Vaswani et al., 2017 (arXiv:1706.03762)](https://arxiv.org/abs/1706.03762) 2. [Scaled Dot-Product Attention — §3.2, Attention Is All You Need](https://arxiv.org/abs/1706.03762) 3. [Multi-Head Attention — §3.2.2, Attention Is All You Need](https://arxiv.org/abs/1706.03762) 4. [CLIP: Learning Transferable Visual Models From Natural Language Supervision — Radford et al., 2021 (arXiv:2103.00020)](https://arxiv.org/abs/2103.00020) 5. [CLIP Maximum Token Length (77 tokens) — OpenAI CLIP GitHub](https://github.com/openai/CLIP) 6. [High-Resolution Image Synthesis with Latent Diffusion Models (Stable Diffusion) — Rombach et al., 2022 (arXiv:2112.10752)](https://arxiv.org/abs/2112.10752) 7. [Denoising Diffusion Probabilistic Models — Ho et al., 2020 (arXiv:2006.11239)](https://arxiv.org/abs/2006.11239) 8. [Training language models to follow instructions with human feedback (InstructGPT / RLHF) — Ouyang et al., 2022 (arXiv:2203.02155)](https://arxiv.org/abs/2203.02155) 9. [Constitutional AI: Harmlessness from AI Feedback — Bai et al., 2022 (arXiv:2212.08073)](https://arxiv.org/abs/2212.08073) 10. [Compositional Generalization — Lake et al., 2017 (arXiv:1711.00350)](https://arxiv.org/abs/1711.00350) 11. [Attribute Binding in Text-to-Image Generation — Rassin et al., 2023 (arXiv:2301.13826)](https://arxiv.org/abs/2301.13826) 12. [Attend-and-Excite: Attention-Based Semantic Guidance for Text-to-Image Diffusion Models — Chefer et al., 2023 (arXiv:2301.13826)](https://arxiv.org/abs/2301.13826) 13. [VSF: Simple, Efficient, and Effective Negative Guidance in Few-Step Image Generation Models By Value Sign Flip — Wenqi Guo et al., 2025 (arXiv:2508.10931)](https://arxiv.org/abs/2508.10931) 14. [Gemini: A Family of Highly Capable Multimodal Models — Google DeepMind (arXiv:2312.11805)](https://arxiv.org/abs/2312.11805) 15. [Google GenAI SDK — Node.js / JavaScript Reference](https://googleapis.github.io/js-genai/) 16. [gray-matter — npm](https://www.npmjs.com/package/gray-matter) 17. [Softmax Function — Wikipedia](https://en.wikipedia.org/wiki/Softmax_function) 18. [Big O Notation — Wikipedia](https://en.wikipedia.org/wiki/Big_O_notation) 19. [Cosine Similarity — Wikipedia](https://en.wikipedia.org/wiki/Cosine_similarity) 20. [Latent Space — Wikipedia](https://en.wikipedia.org/wiki/Latent_space)