character-portraits

Verified@baize/character-portraits
Use it in Cohub

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

Use the skill: character-portraits. Run `npx -y @neta-art/neta-search-cli@latest skills use @baize/character-portraits --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 @baize/character-portraits
Summary

Full-body standing portraits of your characters, all drawn in one art style and collected in a gallery page.

  • Draw up to 6 characters at once, one tall full-body portrait each on a plain background, all sharing the same look.
  • Pick your world's own art style, or one of 130+ tested styles such as ink wash, shadow puppet, risograph, kawaii chibi or line art.
  • Every portrait sits in a gallery page next to the character's old picture. Tap to compare, then save one, a set, or everything as a zip.
  • Each portrait comes with a short caption about the character as drawn. Later sets are added to the same gallery.
  • Your world stays untouched unless you choose portraits to use as character covers. The old covers can be restored.
SKILL.md
# Character portraits

Full-body standing portraits of one or more characters of this world, all in one look, on a plain
background. The creator gets the world's **portrait gallery** — a page with every set they have
made, each portrait next to the picture it had before — not loose images. The world itself stays as
it was; covers change only when the creator picks portraits for that afterwards.

## Requirements

This skill decides *what* the portraits are; the neta platform's `generate` skill does the
generating.

- Step 1 (who and look, maybe one questionnaire round) comes first. Only once both are settled,
  read `/mods/neta/.agents/skills/generate/SKILL.md` and
  `/mods/neta/.agents/skills/generate/playbooks/image.md` and follow their core loop (declaration,
  references, submit, look). Steps 2–5 run inside that loop.
- If `/mods/neta` does not exist in this world, say image generation is not available here and stop.
- `<skill-dir>` = the folder this SKILL.md sits in. The scripts need `python3` and ImageMagick
  (`magick` or `convert`), both in the Studio sandbox.

## Inputs

| Input | Required | Counts as given when |
|---|---|---|
| Who | yes | the message names characters of this world, carries selected character cards, or says the whole cast ("all my characters", "整个世界的角色"). A description that matches no card does not count → ask. |
| Look | asked when not given | the message names a look: the world's own look in any wording (e.g. "like the world", "in the world's own art style", "keep the world's current look", "原画风", "世界画风", "按世界自己的画风" — examples, not a full list), a style name in any language, a style id or an old handraw number |
| Language | no | the user's language; the world's language if the user wrote none |

**At most 6 characters in one set.** The whole cast with more than 6 → the 6 main ones (the order
of `manifest.json` / the world text), and offer the rest as the first next-step.

Ask at most **one** round, with the platform questionnaire, only for what is open (see
[`references/questionnaire.md`](references/questionnaire.md)). Everything else has a default.

## Looks

| Look | When | How it is made |
|---|---|---|
| **world** | asked for, or picked in the questionnaire | the world's own `visualStyle` (in `world/WORLD.md`) with the character's cover as the reference image, on the creator's own image model |
| **styled** | the user names one of the tested styles, or picks it in the questionnaire | style first: the style's sample image is the only image, the character is described in words, on GPT Image 2.5 |

Styled looks: in `references/portrait-styles.json`, each tested by drawing the same characters as
full-body portraits. Do not read the file whole; use `python3 <skill-dir>/scripts/styles.py` —
`find "<words>"` (a name in any language, a keyword, an id like `FE-004`, an old handraw number),
`get <id>` (`prompt`, `note`, `sample`, `preview`), `tiles --kind modern|guofeng|creature` (the
questionnaire's choices). Add `--lang <user language>` for names. Not in the list → the closest
`find` result, and say which.

## Deliver

**One gallery per world**: a published web Work, `works/web/<world-slug>-portraits`. Each run adds a
**set** (one look, its characters) on top. Every portrait can be tapped to compare with the
character's picture from before; save one, a set, or everything as a zip; page UI in the user's
language. Re-publishing the same Work keeps one gallery card on the Board. Handed over with an
interaction-components work card and one line per portrait, then next-steps. The generated images
stay ingredient image works, not placed on the Board.

## Steps

1. **Who and look.** Read `manifest.json`.
   - Who is *given* only as in the table above. Read each card
     (`node /mods/neta/neta atom get "world/characters/<Name>.md"`) and the world (`world/WORLD.md`).
   - The look is given when the message names one (above).
   - Who and look both given → no questionnaire, no plan or confirmation message: go straight to
     step 2 and draw.
   - Anything open → one questionnaire round per [`references/questionnaire.md`](references/questionnaire.md).
     Nothing is generated before the answers are back.

2. **Write each portrait.** For every character, following
   [`references/portrait-prompt.md`](references/portrait-prompt.md):
   - **anchors** — 4–6 colour anchors from the card's text and its cover (look at the cover);
   - **pose and prop** — one sentence from the card (their signature item, a stance that fits);
   - **line** — what the gallery shows under the portrait: one short line in the user's language
     (≤ 24 CJK characters or ≤ 60 Latin characters) about this character as drawn ("拿着地图和点心,
     一路吃到南方").
   - Do not stop to confirm.

3. **Generate** (one image per character, 2:3 vertical). Read the declaration first
   (`node /mods/neta/neta generate image params`, with the model prefix below for styled looks):
   `size` → `1024x1536`, `aspect_ratio` → `2:3`, `quality` → `high` if offered. Submit all of the
   set **synchronously as parallel tool calls**, each with `--work-title "<Name> · <look name>"`.
   - **World look**: the world-look prompt; `--image <the cover's media path>` when the card has one.
   - **Styled look**: prefix each command with `NETA_DEFAULT_IMAGE_MODEL=gpt-image-2.5`; the styled
     prompt with `styles.py get <id>`; `--image <sample URL>` is the only image — no `@Name`, no
     cover. Say in one short line that it takes 1–2 minutes.
   - **Blocked portraits** — read the error first:
     - *Copyright* ("copyright restrictions", the generated image was rejected; or a content-policy
       refusal of a franchise character): the model will not draw that character's well-known
       look, and rewording rarely helps. Resubmit that one portrait **once** with the character's
       and the franchise's names left out and the signature marks made generic (keep the colour
       anchors); blocked again → that character is dropped from this set.
     - *Anything else* (weapons, injury, safety): soften its pose and prop once and resubmit it
       alone; blocked again → dropped.
     - When the first two copyright refusals in a set are for two different characters, do not
       retry them and do not submit the characters not yet sent: this world's well-known
       characters are being refused, stop.
   - **Budget: at most (characters + 3) submits for one set, counted.** Never switch to another
     image model, never send a test or probe image, never split one prompt into pieces.
   - Some portraits drawn → finish the set with those and say plainly who is missing and why (the
     image model refused them). None drawn → no gallery; say so plainly, change nothing, and offer
     next-steps: original characters of this world, or other characters by name.

