---
title: "Brand-Exact Composite — your real asset, inside a generated scene"
tier: hero
format: product-marketing
theme: product-marketing | brand | packshot | ui | trademark-safety
persona: product marketer, brand designer, founder, developer advocate, agency creative
duration: "a launch-ready still in ~4 minutes when the plate clip is built locally; ~6 if the plate has to be animated"
budget_usd: "~$0.13 composite route with a locally built plate clip · ~$0.45 if the plate must be animated · ~$0.12 generative route"
caps: ["flux-dev", "grok-image-2-edit", "qwen-image-3-edit", "gpt-image-edit", "krea-2", "hyperframes-render", "ffmpeg-overlay"]
verbs: ["run_capability", "create_media", "upload", "logo_composite"]
skills: ["livepeer-cli"]
showcases:
  - title: "The audit that caught it — 33/33 vs 30/33 on the same asset"
    href: "/chapters/brand-exact-composite-example.html"
status: "live (2026-08-14, revised 2026-08-19) — run end-to-end on production. The audit gate caught a real 30/33 miss; the composite route recovered every character; keystone, cylinder and the combined mesh warp are each verified on the production renderer. Showcase is unretouched output. The 2026-08-19 revision adds the glass-fit step and measured geometry checks, and follows two shipped fixes: the overlay document now travels by handle rather than through the agent's context, and the idempotency key now includes the overlay, so a nudged corner re-renders instead of replaying. One defect remains open and is documented rather than fixed — the keystoned edge is not antialiased (docs/caps/hyperframes-render-edge-aliasing-2026-08-19.md)."
reliability: 4.4 # 5 − .4 the edit re-renders the mark (3 measured runs: 33/33, 32/33, 30/33) − .2 edit latency is volatile; the two transport defects found on 2026-08-19 are FIXED (overlay travels by handle; idempotency key covers the overlay) so the ~140KB asset budget is real again and the nudge loop works; one cosmetic defect open (keystoned edge not antialiased, measured, not fixable in-page); plate / glass-fit / audit / exact-overlay steps deterministic; +.5 proven E2E on production, recovery path included
---

# Brand-Exact Composite — agent playbook

> Paste this whole file (with the BRIEF filled in) into Claude cowork, chat, or Code. The agent walks through it with you and stops for your approval at every step that matters.

## What you'll get

One launch-ready image where **your real asset appears inside a generated scene** — a plugin interface on a monitor, a label on a bottle, a dashboard on a laptop, a logo on a package:

- **A generated plate** — the room, the light, the surface. Disposable; re-roll it until you like it.
- **The composite** — your **actual file**, keystoned onto that surface in the scene's perspective. Not a re-drawing of it: the same bytes you supplied.
- **An accuracy audit** — every word and number on your asset, read back and compared against the original. This is the step that catches what nobody catches.
- **A trademark check** — evidence the model didn't invent a competitor's name on your product.
- **An image sheet** — a self-contained HTML page holding the original asset, the plate, the final image and the audit table, at `public/launches/{piece_slug}-composite.html`.

**Time**: ~4 minutes wall clock, most of it waiting on renders. **Your attention**: about 4 minutes across 6 quick approvals. **Cost**: ~$0.13 for the composite route, ~$0.12 for the generative one.

The compositor needs a *video* background, which used to make the composite route the expensive one — animating the plate cost more than every other step combined. It doesn't any more: for a still deliverable the plate clip is built locally from the plate itself (Step 3), which is free, instant, and — unlike an i2v model — actually holds still.

## Why this playbook exists

Ask any image model to "put our interface on the screen" and it **invents** one. It looks finished, so nobody checks. In testing, a model handed a blank screen and two reference images returned a plausible audio-plugin browser carrying **four real competitor product names that were never in the prompt**.

### The one rule

> **If your asset contains text, do not let a model render it. Composite the real file.**

