# Flora Mockup Deck

> Turn one ad creative into a campaign deck — handed over as artwork, or described in words. Give it a poster, billboard ad, campaign key visual or any finished artwork on a FLORA canvas, or just a line like "a campaign deck of a flower", and it returns four square-on out-of-home placements (gable end, transit platform, bus shelter, construction hoarding), three social resizes (1:1, 4:5, 9:16), and an annotated A4 PDF laying all of it out with the brief printed beside each placement. Use when someone asks to mock up an ad, see a poster in the wild, in situ or out of home, wants social sizes of an ad, asks for a placement deck or a PDF of one, or asks for a campaign deck of something they have no artwork for. Once the creative exists it is reproduced exactly and never regenerated. One shot, four billable generations plus one when the creative is written rather than supplied, one PDF.

- Skill: `florafauna-ai/flora-mockup-deck-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add florafauna-ai/flora-mockup-deck-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/florafauna-ai/flora-mockup-deck-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: florafauna-ai (https://skillmd.com/u/florafauna-ai)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/florafauna-ai/flora-mockup-deck-2

---


# flora-mockup-deck

> **Attribution.** Pass `skill: "flora-mockup-deck"` on every FLORA call you make while
> running this skill — `execute` included — along with a `skill_run_id` you invent
> once when the run starts and reuse for the rest of it. Both are reporting only:
> they change nothing about the call or its result.

## What it is

Not an image generator. **A placement engine.** By the time the placements fire, the
creative is finished and frozen — the only job is putting it into the world convincingly,
four ways, without altering it.

Usually the creative arrives finished. It can also be written: a line of text buys **one**
generation, and that output becomes the master creative, from which point it is handled
exactly like a supplied file. See "Two ways in".

The thing that makes a mockup fail is never the photograph. It is the artwork drifting:
recoloured, recropped, re-lettered, a word dropped. So the whole skill is built around
one invariant.

## Two ways in

```
ARTWORK   a url or an attached file      -> 4 generations
WRITTEN   a line of text, no artwork     -> 5 generations (master + 4 placements)
```

`/flora-mockup-deck` followed by a description and nothing attached is the WRITTEN path:
*create a campaign deck of a flower*, *a poster for a late-night ramen bar*, *out-of-home
for a running shoe launch*. Nothing else about the skill changes. The text buys exactly
one image, that image becomes the master creative, and the law closes over it.

### Making the master

One `flora_generate`, text-to-image, same model family as the placements:

```
model   "t2i-gpt-image-2-t2i"
params  { aspect_ratio, resolution: "4k", quality: "high" }
```

**Prompt it as a printed poster, not as a photograph of a thing.** The text names a
subject, and a subject is not an ad — "a flower" un-elaborated returns a stock botanical
photograph, which then gets pasted onto a gable end and reads as a picture of a flower on
a wall rather than as a campaign. Build the master prompt as:

    A finished out-of-home poster. Flat artwork, square on, filling the frame edge to
    edge. Print-quality graphic design — NOT a photograph of a poster and NOT a mockup:
    no wall, no frame, no shadow, no room, no perspective, no torn edges, no border.

    THE SUBJECT — <the request, expanded into one concrete image>
    THE TYPE — <the headline, or "no type at all">

Then the house grade clause, verbatim, exactly as the placements get it. The master and
the four placements have to be graded the same or the deck reads as two different shoots.

**The shape is yours to choose here, so choose portrait.** A supplied creative forces the
site-matching problem below; a written one does not. `aspect_ratio: "2:3"` makes all four
default sites valid and skips the trap entirely. Go landscape only if the request names a
billboard or a landscape medium.

**Type, when the request gives you no copy.** An ad usually has words and a one-line
request usually has none. Never generate a real company's logo or wordmark. Either write
one short headline of your own or generate the poster with no type at all — then say in
one line which you did, so the user can hand you copy and re-run instead of wondering why
the poster is silent.

### The gate — show the master, then fire

**This is the one place the skill stops, and it is not a draft round.** The master is not
a preview of the deck; it is the thing the entire deck reproduces. Every placement carries
it unchanged and nothing downstream is re-rollable, so a wrong master is four wrong
placements with no way back — about $3.50 spent reproducing the wrong picture perfectly.

Fire the master. Report the url. Get a yes. Then fire the four placements in one pass and
run to the end without stopping, exactly as the ARTWORK path does.

Re-rolling the master is allowed and bills again — say so before the second one. Once the
user says yes the master is frozen and the law applies to it in full: from that point it
is a supplied file that happens to have come from FLORA.

## One shot, minimal outputs

The skill runs **once** and emits exactly the deliverable. No draft round, no preview
pass, no variant sprawl.

```
1  generation    the master creative — WRITTEN path only, skipped when artwork is given
4  generations   the placements — one per site, not four angles on one site
3  resizes       1:1, 4:5, 9:16 — NOT generations, see below
1  contact sheet the four placements in one grid — needs the actions entitlement
1  PDF           the annotated deck — free, and reachable on every surface
```

**Four generations — five from a line of text — three deterministic resizes, one contact
sheet.** Nothing is repaired,
re-rolled or replaced, and nothing is measured. Generate the four, build the resizes,
package everything, ship. If a placement is visibly wrong, say so in a sentence.

### The resizes are not generations

Regenerating a resize **re-letters the type** — the exact failure this skill exists to
prevent. So the resizes are done with prebuilt FLORA actions, which are deterministic,
credit-free, and never touch the pixels of the plate itself.

Run each with `flora_run_action`, passing **the master creative's** url as the image
input — never a placement url. These are social crops of the ad itself, which is what
the SOCIAL page delivers; cropping a placement photograph would deliver a picture of a
bus shelter at 9:16.

```
1:1 and 9:16   change-image-ar-browser   { aspect_ratio, fit: "pad",
                                           background_mode: "blur" }
