# Art director playbook

**Read this when:** "generate prompts", "generate assets", "make images / video / VO / music", "regenerate scene-XX <slot>", "try a different model", "A/B variant", "how much will it cost".

> **Anchor order discipline (every multi-scene project):**
> 1. **Location-master-plate first** — for any project where ≥2 scenes share a setting, generate the room / location plate as **anchor #1**, BEFORE any character or scene anchor. Pass the plate as `--ref` alongside character masters on every subsequent scene gen. Skipping this cost noski-people-001 $0.45 image-regen + 45 min user-feedback loop ("in every shot they're sitting on a different couch and I asked for the same one" — three different couches across three anchors). For ≥25-scene projects, the plate alone isn't enough — generate ≥3 unique anchor angles per recurring subject (location, hero character, hero product). Full discipline + CLI shape + worked example: [`art-director/location-plate.md`](art-director/location-plate.md). This is the single highest-leverage rule in this playbook.
> 2. **Character / persona masters second** — one per cast member, each generated with the location plate as `--ref`. Pass both (location + character) on every downstream scene gen to lock identity + setting.
> 3. **Scene anchors third** — scene-01 first, surfaced to user → wait → scene-02 → wait → … only batch 4-6 anchors at a time AFTER two solo gens land with user approval.
> 4. **i2v / video generation last** — never i2v an unapproved scene anchor.
>
> **Photoreal-human projects:** read [`art-director/photoreal-humans.md`](art-director/photoreal-humans.md) before drafting prompts — TV-commercial register (Tom-Ford / chiaroscuro / marble) is the wrong default for natural-feeling UGC; use Sony A7 IV + Sigma 35/85mm + Kodak Portra 400 still-photo register instead. Venom-bodywash-001 burned ~$3 on this miscalibration.
>
> **Aesthetic-lock checkpoint (HARD gate, applies before anchor fan-out):** after the location-master-plate (anchor #1) and the character master(s) (anchor #2) are approved by the user, the agent MUST (a) name the **register** in one phrase ("still-photo candid documentary" / "Old-Spice high-key commercial" / "liminal-spaces analog-horror" / "CGI translucent specimen" / etc. — taxonomy at the top of [`photoreal-humans.md`](art-director/photoreal-humans.md)), (b) run the [`character-fit.md`](art-director/character-fit.md) check (clean mascot + gritty register? pick reinterpret / distressed-variant / shift-register BEFORE fan-out), (c) write the one-line **aesthetic-lock string** into `STORYBOARD.md`, and (d) generate **one cover per register** and surface it to the user. **Refuse to fan out scene anchors until the cover is approved.** Skipping this is the documented cause of `ralphy-vs-higgsfield-001`'s three-register shipping defect (Lesson #7) and `ralphy-carousel-001`'s clean-ghost-in-punk drift (postmortem #2). When in doubt about which register applies, run `ralphy ref pull <one-canonical-url> --frames` and READ the frames — do NOT scrape-summarize (`biofix-hypnic-en-001` defect class).
>
> **Model drift handling:** read [`art-director/regeneration.md`](art-director/regeneration.md) — **one retry max** on a kling/seedance prompt that misses; then **redesign the scene**, don't fight model basins. Glitter-cream-001 lost 2× $0.42 fighting "jar near cheek → powder compact" drift across 3 retries.

Between "scenario approved" and "assets on disk for the editor" — that's my zone. Prompt engineering, API orchestration, single-slot regeneration, A/B variants, cost discipline. Never invent model-id from memory — always cross-check `MODELS.md`.

> **STOP rule.** Every model call goes through `ralphy generate`. No raw `fetch` / `curl` / `bunx tsx` against a media API — gen-log + asset-manifest + cost rollup all depend on the CLI. AGENTS invariant #2.

## CLI cookbook

**Every model call goes through `ralphy generate`. No raw `fetch` / `curl` / `bunx tsx` against media APIs — the gen-log + asset-manifest depend on it.** Cross-check `MODELS.md` for `--model` overrides.

```bash
# Image (default model: google/gemini-3-pro-image-preview)
ralphy generate image --project <id> --slot scene-01-bg --prompt "<text>" \
  [--ref <url> ...] [--model <id>] [--size 1080x1920] [--negative "<text>"]

# Video (default model: kwaivgi/kling-v3.0-pro)
ralphy generate video --project <id> --slot scene-01-vid --prompt "<motion>" \
  --duration 5 [--image <ref-url>] [--model <id>] [--audio]   # --audio only with veo-3.1

# Voiceover via ElevenLabs (eleven_multilingual_v2)
# Parallel calls targeting the SAME slot are serialized by an in-process file lock
# (#039) and verified via ffprobe after write — a corrupted 0-duration mp3 is
# treated as a transient blip and retried once before failing hard. Cross-slot
# fan-out stays parallel (TTS endpoint cap is 3 in-flight via #007 semaphore).
ralphy generate voiceover --project <id> --slot scene-01-vo --voice <voiceId> --text "<line>"

# Music bed via ElevenLabs Music
ralphy generate music --project <id> --slot bed-01 --prompt "<genre, tempo, mood>" --duration 30

# Captions via ElevenLabs Scribe v1 (word-level, ≤25MB audio)
ralphy generate captions --project <id> --audio <vo.mp3>

# Single-slot regen — APPEND-ONLY: new file lands at <slot>.v<N>.<ext>, never overwrites.
# Manifest gets a new version entry; the previous file stays on disk for diff / rollback.
ralphy generate video --project <id> --slot scene-03-vid --prompt "<new>" --duration 5

# Inspect what's on disk + cost so far
ralphy project show <id> --assets        # asset-manifest.json
ralphy project show <id> --prompts       # prompts.json
ralphy project log <id> --type generations --limit 50    # cost + latency + errors
ralphy asset list --project <id>         # disk inventory by slot
```

If you reach for a backend that isn't covered (e.g. lipsync, image editing, talking-head) — STOP. Don't write a script. Either `MODELS.md` already documents the route, or propose adding the verb to `cli/commands/generate.ts`.

## Sub-docs (read on demand)

| File | When to read it |
|---|---|
| [art-director/location-plate.md](art-director/location-plate.md) | Multi-scene-same-room project — generate one wide `location-master-plate` anchor BEFORE any character / scene anchor; ≥3 angles per recurring subject on ≥25-scene projects |
| [art-director/photoreal-humans.md](art-director/photoreal-humans.md) | Photoreal human characters — still-photo register (Sony A7 IV + Sigma + Kodak Portra 400), 5-cue checklist, anti-AI-slop block, wider register taxonomy |
| [art-director/character-fit.md](art-director/character-fit.md) | Clean brand mascot paired with a gritty register (punk / acid / horror / xerox) — three choices: reinterpret-in-medium, distressed variant, or shift register. Decide at cover-first checkpoint |
| [art-director/prompt-style.md](art-director/prompt-style.md) | Authoring prompts — register-first axis, 4-layer structure, slot-specific rules |
| [art-director/model-choice.md](art-director/model-choice.md) | Picking a model / cost preview / mid-project switch |
| [art-director/ref-photo-policy.md](art-director/ref-photo-policy.md) | Named persona/brand in scenario — when to refuse / when to override |
| [art-director/regeneration.md](art-director/regeneration.md) | Single-slot regen, A/B variants, seed/prompt drift |
| [art-director/quality-gate.md](art-director/quality-gate.md) | scoreImage / scoreVideo gate after each generation |
| [art-director/pre-render-checklist.md](art-director/pre-render-checklist.md) | HARD snapshot-review gate before handing to editor |

## Sub-tasks

| Sub-task | When | Sub-docs |
|---|---|---|
| `prepare-prompts` | scenario.json ready, prompts.json missing/stale | prompt-style |
| `generate-assets` | prompts.json ready, asset-manifest incomplete | regeneration |
| `regenerate-slot` | "regenerate scene-XX", model/prompt/seed change | regeneration + quality-gate |
| `compare-variants` | "I want 2-3 variants of this shot" | regeneration |
| `cost-preview` | "how much will N videos cost" | model-choice |

## What I read on start

- **`AGENTS.md`** — invariants (no FAL, no scripts, ref-required, quality gates).
- **`MODELS.md`** — every model call. Don't hardcode from memory.
- **`.agents/skills/ugc-*`** (and `/poster`, `/carousel`, …) — the content-niche craft-overlay skill for the brief's *kind* of content, loaded on top of the matched format / template as the prompt-authoring overlay. The format / template is the primary route (`docs/templates-index.md`, `ralphy template suggest --help`); a *style* template doubles as a remix target only when the user pointed at a specific video to reproduce. See [`docs/skills-vs-templates.md`](../skills-vs-templates.md).
- **`docs/creative-library/personas/ARCHETYPES.md`** — 8 archetypes (when there's a persona slot).
- **`docs/creative-library/scenes/SETTINGS.md`** — 9 scene settings (when you need to pick a setting).
- `.ralphy/workspaces/<ws>/projects/<id>/scenario.json` — slots + VO text.
- `.ralphy/workspaces/<ws>/projects/<id>/prompts.json` — what already exists.
- `.ralphy/workspaces/<ws>/projects/<id>/asset-manifest.json` — what's already on disk (skip).
- `.ralphy/workspaces/<ws>/projects/<id>/logs/generations.jsonl` — on regeneration, to avoid repeating a failure.
- `templates/<slug>/{TEMPLATE,hooks,prompt-cookbook}.md` (or `.ralphy/workspaces/<ws>/templates/<slug>/`) — if the project was scaffolded from a template, the cookbook is your prompt-writing reference.

## Step 1 of every gen — read the library (02.0L.03)

Before writing a prompt for any slot, run `ralphy prompts library lookup --goal "<one-line description of the slot>"` and read the top-matched `entry.md`. The library is organized by goal/situation (not by model) — it carries the Bad / OK / Ideal worked-example pattern for hooks, product reveals, selfie monologs, caption styles, music modes, and so on.

`ralphy prompts modes --kind <video|voice|music>` lists the cookbook mode files when the agent already knows which model family to call. Pair the library entry (goal layer) with the per-model adapter (shape layer) — the adapter consumes a `NormalizedPrompt` and emits the model-specific syntax automatically (see `cli/lib/providers/prompt-adapter/`).

## Hard rules (inherited from AGENTS.md)

1. **All calls go through `ralphy generate {image|video|voiceover|music}`.** No runtime TS scripts in `.ralphy/workspaces/<ws>/projects/<id>/scripts/`. If an operation isn't covered — stop and extend `cli/commands/generate.ts`, don't copy code into the project.
2. **Reference-required gate (named real entities only).** See [art-director/ref-photo-policy.md](art-director/ref-photo-policy.md). The gate fires for a named person / recognizable brand product / IP. Generic briefs do not trigger. Override path: `ralphy generate ... --no-ref-consent "<reason>"` on the specific failing call; the CLI auto-appends `stage: "no-ref-consent"` to `user-prompts.jsonl`.
3. **Quality gate.** See [art-director/quality-gate.md](art-director/quality-gate.md). Two failures in a row → stop, report concrete options to the user. Refuse, do not warn (AGENTS invariant #4).
4. **MODELS.md is the only source.** See [art-director/model-choice.md](art-director/model-choice.md). Always pick the best model per kind — there is no "cheaper draft" path. Budget caps (`docs/playbooks/producer.md#budget`) are the lever to control cost, not model downgrade (`04.0A.03`).
5. **Iterate by single-slot regen, never overwrite.** "Rework scene-03" → `ralphy generate <kind> --project <id> --slot scene-03-<kind> --prompt "<new>"`. Append-only versioning writes `<slot>.v2.<ext>` (then `v3`, `v4`, …). The prior version stays on disk for diff / rollback; the manifest tracks both. Pass `--force-overwrite` only when the user explicitly asks for legacy destructive behavior (`04.01.03`).
6. **Generation logging is automatic** via `ralphy generate` (logs are written to `generations.jsonl`). **User-prompt logging is NOT automatic — you MUST log it.** Every user feedback turn on an anchor / prompt / model swap goes to `user-prompts.jsonl` via `ralphy project log-prompt <id> --text "<verbatim>" --stage <feedback|approval|critique|rejection>` BEFORE you regenerate. Same MUST-log discipline as the scenarist playbook (see [`scenarist.md` → "User-prompt logging"](scenarist.md#user-prompt-logging-must-every-turn)). "Try v2 with a wider lens", "approve scene-03", "scene-05 looks AI-slop" — all log-prompt turns. Sparse logs are the documented cause of unreliable postmortems.

## Prompt hygiene

Three small rules every `ralphy generate` call should clear before submit. Each is a one-liner; each saves one regen cycle (~$0.15–$1) per occurrence and was filed from a real postmortem. See [notes/issues/done/050-anti-mockup-and-prompt-hygiene.md](../../notes/issues/done/050-anti-mockup-and-prompt-hygiene.md) for the bundle.

### 1. Anti-mockup directive (nano-banana / gemini-3-pro-image-preview)

`nano-banana` defaults to a **tiny iPhone-mockup-in-corner** composition unless the prompt explicitly forbids it. Without the forbid-string, even a clearly full-bleed brief (poster, hero, magazine layout) comes back with a postage-stamp phone floating in the lower third. Validated on `appstore-takeaminute-001` (`screen-01-hero-v2`/`-v4` both leaked mockups; 2 paid regens at $0.15 each before the directive went in; ~8 further regens prevented across the run).

**Rule.** Any full-bleed slot prompt for nano-banana / gemini-3-pro-image-preview MUST lead with the verbatim block below. The HERO / TROPE / CTA register from `appstore-takeaminute-001` is the canonical wording:

```
CRITICAL: This is a FULL-BLEED MAGAZINE POSTER LAYOUT — NO iPhone
device frame, NO phone mockup, NO screen bezel. The poster IS the
entire image edge-to-edge.
```

The player-UI-overlay variant (HD / video-content register) — use when the slot is "video still with player chrome drawn on top", not a poster:

```
CRITICAL: This is FULL-BLEED video content with a PLAYER UI OVERLAY
drawn directly on top — NO iPhone device frame, NO phone-mockup bezel
around the image. The video scene IS the entire image edge-to-edge;
the player chrome (scrubber, quality pill) sits ON TOP of the video
like a watermark.
```

When the slot genuinely IS a phone-mockup (LIBRARY / NEW screen in an App Store pack — i.e. the phone is the intended subject), invert the rule and **name the mockup explicitly** so nano-banana places it deliberately instead of as a leak:

```
CENTER VISUAL: sleek 3D angled iPhone mockup floating tilted, screen
showing <concrete scene description with named in-app content>. Soft
<brand-color> glow under the phone, additional cards spilling out
behind in 3D depth.
```

Naming actual in-app content (real series titles, real card text, the brand's real palette) makes nano-banana populate the mockup with plausible on-brand artwork instead of generic placeholders. Source: `.ralphy/workspaces/<ws>/projects/appstore-takeaminute-001/POSTMORTEM.md` § "Prompt patterns that worked (verbatim)".

### 2. Markdown punctuation in quoted strings

Markdown emphasis (`**bold**`, `_italic_`, `~strike~`) inside a typography slot **bakes literal asterisks / underscores into the rendered glyphs**. `appstore-takeaminute-001` shipped a prompt with `**EVERY DAY**` and got back a poster with actual `**` characters set in the headline — a $0.15 regen.

**Rule.** Before submitting any `ralphy generate image` / `ralphy generate video` prompt that quotes on-poster / on-screen copy, scan the quoted strings for `**`, `__`, `~~`, and stray single `*` / `_` used as emphasis. Strip them, OR replace them with a non-markdown emphasis directive (e.g. `the word EVERY DAY set larger / in the accent color`). The model only sees plain text — there is no markdown renderer between you and the typography. The CLI-side fix (auto-strip at the `cli/lib/providers/media.ts` submit boundary, or warn-on-detect) is a future cleanup; until it lands, this is an agent-side hygiene step.

### 3. Background-job file hygiene

Mirror of AGENTS.md invariant #17. `ralphy generate image --prompt-file` reads prompt / ref files **lazily during the run**, not eagerly at submit. Deleting or rewriting those files while the daemon is running fails silently — `ralphy-carousel-001` lost slides 03-05 of a 6-slide dark-background loop because `rm prompts/slide-0?.txt` ran mid-loop and the daemon reported `--prompt arg missing` without aborting.

**Rule.** While any background `ralphy generate` is in flight against this project, treat its `--prompt-file`, `--ref`, and `prompts/` paths as read-only. To swap a prompt, kill the job first and relaunch; do not edit-in-flight. The CLI-side fix (snapshot prompt-file contents at submit time, or warn-on-delete-of-referenced-file) is tracked in the same issue.

## Split-scene-instead-of-regen (repeat-failure rule)

**Rule.** When a single scene fails twice on the same axis — the same motion beat, the same camera move, the same physically-impossible action — **stop re-prompting and split it into N micro-shots inside the original slot's time budget.** Don't try a third prompt variant; that loop converges nowhere.

The default agent instinct is re-prompt-on-fail (tweak verbs, try a different model, add a negative). For "one beat the model can't deliver" failures, that instinct is wrong. Splitting converts an impossible 5s shot into three possible ~1.6s shots — each a beat the model *can* hit — and the editor stitches them within the same slot duration. The total cost is usually lower than a third regen and the result actually lands.

**Concrete example — `flipper-hypermotion-001` scene-03 (POSTMORTEM rule #11).** Scene-03 was a single 5s hypermotion shot the model couldn't sustain; two regens on the same prompt axis drifted the same way. The redo (one scene, split into micro-shots) cost **$1.28 — ~10% of the entire project budget** — and produced more lessons per dollar than the rest of phase 3 combined. The lesson the postmortem locked in: the second failure on the same axis is the signal to restructure, not to re-prompt.

**Structural pairing.** Splitting becomes much cheaper once `ralphy ref extract-frame` + `ralphy generate video --extend-from <slot>` ship (see [notes/issues/done/012-no-frame-extract-or-i2v-extend-verbs.md](../../notes/issues/done/012-no-frame-extract-or-i2v-extend-verbs.md)) — that pair lets you i2v-anchor each micro-shot from the previous one's last frame, keeping continuity without a fresh anchor for every sub-beat. Until those verbs land, hand-author the split by reusing the scene anchor as `--ref` on every micro-shot.

**Operationally.**

1. After the second failure on the same axis, write down the axis in one line ("camera can't crash-zoom through the prop on contact") and stop regenerating.
2. Rewrite the scene as 2-4 micro-shots whose durations sum to the original slot. Each micro-shot must be a beat the model has hit in this project before.
3. Update `scenario.json` slot list (`scene-03a`, `scene-03b`, …) via `ralphy project update`, regenerate prompts for the new slots only, then `ralphy generate` each.
4. Editor stitches the micro-shots back into the original scene's time window.

## Pre-render self-review (HARD gate)

Before handing the project to the editor for `ralphy render <id>` — every project, no exceptions — walk the pre-render checklist. This is not a soft "should snapshot key beats"; it is a refuse-not-warn gate.

- **MUST snapshot every beat** in `STORYBOARD.md` via `bunx hyperframes snapshot .ralphy/workspaces/<ws>/projects/<id>` before render.
- **MUST eyeball every snapshot for anatomy** (hands, eyes, limb clipping — the `noski-people-001` failure class).
- **MUST eyeball every snapshot for location continuity** (same couch / wall / light across scenes that share a setting).
- **MUST eyeball every snapshot for pivot / camera-axis sanity** (180° line, camera height, no v1→v2 mirror flip).
- **MUST cross-check identity locks** against each cast master shot.
- **MUST verify on-prompt props** present at the right timestamp.

A single fail aborts the render — fix at this layer via `regeneration.md`, then re-snapshot. Full worked rationale + `noski-people-001` / `odindoma-fb-ad-001` postmortem evidence in [art-director/pre-render-checklist.md](art-director/pre-render-checklist.md).

The future `--require-snapshot-review` flag on the `ralphy hyperframes render` namespace (out of scope here — tracked in [notes/issues/028](../../notes/issues/done/028-no-ralphy-hyperframes-namespace.md)) will mechanise this gate. Until it ships, the agent enforces by reading the sub-doc.

## Handoff

- After `generate-assets` with all slots filled → **editor playbook** (compose + render).
- After `regenerate-slot` → re-render via `ralphy render <id>` if the editor has already composed.
- If VO changes → captions are regenerated inside `generate-assets` (after VO).
- If the scenario doesn't hold up → handback to **scenarist playbook**.
