Every guide on getting text into AI-generated images says the same thing: keep it short, keep it big, and hope. That is folk wisdom, and folk wisdom is what you get when nobody isolates the variables.
So I isolated them.
The setup
Think of a text-to-image call as a function with a lot of arguments, most of which nobody holds constant when they benchmark it. Model, seed, aspect ratio, subject description, style, and the actual text instruction all move at once in a typical comparison, and then somebody publishes a conclusion about text rendering.
My setup pinned everything except one argument:
generate(
model: "Tsubaki.3", // fixed
seed: <locked>, // fixed, identical on every run
ratio: "3:4", // fixed
subject: <one paragraph>, // fixed, reused verbatim
style: none, // no preset, no LoRA
batch: 1, // single generation, never best-of-four
text: <THE ONLY VARIABLE>
)
That last constraint matters more than it looks. Generating four and picking the nicest one measures your fourth attempt, not the model's instruction following. Every result below is a first and only run.
I also ran a control with the text argument empty, which turned out to be the most informative run of the whole set.
The subject was a woman in her fifties repairing box kites on a coastal dune, carrying four checkable details: short grey hair, goggles on her forehead, a brown patch on the right hip of her apron, and a spool of orange cord on her left forearm. Countable features give you a diff target.
Test 1: two text elements
A title and a subtitle, requested in large capitals in the empty sky at the top.
Both correct. Title cap height 163 pixels, subtitle 31, clean gap between them, nothing overlapping the character.
Test 2: five text elements
Title, tagline, date, a two-line information block, and a corner badge, in one call. I chose the strings so no word repeated anywhere on the poster, which gives you traceability: every rendered string maps to exactly one instruction.
All five elements appeared in the requested positions. Four were correct. The fifth was a small corner block where NINE rendered as NIVE, while NORTH DUNE on the line beneath it came out right.
The hierarchy is the interesting part, because it is measurable rather than a matter of taste. Cap heights, top to bottom:
| Element | Cap height |
|---|---|
| Title | 164 px |
| Tagline | 31 px |
| Date | 22 px |
| Badge | ~18 px |
| Info line 2 | 17 px |
| Info line 1 | 15 px |
That ordering matches the importance I assigned each element in the prompt. I never specified a single size. The model inferred the ranking from the semantic role of each string, which is useful behavior if you are generating a lot of layouts.
Note where the error landed: the 15 pixel line, the smallest thing on the poster. Two pixels separate it from the correct line directly below.
Test 3: swap the script, hold everything else
Same seed, same position, same instruction shape, comparable title length. Only the writing system changed.
- Latin: correct
- Japanese (3 characters, kanji plus hiragana): correct
- Korean (4 Hangul blocks): 1 of 4 blocks correct
The Korean failure mode is the one to internalize. All four blocks are valid, well formed Hangul syllables. Spacing is even. Stroke weight is consistent. The output looks completely correct and says the wrong thing.
This is the difference between a crash and silent data corruption, and it has the same implication: you cannot validate this by looking at it, only by having someone read it.
Test 4: relational layout instructions
Three simultaneous position constraints in one prompt, expressed relative to the subject rather than to the canvas: title above her head, date in the lower left, badge in the upper right, all text clear of her face and the kite.
Then the same prompt with one addition: reserve the top third as empty sky.
Run A honored all three positions and put a lowercase i in the middle of an all-capital title, plus a small non-letter mark after the final D.
Run B produced every word correctly and solved the reservation by building something I never asked for: a solid header strip across the top 23.7 percent of the frame with the title set inside it. Her head starts at 34.8 percent, the kite at 36.2, both below the band as requested. The badge left its assigned corner and attached itself to the end of the title.
Across both runs: 15 element position requests, 14 honored.
The finding nobody benchmarks
Here is why the empty control run mattered. I measured the vertical position of the top of the character's head as a fraction of frame height:
| Run | Head position |
|---|---|
| Control, no text | 25.7% |
| Layout run A | 28.2% |
| Japanese | 33.0% |
| Five elements | 34.2% |
| Korean | 34.7% |
| Layout run B | 34.8% |
| Two elements | 35.0% |
Same seed on every row. Every run carrying text pushed the subject lower and made it smaller, by 2.5 to 9.3 percentage points.
The text is not composited over a finished illustration. It is drawn in the same pass, out of the same frame budget, and the artwork is what gets compressed to make room. In run B a quarter of the output stopped being a picture entirely.
If you are generating assets programmatically, this is a real constraint on your pipeline: you cannot add a text argument and expect the visual to stay put, and you cannot fix a bad string with a retry without rebuilding the composition around it.
Aggregate results
- 29 Latin words requested, 27 rendered exactly
- 2 failures: one wrong letter, one case defect
- Large display type: correct in 100% of runs
- Both failures at small size or under heavy layout constraints
- Japanese correct, Korean 1 of 4 blocks
- 15 position requests, 14 honored
- Hierarchy correctly ranked in every layout, unprompted
What I would ship
Generate the composition and the headline together, since that is where the model is strong and where it does work you would otherwise do by hand. Treat every returned string as untrusted input and read it character by character. Set fine print, dates, prices, and anything legally fixed in an editor afterward. And never ship a script you cannot read without a human check, because the failure mode there is invisible.
Short display text is close to solved. Everything below about 20 pixels of cap height is still yours to handle.
You can run the same test on your own character at https://eap.pixai.art/go/balazs


Top comments (0)