[![中文](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-f2e3e3?style=for-the-badge&labelColor=f2e3e3&color=b07070)](README.md)
[![English](https://img.shields.io/badge/English-8b1a1a?style=for-the-badge)](README.en.md)
[![Follow on X](https://img.shields.io/badge/Follow-%40eternityspring-b07070?style=for-the-badge&labelColor=8b1a1a&logo=x&logoColor=f2e3e3)](https://x.com/eternityspring)

# character-refs

**Actually generates** reference images for a character from any story — a novel adaptation, your own original story, or a one-off character; no source text needed. Describe a character in one message and get a set of reference images ready to attach to video models (H3 / Wan / Seedance).

![Character references report](assets/report.webp)

**Each set has a single root**: a front full-body anchor. It is the only text-to-image generation; the headshot, profile, back view and details all reference only that anchor, never each other — so a bad image is regenerated on its own without knocking anything else over.

## Tiers — tier 2 by default

| Tier | Contents | When |
| --- | --- | --- |
| 1 | Front full body (the anchor) | Always, first |
| 2 (default) | Front headshot, 90° profile, back view | Generated by default |
| 3 | Details: the ones the user names, or the character's most recognizable features; falls back to hair / accessory, neckline, cuff, shoes | Close-ups of accessories or garment details |
| 4 (off by default) | 45° headshot | Only when asked |

Based on an H3 test: with only the full-body anchor attached, costume and back view come out right but the face drifts; adding a headshot brings the face closest. So tier 2 is generated by default; details and the 45° headshot are added on demand.

## One-shot input

No form to fill in. One message is enough — the agent splits it into identity, face, hair, build, skin, top, bottom and detail fields, fills the gaps, tags each field's source (stated / inferred / default) and prints a confirmation table. It only asks if age, gender or era can't be inferred.

## Models

Four options, chosen on first use and kept after that:

| | Notes |
| --- | --- |
| `qwen` | Qwen Image 2.1 on your own ComfyUI. 25–50 s per image, exact sizes, no subscription quota |
| `codex` | Local codex built-in image generation (GPT). No API key, but it uses your ChatGPT plan quota (Plus hits the limit after ~25 images) |
| `openai` | OpenAI Images API, gpt-image-2 by default, pay per use |
| `custom:<name>` | Your own command, templated with `{prompt_file}` `{refs}` `{out}` `{width}` `{height}` |

**The anchor and the derived images can use different models, and any single image can be regenerated with another one.** Consistency comes from sharing one anchor, not one model — Qwen anchor + GPT derivatives, GPT anchor + Qwen derivatives, and mixing both within one set all held together in testing. Each image records its model; mixing only raises a note.

## Styles

Two built-in presets, with Chinese and English names:

```bash
node scripts/character-refs.mjs new 阿禾-intake.json --out out/                  # anime by default; --look realistic for photos
node scripts/character-refs.mjs looks                                             # list all presets
```

| Preset | Names | Tested |
| --- | --- | --- |
| Anime (default) | `anime` / `cartoon` / `动漫` | Qwen, all four tiers passed |
| Realistic photo | `realistic` / `写实` | Qwen and GPT, several characters |

For your own style: `look-template anime > my-look.json`, edit it, then `--look my-look.json`. To change the style of an existing character, use `restyle`; every image becomes stale and you regenerate from the anchor. Style and character are separate layers, and style words are rejected in character descriptions — the same character under another preset is simply drawn another way.

## Labels and regeneration

Every image carries a label `character/outfit/view/version` (e.g. `阿禾/default/face-front/v2`) in its filename, in `asset.json` and in the PNG metadata.

```bash
node scripts/character-refs.mjs gen 阿禾/asset.json detail-neck --model codex   # regenerate one image as a new version
```

Old versions are never overwritten. Only three things are frozen: the anchor file, the style snapshot and the text description. When one changes, whatever depends on it is marked stale: a new anchor makes everything else stale; a new headshot makes the hair / neckline details stale; an edited description makes everything stale. Stale images are marked, never deleted.

## Quality gates

Checked in code; `check` exits 1 on failure: side length 300–5760 px, aspect ratio 0.4–2.5, the view's target ratio (2:3 / 4:5 / 1:1), ≤ 20 MB (the intersection of H3 / Wan / Seedance reference limits), a white background (all four edges for full-body views, top and upper sides only for headshots, explicitly skipped for details), and staleness. **The gates can't judge likeness, angle or framing** — look at the images.

## Report

`render` writes a self-contained HTML page: a single card at tier 1, a character-sheet layout from tier 2 (headshot on the left, front / profile / back top right, detail strip bottom right), details hidden by default with a one-click toggle. Stale images get a red outline, explained by a legend at the top; hover over one to see why. The layout is done in code rather than generated as one composite image — a composite used as a reference makes models draw the person smaller. The report UI ships in Chinese, English and Japanese (`render --lang en`); for any other language the agent translates the UI strings on the spot. Character descriptions stay as written, and image prompts are always English.

## Command line

```bash
node scripts/character-refs.mjs config --model qwen --confirm-anchor yes --qwen-env-file ~/comfy.env
node scripts/character-refs.mjs intake-check examples/阿禾-intake.json
node scripts/character-refs.mjs new examples/阿禾-intake.json --out out/
node scripts/character-refs.mjs gen out/阿禾/asset.json          # anchor, then stop for confirmation
node scripts/character-refs.mjs confirm out/阿禾/asset.json
node scripts/character-refs.mjs gen out/阿禾/asset.json          # fill tier 2: headshot, profile, back
node scripts/character-refs.mjs gen out/阿禾/asset.json --tier 3 --reason "hair accessory close-up in E03"
node scripts/character-refs.mjs check out/阿禾/asset.json
node scripts/character-refs.mjs render out/阿禾/asset.json --out out/character-refs.html
```

## Selftest

```bash
node scripts/selftest.mjs
```

233 assertions, no model calls, no quota. The Qwen and GPT Image adapters are checked against local mock servers; the full pipeline runs end to end with a custom command that produces blank white images.

## Known limitations

- Only the realistic and anime styles are tested; the anime 45° headshot also under-rotates (about 20–30° on Qwen)
- Small scars tend to look like fresh wounds; large details like double-breasted buttons get framed too wide
- Qwen renders coarse cotton in neckline details with a velvet-like sheen; regenerate that one with codex
- The `openai` adapter has only been checked for request format, not run with a real key
- **Tested on macOS + Node 24 only**