A model handed your UI does not paste it — it *re-draws* it. That re-drawing is where labels turn to mush, `80` becomes `60`, corner radii change, and a version number appears that your product doesn't have. It is not a prompt problem and no amount of "do not redesign" fixes it, because reproducing pixels is not what the model is doing.

So the route is chosen from **what your asset is**, not from how the first attempt happens to look:

| Your asset | Route |
|---|---|
| Any text, labels, numbers, a wordmark, a chart, a legal mark, a UI | **Composite it** (Step 3). Non-negotiable. |
| Pure shape and colour — a silhouette, an icon, an abstract mark | Either. The generative pass (Step 6) buys you nicer light. |

The composite route is not a fallback in this playbook. It is the default, and Step 6 is the alternative.

## Tell the agent about the piece

```yaml
piece_name:         # e.g. DreamSampler XT-2 launch still
asset_url:          # public https URL of your EXACT asset (transparent PNG if it sits on a surface)
asset_must_survive: # the exact strings that have to be right, comma-separated
                    # e.g. DREAMSAMPLER, v2.4.1 BUILD 8871, CUTOFF 2.4 kHz, MIX 72 %
scene:              # the world around it, e.g. producer's studio desk at golden hour, warm rim light
surface:            # where the asset lands, e.g. the widescreen monitor
shape:              # 16:9 | 1:1 | 9:16
piece_slug:         # kebab-case, used in filenames, e.g. dreamsampler-xt2
```

No public URL for the asset? Say so at Step 0 — the agent hosts it for you, from your machine or from pasted bytes.

## How the agent should run this (interaction contract)

1. **CONFIRM (one message, ≤1 question):** restate in one line ("your interface keystoned onto a generated studio plate, 16:9, ~$0.13, ~4 min"). If the asset has text, do NOT offer the generative pass as a coin-flip — state that you will composite the real file and why, and let them override. One question max.
2. **PREVIEW CHECKPOINT:** the plate is the last cheap decision. Never move on until the user has approved a plate whose surface is genuinely **blank** and whose four corners are all in frame — and read those corners off the plate yourself rather than asking the user to measure them. The corners you want are the **glass**, not the panel; Step 1b is how you find them and how you prove you found them.
3. **NARRATE:** the composite is one call that runs 30s–5min depending on model and queue. Post a one-liner when it starts and at least every 2 minutes while it runs. Never go silent through it.
4. **FAIL GRACEFULLY:** a corner-pin that looks off → move one corner, don't switch routes (and vary the `idempotency_key`, or you will be handed the previous render — see Step 3) · render times out → re-run in the background and hand back a job id rather than blocking · on the generative route, a wrong string → do NOT re-prompt the same model twice (switch models, they fail differently), and after two models composite the real file instead. Two attempts per model, maximum.
5. **DELIVER:** the image sheet plus one honest line about which route ran and what that guarantees ("this is your actual PNG keystoned onto the screen — every character is the file you gave me") and ONE next step.

## How this works

Six gates on the main path — brief, plate, glass fit, route, placement, accuracy — plus one each for the optional fan-out and the generative alternative. At each one the agent shows you what it made and asks one question. You answer in one word or one short phrase. "approve" → next step. "refine [direction]" → redo this step.

Nothing here is expensive. The route gate at Step 2 is what keeps a text-bearing asset away from a model; the fit gate at Step 1b is what keeps the asset on the glass instead of across the bezel; and the accuracy gate at Step 5 is what catches either failure if it slips through.

## The steps

### Step 0 — Confirm the brief, and get the asset online

The agent reads back your BRIEF so you can spot typos, and confirms it can actually fetch your asset.

If you don't have a public URL, the agent hosts the file and uses the URL it gets back. It'll tell you which route it took.

