# Generating the images with codex CLI

The whole chain runs from one place. `codex exec` has a built-in image generation tool, so
phase 4 needs no API key and no separate service — the user's existing ChatGPT login is
enough.

Verified working with `codex-cli 0.144.6`.

## The call

```bash
codex exec -s workspace-write \
  -i assets/images/originals/master/<master>.png \
  --output-last-message /tmp/result.txt \
  "$(cat prompt.txt)"
```

| Flag | Why |
| --- | --- |
| `-s workspace-write` | Required. Without it the write is rejected: *patch rejected: writing outside of the project*. |
| `-i <file>` | Attaches reference images to the prompt. **Repeatable.** This is the consistency mechanism. |
| `--output-last-message` | Captures the final report so you can verify what it claims it produced. |

**`git init` the project before the first generation.** Outside a git repository codex
refuses to run — *"Not inside a trusted directory and --skip-git-repo-check was not
specified"* — and it fails almost silently: exit code 0, an empty result file, no images.
Every site ends up in version control anyway, so initialise and commit the placeholder
build first rather than reaching for `--skip-git-repo-check`.

Check the log, not just the exit code — codex can report success having produced nothing.

**Redirect stdin from `/dev/null` whenever the call is backgrounded.** Without it codex
blocks — but at the *end*, after the work is done and the file is already written. The tell
is a log containing nothing but `Reading additional input from stdin...`, a live process,
and an empty `--output-last-message` file. The trap is the obvious reaction: seeing no
report, you conclude nothing ran and start again, when the asset on disk has already been
replaced. Before assuming a silent run produced nothing, check the file's mtime and
`~/.codex/generated_images/`.

Each image costs roughly 50k tokens and 1–3 minutes. Run it with `run_in_background` and
wait on a file-count condition rather than polling.

## Aspect ratio is controllable — and exact

State the ratio per image and mark it a hard requirement, plus an instruction to *say so
rather than silently return a square*. In a 19-image run every single output landed on
its exact requested ratio:

```
4:3   → 1448 × 1086      3:2  → 1536 × 1024
3:4   → 1086 × 1448      1:1  → 1254 × 1254
16:9  → 1664 × 936
```

This matters more than it sounds: it means the placeholder's **binding ratio can drive
generation directly**, with no post-crop, so nothing is lost and the layout never moves.
Verify it anyway — read the PNG header rather than trusting the report:

```python
import struct
with open(p,'rb') as f: f.read(16); w,h = struct.unpack('>II', f.read(8))
```

## The consistency strategy

This is the part that decides whether the set looks like one real business or twenty
stock photos.

**0. List the invariant subjects first.** An invariant subject is anything that must look
*identical* in every image it appears in. The number of masters is not a fixed 5 — it is
one per invariant subject, and the business decides how many there are.

| Business | Invariant subjects | Masters |
| --- | --- | --- |
| Apartment building | The block · the unit interior finish · the common areas | 3–5 |
| Sole practitioner | **The practitioner** · the premises | 2–3 |
| Restaurant | The room · the tableware and plating style · (the chef, if person-led) | 2–3 |

Nothing gets generated before its subject has a master. This is the whole mechanism: an
image with no master to reference will silently invent its own version of the subject, and
that is where a set stops looking like one real business.

