duotone-editorial-poster

Officialneta-search/duotone-editorial-poster
Use it in Cohub

Paste this into your agent's chat — it fetches the skill and follows it.

Use the skill: duotone-editorial-poster. Run `npx -y @neta-art/neta-search-cli@latest skills use neta-search/duotone-editorial-poster --via site` and follow what it prints. Read its complete output, redirecting it to a temporary file first if necessary, and resolve relative paths from the skill directory it prints.
Use in Neta Studio Open it in Neta Studio and see what people made with it.
Install from a terminal
npx -y @neta-art/neta-search-cli@latest skills use neta-search/duotone-editorial-poster
Summary

Turn a character or scene into a retro print poster with two inks and halftone dots.

  • Start with a picture you own: upload one or pick a character or location card from your world.
  • You get a poster with huge serif type over the figure, mechanical dots in the shadows, and big blank paper areas.
  • Choose two-ink colors from four presets: blue and orange, black and red, gold and navy, or just one ink.
  • The poster comes with its generation prompt and a recipe so you can run the same print style on other images.
  • Works best with a clear subject: a character with a strong silhouette or a dramatic location shot.
SKILL.md
# Duotone editorial poster

Read this whole file before any generation or file write.

Turn an owned or licensed reference into an editorial print poster: neutral paper, at most two inks, a super-large title over the figure, and deliberate empty paper. Deliver **one poster image + the final generation prompt + a recipe**.

v1 is a single sheet in a single system (palette whitelist P1–P4, layout L1 only). Do not install third-party skills. Do not recreate source-post characters, fixed slogans, or brands.

## Contract

### When to pick it
The user already has an image and wants a **print object**, not a normal illustration: duotone (or one ink), mechanical halftone, oversized serif title, a tiny corner line, large paper fields. They also want the recipe so another image can run the same system.

### Stop when the pick is wrong
Ordinary generate / restyle / movie poster / turnaround / no type constraints → hand off to `generate`. Motion posters, video, or web-ad posters → stop; out of scope.