4:5            resize-image-browser      { mode: "exact", width, height,
                                           fit: "contain" }
```

`4:5` is not in `change-image-ar-browser`'s aspect list, which is why it goes through
the resize action with explicit dimensions instead.

**Always `fit: "pad"`, never `"crop"`.** Cropping to a new ratio trims the plate, which
breaks the law as surely as regenerating does. Padding leaves every pixel of the artwork
untouched and adds ground around it.

**What this does not do.** It does not reconstruct the ground the artwork sits on. A
seamless extension — measuring the edge band, estimating its colour and grain, and
continuing it past the plate — is not something these actions perform;
`background_mode: "blur"` derives a soft field from the image, which reads convincingly
on a photographic ground and less so on flat colour or full-bleed type. When the result
needs to be a true extension rather than a pad, that is a signal to build it as a FLORA
action or technique, not to reach for a generation.

## The law

> **The artwork is reproduced. The world is generated.**
> Any change to the creative — colour, crop, wording, letterforms, logo — is a fail.

The division is between what is *made* and what is *carried*: FLORA makes the place,
and the artwork passes through it untouched.

## Inputs

```
CREATIVE    the ad itself                 required — an HTTPS url, an attached file,
                                          or a line of text describing it
PLACES      4 named placements            optional, default: the four archetypes
BRIEF       one line on where/what mood   optional
```

`CREATIVE` is the only required input and it takes either form. Text alone is not a
missing input — it is the WRITTEN path, and you generate the master before anything else.
Ask for artwork only when the request names a specific existing creative you have no url
for.

**Getting the creative in.** Files attached in ChatGPT are already hosted on
`files.openai.com` or `cdn.openai.com`, both allowlisted — pass that url straight to
`flora_create_asset` as `source` and FLORA fetches it server-side. A FLORA output at
`media.flora.ai` works the same way — including the master you just generated, which
arrives as a url and needs no upload step. Never base64-encode the file and never try to
upload bytes; if a supplied artwork genuinely has no url, say so and ask the user to add
it to their FLORA project.

You do not wire the artwork to anything. Each placement is its own `flora_generate`
call carrying the artwork url in `params.image_url`.

Default placements, chosen because they are four genuinely different media buys:

| tag | what it is | why it earns a slot | plate shape |
|---|---|---|---|
| `gable` | flat brick end wall of a terrace | scale against windows and downpipes | any |
| `transit` | flat underground platform panel | interior, artificial light, close viewing | any |
| `shelter` | backlit 6-sheet in a bus shelter | street furniture, lit from within | portrait |
| `hoarding` | plywood construction hoarding | pavement level, read close and fast | portrait |

**The plate-shape column is not decoration.** Read it against the creative before firing
— see "Match the site to the creative's shape" below. Never a corner wrap: it bends the
artwork across two planes and was the worst result of the whole test set.

## Resolve the ids once, before you spend anything

**`flora_generate` requires `project_id`.** It is not optional and there is no default.
An agent that has not decided on a project has to produce one at fire time, and what it
produces is a guess — the most recently touched project from `flora_list_projects`, or a
plausible-looking `prj_` string. Both are wrong, and neither errors in a way that looks
like a mistake. **This is the single most common defect in a run of this skill**, and
everything below exists to stop it.

So resolve two ids **once**, at the top of the run, and thread the same two through every
call — placements, resizes, contact sheet, and the deck's footer:

```
WORKSPACE   ws_...   flora_list_workspaces
PROJECT     prj_...  flora_list_projects, or one the user named
```

**Copy ids verbatim. Never retype, shorten, or reconstruct one.** They are long opaque
strings with no checksum, so a transposed character produces a valid-looking id that
fails somewhere else entirely.

**The two must belong together.** A project from one workspace paired with another
workspace's id fails at fire time with a 400 that names the problem exactly:

```
input_validation_error   "Project does not belong to the specified workspace."
```

Measured, on an account with two workspaces — which is the ordinary case, since a
personal workspace and a team workspace is the default shape. Taking `workspaces[0]` and
a project the user mentioned is precisely how this happens. **Confirm the project appears
in `flora_list_projects` for the workspace you are billing** before the first generation.
One free call; four billable ones ride on it.

**Ask which project when there is any doubt, and quote the name back.** Projects are
overwhelmingly called `Untitled`, and duplicates of a real name are common — an account
here had two live projects both named `Perfume Bottle Photography`. Name-matching picks
one at random and there is no way to tell from the result which one it picked. `origin`
separates them a little: `canvas` means a human made it in the UI, `chat` means an agent
did.

**`flora_create_project` works, but not in every workspace.** Measured on one account: it
creates cleanly in one workspace and fails reproducibly in another with
`400 input_validation_error` whose message is the literal string `Server Error` plus a
request id — no field named, nothing wrong with the payload. It tracks the **workspace**,
not the request: the workspace that refuses to create is the same one that returns 403
for actions. Not a project cap either — the failing workspace held 4 projects, the
working one 839.

So creating a project is worth trying and is **not** worth debugging. If it 400s, do not
reshape the payload and do not retry with a different name — fall back to a project that
already exists, and say which one you picked and why.

### What the project link actually contains

```
the project     https://app.flora.ai/projects/<project_id>
one node        https://app.flora.ai/projects/<project_id>?focus=<node_id>
```

`?focus=` opens the canvas centred on a single node — use it to point at a specific
placement instead of making the reader hunt. Node ids come from
`flora_list_canvas_nodes`.

**Only the placements are on the canvas.** `flora_generate` writes to the project;
`flora_run_action` does not. The tool says so and it measures true — an action's output
lands under `media.flora.ai/code-sandbox/...` and never appears in
`flora_list_canvas_nodes` for the project it was scoped to. On an action run `project_id`
buys authorization and a generation-history row, nothing more.

So **the resizes and the contact sheet are not in the project**, and a link that implies
otherwise sends the user looking for files that were never there. Either say what the
link contains — "the four placements; the resizes and the sheet are urls below" — or put
them on the canvas deliberately with `flora_add_action` then `flora_run_canvas_action`.

## Match the site to the creative's shape

**On the WRITTEN path this is already solved** — you chose the master's shape, so a `2:3`
master and the four defaults never conflict. Read the rest of this section when the
creative was supplied.

**Read the creative's aspect ratio before choosing sites.** The defaults are not all the
same shape: a 6-sheet and a hoarding are portrait media, a gable end and a platform panel
take anything. Give a landscape creative to a portrait site and the model resolves the
conflict the only way it can — by recropping the artwork. That is a silent breach of the
law, and it is the likeliest way this skill fails.

Measured, a 3:2 landscape creative against the four defaults unchanged: gable and transit
came back with the plate intact, shelter and hoarding both came back recropped to
portrait with a third of the composition gone. Nothing errored. The deck looked finished.

```
creative is landscape   gable · transit · billboard · a landscape hoarding panel
creative is portrait    the four defaults as written
creative is square      any of them
```

Then state the plate's shape in the prompt, so the site is built around the artwork
instead of the artwork being trimmed to the site:

> THE PRINTED PLATE IS <W>:<H>, THE SAME SHAPE AS THE SUPPLIED ARTWORK. The panel,
> board or pasted area is built to that shape. Do not trim the artwork to fit a
> differently-shaped surface — size the surface to the artwork.

`params.aspect_ratio` sets the shape of the PHOTOGRAPH, not of the plate inside it. Both
matter: `"3:2"` or `"16:9"` gives a street scene that tiles into an even contact sheet,
while the clause above governs the plate.

## Model routing

**GPT Image 2 at `resolution: "4k"` — for every placement. This is the only model.**

Tested head to head against Nano Banana Pro on identical prompts and grade. GPT wins on
all four sites, and not on pixel count:

- **The fine detail of the creative survives.** With a type-heavy or textured artwork,
  NBP mushes the fine structure into a flat texture at distance; GPT still resolves it.
  That structure is usually the artwork's identity, so it is the thing that matters most.
- **The environments are better observed** — real street furniture, believable
  bystanders, correct light spill from a lit panel onto its surroundings.

A single generation is ~107s and **4k is no slower than 2k**. Fired concurrently, four
placements land in about **five minutes** — measured in live use, not estimated. Earlier
figures of 15–20 minutes were staggered firing plus a repair round, not the model.
**Do not add a fast draft pass.**

`resolution: "4k"` lowercase for GPT Image 2. (Nano Banana Pro also accepts `"4K"`
uppercase — it is simply not the right model here.)

`i2i-gpt-image-2-i2i` for the placements, `t2i-gpt-image-2-t2i` for the master on the
WRITTEN path. Same model, same grade language, same lowercase resolution values — which
is why the master and the four placements hold together as one set.

**Never Krea.** It reinterprets what you wire it, which is the one thing that must
never happen to the creative.


## Speed — fire everything at once, then wait

Measured, three generations fired at the same instant:

```
Nano Banana Pro  2K    63s
GPT Image 2      2k   ~107s
GPT Image 2      4k   ~107s      <- 4k costs nothing over 2k
```

**But GPT Image 2 throughput swings hard.** That best case is real and repeatable; so is
a batch of four taking **10–15 minutes** with the identical setup. Do not promise a
runtime. What you control is not adding delay of your own:

- **Fire all placements in one pass with no gap between them.** One `flora_generate`
  call per placement, each with `model: "i2i-gpt-image-2-i2i"` and
  `params: { image_url, resolution: "4k" }`. Do not stagger on a timer — that converts
  a one-generation wait into an N-generation wait for nothing.
- **Fire the resizes in the same pass** if any need generating. They share no dependency.
- Only the contact sheet waits, because it needs the images.
- **Poll once, centrally.** One `flora_list_generations` call filtered to the project
  covers every placement at once. Do not poll each run separately — that turns one wait
  into N waits for nothing.

The honest expectation to set with the user: **a couple of minutes if GPT is quick,
fifteen if it isn't.** Say that up front rather than predicting a number.

The one hard rule: **Krea rate-limits.** Firing many Krea calls at once returns
`GENERATION_DOWNSTREAM_SERVICE_ERROR` and loses most of the batch — pace those at ~5s.
GPT Image 2 and Nano Banana Pro need no pacing.

**"One shot" means one deck, not one generation round — and you do not stop to repair.**
Stated plainly because the ambiguity itself cost time in live use: a run hesitated over
whether it was allowed to fix a bad placement. It is not.

The master gate on the WRITTEN path is the single exception, and it resolves before any
placement exists. Once the four are firing, nothing stops.

## Prompt architecture

One invariant block, four scene lines. Only the scene changes.

```
Place the supplied artwork into a real photograph of the world as an out-of-home
advertisement.