4. **Look, then file them into the gallery.**
   - Open each portrait: the right character (anchors visible), seen whole head to feet, plain
     background, the look right, no garbled text. One wrong → one rework of that one
     (`--target-work`), then go on.
   - Find or create the gallery and add the set:

     ```bash
     G=<skill-dir>/scripts/gallery.py
     python3 $G find                                   # {"work": "works/webs/<world-slug>-portraits", "sets": [...]} or {"work": null}
     # gallery found: WORK=<the "work" find printed>, skip to `next`.
     # no gallery yet: settle the world slug first, then build the path from it
     node /mods/neta/neta world slug get               # null → node /mods/neta/neta world slug set <readable-kebab-case-from-the-world-name>
     WORK="works/web/<world-slug>-portraits"
     node /mods/neta/neta work init web "$WORK" --title "<World> · <Portraits in the user's language>"
     WORK=<the workDir init printed>                   # it may normalise works/web/ → works/webs/; use it as printed
     python3 $G init --work "$WORK" --title "<World> · …" --subtitle "<world name>" --lang <code> --file-base <world-slug>
     # either way:
     DIR=$(python3 $G next --work "$WORK" | python3 -c "import sys,json;print(json.load(sys.stdin)['dir'])")
     # cards.json, in the order drawn:
     # [{"image": "<mediaPath>", "work": "<workId>", "card": "world/characters/<Name>.md",
     #   "name": "<name as shown>", "line": "…", "original": "<the card's first media entry, if any>"}, …]
     python3 $G add --work "$WORK" --dir "$DIR" --look "<look name in the user's language>" \
       --style <style id | world> --cards-file cards.json
     ```

     `original` is the character's cover at drawing time, exactly as its card lists it (an
     `https://` URL or a `../media/…` path); the gallery keeps a copy for the comparison.
     The look name: "World style" / "原画风" for the world look, the style's `name` otherwise.
     `init` on a gallery that already exists keeps it (`"kept"`) — carry on with `next`.
   - **`add` is mandatory**: it is the only command that files the portraits (`page` in step 5 only
     re-renders). **Check before publishing**: `ls "$WORK/$DIR"` must list one `NN.webp` per
     portrait (`01.webp`, `02.webp`…, next to any `NN-orig.webp`). Any missing → run `add` again
     (never hand-edit `gallery.js`), then check again.

