<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andy</title>
    <description>The latest articles on DEV Community by Andy (@guaiss).</description>
    <link>https://dev.to/guaiss</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3986043%2F2d86007e-5c12-4d4f-bd09-e89c0714653b.png</url>
      <title>DEV Community: Andy</title>
      <link>https://dev.to/guaiss</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/guaiss"/>
    <language>en</language>
    <item>
      <title>"Do Not Duplicate the Character" Duplicated the Character</title>
      <dc:creator>Andy</dc:creator>
      <pubDate>Thu, 13 Aug 2026 08:39:31 +0000</pubDate>
      <link>https://dev.to/guaiss/do-not-duplicate-the-character-3egb</link>
      <guid>https://dev.to/guaiss/do-not-duplicate-the-character-3egb</guid>
      <description>&lt;p&gt;We built a thing that takes one photo of a child and paints that child into a pre-written 10-page picture book. Thirty books, eleven pages each, on a free image model. Every page has to look like the same kid.&lt;/p&gt;

&lt;p&gt;Getting from "mostly works" to "every page of every book is clean" took about three weeks, and most of what I learned contradicted what I thought I knew about prompting. Here are the parts that generalize.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline finding
&lt;/h2&gt;

&lt;p&gt;At one point we had a page — a doctor's office scene — that kept rendering &lt;strong&gt;two children and two doctors&lt;/strong&gt;. Standard debugging instinct: be more explicit. So the prompt grew:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EXACTLY ONE child&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;the child appears only once, never duplicated&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;do not add a second doctor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;…plus a verbatim re-description of every character, their positions, the art style, the composition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Final prompt: 1,924 characters, with some version of "do not duplicate" appearing &lt;strong&gt;seven times&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Result: two children, two doctors.&lt;/p&gt;

&lt;p&gt;Then we cut the whole thing down to one sentence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add the child from the first image into the second image, &amp;lt;one short action&amp;gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. First try.&lt;/p&gt;

&lt;p&gt;We reproduced this on a second page from a different book with the same outcome. The mechanism, as far as we can tell:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Mention-is-summon applies to duplication itself.&lt;/strong&gt; Every time you write "do not duplicate," you put another instance of the subject into the context. The model is not parsing your negation; it is being handed the noun again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the same reason &lt;code&gt;no other moon&lt;/code&gt; in a prompt produces a second moon, and &lt;code&gt;no second kitten&lt;/code&gt; produces a second kitten. We hit both. Negation doesn't subtract — it just mentions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule we ended up with: to suppress something, never name it.&lt;/strong&gt; Not even to forbid it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state-level version, which is nastier
&lt;/h2&gt;

&lt;p&gt;"Mention is summon" is well known for objects. It also applies to &lt;em&gt;states&lt;/em&gt;, and that one cost us a day.&lt;/p&gt;

&lt;p&gt;We had a page that needed a &lt;strong&gt;collapsed&lt;/strong&gt; sandcastle. The scene description literally said &lt;code&gt;a sandcastle collapsing into a heap&lt;/code&gt;. The model painted an intact, perfect sandcastle. Every time.&lt;/p&gt;

&lt;p&gt;We tried &lt;code&gt;knocked down&lt;/code&gt;, &lt;code&gt;broken heap&lt;/code&gt;, &lt;code&gt;towers toppled&lt;/code&gt;. All produced pristine sandcastles.&lt;/p&gt;

&lt;p&gt;What worked was refusing to say the word at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a big flattened mound of damp sand where something was built and then squashed,
one broken turret fragment lying on its side
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3/3 clean.&lt;/p&gt;

&lt;p&gt;The noun &lt;code&gt;sandcastle&lt;/code&gt; summons the canonical form of a sandcastle, and adjectives are far too weak to overcome that. &lt;strong&gt;If you want a non-canonical state — broken, empty, collapsed, abandoned — describe the state without ever naming the object.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Commas make extra bodies
&lt;/h2&gt;

