Measured with the image check: full-body turnarounds 0/5 -> 10/10 (Karyla), 12/12 on Karyla, Darrag and Eolan after the face fixes. The 4-step model loses the tail of a ~3000-char prompt. Scars are left out of image prompts: any scar wording, even "fully healed", rendered as fresh gashes. v1.40.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
11 KiB
SOP — Character visual identity and appearance forms
Status: implemented. Covers how a character's avatar and concept (reference) sheet stay the same person, and how one character holds several appearances ("forms") without splitting into several characters or voices.
Single source of truth
- Appearance facts come from the sheet's
physical,clothing,age_estimate,race_species,gender,title,profession,archetype(and the user overridesvisual_appearance,visual_species,visual_costume). Both prompts are built by_character_visual_factsinroutes/conversation.py; the active form is overlaid first (_character_form_sheet, which never lets a form change its ownid/name). image_prompt(portrait) andconcept_art_prompt(reference sheet) are deterministic — built server-side from those facts with fixed camera/lighting/period text. Sheet extraction no longer asks the LLM to writeimage_prompt(it drifted from the concept sheet); the prompt is generated right after extraction bycsAutoGenerateExternalPrompts, and again before any avatar-only or per-form generation. A hand-edited prompt (*_user_edited) is never overwritten.- Only the two prompts describe an appearance.
voice_design_promptand the SillyTavern prompt belong to the character as a whole, so they always live on the base sheet.
Stale detection
_character_identity_fingerprint (server) and _libIdentityFingerprint (browser) hash the same
field list; a test runs the browser code in Node and compares, including list values and non-BMP
characters. The three visual_* overrides are appended only when set, so pairs made before they
existed keep a valid fingerprint. A pair needs review when its prompt version is not current, its
fingerprint is missing or differs, or identity_consistent is false. Regenerating only the
avatar or only the reference image sets identity_consistent = false on an existing pair,
because a lone regeneration produces a new face.
Where the data lives
| base appearance | a form | |
|---|---|---|
| profile image | record image |
form.image |
reference image, version, fingerprint, digests, identity_consistent |
sheet.* |
form.* |
| Physical / Clothing text | sheet.physical/clothing |
form.physical/clothing |
All visual results are written through one function (_libSaveCharacterVisual), which re-reads the
stored record, writes to the active form or the base, and saves. (It used to write the base
reference image to the record root, and the consistent-pair generator kept it in memory only, so a
base pair lost its reference image and metadata on reload.)
GET /api/characters/visual-pair-review checks the base pair and every form that has images, each
against its own metadata and its own fingerprint, and reports form pairs under the form name.
Forms (max 8 per character, enforced in core/validation.py)
Add (starts as a copy of the base Physical/Clothing; only non-empty keys are stored because an empty key would blank the base value when overlaid), rename (unique, ≤160 chars), delete (confirm; the character, its voice and the base look stay), show, and generate matched images for one form or for all forms. Generation rebuilds that form's prompts first (unless hand-edited), saves the pair on that form, then restores whichever appearance was active. Editing Physical/Clothing while a form is active changes only that form.
Image prompt hygiene and the turnaround (1.34.x)
- Setting. Prompts describe the books as The Dark Eye high-fantasy medieval dress (about 900-1300 AD: wool and linen tunics, belted robes, leather, mail where the sheet names it). The earlier "early Renaissance 1420-1500" wording pulled uniforms, coats, hats and boots that read as musketeer/cowboy/wartime.
- Wording. Extracted costume text is rewritten at prompt time (
_period_wording: Uniform, Weste, Hut, Sporen, Jacke, Schal … become surcoat, jerkin, hood, boots, tunic …). Stored sheets are not changed. - Injuries and dirt.
_strip_injury_clausesremoves clauses about blood, wounds, bandages, burns, limping, torn clothes, soot and dirt from appearance, costume and the story moment; the rest of the sentence stays. The portrait baseline asks for clean skin and garments. - Variety. Characters with armour or weapons get an individual garment cut and colour scheme (
_character_design_variation, stable per name; colours the sheet names win) instead of one shared list. - The turnaround stays one T2I triptych. Drawing front/side/rear one by one with the edit model was tried (2026-09-20) and reverted the next day: it returned three near-identical front views, mixed rendering styles between panels, cropped bodies, and copied the wireframe pose guide into the costume — exactly the failure the triptych was introduced to fix. The portrait/sheet face mismatch is still open and needs a different approach.
- Props get an object-only period sentence, so a tile no longer turns into a portrait of the character.
- Older saved prompts are cleaned the same way at render time. Existing image pairs are unchanged until regenerated.
Automatic image check (POST /api/character-image-check)
A vision model reports only what it sees — figure count, the view of each figure left to right,
whether every head and foot is in frame, whether the figures are the same person, apparent gender,
ear shape (pointed/round/hidden), wounds, and background (plain/scenery). The expected values come from
the character sheet and the comparison happens in code (core/character_image_check.py). Asking a model
"is this a woman, as required?" invites agreement; asking "what gender is this?" does not, and the
expected gender is never revealed in the question.
- Kinds:
portrait,turnaround,scene,same_person(profile vs. a sheet figure). - Fail-safe: an unreachable checker, an unreadable answer, or a missing observation is a failure, never a pass. An unreadable answer is returned verbatim (truncated) so it can be diagnosed.
- Thinking is switched off: a reasoning model spent the whole budget deliberating over a confusing image and returned nothing.
- Model: the active LLM by default (
llm_model), overridable with theimage_check_modelsetting. The model already loaded reads images, so a check never makes llama-swap unload it for a separate vision model on a GPU with no room for both.
Calibration (2026-09-21) on seven panels whose defects were known (three front views, elf ears on a human, a wireframe rendered as a figure, cut-off feet, a man where the sheet says woman, a forest behind a studio portrait, and one correct same-person pair), run twice:
| model | correct | wrong | unreadable | per check |
|---|---|---|---|---|
local Qwen3.8-27B (default) |
14/14 | 0 | 0 | ~4 s |
Ling 3.0 Flash VL (vision, free, OpenRouter) |
8/14 | 0 | 6 | ~17 s |
Ling never judged wrongly but on every turnaround ignored the JSON-only instruction and answered in prose. It also has free-tier rate limits and sends the images to a third party.
Generation with checks and retries (face from the profile)
The generation job (POST /api/character-generate-consistent-images) now runs:
- Identity image (text-to-image, full body). The profile picture is cut from it. Portrait check
on exactly that crop (one person, gender, ears, wounds, plain background). On failure: a new seed,
up to
image_check_max_attempts(default 3), keeping the attempt with the fewest faults. - Sheet (turnaround triptych + story moment + props, one ComfyUI run). The profile's face is then
transferred with ReActor (
inswapper_128, CodeFormer 0.5 restore): turnaround faces left to right (front and side; the rear has none), and in the story panel only the largest face — the character in the foreground, not an extra. Checks: turnaround, story panel, and same-person (profile vs. the front view). Gender is judged after the face transfer, on what a viewer actually sees. Retried the same way. - The result carries
image_check={verified, passed, attempts, failures[], checks{}}, saved on the sheet and shown on the character page: verified with no faults / faults still found (listed) / not verified.
If the checker itself is unavailable, retries stop at once and the pair is marked not verified: re-rolling images cannot fix a missing checker, and would burn up to three renders per character.
Why face transfer and not rendering the views from the portrait: the edit model keeps the source pose (three front views, the pose guide copied into the costume — tried and reverted 2026-09-21). The text-to-image triptych gets the poses right; swapping only the face afterwards fixes the identity.
Settings: image_check_enabled (default on), image_check_max_attempts (1–5, default 3),
image_check_model (default: the active LLM).
The turnaround prompt is short and framing-first (2026-09-21)
Measured with turnaround-only renders scored by the image check (scratchpad feet_experiment.py):
| turnaround prompt | full body, head to shoes | plain backdrop |
|---|---|---|
| long compact prompt (~3 000 chars, framing rules mid-prompt), 1536x1024 | 0/5 | 2/5 |
| same, taller 1536x1280 canvas | 2/5 | 0/5 |
| same + "visible floor, contact shadow" wording | 0/5 | — |
| short, framing first (~770 chars), 1536x1024 and 1536x1280 | 10/10 | 10/10 |
| the app's new prompt on Karyla, Darrag, Eolan (4 seeds each, after the fixes below) | 12/12 | 12/12 |
The 4-step Qwen-Image model follows the start of a prompt and loses the tail, so wording changes inside the long prompt did nothing. The prompt now opens with the framing (three full-body views side by side, head to soles, empty space above and below, plain white studio), then the character (subject capped at 700 chars), then only the rules a sheet needs, one short sentence each: leather-only armour, headwear, elf ears (only for elves), child/teen proportions, clean skin and clean well-kept clothes.
What the validation runs found, each fixed and pinned by a test:
- Passing states drawn as features — "rote Stirn bei Wut, Tränen … vor Wut" became red marks (Darrag); "Zunge streicht über die Lippen" a tongue sticking out (Eolan). Anger, tears, trembling, twitching, blushing, sweating, licking are stripped like wounds.
- Scars — even a scar the sheet calls almost faded, with "fully healed, pale silvery line" added, rendered as a fresh red gash, and that sentence put slashes on hands and robe. Scars are left out of image prompts entirely; this model cannot draw a subtle one.
- A dropped clause starting a sentence left fragments ("und die rechte Augenbraue"): the whole sentence now goes. "faded blue wool coat" stays (a costume, not a scar).
- Hedges "unklar" / "wird erwähnt" drop speculative sentences.
Open: robes can still come out with ragged hems; the portrait and story-moment prompts are still long and get the same framing-first treatment next, measured the same way.