> **For the agent**: the asset budget is **~140 KB raw** — a 200 KB overlay document, less base64's 4-bytes-per-3. That figure is now reachable, but it was not before 2026-08-19: `logo_composite` used to hand the finished document back to you and you had to relay it into `create_media({ html })` verbatim, which corrupts at around **26 KB of base64** and fails *silently* — a damaged base64 tail damages the tail of the image, so the asset's top renders and its bottom garbles. The document now travels as **`html_url`**, a handle resolved server-side, so the bytes never cross your context. **Pass `html_url` straight through: do not fetch it, re-inline it, or paste its contents** — doing so re-creates exactly the bug it exists to prevent. If `logo_composite` warns that it could not store the document and returned `html` inline, the old ~16 KB ceiling applies for that call and it will say so.
>
> Export as **PNG or WebP, never JPEG**. A 1920×1080 UI measures ~63 KB as PNG and ~60 KB as WebP, but **160 KB as JPEG** — rejected. JPEG is the wrong codec twice over here: bigger on flat UI regions AND softer on exactly the small type you are preserving. Downscaling to roughly the glass quad's own width (typically 800–1000 px inside a 1920-wide frame) costs nothing either, since the homography throws the extra resolution away.
>
> The models take a public https URL — they cannot read the user's disk. If the asset is a local path or missing, host it: `upload({ data, mime_type })` for pasted bytes (~3 MB inline ceiling), `create_upload_url({ content_type })` for anything larger when you have a shell, or `scripts/host-local-file.sh <path>` on a machine with `gcloud` (no size limit). SVG is rejected on upload — convert to PNG first.

**STOP**: "Brief looks right, and I can reach your asset? (approve / edit [field] [new value])"

### Step 1 — Generate the plate (~20s, ~$0.03)

The scene from your brief, with the surface **deliberately empty** — a blank dark screen, a bare bottle, an unlabelled box. Emptiness is asked for explicitly, because whatever the plate puts on that surface you will have to cover.

Two things make the next step easy, so ask for them now:

- **A surface you can see all four corners of.** A screen cropped by the frame edge, or half-hidden behind a plant, has no quad to map onto.
- **Ask for a flat screen** — and then check, because you will often be ignored. Asking `flux-dev` for "a flat matte screen, not curved" produced a curved monitor anyway on a measured attempt, and a user hit the same thing: a curve they never asked for, which the composite then had to fight. If the plate insists on a curve, either re-roll or accept it and use `curve_deg` in Step 3. **Never try to flatten a curved screen by prompting.**

You get one plate. If the room or the screen is wrong, say so — they're three cents and twenty seconds.

> **For the agent**: use `flux-dev` and pass the requested shape as `aspect_ratio`. Put "completely blank / empty / unlabelled" AND "flat screen, not curved" in the prompt (unless the user asked for a curve), and ask for the surface fully in frame. **Watch for a `param_note` in the response** — several image models silently ignore `aspect_ratio`, and the note tells you when the shape you asked for wasn't applied.
>
> Before moving on, read the four corners of the target surface off the approved plate as fractions of the frame (top-left, top-right, bottom-right, bottom-left) and show them to the user. Step 3 needs them, and estimating them from the image you can see is both faster and more accurate than asking the user to measure. **Which four corners is the whole question — do Step 1b before you believe them.**

**STOP**: "Use this plate, or re-roll? (approve / re-roll [direction])"

### Step 1b — Fit the glass, not the panel (~1 min, free)

The corners you need are the **glass** — the image area — not the monitor's outer edge. On a 1920-wide plate those two rectangles differ by roughly **15–25 px a side**: small enough to feel like rounding, and exactly the difference between an image on a screen and a sticker on a bezel. This step is short, it is free, and skipping it is the most common way this playbook produces a frame that is pixel-exact and still obviously wrong.

**Do not look for the strongest edge.** The strongest gradient anywhere on a monitor is the bezel's *outer* edge against the room behind it. Any "walk to the biggest change" method — yours or an automated edge-finder's — locks onto that and hands you the panel. The glass boundary is the *second, weaker* step inward, and on a dark plate it can be a difference of only a few luminance levels.