### Input
Required: a reference image (upload, or name a world card — its picture is the reference). Rights: naming a world card counts as world-original; an upload with no statement is taken as the user's own — write "user upload, assumed own" in the recipe and go on. Do not ask about rights unless there is a real flag (see Stop when input is missing).
**Naming only the world, a series, a faction or a theme is not a reference.** "A poster for this world", "a poster of <world name>", "something summery" name no subject. Which character or place goes on the sheet is the user's call; the whole poster changes with it. Do not pick the protagonist or "the most iconic card" yourself. The reference counts as given only when the user uploads an image or names one specific card (a character, location or other material) that has a picture. A word that is both the world's name and a card's name counts as the world name when the request says "<name> world" / "<name>世界" or similar.
**Finding a card's picture.** Run `node /mods/neta/neta find images "<card name>"` and take the best matching portrait / three-view; if none, use the card's cover: the `media` list in the front matter of its file under `world/` (paths are relative to that file), or `node /mods/neta/neta atom get <name>`. Do not read `manifest.json` for cards.
Optional: title (1–8 English words, or ≤8 CJK characters), corner line, palette P1–P4, ratio 3:4 / 4:5 / 2:3, faithful / abstract.
Defaults: palette **P1** (paper #F2F0EC, blue #2858AD, orange #EB7E51, sampled from the vertical blue-orange source sheet, locally named P1). Ratio **3:4**. Layout **L1**. Treatment **faithful**.

### Transform
intake (no upload and no specific card → one question, then stop) → read subject (3–5 anchors) → optional cutout (specimen only, user consent) → fill recipe manifest → **calibrate: settle anything the brief left out yourself (title, palette) and go on — do not stop to ask** → `scripts/compose-prompt.mjs` builds and validates the prompt from the fixed template → `generate` with the reference, **one** image (size from the palette table) → QA (ratio script + ink script + your own look at title/anchors/dots) → repair the same Work if it fails (≤3 rounds total) → deliver.

A model swap **counts as the current round**. There is no crop fallback for a wrong ratio — retry another model once or report the actual ratio. Changing a locked manifest field returns to calibration and bumps the version.

### Deliver
An image Work (the chosen poster) + final `generation_prompt` (mark fields to swap on a new subject: subject / anchors / display_text) + recipe (palette hex, layout family, type hierarchy, print process, image model, rights, **per-round model and prompt version**). Openable, downloadable, shareable, reworkable with generate. Write a new recipe atom via world-ops only if the user explicitly asks to save it into the world. Do not edit existing atoms.

**Where it lives.** The poster is the image Work that `neta generate` writes (`works/<id>.json`, listed by `neta work list`) — that Work is the deliverable; do not copy it into a folder of your own (a folder under `works/` without `meta.json` is not a Work and never shows in Studio). Place it on the Board per the **board-ops** skill.

**Run folder `W`.** The prompt, recipe and QA report are deliverables too, so they are saved as files, not only written in chat. If the request or the environment names an output folder for this task, `W` is that folder. Otherwise `W=exports/duotone-poster/<slug>/` in the world (add `-2`, `-3` if it already exists; never overwrite earlier files). Every working file of the run lives in `W`, not in `/tmp`. At the end `W` holds `generation_prompt.txt`, `manifest.json`, `qa.json`, `recipe.md` and `rounds.json` for the delivered round (step 10). The hand-over message also carries the final prompt and the recipe.

**Language.** Reply in the user's language — every reply, the calibration table and the hand-over message — whatever language this file is written in. The Work title follows the user's language too. Only the poster's own text (title, corner line) keeps the exact words the user gave or approved.

### Out of scope
- Do not change world settings: `world/WORLD.md` config, `visualStyle`, the world cover, the world slug.
- Do not delete, move or overwrite any file that existed before the run.
- Do not edit existing cards, character profiles, memory, existing Works or roleplays, or this skill's installed files (`scripts/`, `references/`). If a script fails, report it; do not patch it.
- Read only the card the user named (or the candidate list at intake) and its pictures; do not browse other chats' files, old questionnaires or earlier runs' output.
- Nothing beyond the deliverable: no extra posters or series, and do not act on your own next-step suggestions.
- Also not done: arbitrary custom palettes; batch series; text-only posters with no reference; real color-separation / post halftone (print feel is generated in one pass); recreating the source post or its cited authors' characters, slogans, brands, or third-party skill assets; installing or calling external skills; motion / video / web posters; watermark removal; batch world reads/writes.

### Stop when input is missing
No reference — including a request that names only the world or a theme (see **Input**); rights are "unsure / found online"; named card has no picture at all (say so and ask for an image; do not scan the world); palette outside P1–P4; watermark or a recognizable real person without likeness rights; "world-original" that is actually an external lift; every image model unavailable (deliver manifest + prompt only, and say no image was made).

## Workflow

Failure recovery is inline. If the vision model refuses images, fall back to a one-sentence subject from the user. `subject_brief` still needs: subject, anchors(2–5), intent, image_role, all marked "user-stated".

1. **intake** Ask only when the reference is missing: no upload **and** no specific card named. A world name or theme alone is missing (see **Input**). In that case, before any compose-prompt or generate call, list the world's cards (`ls world/*/`) and keep the ones with a picture. Then ask **one** plain chat question in the user's language: name 2–4 candidate cards with a short reason each, mark one as the recommendation, and invite an upload instead. For example: "Which one goes on the poster — A (recommended), B, or C? Or upload your own image. Reply 'go' and I'll use A." Do not ask about title, palette or ratio in that question; those are settled at calibrate. **End the turn there.** Do not draft the manifest or generate in the same turn. Once the user answers (a name, an upload, or "go" = the recommended card), continue without asking anything else. When a specific card or an upload is given, do not ask; go straight on: take its picture (see **Finding a card's picture**) and the rights rule in **Input**. If a watermark is found, swap the image — this skill does not remove watermarks. World materials must not be external lifts.
2. **read-subject** A vision-capable text model lists subject, anchors, intent, image role. If it cannot read, offer 2–3 candidates for the user to pick.
3. **isolate-subject-optional** Call `background-removal` only when image_role=specimen **and** the user ticked consent at calibration. Broken edges → revert to hero and note it in the recipe.
4. **manifest-draft** Fill fields from `references/enums.json` + `palettes.json` + `layout-l1.json`. If the user gave no title, take it from the subject: the card's name (1–8 English words or ≤8 CJK characters, in the language of the request). A title the user gave is used letter for letter, a single word included. Corner line: only the words the user gave for it; if they gave none, leave it empty (no corner line). Do not invent one from the card — every extra word is one more thing the model can misspell, and AC7 counts it as stray lettering. No banned strings.
5. **calibrate** Write the short calibration table into your reply as the record: anchors, title, corner line, palette (hex), ratio, L1, empty-paper target, title zone.
   - **The brief already gives the subject, the exact title text and the palette** → do not stop and do not ask for a confirmation; go straight to compose-prompt. Never re-ask what the user just said.
   - **Something the user gave fails a script limit** (e.g. a title over 8 words) → make the smallest change yourself, mark it "agent-adjusted" in the calibration table with the reason, and go on. Do not stop to offer options; the user can change it after seeing the poster.
   - **Anything else missing** (everything except the reference/subject, which intake settles) → decide it yourself and say what you chose in the calibration table, marked "agent-chosen": title from step 4; palette P1 unless the card's own colours clearly fit P2–P4 better; ratio 3:4; no cutout. Do not open a questionnaire or ask for confirmation — the user can ask for changes after seeing the poster.
6. **compose-prompt** (script) Run folder `W` (see **Run folder** under Deliver); save the manifest as `$W/manifest_v<n>.json`, then `node <skill-dir>/scripts/compose-prompt.mjs $W/manifest_v<n>.json --out=$W`. Pass: exit 0, and `$W/generation_prompt.txt` + `$W/render.json` (the `--param` flags for each model) exist. On exit 1, fix the fields it names and rerun. Never skip validation to generate.
7. **render** `node /mods/neta/neta generate image --image <reference path> --prompt "$(cat $W/generation_prompt.txt)" --work-title "<title> · <suffix>" <flags for this model from $W/render.json>`. The suffix is "duotone poster" in the language of the user's request, not the world's or the card's: an English request gets `Duotone Poster`, a Chinese one `双墨海报`, other languages translate it. One image per round, synchronous. The output's `outputs[0].work.id` is the `<workId>` used below and in the hand-over card. To use another model, prefix the command with `NETA_DEFAULT_IMAGE_MODEL=<model>` — never `--model`; order: the default, then gemini-3.1-flash-image-preview, then qwen-image-2.1. Always take that model's flags from `render.json` (gemini has no `size`; it takes `aspect_ratio` + `image_size`). A gpt-image-2.5 safety rejection on a well-known character's picture is common: move to the next model, do not resend the same call. After **every** generate call — a rejection or error included — log it:
   ```bash
   node <skill-dir>/scripts/recipe.mjs log $W --round=<n> --model=<model used> --prompt=v<n> --result=image|rejected|failed --work=<workId> --qa=$W/qa_v<n>.json
   ```
   A rejected call and the swap that follows share one round number. This log is where the recipe's model and per-round lines come from; skipping it leaves the recipe incomplete.
8. **qa** Open the returned `mediaPath` and look at it. Run:
   ```bash
   node <skill-dir>/scripts/inkcheck.mjs check <mediaPath> --palette=<manifest palette_id> --ratio=<manifest ratio> --crop=$W/crop_v<n>.png --report=$W/qa_v<n>.json
   ```
   for AC1/AC3/AC-NS and the AC6 crop; the report lands in `$W/qa_v<n>.json`. Always pass the manifest's own palette; never re-score a render against another palette. Check AC4/AC5/AC7 yourself. Any `"pass": false` in the script output fails the round, the same as a wrong title: go to repair. Never deliver a failing round as passed. Put the checklist in the hand-over. Fixes by failure:
   - **Title errors** → repair.
   - **AC3 ink over limit** → first run `node scripts/inkcheck.mjs sample <mediaPath> --k=7` and compare its clusters with the palette hex. The fix depends on the cause:
     - The dominant plate printed darker or lighter than its hex, in the same hue (e.g. a navy instead of a cobalt). The prompt cannot pull it back. Switch to the whitelist palette whose dominant ink is nearest the sampled cluster (a near-black plate → P2), re-calibrate (bump manifest version), re-compose and re-render.
     - The accent ink spread beyond its one focal event, or a third hue appeared. Either set the focal event to `accent_on_title`, or switch to P4 to drop the accent plate. Then re-compose and re-render.
     - Do not just repeat the same palette with extra wording.
     - If the user named the palette, do not switch it on your own. Repair within it; if it still fails, report it and offer the palette switch as an option.
   - **Subject unreadable while still faithful** → move the title zone off the anchors, bump manifest version, re-calibrate.
   - **AC-NS empty paper under 30%** → usually a full-bleed reference (a map, a landscape, a busy scene) printed edge to edge. Set `image_role` to `crop` (or `specimen` for one object), raise `negative_space_ratio` to 0.45, bump the manifest version, re-compose and re-render. For such references pick `crop` from round 1.
9. **repair** Change only failing fields, prompt v2/v3, and re-render the same Work with `--target-work <workId>` (no extra cards), ≤3 rounds total. A round is one render that returned an image; after round 3 make no further generate call, whatever failed. If any check still fails after 3 rounds, deliver the best round but report the failure honestly:
   - In the checklist, name the failing item with its measured value and limit (e.g. "AC3 off-palette 20.4% > 8% — not passed"). Never mark it ✓, and never describe the sheet as within palette.
   - Offer the next fix as an option: another palette, "shorter title", or "no-title sheet + recipe".
   - The Work shows the latest render. If an earlier round is the better sheet, say so and give its media path. Do not hand-edit `works/*.json` or `manifest.json` to repoint or delete Works.
10. **deliver** Save the run's files for the delivered round:
   ```bash
   node <skill-dir>/scripts/recipe.mjs finish $W --manifest=$W/manifest_v<n>.json --qa=$W/qa_v<n>.json --work=<workId> --checks="AC4 <pass|fail>; AC5 <pass|fail>; AC6 <pass|fail>; AC7 <pass|fail>"
   ```
   Pass: exit 0 and it prints the five files. On exit 1, do what it names (usually a generate call that was never logged) and rerun. Then confirm the Work is in `node /mods/neta/neta work list` and on the Board (board-ops), hand it over with an interaction-components card whose target is that Work (`<workId>` from step 7), in the user's language. The card is required: a Markdown link or image of the PNG under `world/media/studio/works/` is not a hand-over. Send it together with the poster, the final `generation_prompt` (fields to swap marked), the recipe, and file links to `$W/recipe.md` and `$W/generation_prompt.txt`. Write a recipe card into the world only if the user asks (world-ops); if that write fails, leave the recipe in chat and do not retry.

## How the prompt is built

Template, field map, and a filled P1+L1 sample: `references/prompt-template.md`. The script is the only composer. The agent must not hand-write a "close enough" prompt.

When reusing on a new image, replace only: `subject`, `anchors`, `display_text` (and `corner_line` if the user wants). Palette, L1, halftone, and imperfections stay — that is the same system.

## Acceptance

AC1 Ratio error ≤2%, or the delivery states the actual ratio.
AC2 manifest.ink_hex ≤2 and in P1–P4.
AC3 Off-palette pixels ≤8% (distance 30; see `references/qa-thresholds.md`).
AC4 Title matches display_text letter for letter.
AC5 At least two anchors readable; title does not fully cover the face / main silhouette.
AC6 Dots visible in darks at 100% zoom.
AC7 No watermark, logo, or letters outside the manifest.
AC8 Prompt / corner line / recipe contain nothing in `references/banned.json`.
AC9 The poster is one image Work in `neta work list` and on the Board (no copies, no folder under `works/`); the run folder holds `generation_prompt.txt`, `manifest.json`, `qa.json` and `recipe.md` for the delivered round, and the hand-over carries the final prompt and the recipe (palette hex, layout family, type hierarchy, print process, image model, rights, per-round log).
AC13 Replies, calibration table and hand-over are in the user's language; only the poster text keeps the user's words.
AC10 Calibration before generate: the calibration table is in the reply; missing items are agent-chosen and marked so, not asked; items the user already gave are never re-asked. The one exception is a missing reference/subject: it is asked once at intake, before any generate.
AC11 No edits to existing atoms; the only allowed write is one new recipe material the user asked for.
AC12 ≤3 generate rounds; each logs model and prompt version.

## Cases

### should
「把这张角色立绘做成双墨印刷海报,蓝橙配色,标题用 HOLD STILL。」
Expect: this skill, the user's palette, L1; subject, title and palette are all given — generate directly; deliver image + prompt + recipe.

### one-liner
「用莉迪亚的角色图做一张复古双色海报」
Expect: this skill. Find Lydia's picture (find images / card cover), title from her name, palette chosen and marked agent-chosen, no questions; one image, QA, deliver the Work on the Board.

### world-only
「给这个世界做一张复古双墨印刷海报,要网点和超大标题。」
Expect: this skill. No upload and no specific card — the subject is missing. Ask one question that offers 2–4 cards with pictures, one recommended ("reply 'go' for it"), then stop; no generate in this turn. After the answer, title and palette are agent-chosen with no further questions; one image, QA, deliver.

### shouldNot
「按这张照片改成赛博朋克风。」
Expect: do not fire. No print recipe, duotone, or type system — hand off to generate.

### missing
「做一张编辑风海报,主题是夏天。」
Expect: stop. No reference. Ask for an image or a named material. No text-only generate.

### theme
「换一张场景主视觉,色板用黑红,标题改成 NIGHT SHIFT。」
Expect: same skill, same L1, palette P2, replace subject/anchors/display_text, one more single-sheet loop.

## Gold

Work: *Keep the Measure B* (id `work_01M4FVPFYMHW4K8HN4DYWDYT5A`) — original surveyor bust as reference, P1 + L1 + 3:4, title KEEP THE MEASURE, corner line survey notes.
Still: `examples/gold/poster.png`. QA in `examples/gold/qa.md`, passed, frozen.

## Gaps

- The source post never published the full "hacked prompt"; this template is a local rewrite, not a clone.
- Exact display type is a model weakness; 2 images/round × ≤3 rounds, no one-shot guarantee.
- No real post-halftone. AC3 catches obvious third hues, not "looks close enough".
- Whether the vision model accepts images is decided at run time; if not, fall back to a spoken subject.
- Model params checked 2026-10-30 (`generate image params`): gpt-image-2.5 `size` (multiples of 16, ≤1536); gemini-3.1-flash-image-preview `aspect_ratio` + `image_size`, no `size`; qwen-image-2.1 `max_side` when a reference is present. `compose-prompt.mjs --out` writes them to `render.json`.
- gpt-image-2.5 often refuses a well-known IP character's picture as a reference (HTTP 400, safety system); earlier gate runs recovered on gemini. The swap has to reach the recipe through `recipe.mjs log`; runs that only mentioned it in chat lost it.
- Earlier runs kept the prompt, recipe and QA report in `/tmp` and in chat only, so nothing checkable was left after the run. They now live in the run folder (`recipe.mjs finish`).
- Agents used to invent a corner line from the card (a nickname, a place, a technique). It was misspelled or counted as stray lettering. The corner line is now empty unless the user gives one.