THE ARTWORK IS REPRODUCED EXACTLY. Its composition, colours and type come through
unchanged. Do not recolour, recrop, redraw or re-letter it, do not add or remove a
word, do not add a logo. It reads clearly at a glance.

THE ARTWORK SITS ON THE SURFACE CORRECTLY. It takes the perspective of the surface
it is printed on, takes that scene's daylight and shadow, and picks up the surface
texture underneath. Printed material in a real place, never a flat rectangle pasted
onto a photo.

The photograph around it is real, candid and unstyled — ordinary weather, ordinary
light, ordinary passers-by. Full-frame camera, natural depth of field, no HDR, no
gloss, no lens flare, no CGI sheen.

THE WHOLE ARTWORK IS VISIBLE. Every edge of it sits inside the photograph — nothing
is cropped by the frame, cut off by a pole, hidden behind a tree or run off the top of
the wall. Every line of type in the artwork reads complete.

No extra text anywhere beyond the artwork itself and signage that genuinely belongs
to that street.

THE PLACEMENT — <scene>
```


## The house grade

Extracted by measuring the 36-frame colour-grading field on the canvas, not by eye.
This is the skill's **style**, and it goes in every placement prompt verbatim. It is
described in words, never wired as a reference — wiring a graded still drags its
composition in with it.

```
                measured            reads as