**The method — classify materials by luminance, don't hunt gradients:**

1. Take a horizontal scanline through the vertical middle of the monitor, and a vertical scanline through its horizontal middle. Work at full resolution: a 20 px inset is invisible in a thumbnail.
2. Along each line there are three materials, and they separate cleanly even when two of them are dark:
   - **room / wall** — mid luminance, and it *varies* along the line
   - **bezel** — dark and **flat**: near-zero local variance, because it is matte moulding
   - **blank glass** — dark but **not flat**: it carries the plate's own gradient, glare or a faint reflection, so its local variance is measurably higher than the bezel's
3. Walk inward from outside the monitor. The first large step is wall→bezel. Keep walking: the point where local variance **rises** while mean luminance stays dark is bezel→glass. That is your coordinate on that side.
4. Repeat on all four sides and intersect. Report the four fractions to the user.

**If the plate genuinely has no bezel/glass distinction** — a pure-black panel with no glare at all — fall back to insetting the panel quad by **1.5 % of the panel's width** on each side. That is a guess, so it has to be measured by the two checks below rather than trusted.

**Two numbers prove the fit before you spend anything:**

- **Quad aspect vs the panel's *true* aspect.** A desktop monitor is 16:9 (1.78), a laptop 16:10 (1.60), a phone 19.5:9 (2.17). Compute the quad's implied aspect and compare. Come out at 1.85 on a 16:9 monitor and you have the panel, not the glass; come out at 1.70 and you clipped inside the image area. Tolerance ~3 %. Note that `logo_composite` compares the quad against **the asset's** aspect and warns past 12 % — that is a different and much looser check, and **it will not catch a bezel error**, because a panel quad and a glass quad have nearly the same shape.
- **Bezel luminance, before and after.** Sample the same eight points on the bezel ring (two per side, well inside the moulding) on the plate, and again on the finished render. **Every one must be unchanged.** One point that moved means the overlay is sitting on the bezel. This is what turns "the edges land on the bezel" from an opinion into a pass/fail.

**STOP**: "Here are the glass corners and the two fit numbers. Approve, or re-measure? (approve / re-measure [which side])"

### Step 2 — Pick the route (10 seconds, free)

Look at the asset, not at any output yet:

- **Does it contain text, numbers, a wordmark, a chart, or a legal mark?** → Step 3. Skip Step 6 entirely.
- **Pure shape and colour?** → either; Step 6 buys nicer light.

This gate exists because it was getting decided the wrong way round. The generative pass produces a beautiful frame on the first try, so people shipped it and only later noticed a fader reading `60` where their asset says `80`.

**STOP**: "Your asset has text in it, so I'll composite the real file rather than let a model redraw it. (go / use the generative pass anyway)"

### Step 3 — Composite the real file, keystoned onto the surface (~1–2 min, ~$0.10)

Your **actual asset** is laid onto the plate as a real image layer and projectively mapped onto the four corners of the surface, so it sits in the scene's perspective without being re-drawn. Nothing generative touches it: what you supplied is what appears, to the pixel.

A screen seen from one side is not a rotated rectangle — its two vertical edges are different lengths. That's a keystone, and it's why the corners from Step 1 matter: give the four corners and the asset lands on them exactly.

If the surface is genuinely curved — a can, a bottle, a curved monitor you *want* — say so and it wraps instead:

| Surface | Ask for |
|---|---|
| Flat screen / panel seen at an angle | the four corners (the default) |
| **Curved monitor at an angle** — the usual real case | **the four corners AND `curve_deg: 12–30`, `curve_dir: "concave"`** |
| Curved monitor square to camera | `curve_deg: 12–30`, `curve_dir: "concave"` |
| Can, bottle, jar | `curve_deg: 90–160`, `curve_dir: "convex"`, plus corners if it's off-axis |