**1. Masters first, in a single session.** Generate them in **one** `codex exec` call,
ordered so each builds on the last ("after generating image 1, treat it as the reference
for images 2 and 3"). One session means the model sees its own earlier outputs, which
produces tighter continuity than any prompt wording.

**Cross-check the masters against each other, not only against their briefs.** When one
master contains another invariant subject — a portrait of the practitioner shot inside the
room that is itself a master — the two can disagree, and everything downstream inherits
both. Compare them on the things a later frame will have to reproduce: which wall the
window is on, what order the fixtures run in, where the light falls. If they conflict,
decide which one is authoritative and regenerate the other against it *before* approving
either. Two individually excellent masters can still be an incoherent reference set, and
that is not visible from looking at either one alone.

**2. Approve the masters before expanding.** Everything downstream inherits them. This is
a real checkpoint with the user, not a formality.

**Once approved, the master is the fact — later briefs bend to it, not the reverse.** A
master will often diverge from what you asked for. If the divergence is plausible and the
image is otherwise strong, rewrite the *remaining* briefs and any page copy to match it.
Rerunning a good master to satisfy the original brief trades a known-good result for a
gamble, and the model will not reproduce it. Seen in practice: a doorway master came back
as an external walkway rather than the top of an interior stair, so the stairway brief and
the "how to find me" copy were re-aimed at a walkway. Cheaper, and the set stayed coherent.

**When one element of a good frame is wrong, change that element — do not reshoot.** Attach
the previous version as the first reference and say so: *this is a targeted change to one
part of the image, not a new photograph.* Then name what must not move — camera position,
framing, light, and the specific props you want kept. A shopfront whose composition, light
and propped samples were all right but whose signboard rendered as abstract blocky shapes
came back with only the board replaced, everything else pixel-for-pixel. Regenerating it
from the original brief would have gambled a known-good frame.

**Signage and logos: feed the actual asset, not a description.** Image models cannot set
type, so a brief that describes a wordmark gets shapes that look like writing. Attach the
rasterised `mark.svg` / `lockup.svg` as extra `-i` references and instruct the model to
reproduce the mark rather than invent one. In the run above the mark came back accurate —
four plates in the right order, the registration ring knocked out of them — and the wordmark
came out legible and correctly spelled as a bonus, which is not something to rely on but is
much likelier when the real letterforms are in front of it.

**Name masters so they cannot collide with a placeholder id.** `Placeholder` resolves assets
by *basename*, so a master saved as `master/shop-front.png` silently becomes the asset for
the `shop-front` slot. Prefix them — `master-shopfront.png` — and the two namespaces stay
apart.

**3. Then fan out in parallel batches, each attaching the masters with `-i`.** Batches are
independent sessions, so group by subject and give each the masters it needs:

```bash
# One batch per subject group; each attaches only the masters it needs.
codex exec -s workspace-write -i $M/<interior-master>.png  "$(cat batch-a.txt)" &
codex exec -s workspace-write -i $M/<interior-master>.png  "$(cat batch-b.txt)" &
codex exec -s workspace-write -i $M/<exterior-master>.png -i $M/<common-master>.png "$(cat batch-c.txt)" &
wait
```

Three concurrent runs work fine. Cross-batch continuity held: cabinets, appliances,
flooring, curtains, wall colour and even the armchairs carried across sessions that never
saw each other's output — only the shared masters.

### Rebasing: changing a scene while preserving something exact

`-i` is not only for masters. When a frame is right in one respect and wrong in another —
a whiteboard carrying a correct worked equation in a room with the wrong layout — attach the
existing image as the base and describe **only the delta**, spelling out verbatim what must
survive:

```
The attached photograph is the CURRENT image. Produce a modified version.
CHANGE: <the one thing>
MUST NOT CHANGE: <reproduce the exact text, line by line, including the heading>
If any character of it changes, the image is unusable.
```

This preserved a six-line Thai algebra derivation through a full re-composition. Write the
new file under a **different name**, inspect it, and only then swap it in — the production
file is never in a broken state, and the superseded versions stay as evidence.

**Keep every superseded version**, outside the asset graph (`assets/images/reference/`, not
`assets/images/originals/`). Regeneration loses details silently, and the earlier frame is
the only record of what a later one gave up.

## When a person is an invariant subject

Person-led businesses — a sole practitioner, a named chef, a one-person studio — make the
proprietor the hardest invariant in the set. Faces drift far more than cabinets do, and a
viewer who would never notice a changed countertop will notice instantly that it is not
the same woman.

- **Generate the person master before anything else**, and attach it with `-i` to every
  subsequent image she appears in — including images where she is incidental.
- **Fix her in the invariant block** the way materials are fixed for a building: approximate
  age, hair length and style, skin tone, build, uniform colour and cut, jewellery, whether
  glasses. Copy that block verbatim into every prompt.
- **Prefer poses that carry the brand over portraits that carry the face.** Hands at work,
  a *wai* at the door, mid-treatment from behind the client. These are both more useful
  commercially and more forgiving of drift.
- **Check identity per image, not per set.** Put the master and the new image side by side
  and ask whether a returning customer would recognise her. Same clothes is not the same
  as same person. Crop both faces to a common size before judging — at different scales
  and angles you will talk yourself into a match that is not there.
- **An age or appearance modifier is a licence to redraw the face.** "Five years younger",
  "when he first opened", "before the weight came off" — the model reads these as permission
  to regenerate the person, and hands back a different individual wearing the right clothes.
  Hold the face fixed and vary only the context.
- **Escalate rather than reword.** The ladder is: reword the description → state that the
  face is *copied, not reinterpreted*, and that only the setting changes → **reframe so the
  face is not in the shot at all**. Two failed rewordings mean the model holds a prior you
  are not going to argue it out of, so go to the third rung instead of writing a third
  description. A historical frame shot from behind at middle distance carries the gown, the
  certificate and the day perfectly well, and puts nothing on trial.
- The person is fictional. Give the business a name that is **checked against real
  businesses in that city and trade** before it is used — a common first name plus a
  common trade is exactly the collision that produces a demo resembling someone's actual
  livelihood.

## Prompt structure

Build every batch prompt from one shared header plus per-image blocks:

```
[header]  Reference-image instruction · subject invariants · photographic treatment ·
          forbidden list · output rules (exact paths, hard ratios, "don't touch other files")
[per image]  FILE · ASPECT RATIO · SCENE · MOOD
```

Keep the invariants and the forbidden list byte-identical across batches. The `MOOD` line
is what stops the output drifting into stock-photo blandness — write it about the viewer,
not the picture.

## The register trap — dated, dead, or over-corrected

The most expensive failure family in this phase, and the one that produced four
regenerations across three sites before it was understood. Every step of it came from a
brief that was obeying itself.

### "Not aspirational" and "not contemporary" are different axes

A brief that says *unglamorous · documentary · slightly underexposed · do not flatter it*
is trying to stop stock-photo gloss. What the model has no other way to hear is **"make it
look old"** — because in its training the visual correlates of "not glossy" are sodium
street light, tube fluorescents, yellowed paint, beige plastic and no digital equipment.
Three sites came back looking like **2005**: a Thai accounting office with no monitor worth
the name, a coordinator's desk with a beige handset and a desk lamp for light, a classroom
lit by bare tubes.

Showing a working business as shabby is not honesty. It is a different inaccuracy, and it
**undersells the client the demo exists to win** — a real owner looking at it sees a photo
of a worse business than theirs.

So state the era **positively**, as an invariant with a reject test, in the site's own
`requirements.md` and in the prompt header:

> **Era.** Present day, unambiguously. Cool-white LED lighting, never sodium and never
> exposed fluorescent tubes. Flat panels, never a CRT or a beige tower. Clean paint, intact
> surfaces, tidy cabling. **If a frame could plausibly be a photograph from twenty years
> ago, reject it.**

Keep the anti-gloss instruction — it is still right — but pair it with the era line. The
target is *modest and current*, and the two words do different work.

### The correction is not to stack devices

The obvious over-correction is to list digital equipment in the brief. The generator will
dutifully stack it: one attempt returned a desk with **three displays and no keyboard and
no mouse anywhere**. That reads as a showroom, not a workstation, and it costs more
credibility than the screens buy.

> **Equipment density.** One screen per person, plus the things a hand actually touches —
> keyboard, mouse, pen, paper, a handset off its cradle. A desk is credible because of what
> is reachable and worn, not because of how many panels are on it.

### A room with nobody in it reads as closed

The same over-correction removed people. An operations business, a school, an office
photographed entirely without anyone looks abandoned — and it usually **contradicts the copy
beside it**: an empty coordinator's desk under *the coordinator is a person, on a phone, not
a voicemail*; an empty classroom under *six students to a class, two seats left*.

> **People are required in at least one frame, not merely permitted.** Adults who look like
> the staff who do this work at this hour, in ordinary clothes, working — seen from behind
> or three-quarter, never facing camera, never posed.

**A blanket `NO PEOPLE` is almost always a rule that escaped from somewhere else.** In this
family it originated as a real, correct constraint on one site — a tutoring centre, where no
child may be identifiable — and was copied into two sibling briefs where nothing justified
it. When a person-related ban appears in a brief, find the site it came from and check
whether the reason travels.

### The layout must be the copy made visible

The subtlest one, and it survives an era fix untouched. A tutoring centre that caps every
class at six was rendered as **rows of state-school desks with the teacher lecturing from
the front** — modern, well lit, and still wrong, because that is not what six-to-a-class
looks like. Six students sit round one table and the teacher leans in over the work.

Ask of every interior: *does the arrangement of the furniture assert the same thing the
content model asserts?* An image can contradict the content model as surely as a stale
share card can, and it is the same class of defect.

### Feature tests, not pose tests

When a constraint protects someone, write it as the feature that must be absent, not as the
pose that must not occur. `nobody turning` is a proxy: a child bent over a book *is* turned,
shows a jaw edge, and identifies nobody — so the proxy rejects safe frames while saying
nothing about the reflection in the window behind them. The rule that works:

> No eye, no nose and no mouth visible, **by any route, including reflections in glass,
> whiteboards and screens**.

Stricter where it matters, permissive where it does not, and checkable.

## Review traps

**Check every frame against the sentence it sits beside, not only against its brief.** The
brief is something you wrote, so verifying the image against it is a closed loop: the model
satisfies the brief, the check passes, and both can still be wrong together. A frame can
match its brief exactly and contradict the copy next to it — a photograph of a session
placed under a heading that says *the first meeting is free and you don't train*. Read the
section heading and the image together, in the built page, the way a visitor meets them.

**Do that mechanically, from the built HTML, rather than from memory of what you intended.**
Pair every `<img>` with its nearest preceding heading and print the two together. Reading
the list takes a minute and it surfaces things recall cannot:

```python
body = html.split('<main', 1)[-1].split('</main>')[0]
toks = re.findall(r'<h[1-3][^>]*>(.*?)</h[1-3]>|<img[^>]*?alt="([^"]*)"', body, re.S)
heading = ''
for head, alt in toks:
    if head: heading = re.sub(r'<[^>]+>', '', head).strip()
    elif alt: print(f'  [{heading}]\n     -> {alt[:100]}')
```

In one pass this found two defects nothing else would have: a **double-escaped ampersand**
(`&amp;amp;`) introduced by an *earlier alt-text correction*, which a screen reader would
read out literally; and **both machine photographs sitting under the wrong machine's
heading**, because the loop rendered two spec cards and then two images, so the wide-format
printer's photo landed beneath "Colour digital press" and read as its caption. Neither is
visible in the source — only in the order a visitor meets them.

**Inspect every human silhouette at 3x for generation artifacts.** Hair, shoulders and
jaw edges are where diffusion models fail, and the failure is a clean geometric bite — a
straight-edged notch out of a hairline — which is invisible at page scale and unmistakable
once seen. One shipped, and a reader found it before any check did. Crop each figure, scale
it up, and look at the outline specifically; do not rely on noticing it while reviewing
composition.

**Judge at full resolution, never from a contact sheet.** A grid of thumbnails is the
efficient way to review composition and the wrong way to review anything else — and because
it is efficient, it is what you will reach for. At 640px a brushed stainless gate reads as
painted cream and was one keystroke from being rejected as the wrong material; the same
downscale hides an unchained gas cylinder, a missing grinder guard and a weld bead run with
the wrong process. **This applies to material and finish, not only to safety** — the surface
of a thing is exactly the information a downscale destroys first. Open the file and zoom.

**Ask whether the practice shown in the frame is competent — with the eyes of someone who
does that work.** Where a business sells expertise, its photographs depict that expertise
being performed, and every visitor who has it will audit them. This is the failure that
refutes the site's entire argument faster than any copy can repair, and it is invisible to
a reviewer who does not know the trade: a wrong instrument grip, raw meat beside prepared
salad, a car held on a single jack, a barbell squatted outside the rack with no safety arms
fitted and the trainer reaching across the bar from the front. Nothing else on a review
checklist catches it, because everything else asks about framing, consistency and mood.
Put the question first, and if nobody in the room knows the trade, go and read what good
practice looks like before approving the set.

**Real-world trade dress sneaks in.** *Incident:* a street scene came back carrying a
real convenience-store chain's green / orange / red livery — no readable text, but
unmistakable branding.
Models reproduce what the scene "should" look like. Inspect for recognisable brand colour
schemes, not just readable logos. If the user decides to keep one, record it in the image
design system as an approved exception so a later pass doesn't "fix" it.

**Update the `alt` to describe the frame you actually got.** Generations drift from the
brief, and sometimes the drift is an improvement — a requested close-up of hands came back
as a wider frame that included the practitioner's face, which served a person-led brand
better. But the alt text still said the face was out of shot. Alt describes the image, not
the instruction: re-read every alt against its delivered asset before shipping, or screen
reader users get the brief instead of the photograph.

**Blank images in screenshots are usually a lie.** Multi-megabyte PNGs decode slowly and a
screenshot can catch them mid-paint. Check `naturalWidth` before believing an image failed
to load.

## Two-stage delivery

Stage 1 embeds the **unprocessed originals** so what you review is byte-identical to what
was generated. Serve them from a `public/` symlink and plain `<img>` — not Astro's image
pipeline, which would convert them.

```
ln -s ../assets/images/originals public/img
```

Have the `Placeholder` component resolve an image by `id` and fall back to the placeholder
box when none exists, so the swap is a data operation and no page is edited:

```ts
const files = import.meta.glob('../../assets/images/originals/**/*.png');
const byId = new Map(Object.keys(files).map((path) => {
  const rel = path.split('/originals/')[1];
  return [rel.split('/').pop().replace(/\.png$/, ''), `/img/${rel}`];
}));
```

Emit `data-generated` so the manifest reports progress (`19 generated, 0 outstanding`).

Expect stage 1 to be heavy — 19 originals is ~42 MB and the built site ~97 MB. That is the
accepted cost of reviewing what was actually generated. Stage 2 converts to responsive
WebP through the image pipeline and the numbers collapse.

Keep every original that was not rejected, under `assets/images/originals/`, and commit
them — they are the only reproducible source if the site is ever re-cut at different
crops.