shadows         neutral, black pt 9  deep, never crushed
midtones        R-8.3 G+6.9 B+1.5    green-cyan cast
highlights      R-21.3 G+16.8 B+4.5  strongly green-cyan
white point     143 / 255            rolls off early — nothing reaches white
contrast        41 (std luma)        flat curve, no punch
saturation      48%                  rich but never vivid
```

The clause:

> THE HOUSE GRADE — the whole photograph is graded this way, and this matters as much
> as the composition.
>
> Shot on film and printed slightly flat. The tonal range is COMPRESSED: shadows deep
> and neutral but never crushed, and the highlights ROLL OFF EARLY — nothing reaches
> paper white, not the sky, not a lit sign. The brightest thing in frame sits well
> below white. Low contrast, gentle S-curve, no punch.
>
> A GREEN-CYAN CAST runs through the midtones and especially the highlights — skies,
> pale walls, concrete and daylight lean eucalyptus and sea-green rather than blue or
> warm. Reds and skin pulled back and desaturated. There is no orange-and-teal, no
> warm/cool split. The cool green IS the light.
>
> Colour moderately rich, never vivid. Fine film grain. Slight halation on the
> brightest edges. No HDR, no clarity, no glow, no saturation boost.

**A strongly-coloured LIGHT direction overrides the grade.** Measured: a placement
directed "blue hour, pavement wet" came back blue, not green-cyan, while the three
daylight and interior sites in the same batch held the grade exactly. Time-of-day words
carry their own colour and they win. Keep the set to overcast, flat grey, daylight and
interior artificial light; if a brief genuinely needs dusk, restate the green-cyan cast
inside that placement's LIGHT line rather than trusting the shared clause to hold.

**The trap this avoids.** Writing "ordinary weather, ordinary light, ordinary
passers-by" produces exactly that — flat, characterless placements. Light and camera
must be *directed* per placement (focal length, height, time of day, one human
moment), and the grade holds them together as a set.

## Direction, per placement

Every scene line carries three things beyond the location:

```
SHOT     focal length, camera height, angle       e.g. 85mm compressed from down the road
LIGHT    time of day and what it does to surfaces e.g. overcast, wet road holding reflection
MOMENT   one human beat                           e.g. one person stopped, looking up
```

Without these the model defaults to eye-level, midday, nobody — and every placement
looks the same.

**Keep the human beat OFF the artwork's plane.** The invariant block bans the artwork
being "hidden behind a tree", but the MOMENT direction actively asks for a person near
the ad, and the two pull against each other. On the live run the transit brief said
"walking past mid-frame" and the model put the commuter squarely across the panel,
blocking the left of the lower two lines. The plate was unaltered — an occlusion, not a
re-letter — but the placement no longer read.

So place the figure deliberately, in the direction itself: *in the near foreground and
cropped*, *at the far end of the shelter*, *stopped on the opposite kerb*. Say where the
person is relative to the ad, never just that they are in shot. The three placements that
did this came back clean; the one that said "mid-frame" did not.


## Square on, always

The single biggest driver of placement quality. Three-quarter and corner-wrap views
bend the artwork across two planes; the type distorts and stops reading. Tested
directly: a scaffold banner wrapping a building corner was the worst result of the
whole set, and the same creative shot perpendicular was the best.

Goes in every placement prompt:

> THE CAMERA IS SQUARE ON TO THE ARTWORK. The lens is perpendicular to the printed
> surface, so the ad sits in frame as a TRUE RECTANGLE, flat and undistorted, read
> straight. Only slight keystone is acceptable.
> - NO three-quarter view. NO oblique or angled view of the surface.
> - The artwork NEVER wraps a corner and NEVER bends across two planes.
> - It is on ONE flat plane facing the camera.
> - No fisheye, no wide-angle bowing, no perspective warp through the type.
>
> AND THE SURFACE ITSELF RUNS FLAT ACROSS THE FRAME. The wall, hoarding or panel the
> artwork sits on does not recede to a vanishing point, is not seen down its length,
> and its far end is not visible. Both the artwork AND the thing it is printed on face
> the camera.

**Why the second half exists.** A hoarding once obeyed "the artwork is on one flat plane"
while running the *hoarding* to a vanishing point — so the artwork bent along with it and
the type foreshortened. Banning oblique views of the artwork is not enough; the surface
has to be called out separately.

**Get variety from SITE and SCALE, not from camera angle.** A 6-sheet bus shelter, a
pavement-level hoarding and a billboard over traffic are genuinely different media
buys. The same wall from three angles is one placement photographed three times.

The four sites, fixed — chosen because they are genuinely different media buys:
`gable` (flat end wall) · `transit` (platform panel) · `shelter` (backlit 6-sheet) ·
`hoarding` (construction). Swap one only if the brief names a specific environment.

## Gotchas

```
API SHAPE
flora_generate        the ONLY way to run a generation. Returns a run_id; poll it with
                      flora_get_run. Wiring nodes with flora_add_to_canvas creates them
                      INERT — nothing on this server runs a wired generation node, so
                      never expect a canvas patch to fire anything.