**The two combine.** A curved monitor is almost never square to camera, so give both: the corners place it, the curve bends it, and the asset is warped across a mesh that does the two at once. Don't choose between them.

> **For the agent**: `logo_composite` builds the payload; `create_media({ model_override: "hyperframes-render" })` renders it. **Pass the SAME `aspect_ratio` to `logo_composite` that you pass to the render** — corner fractions resolve against the output canvas, so if you omit it the tool assumes 1:1 (1080×1080) while your render is 1920×1080 and every corner lands in the wrong place. The tool now says so when you leave it out; don't ignore that line. Pass `corners: [[x,y] × 4]` as fractions of the frame in TL, TR, BR, BL order — that is a true homography, and it is the right transform for any real photograph. **Do not reach for `rotate_y_deg`**: it produces a symmetric tilt and cannot make two vertical edges different lengths, which is the whole shape of a screen seen from one side.
>
> Pass `corners` AND `curve_deg` together for a curved surface seen at an angle — they compose into a mesh warp rather than one overriding the other. That is the common case, not an edge case: the plate model hands you curved monitors whether or not you asked.
>
> Three constraints the renderer enforces, and the first one has a trap:
>
> 1. **The background must be a real video** — it is mounted in a `<video>` element, and `logo_composite` rejects a still rather than burn a render on a black frame. **Do not satisfy this with `create_media({ action: "animate" })` for a still deliverable.** An i2v model adds camera motion whether or not you asked: a measured run with an explicit locked-off instruction came back with a push-in that slid the surface out from under the static overlay, invalidating corners measured on the plate. Build a clip that genuinely does not move, locally — free, exact, instant:
>    ```bash
>    ffmpeg -loop 1 -i plate.png -t 2 -r 24 -c:v libx264 -tune stillimage \
>      -pix_fmt yuv420p -movflags +faststart plate-still.mp4
>    ```
>    then host it (`scripts/host-local-file.sh plate-still.mp4` on a machine with `gcloud`) and pass that URL as `background_url`. This removes both the most expensive step in the run (~$0.32 of ~$0.45) and the only source of unwanted motion.
>    **No shell** (Claude Desktop, Cowork): you have to animate, so expect motion — read the corners off the **first frame of the returned clip**, not off the plate, and keep the duration short.
> 2. The overlay **cannot fetch remote URLs**, so the asset is inlined as a `data:` URI.
> 3. The document must stay under 200 KB, which is what caps the asset at ~140 KB raw. Pass it as the **`html_url`** handle `logo_composite` returns, not as inline `html` — see Step 0.
>
> The automatic idempotency key now covers the overlay (`html`, `html_url` and `background_video_url`), so moving one corner and re-rendering produces a new render rather than replaying the old frame. Before 2026-08-19 it did not, and every nudge inside a 10-minute window silently returned the previous take. If you ever see a render come back identical to the one you were trying to fix, check `structuredContent.idempotency_replay` before re-deriving the geometry.
>
> Nudge with the corners themselves — move one corner rather than reaching for offsets. If the tool reports that corners or the curve were ignored, it could not read the asset's dimensions from its header; re-export the PNG and retry.

Three things separate "pasted on" from "photographed" — and they are three *different* problems, so reaching for the wrong one fixes nothing:

- **Missing glare → `sheen: 0.10–0.18`.** The overlay is opaque, so it erased the plate's own reflection; this puts a believable one back, and being a CSS gradient it cannot alter a character. Skip it on a matte surface.
- **Hard, stair-stepped edges → nothing you can pass fixes this.** The keystoned layer is composited through a projective transform and its boundary is **not antialiased**: measured, the edge of a pinned quad has *zero* blended pixels — a clean binary step — while an ordinary 2-D rotation in the same browser scores 1.08. `sheen` is painted *inside* that same clipped quad, so it cannot reach the edge, and it is not the remedy the old guidance claimed. A transparent border on the asset, a fractional inset, and supersampling the overlay in-page were each measured and each fails (0.00–0.71 against a 1.08 control). The real fix is a 2× raster in the renderer itself and has not shipped; the measurements are recorded in `docs/caps/hyperframes-render-edge-aliasing-2026-08-19.md`. Until it does, it is least visible when the surface is **large in frame** — one more reason to prefer a plate where the screen fills a good part of the shot.
- **Wrong shape → read the stretch warning.** When the tool says the quad and the asset disagree in shape, stop and fix the corners. Don't ship a squashed interface because the placement looked right.

