Visual identity: asset spec, readiness, and probes
Visual identity: asset spec, readiness, and probes
The exact rules for identity assets, what makes a character image-ready, how probes report consistency, and batch inventory generation.
LoreOS image generation is identity-anchored: every selfie a character sends is grounded in an uploaded identity image plus text identity anchors, so the same character looks like itself over time. This page is the precise contract for that system — what you must upload, what “image-ready” actually checks, what an image probe returns, and how to batch-generate inventory.
For the concept overview, see Visual system. For the behavioral knobs that turn images on at all, see Character authoring.
Selfies are off until three things are true: (1) an identity image is uploaded and
active, (2) profile.identity_anchors is set as a dict of visual keys (a list does not
work for images — see the dict requirement),
and (3) behavioral_thresholds
image_proactivity / selfie_comfort are > 0 and daily_image_cap is > 0. Miss any
one and the character will never send an image. GET /v1/characters/{slug}/visual-readiness
checks all of this for you.
The identity asset (upload this first)
Before you can upload any other visual asset — or generate any image — the character
needs an active identity asset. The identity image is what the engine uses to anchor
every generated selfie and keep the face consistent across images.
This is a multipart/form-data upload, not JSON. The form fields:
Identity is special
When you upload category=identity, LoreOS activates it immediately (status active,
promotion approved_anchor) so the selfie pipeline can use it at once. Every other category
is uploaded as a candidate (you promote it later — see
Promoting assets).
Other categories are blocked until identity exists
Uploading any non-identity asset before an active identity exists returns:
This is a 409 (a precondition is unmet), not a 422. Upload the identity image first,
then everything else.
What makes a good identity image
The identity image is the face/look reference the engine sends with every generated selfie, so its quality sets the ceiling for how consistent and recognizable the character looks over time. It is worth getting right.
What the platform actually enforces (hard checks). Only these are validated — get them wrong and the upload or the selfie pipeline fails outright:
- Format: the file must be a
PNG,JPEG, orWebPimage. Anything else is a422(image_rejected). - Size: up to ~8 MB per file. Larger is a
422(image_rejected). - Fetchable over HTTPS: LoreOS stores the upload and the image must be reachable at an
https://URL — the generator fetches it as the reference. (You upload bytes; LoreOS hosts them, so this is automatic for uploaded assets.) - At least one active identity asset plus a non-empty
profile.identity_anchorsdict (next section).GET .../visual-readinesschecks both.
What makes it good (best practice, not enforced). The platform does not check resolution, aspect ratio, face angle, or framing — there are no pixel thresholds in the readiness logic. But the generator anchors on what you give it, so these meaningfully improve the result:
- One clear, front-facing portrait is the single most important asset. A head-and-shoulders shot where the face is large, sharp, and unobstructed gives the strongest face anchor. Prefer a portrait crop for the identity anchor — it is what facial consistency is measured against.
- High enough resolution that the face is crisp. A roughly square-to-portrait image where the face fills a good part of the frame works well; a tiny or heavily compressed face gives the generator less to lock onto. (No minimum is enforced — “the face is clearly resolved” is the bar, not a specific pixel count.)
- Well-lit, neutral, unobstructed face. Even lighting and a plain-ish background read best.
- Portrait vs full-body: use a portrait as the identity anchor (face fidelity). If you
also want reliable head-to-toe shots, add a full-body image too — but as an additional
identity (or inventory) asset, not as the primary face anchor. You can also test a full-body
result on demand with the
full_body_identityprobe. - How many is ideal: one strong front portrait is the minimum and is often enough. Two or three — e.g. a front portrait plus a slight three-quarter angle, or a full-body — can help the generator, and all active identity assets count toward readiness. There are diminishing returns; quality of the front shot matters more than quantity.
- Does angle coverage help? A little extra angle coverage can help the generator generalize, but it is not required and is not a readiness check. A single excellent front portrait beats several mediocre angles.
Bad examples to avoid (each weakens the anchor or causes inconsistent selfies):
- a cropped or partially cut-off face (forehead or chin missing);
- a heavy filter / beauty-smoothing / stylization that hides real features;
- side-profile-only with no front shot (the generator has no frontal reference to anchor);
- a tiny, low-resolution, or motion-blurred face;
- sunglasses, masks, hands, or hair fully covering the face;
- a busy group photo where it is ambiguous which person is the character.
Pair the image with structured anchors in profile.identity_anchors (see the next section) —
the image anchors the face, and the identity_anchors dict anchors hair / style /
recurring visual constants. For images this field must be an object, not a list.
Visual readiness and the exact pass or fail rules
GET /v1/characters/{slug}/visual-readiness answers one question: can this character
generate good, consistent selfies? It runs four deterministic checks and rolls them into a
status.
The aggregate status is:
needs_attentionif any check ismissing(i.e.identity_assetoridentity_anchors),ready_with_warningsif any check isweak/disabled(e.g. no inventory yet, or images still off),readyonly when all four areok.
identity_anchors is version-scoped; the identity image is not. Read the scope_note:
identity_anchors and image_generation reflect the active character version, while
identity_asset and inventory_coverage are character-global (they persist across versions).
A PATCH /v1/characters that sends a new profile replaces the profile wholesale — if you
omit profile.identity_anchors in that PATCH, you silently drop the text anchor from the new
active version, and selfie generation will start failing even though the identity image is
still there. Always include identity_anchors in any profile you PATCH.
For images, identity_anchors must be a dict, not a list. This is the most
surprising part of the visual system. The character authoring contract types
profile.identity_anchors as list[str] | dict, so both shapes are valid through
POST /v1/characters — sending the dict form is not a 422. The distinction is at
runtime: a list of text anchors satisfies authoring readiness (it doubles as voice/persona
anchors), but the image pipeline reads identity_anchors as a structured object with
visual keys, and the visual-readiness identity_anchors check, the selfie-bootstrap step, and
the image-continuity source all require a non-empty dict. A list-shaped identity_anchors
passes authoring readiness but makes visual-readiness report identity_anchors: missing and
causes selfie generation to fail with “non-bootstrap selfie requires a self visual
reference.” A dict satisfies authoring readiness too, so if your character sends images,
just author identity_anchors as a dict — set it directly on the profile you POST/PATCH.
identity_anchors is the visual consistency seed the selfie pipeline reads. The engine pulls
visual_summary, hair, style_constants (a list), and avoid_visual_changes (a list) out
of it to anchor every generated image; additional descriptive keys are carried through. Set it
on profile as an object:
Without a usable dict here the pipeline rejects a non-bootstrap selfie with “non-bootstrap selfie requires a self visual reference” — even with an identity image present.
A broader, grouped view of all assets (identity plus every inventory category, with counts by
status) is at GET /v1/characters/{slug}/visual-inventory. The character-wide launch readiness
(persona, voice, budget, delivery, and the visual checks) is at
GET /v1/characters/{slug}/readiness.
Image probes — test the visual setup on demand
You don’t have to chat until the character decides to send a selfie. An image probe
force-generates one image of a chosen kind, runs it through the same engine the
runtime uses (it only bypasses the per-character cap/cooldown), and delivers the result onto
the session event log as an image.ready event — exactly like a real selfie. The probe report
is then a deterministic read of what actually happened: the image URL, which reference assets
were sent, and the engine’s consistency verdict.
Each probe is one real, metered image generation — it counts against budget and the
per-user/app credit cap, just like a runtime selfie. Probes also require a persistent
developer key (demo sandbox keys are rejected with 403).
Run a probe
A probe runs inside one of the character’s existing sandbox sessions (the image.ready
event lands on that session’s log), so pass a session_id you already have open.
The seven probe types:
The POST responds as soon as generation is enqueued (it is async). Preconditions are
checked synchronously: a missing/unowned session_id is 404; an exited session is 409
(session_exited); no active identity asset is 409 (needs_identity); an inability to seed
the self visual reference (e.g. the identity image isn’t fetchable over HTTPS, or
profile.identity_anchors is unset) is 409 (needs_identity with a fix pointing at
visual-readiness); over budget is 402 (budget_exceeded).
Poll the probe result
GET /v1/characters/{slug}/image-probe/{probe_id} is the result. It is a redacted projection
over the underlying image request — no raw prompts or generation payloads. The full schema:
The consistency object, when present:
consistency.status is one of passed | manual_required | failed.
servable: true does not mean success. An image can be generated and viewable
(servable: true, image_url set) while the request was rejected — e.g. a safety block —
so request_status is failed, promotion_eligible is false, and it is not
auto-delivered as a successful result. Always read summary + request_status together, not
just image_url. The three cases:
- generated and verdict passed → promotable into inventory;
- generated but the request didn’t fully succeed → viewable, not promotable, not delivered;
- no servable image → see
failure_reason.
For continuity-dependent probes (accessory_carry_forward, same_scene_followup), run an
identity_selfie first so there is a parent image to follow — otherwise the verdict reports a
new episode with nothing to compare against. GET /v1/characters/{slug}/image-probe lists
recent probes (most recent first) and echoes the valid probe_types.
Batch-generate inventory
Once identity is in place you can generate a character’s reusable inventory (outfits,
props, scenes) from its identity, instead of uploading every asset by hand.
POST /v1/characters/{slug}/inventory/generate runs the same engine once per requested image.
The request body:
The ≤ 8 per batch cap
categories.length × count_per_category must be ≤ 8 — each image is a real, metered
generation, so the batch is capped for cost. Exceed it and you get:
Cost is categories × count_per_category metered generations. Each image in the batch is
one real, metered image generation (same unit a runtime selfie or a probe costs). So
["wardrobe","accessory","prop"] with count_per_category: 2 is 3 categories × 2 = 6
metered generations — and is fine (≤ 8). 3 × 3 = 9 exceeds the cap and is rejected. Budget
the batch accordingly; the whole batch is also gated by the same 402 budget_exceeded
preflight as a normal turn.
Active by default, or held for review
hold_for_review: false(default) — generated assets go straight to active: they are immediately usable by the runtime and show up active invisual-inventorytyped by category. Best when it’s effectively just you using the character.hold_for_review: true— generated assets are kept as candidates; you approve the ones you want viaPATCH /v1/characters/{slug}/visual-assets/{asset_id}before they’re used. Best when you want a human to curate.
The same identity-and-anchors preconditions as probes apply (needs_identity 409 if missing),
and the same budget gate (402).
The generate response (what was queued)
POST .../inventory/generate responds as soon as the batch is enqueued — it does not wait
for the images. The data reports how many were requested, how many actually queued, and a
per-item batch entry:
requested is categories × count_per_category; queued counts the entries that got a
request_id (an entry that failed to enqueue has request_id: null — see partial failure
below).
Poll the generation status
The images generate asynchronously. Poll
GET /v1/characters/{slug}/inventory/generations for per-image status (and
GET /v1/characters/{slug}/visual-inventory to watch the active assets land). Each generation
is reported with the same redacted report shape as an image probe, plus the typed
category. The developer-facing lifecycle field is state (you do not need to know the
raw pipeline status):
state is the lifecycle to drive your UI off: queued → generating → ready (or
failed / blocked). servable: true with image_url set means a fetchable image
exists; promotion_eligible is true only when the request succeeded and the consistency
verdict passed.
What a partial failure looks like
A batch is per-image: some images can succeed while others fail, and you still get a 200.
Two distinct shapes:
-
Failed to enqueue — surfaced inline in the generate response
batch, with norequest_id:(
queuedwill be less thanrequestedwhen this happens.) -
Enqueued but the generation did not fully succeed — surfaced in
inventory/generations, the same way a failed probe is:state: "failed"(orblocked),servable: false,image_url: null,promotion_eligible: false, and afailure_reasonstring. Note the confusing-but-correct case whereservable: trueandrequest_statusis notsucceeded: an image was produced and is viewable, but the request was rejected (e.g. a safety block) — so it is not promotable and not delivered as a successful result. Readsummary+request_statustogether, not justimage_url.
The successful items in the same batch are unaffected — they land active (or as candidates), independently of the failed ones.
Candidate vs active outcome, and approve/reject
The outcome of a succeeded image depends on hold_for_review:
hold_for_review: false(default) → the asset is active immediately (usable by the runtime, shows up active invisual-inventorytyped by category). Nothing more to do.hold_for_review: true→ the asset is a candidate; curate it before it is used. Read the candidates ininventory/generations/visual-inventory, then:- approve →
PATCH /v1/characters/{slug}/visual-assets/{asset_id}with{"promotion_status": "approved_keyframe"}(orapproved_anchor) to promote it into active inventory; - reject →
PATCH .../visual-assets/{asset_id}with{"asset_status": "rejected"}, orDELETE .../visual-assets/{asset_id}to remove it.
- approve →
(The asset_id for promotion is the visual asset that the generated image produced — read it
from visual-inventory; the probe_id in the generation report is the image request id, not
the asset id.)
Promoting and managing assets
PATCH /v1/characters/{slug}/visual-assets/{asset_id} updates an asset’s promotion_status /
qa_status — this is how you promote a candidate (an uploaded non-identity asset, a held
inventory result, or a passing probe image) into active inventory, and how a passing probe
result becomes a reusable keyframe. DELETE /v1/characters/{slug}/visual-assets/{asset_id}
removes an asset. Asset statuses are candidate / active / retired / rejected; promotion
statuses include candidate / approved_anchor / approved_keyframe.
End-to-end order
Upload an identity image
POST .../visual-assets with category=identity. It activates immediately and unlocks every
other asset and all image generation.
Set profile.identity_anchors as a dict
POST/PATCH /v1/characters with profile.identity_anchors authored as an object
(visual_summary / hair / style_constants / avoid_visual_changes …). Required for selfie
generation, must be a dict (not a list), and is version-scoped.
Enable images
Set behavioral_thresholds.image_proactivity / selfie_comfort > 0 and
daily_image_cap > 0.
Check visual readiness
GET .../visual-readiness should reach ready (or ready_with_warnings if you haven’t added
inventory yet).