params.image_url      the input image for an i2i model, as a SINGLE STRING. This is the
                      whole image-to-image mechanism.
                      params.image_urls (plural, array) is accepted without complaint,
                      silently IGNORED, and still billed — you get a text-to-image
                      render of the prompt with the creative nowhere in it. Measured.
flora_get_run         DO NOT poll a batch with this. It can report status "running" and
                      progress 0 for a run that has already finished — measured at 16
                      MINUTES of "running" on a run whose own record showed
                      started_at -> completed_at 120s apart, with outputs present the
                      whole time. A loop waiting for it to flip never exits.
run status            poll flora_list_generations filtered to the project instead: one
                      call covers the whole batch and reports terminal state. Key on
                      status == "completed" AND outputs being present — completed_at on
                      its own does not mean done, and neither does a "running" status
                      mean it is not.
flora_run_action      runs a prebuilt action headlessly on inputs supplied inline.
                      Credit-free and deterministic — where the resizes and the contact
                      sheet come from. It does NOT touch the canvas: outputs land under
                      media.flora.ai/code-sandbox/... and never appear as canvas nodes.
                      project_id only scopes authorization and generation history.
                      Entitled per workspace: a workspace without it returns
                      403 forbidden "Actions are not enabled for this workspace.
                      Upgrade your plan to use actions." That kills BOTH the resizes and
                      the contact sheet, so check it before promising either. The PDF
                      does not depend on actions and still builds.