**STOP**: "Here it is on the surface. Approve, or nudge a corner? (approve / nudge [which corner] [direction])"

### Step 4 — Fan out to more scenes (optional, ~$0.10 each)

Same asset, different rooms. Only the plate and its corners change; the asset file is byte-identical across the set, so the mark cannot drift between variants. Useful for a launch page, a channel set, or regional variants.

Skip it if you only need one image.

**STOP**: "How many more scenes, and where? (skip / [n] scenes: [rooms])"

### Step 5 — Read the numbers back (the gate that matters)

On the composite route this is quick and should come back clean: the asset was never re-drawn, so the only things that can be wrong are placement and crop. **On the generative route (Step 6) it is the gate that decides whether you can ship at all.**

The agent puts your original asset and the finished image side by side and checks **every string you listed**, one at a time, at full resolution. Not a glance — a list, with a tick or a cross against each.

Then the trademark check: does any brand, product or company name appear that you did **not** supply? This is the one that bites. A model asked for a blank screen has been observed filling it with real competitor products, unprompted — and a wrong name on a marketing image is a legal problem, not a cosmetic one.

The agent prints the audit as a table and says plainly what the image is safe for. Six boxes, every run — and the three geometry boxes are **measurements, not impressions**, because "it looks right" is what let the bezel error through in the first place:

- [ ] Every string in `asset_must_survive` reads **exactly**, checked zoomed in at full resolution.
- [ ] **No invented branding** — no product, company or version name you didn't supply. *This is the trademark check; do it before publishing.*
- [ ] **The bottom of the asset renders.** Look at the lowest quarter of the asset region specifically. Garbling there and a clean top is the signature of a corrupted inline asset (Step 0), and it is invisible in a thumbnail because the top 60 % looks finished.
- [ ] **Quad aspect** — state two numbers: the fitted quad's aspect vs the **panel's true aspect** (within ~3 %), and vs the **asset's own aspect** (within ~5 %). The first catches a bezel/panel mix-up; the second catches a squashed interface. A composite cannot change a character but it can change its shape.
- [ ] **Bezel luminance unchanged** at the eight points sampled in Step 1b, plate vs render. Any point that moved means the overlay is on the bezel, not on the glass. This replaces "the edges land on the bezel", which was ambiguous enough to describe the error.
- [ ] The **rest of the frame** is your plate, not a new room.

> **For the agent**: inspect the actual pixels at full resolution — download and zoom the region rather than judging from a thumbnail. Downscaling both invents and destroys characters; a `4.2` can read as `4,2` in a preview and be correct in the file. Report **counts** ("14/14 labels, 14/14 values, 5/5 chrome strings") and quote any string that differs, both versions. Do not soften a miss: one wrong digit is a fail, not a "minor drift".
>
> Print the two geometry checks as numbers next to their tolerances ("quad 1.79 vs panel 1.78 ✓ · vs asset 1.94 ✗ −8 %"), not as ticks. A tick against a check you did by eye is worth nothing, and both of these caught real errors that every string in the audit passed.

**STOP**: "Audit is on screen. Ship it? (ship / fix [what])"

#### What "it usually works" actually looks like — and why Step 6 is not the default

Three production runs of this exact playbook, same asset, 33 checkable strings each:

| Run | What changed | Result |
|---|---|---|
| Golden-hour plate | — | **33 / 33** |
| Neon plate, *same model, same prompt shape* | only the plate | **30 / 33** — `BODY 80→60`, `SUB 66→65`, badge `XT-2→v1.2` |
| Golden-hour plate, different model | `qwen-image-3-edit` | **32 / 33** — `TRIM 38→30` |

Read the middle row twice. **Nothing about the request changed except the background**, and three strings moved — including an invented version number on a product image. This is not a model you can screen out in advance; it is a property of each individual render. That is why the check is per-render and why it is a gate.

See it end to end, including the recovery: [the audit that caught it](/chapters/brand-exact-composite-example.html).

### Step 6 — The generative pass (only for shape-led assets)

**Skip this if your asset has any text on it.** It's here for a silhouette, an icon, an abstract mark — something with no characters and no exact geometry to lose.

What it buys, and it is real: the model relights the asset into the scene. Reflections in the glass, bounce from the room, contact shadow, and it will follow a curved or irregular surface without being told the geometry. A corner-pin cannot do any of that — it maps flat pixels onto a quad.

What it costs: the asset gets re-drawn. See the table above.

> **For the agent**: this needs `run_capability`, not `create_media` — `create_media` models exactly one image (`source_url`), so a second reference has nowhere to go. Call it with `inputs.image_urls: [plate, asset]`, in that order, and refer to them **by position** ("the first image", "the second image"). Always pass `timeout` (an abort is billed whether or not you receive the image) and prefer `async: true`.
>
> Pick by asset: type-heavy → `grok-image-2-edit` with `inputs.resolution: "2k"` (the strongest measured, and still not exact) · shape-led → `qwen-image-3-edit`, cheapest · keeps redesigning → `gpt-image-edit`, most literal and priciest.
>
> Keep all four phrases: address images by position · say **"do not redesign"** · name the strings that must survive · name what must NOT change, or it re-renders the whole frame.
>
> Then go back through Step 5. On this route the audit is a gate, not a formality.

**STOP**: "Here's the generative version. Approve, or composite the real file instead? (approve / composite)"

### Step 7 — Hybrid: keep both halves (optional)

The two routes are not exclusive, and for a hero image the combination is usually best:

1. Run Step 6 and keep it as a **lighting reference** — it shows you how the surface should glow and reflect.
2. Composite the real asset (Step 3) on top.
3. Ask for a light pass on the **plate only**, never over the asset region.

You get the model's sense of light with your own pixels on the surface.

### Step 8 — The image sheet

The agent writes a self-contained HTML page — your original asset, the plate, the final image, the accuracy audit table and what it cost — from the template at `public/playbooks/_templates/brand-exact-composite.html`. Saved to `public/launches/{piece_slug}-composite.html`.

Open it in a browser or send it as an attachment; it needs nothing else to render.

## When you're done

The agent prints:

```
✅ Image sheet:   public/launches/{piece_slug}-composite.html
✅ Final image:   <URL>
✅ Plate:         <URL>
✅ Accuracy:      {N}/{N} strings exact · no unsupplied brand names
   Route:         generated composite   (or: exact overlay)
   Known limits:  <e.g. control angles were re-drawn>

Total spent: $X.XX
Total wall-clock: MM:SS
```

## What to do next

1. **Send the image sheet for sign-off** — the audit table is the argument. Whoever approves brand assets can see exactly what was checked.
2. **Re-run with a new plate** for each channel or region. Your asset stays the second reference; only the room changes.
3. **Save the brief.** The next version of the product is the same run with a new asset URL — that's the point of keeping the asset out of the prompt.
4. **If this is going in front of legal**, use the composite route (Step 3) regardless of how good a generated one looks. "Indistinguishable from correct" and "correct" are different claims.

---

## Notes for the agent (only read if a step fails)

**The multi-reference call was refused before it ran.** Check the shape: the references go in `inputs.image_urls` as an **array**, and an empty array counts as missing. A single `source_url` also works and is converted, but you cannot pass a second reference that way.