5. **Publish and place.**

   ```bash
   python3 <skill-dir>/scripts/gallery.py page --work "$WORK"     # re-renders only; files nothing — run after add
   node /mods/neta/neta work publish web "$WORK"
   T=$(python3 -c "import json;t=open('$WORK/gallery.js').read();print(json.loads(t[t.index('{'):t.rindex('}')+1])['title'])")
   node /mods/neta/.agents/skills/board-ops/scripts/place.mjs --title "$T" --work "$WORK"
   ```

   - `world_slug_required` → set a readable kebab-case world slug (`neta world slug set`), publish again.
   - Publishing fails → place the image works instead and say plainly the gallery did not publish.

6. **Reply and close.** Hand over the gallery with an **interaction-components** link card to the
   work (`{ "type": "work", "workId": "<jq -r .cohubWorkId "$WORK/meta.json">" }`, a real
   `<interaction-json>` block). Under it, one line per portrait — name and what you **saw**. Say in
   one line that the world's cards are unchanged. Then **next-steps**: first "use some of these as
   covers" (in the user's language, e.g. "把这几张设成角色封面"), then the same cast in another look
   (no look named, so the questionnaire comes up), then characters not drawn yet, by name.

## Use as cover (only when the creator asks)

"Use these as covers", "把林昭月那张设成封面", or the next-step above:

1. `python3 <skill-dir>/scripts/gallery.py covers --work "$WORK"` (`--set set-NN` for an older set).
   The creator named which → those; otherwise the cover round in
   [`references/questionnaire.md`](references/questionnaire.md) — the only questionnaire of that turn.
2. For each chosen portrait: read the card's first media entry (`was`), then

   ```bash
   node /mods/neta/neta atom cover set "<card>" --work <work>
   python3 <skill-dir>/scripts/gallery.py covered --work "$WORK" --card "<card>" --was "<was>" --set <set> --n <n>
   ```

3. Publish the same Work again (`node /mods/neta/neta work publish web "$WORK"`; `covered` already
   refreshed the page): the gallery marks those portraits "cover now".
4. Reply: which cards changed, that the old pictures are still in each card and can come back
   ("换回原来的封面"). Nothing else changes: not the world's `visualStyle`, not the world cover, not
   other cards.

**Undo** ("change it back", "换回原来的封面"): for each card,
`python3 <skill-dir>/scripts/gallery.py undo-arg --work "$WORK" --card "<card>"` →
`node /mods/neta/neta atom cover set "<card>" --url "<url>"` →
`python3 <skill-dir>/scripts/gallery.py uncovered --work "$WORK" --card "<card>"`; then publish the
same Work again.

## Changing one portrait later

"Make Bai Su hold her basket", "redo the prince": rework that portrait's image work with
`--target-work`, same look and prompt with the change, then
`gallery.py replace --work "$WORK" --dir <the set's dir> --card <N> --image <new mediaPath> --work-id <id>`
(`--line` to change its line) and publish the same Work again.

## Out of scope

- No changes to the world, its settings or any card, except covers the creator picked in
  **Use as cover**. Never the world's `visualStyle`.
- No deleting files that existed before the run.
- No scenes, no groups in one picture, no turnarounds; no real people; one portrait per character
  per set.

## Not this skill

- Restyle the whole world's covers in one go → the `generate` skill's batch restyle.
- A three-view model sheet → `character-design-sheet`. Stickers → `character-emotes`.
- Postcards of a place → `location-postcards`. A scene with several characters → `generate`.