Sign inStart creating

Character Engine

Implementation: src/server/services/character.ts.

Two jobs, and they are separate concerns that people routinely conflate:

  • Identity — does this look like the same person? (Part 4.1)
  • State — is this the same person at this point in the story? (Part 4.2)

A model can nail identity and still show a healed face two scenes after the fight.

Identity Package

compileIdentityPackage(characterId, { atSceneOrder }) returns the bundle injected into every generation featuring that character. The creator never re-uploads references.

Contents:

  • promptDna — structured fields → ordered prompt fragments. Order matters: models weight earlier tokens more, so identity-critical descriptors lead. Never freetext-only; the schema forces structure.
  • invariants — distinguishing features (scars, tattoos, chipped tooth). Emitted as a MUST PRESERVE: clause, because these are what a model silently drops first.
  • refs — reference images, priority-ordered by slot. When a model caps refs at 3, three_quarter and close_up survive; back does not. Identity reads strongest from those angles.
  • loraRef, embedding, voice, personality.

Version pinning

A shot in scene 4 must use the canon valid in scene 4, even if the character was redesigned for scene 30. CharacterVersion carries validFromSceneOrder / validToSceneOrder, and compileIdentityPackage selects by scene. Redesigns never corrupt earlier footage.

Consent

A cloned voice is unusable without a verified ConsentRecord. This is not advisory — moderation.screenLikeness emits a block flag, so the film cannot be published.

State Machine

resolveStateAt(characterId, sceneOrder) replays events in scene order and returns what the character is carrying right now.

Event types: injury, blood, dirt, wardrobe_change, aging, emotional, inventory_gain, inventory_loss, heal, hair_change.

Resolution rules:

  • Events at or before sceneOrder apply; later ones do not exist yet.
  • clearedAtSceneOrder stops propagation past a clear point.
  • payload.healsAfterScenes gives injuries a natural fade window.
  • heal events cancel matching conditions.
  • Wardrobe changes replace the current outfit; inventory accumulates.

Output includes promptFragments, appended to every shot prompt for that character. That is what kills the classic AI-film failures: vanishing injuries, teleporting outfits, resetting hair.

Cascade wiring

graph.addStateEvent creates the event and wires edges only to shots at or after its scene, then plans a priced cascade. "Maya has a scar from Scene 13 onward" affects scenes 13+ and nothing earlier — which is both correct and much cheaper than a blanket regeneration.

Composition

composeCharacterPrompt(characterId, sceneOrder) merges identity + invariants + resolved state into one segment. shots.composeShotPrompt calls it per cast member and adds location, props, camera and Style DNA.

The caller passes a shot id and nothing else. Everything is read from the graph — which is the entire point.

Model sheet

MODEL_SHEET_SLOTS defines the nine-angle pack auto-generated from one seed image: front, three-quarter, both profiles, back, full body, close-up, expression set, action set. A character with zero references produces an amber continuity flag, because identity will drift without them.

Gaps

  • Nothing generates embeddings. identityDrift() in server/lib/vector.ts returns { checked: false } without an embedding provider, and the Continuity Engine reports "needs visual review" rather than "identity verified". Do not read the absence of a drift flag as confirmation.
  • LoRA training is not wired. The schema and Identity Package carry loraRef; the training job does not exist. loraTrainable on a model is a capability declaration, not a trained artifact.