&lt;p&gt;This one is stupid and mechanical and cost us more pages than anything else.&lt;/p&gt;

&lt;p&gt;Our per-page prompts have an &lt;code&gt;action&lt;/code&gt; field. Actions written with commas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kneeling by the pond, holding a jar, looking up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…get read as parallel subjects, and you get two or three bodies performing them separately. One sentence, zero commas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kneeling by the pond and holding a jar while looking up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we finally audited the library, &lt;strong&gt;269 out of 310 pages had commas in the action field.&lt;/strong&gt; We had only noticed on the ~39 pages that were failing loudly enough to investigate. The rest were quietly carrying the same risk and we had never tested them.&lt;/p&gt;

&lt;p&gt;Worth internalizing: the pages that fail visibly are a sample, not the population.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position anchors: unique, or the model builds two
&lt;/h2&gt;

&lt;p&gt;Two related failures, and the fix for one is the cause of the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No position at all is the worst case.&lt;/strong&gt; If you describe a prop centered in frame with symmetric empty space on both sides, the model resolves the ambiguity by putting one on each side. We hit this on multiple pages; it looks like a "duplication bug" and it isn't — it's composition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A non-unique anchor is nearly as bad.&lt;/strong&gt; &lt;code&gt;at the right of the shelf&lt;/code&gt;, on a page where the shelf runs the full width, fails. &lt;code&gt;directly beneath the model ship&lt;/code&gt; — where there is exactly one model ship — works.&lt;/p&gt;

&lt;p&gt;And the two fixes are &lt;strong&gt;not independent&lt;/strong&gt;. On one page, removing commas alone gave 0/4 clean. Adding an anchor alone gave 3/4 duplicated. Both together: clean. On another page the pattern was reversed. We stopped trying to isolate them and just applied both everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the prompt is not the problem
&lt;/h2&gt;

&lt;p&gt;The most useful heuristic we developed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If six blind re-rolls of the same prompt can't produce one clean image, stop re-rolling and stop rewriting words. Go look at the base image.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The canonical case: a page whose background art had the main subject filling the entire frame. There was nowhere for a person to stand. Six different prompt rewrites × two base images = 22 duplicated results out of 25.&lt;/p&gt;

&lt;p&gt;We regenerated the background — wider shot, subject pushed to one side, deliberate empty ground on the other — and the same prompt went 3/3 clean immediately.&lt;/p&gt;

&lt;p&gt;Composition problems masquerade as prompt problems. Sampling harder just buys you more expensive failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: never reference the source photo per page
&lt;/h2&gt;

&lt;p&gt;The part that actually made cross-page consistency work is structural, not textual.&lt;/p&gt;

&lt;p&gt;The naive approach is to pass the child's photo as a reference on every page. Don't. Real user photos are hostile input — hair over the face, three-quarter angles, motion blur, other people in frame — and every page inherits that noise independently, so the kid drifts.&lt;/p&gt;

&lt;p&gt;Two stages instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uploaded photo
  → ONE canonical character sheet (clean front-facing full body, book art style, fixed outfit)
      → per page: image-to-image with refs = [character sheet, blank page art]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every page anchors to the same canonical image. Messy input gets normalized exactly once, in a step where you can inspect the result before spending eleven more generations. Cross-page consistency went from "usually" to "reliably."&lt;/p&gt;