flora_create_project  works in some workspaces and 400s in others on the SAME account,
                      tracking the workspace rather than the request. The message is the
                      literal string "Server Error" with a request id and no field — it
                      is NOT your payload, so reshaping it does nothing. Fall back to an
                      existing project. The workspace that refuses this is the same one
                      that refuses actions.
flora_list_canvas_nodes  returns media nodes with their asset urls. Use
                      flora_get_canvas for structure and how nodes connect.
ids                   project_id is REQUIRED on flora_generate, and it must belong to
                      the workspace you pass, or: 400 input_validation_error "Project
                      does not belong to the specified workspace."
credits               every placement bills. State the total and get a yes before the
                      first call. Nothing is refundable and retries bill again.
                      The charged_cost flora_generate returns AT FIRE TIME UNDERSTATES
                      the bill: measured 0.253 quoted against 0.873 actually charged,
                      3.45x. Quote from a completed run's charged_cost, or say plainly
                      that the figure is a floor. Four 4k placements are ~$3.50, not ~$1.
                The WRITTEN path adds a fifth generation at the same rate — quote ~$4.40,
                and quote it BEFORE the master, not before the placements.
media urls            fetchable with no credentials. Path contains the date — read the
                      full url, never reconstruct it.

MODEL PARAMS
GPT Image 2           resolution "2k" / "4k" lowercase. 4k costs no more time than 2k.
Nano Banana Pro       resolution "2K" / "4K" uppercase.
Krea                  creativity is an enum: raw | low | medium | high.
generate_audio        agent-gated; including it rejects the WHOLE changeset.
changeset validation  ONE bad param fails the ENTIRE changeset, not one node. Probe a
                      single node when unsure — the error returns valid_values.

RATE LIMITS
Krea                  rate-limits hard. Pace at ~5s or lose most of the batch to
                      GENERATION_DOWNSTREAM_SERVICE_ERROR.
GPT / NBP             no pacing needed. Fire concurrently.

RESULTS
media urls            fetchable with no credentials. Everything this skill produces is
                      a url, not a local file — there is no filesystem on this surface.
                      Report urls; never claim to have written or opened a file.