**The composite timed out.** Edit models run from ~30s to over 5 minutes depending on queue. An abort does not cancel or refund the upstream job, so a timeout costs the same as a success — re-run with `async: true` and a generous `timeout` rather than retrying synchronously and paying twice.

**One model errored twice.** Switch models rather than re-prompting; they fail differently. `gpt-image-edit` is the most likely to refuse and the slowest; `qwen-image-3-edit` is the most likely to be queue-bound.

**The result looks right but a number is wrong.** This is the expected failure mode, not an anomaly — see the three-run table under Step 4. It is invisible unless you compare against the source. If you are on the generative route, stop and composite the real file (Step 3) instead.

**The overlay sits flat on an angled or curved surface.** Pass `corners` for a flat surface seen at an angle (a true keystone), `curve_deg` + `curve_dir` for a curved one square to camera, or **both together** for a curved surface at an angle — see the table in Step 3. If it still looks flat, check you didn't reach for `rotate_y_deg`: that tilts a plane and cannot bend one. If the tool reports that `curve_deg` was ignored, it couldn't read the asset's dimensions from its header — re-export the PNG from a normal tool and retry.

**Everything is right except the knobs / needles / bar heights.** Expected, and worth saying out loud to the user: a generative edit re-draws control *positions* even when it reproduces every label exactly. If a control's angle or a bar's length encodes a value a customer will read, that frame isn't safe at any model quality — composite the real file (Step 3), or render the chart deterministically.

**The type looks subtly stretched or squashed.** The corners were wrong, or the plate's screen is the wrong shape for the asset — a homography fills whatever quad you give it. Re-read the corners zoomed in and compare the quad's aspect against the asset's. The tool warns past 12% disagreement, but a 10% squash is still visible on type.

**It looks pasted on — no highlight.** Add `sheen: 0.10–0.18`, and prefer a matte surface in the plate. An opaque overlay removes the plate's glare, and a screen with no highlight at all reads as a flat rectangle.

**It looks pasted on — the edges are a hard staircase.** A different fault, and `sheen` is not the fix: it is painted inside the same clipped quad. The keystoned layer's boundary is not antialiased and **there is no parameter that changes that** — zero blended pixels measured against 1.08 for an ordinary 2-D rotation, unchanged by removing the layer-promotion hint, and unfixed by a transparent border, a fractional inset, or an in-page 2× supersample (all measured — see `docs/caps/hyperframes-render-edge-aliasing-2026-08-19.md`). The fix is a 2× raster inside `tool-hyperframes` and is open. Mitigation meanwhile: favour a plate where the surface is large in frame, where the staircase is smallest relative to the content.

**The render came back identical to the last one.** Fixed 2026-08-19 — the automatic idempotency key now includes `html` / `html_url` / `background_video_url`, so a moved corner re-renders. If you still see it, confirm with `structuredContent.idempotency_replay` and pass an explicit incrementing `idempotency_key` as a belt-and-braces override. Before that date this was guaranteed for any nudge inside 10 minutes, because a composite carries no prompt and nothing else about the call had changed.

**The interface renders correctly at the top and garbles lower down.** The asset was corrupted in transit, not by the renderer: a damaged base64 tail damages the tail of the image, so the top still looks finished. This is what the `html_url` handle exists to prevent — so the first thing to check is whether the document was inlined instead. Either `logo_composite` warned that it could not store it (fall back to a ≤16 KB asset for that call), or the handle was fetched and re-pasted somewhere in the chain, which re-creates the bug exactly. Pass `html_url` through untouched.

**The asset is too big to inline.** For Step 3 the whole overlay document must stay under 200 KB, and base64 costs 4 bytes for every 3 — so the asset itself needs to be under ~140 KB. Optimize the PNG or export smaller; a flat-colour mark compresses hard.