&lt;p&gt;Two supporting tricks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed costume as an identity anchor.&lt;/strong&gt; The character's outfit is written verbatim into every page's identity clause and never changes. Pose and expression are the variables; the costume is the constant. If you're designing the source material yourself, give the protagonist one outfit for the whole book. (Watch the edge cases — we shipped a bedtime page where the kid slept in a raincoat, because the costume rule outranked common sense.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supporting characters live in the prompt, not the base image.&lt;/strong&gt; Image-to-image on these models is a full repaint, not a local edit, so "keep the teacher exactly as she is" always fails — she drifts or gets replaced. The fix is a per-page &lt;code&gt;cast&lt;/code&gt; string describing each supporting character, injected into the generation prompt. Then they get repainted &lt;em&gt;back into the same person&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two API-level things that cost real time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reference images were being silently dropped.&lt;/strong&gt; Our provider has an OpenAI-compatible endpoint that ignores unknown top-level fields. We were sending &lt;code&gt;image&lt;/code&gt; at the top level; it needed to be inside &lt;code&gt;extra_body&lt;/code&gt;. No error, no warning — every "image-to-image" call was quietly running as pure text-to-image, in production, for weeks. Character consistency on the free tier had been running on the text description alone.&lt;/p&gt;

&lt;p&gt;The tell was in the response: output URLs contained &lt;code&gt;/images/t2i/&lt;/code&gt; instead of &lt;code&gt;/images/i2i/&lt;/code&gt;. If your provider gives you any observable signal like that, assert on it in a test.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/images/generations&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1024x768"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extra_body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64 data-uri&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"response_format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"url"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also: &lt;strong&gt;pass references as base64, not URLs.&lt;/strong&gt; A hosted URL that expires or blips takes the whole job down, and it makes resuming a long batch depend on that URL still being alive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text-to-image and image-to-image can fail independently.&lt;/strong&gt; We spent an afternoon on what looked like a total outage. Same key, same model: t2i returned 200 in 14 seconds while i2i silently hung on every request regardless of reference or size. Not a 503, not a queue-full — just nothing. If you have both paths, health-check both.&lt;/p&gt;

&lt;h2&gt;
  
  
  QA: partial verification lies
&lt;/h2&gt;

&lt;p&gt;Last one, and it's a process lesson rather than a technical one.&lt;/p&gt;

&lt;p&gt;We reviewed in rounds, and each round only re-checked the pages that had been regenerated in that round. Completion looked like ~95%.&lt;/p&gt;

&lt;p&gt;Then we did one strict pass over every page of every book, and immediately found problems in pages that had been marked PASS rounds earlier: a bike that still had training wheels after the story said they came off, a duplicated object on a cover, one page that had never generated at all.&lt;/p&gt;

&lt;p&gt;Incremental QA over a generative pipeline accumulates false confidence, because "this page was fine last week" is a claim about a &lt;em&gt;sample from a distribution&lt;/em&gt;, not about a build artifact. Re-verify the whole thing before shipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Never name what you want suppressed — negation summons.&lt;/li&gt;
&lt;li&gt;To get a non-canonical state, describe the state and never say the noun.&lt;/li&gt;
&lt;li&gt;One sentence, no commas, in action descriptions.&lt;/li&gt;
&lt;li&gt;Anchor positions to something unique; no anchor is worse than a bad one.&lt;/li&gt;
&lt;li&gt;If K=6 re-rolls all fail, the base image or the composition is wrong, not the wording.&lt;/li&gt;
&lt;li&gt;Normalize identity once into a canonical reference; anchor every page to that.&lt;/li&gt;
&lt;li&gt;Assert that your reference images actually arrived.&lt;/li&gt;
&lt;li&gt;Re-QA everything before shipping, not just what you touched.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most of these reduce to the same underlying fact: &lt;strong&gt;the model is not following your instructions, it is conditioning on your tokens.&lt;/strong&gt; Every token you add is a vote for what appears. "Don't" is not a strong enough word to reverse a vote.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://www.doodara.com" rel="noopener noreferrer"&gt;Doodara&lt;/a&gt;, a free AI storybook maker for kids — the template books described here are &lt;a href="https://www.doodara.com/template-books" rel="noopener noreferrer"&gt;live&lt;/a&gt; if you want to see the output. Happy to answer questions about the pipeline in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>promptengineering</category>
      <category>machinelearning</category>
      <category>lessonslearned</category>
    </item>
  </channel>
</rss>