```

## The deck is an annotated PDF

**The deliverable is a paginated A4-landscape PDF**, built locally from HTML and printed
by headless Chrome. Not a server-side render — there is no PDF endpoint — and not a
contact sheet standing in for one.

"Annotated" is the whole point. A grid of placements is a contact sheet: it shows what
came back. The deck prints the SHOT / LIGHT / MOMENT you actually asked for beside each
placement, so a media planner reads the brief next to the result and can act on it. Print
what you **asked for**, not a description of what came back.

```bash
cd <project>/Deliverables
python3 build_mockup_deck.py deck.json   # fetches the urls in deck.json itself
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless \
  --disable-gpu --no-pdf-header-footer --print-to-pdf="<Name>-deck.pdf" \
  --virtual-time-budget=25000 "file://$PWD/out.html"
```

The builder is in the appendix at the foot of this file, and its docstring carries the
`deck.json` shape. Put the placement urls straight into it — the builder downloads and
caches them into `src/` on first run, so there is no separate fetch step to get wrong. Write it out beside the deliverables and run it there. Land the HTML
next to the PDF — it re-renders in about two seconds, so a layout tweak never costs a
regeneration.

**Check the render, do not assume it.** Read the PDF pages back as images before
reporting. `pdftoppm -png -r 60 out.pdf pg` is enough. Overflow into the footer is the
failure to look for.

**Say the full path to the PDF in the final message.** A deck nobody can find is not a
deliverable.

### Get the PDF out on every surface

**The PDF is the deliverable. Take the best route the surface allows — do not skip
straight to "can't".** Chrome is how route 1 prints; it is not what makes the deck
possible.

```
1  shell + Chrome     build, then print headless          -> a .pdf on disk
2  shell, no Chrome   build, user opens out.html, Cmd-P   -> Save as PDF
3  no filesystem      run --remote, hand over out.html    -> user prints it
```

Route 2 costs nothing: `@page { size: A4 landscape }` is honoured by the browser's own
print dialog, so **File → Print → Save as PDF** produces the same document the headless
flag would.

Route 3 is what makes this work on **claude.ai and ChatGPT**. With `--remote` the html
references the `media.flora.ai` urls instead of inlining the bytes, which drops it to
**8.5 KB** on a four-placement set — small enough to hand over as a file. Measured: those
urls load into a browser with no credentials and print exactly as inlined images do. So
the answer on a hosted surface is "here is the deck, open it and print" — one click from
a PDF, not unavailable.

Only say the deck cannot be built if all three fail. Never synthesise a PDF out of tool
output.

**Downscale before building, or it will not send.** Chrome re-embeds source images badly:
measured on a real four-placement set, 58.1 MB from full-resolution PNGs against 4.4 MB
at 2400px JPEG — 13x, with nothing lost, since the widest slot in the layout resolves
~2185px at 300dpi. A deck nobody can attach to an email is not a document you can send.

### The contact sheet, as a fallback

Still worth building where actions are available: one composite image, credit-free and
deterministic, that a user can drop straight into a deck of their own.

```
flora_run_action  side-by-side-composite-browser
  inputs  the four placements, in site order: gable, transit, shelter, hoarding
  params  { layout: "grid-2x2", normalize: "match-shortest", gap: 24,
            background: "#ffffff" }
