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 aMUST 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_quarterandclose_upsurvive;backdoes 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
sceneOrderapply; later ones do not exist yet. clearedAtSceneOrderstops propagation past a clear point.payload.healsAfterScenesgives injuries a natural fade window.healevents 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()inserver/lib/vector.tsreturns{ 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.loraTrainableon a model is a capability declaration, not a trained artifact.