```

Use `layout: "auto"` when the count is not four. `normalize: "match-shortest"`
downscales to the smallest edge, which keeps every tile the same size without upscaling
anything.

**Labels, if the user wants them,** come from `add-text-to-image-browser` run over the
composite — not from burning text into a placement. Never annotate a placement itself;
the plate has to stay clean.

**This is a generalist skill.** It runs on any poster for any client, so nothing is
branded: no logo, no mark, no client name in the chrome. If a client name is wanted, it
is a text parameter on the label pass, never a template edit.

## Naming and what comes back

```
the master       one url, from flora_generate — WRITTEN path only
the placements   four urls, one per site, from flora_generate — ON the canvas
the resizes      three urls, from flora_run_action — NOT on the canvas
the contact      one url, from side-by-side-composite-browser — NOT on the canvas
the deck         a local .pdf path, built by the deck builder
the project      https://app.flora.ai/projects/<project_id>
```

Everything except the PDF is a url. Report the project link with the ids you resolved at
the top of the run — not one reconstructed at the end, and not the workspace id, which is
the substitution to watch for since both are long `_`-prefixed strings.

Name the **work**, not the client — the poster's title, kebab-cased — and use it as the
label when you report each url, so a user collecting several runs can tell them apart.
`Many Hands` -> `Many-Hands-gable`, `Many-Hands-contact`. If the creative has no title,
synthesise one from what is actually in the picture. On the WRITTEN path, synthesise it
from the request *before* firing the master, so the master and everything descended from
it carry one name — `Many-Hands-master`, then `Many-Hands-gable`.

The urls are public, unsigned and permanent. Putting one in a transcript discloses that
asset to anyone who sees the transcript — worth a word to the user when the creative is
unreleased.

## Delivery — the deck structure

One page per thing. Never grid placements two-up; a placement is the deliverable and it
gets a page. **Every layout the skill produced appears in the deck** — if it was made,
it ships.

This is the page plan the builder implements. The footer carries `PROJECT` and `DATE` and
nothing else — and `PROJECT` is the resolved `project_id`, linked, so the deck is
traceable back to the canvas that made it a year from now.

```
1  COVER        title, one-paragraph standfirst, and a spec strip:
                CREATIVE / SOURCE (px + ratio) / PLACEMENTS / SOCIAL.
                SOURCE reads SUPPLIED or GENERATED, and on the WRITTEN path the
                standfirst carries the request verbatim, so the deck records what
                was actually asked for.
                The creative sits on this page — text one side, plate the other.
                The standfirst MUST state that the creative is reproduced and
                never regenerated; that sentence is the deck's only statement of
                the law, so it does not get cut. On the WRITTEN path that law
                starts at the master, not before it — say so in the same sentence
                rather than implying the poster was handed over finished.
2+ PLACEMENTS   ONE PAGE EACH. Image left at ~70% width. Right column carries:
                  <site name> as a heading
                  SHOT    focal length, camera height, angle
                  LIGHT   time of day and what it does to the surfaces
                  MOMENT  the human beat
                then one sentence of plain observation about the site.
n  SOCIAL       REQUIRED, never omitted. The master and all three resizes at
                MATCHED WIDTH, bottom-aligned on a shared baseline, heights left
                free to climb. Each under a TWO-PART CAPTION — the USE on the
                left, the RATIO right-aligned against it:
                  MASTER          16:9
                  FEED            1:1
                  FEED PORTRAIT   4:5
                  STORY · REEL    9:16
                The resizes are part of the deliverable — a deck without them is
                half the job.
```

**No standalone blow-up of the creative.** An earlier version of this structure spent
page 2 re-showing the artwork large and alone, directly after the cover had already shown
it. That is padding: the reader has just looked at it, and a second look at the same
image teaches them nothing they did not have a page ago.

The cover carries the plate AND the standfirst that states the law, which is everything
the blow-up page was there for. Six pages that each do a job beat seven where one is a
repeat — a deck earns its length by what changes page to page.

Cut it from the total too, not just the sequence: renumber so the footers run 1..6 with
no gap where the old page used to be.

**Caption the use, not just the ratio.** `9:16` is a number; `STORY · REEL` is where the
file goes. A media planner reads the deck to find out what they have been given, and a
column of bare ratios makes them do the translation themselves. So the use is the
caption's headline — set in the deck's label weight, left-aligned under the image — and
the ratio sits quietly right-aligned on the same line as the supporting fact.

Getting this wrong is subtle rather than dramatic: leading with the ratio and dropping
the use to small print underneath *looks* complete and still reads as a contact sheet.
The use-name is the part that makes the page a delivery.

**Match the width, not the height.** "Shared baseline" means a common BOTTOM EDGE with
every crop set to the same width — so the heights climb left to right and the page shows
you, at a glance, that 9:16 is tall and 16:9 is a letterbox. The shapes are the content
of this page.

Height-matching instead is the trap, and it is an easy misreading of "baseline". It
makes 1:1, 4:5 and 9:16 come out at nearly the same width, the ratios stop being legible
AS ratios, and the one thing the page exists to demonstrate quietly disappears — while
still looking like a finished layout.

Size it off the tallest crop, which is 9:16. With a matched width W the stack is
`W x 16/9` tall, so W is bounded by the vertical space left under the header, and the row
is bounded by `4W + 3 gaps` across. Solve the vertical first — running the 9:16 off the
bottom of the page is the failure mode here, not running out of width.

Print the SHOT / LIGHT / MOMENT you actually asked for, not a description of the result.
It is the brief, and it makes the deck a document someone can act on rather

…(truncated)
