diff --git a/docs/image-v2.md b/docs/image-v2.md
index ba4cd9a..3a504ba 100644
--- a/docs/image-v2.md
+++ b/docs/image-v2.md
@@ -100,7 +100,7 @@ Generate Krea (`krea_v2_generate.json`):
Krea Edit / Compose / Refine use the same UNET / CLIP / VAE. They encode the still (`VAEEncode`) and sample with denoise < 1. Krea has no Klein `ReferenceLatent`. Compose stitches A then B (`ImageStitch`) so both stills are in the latent. Missing Krea models fail the job. No Flux fallback.
-Krea does not load Klein UNET, Klein CLIP, Klein VAE, Klein Consistency, or the Klein concept LoRA. Concept LoRA (`klein_snofs_v1_4`) is xAIGen only; AIGen bypasses that node. On Krea it stays off unless `NUXT_KREA_CONCEPT_LORA` is set on xAIGen.
+Krea does not load Klein UNET, Klein CLIP, Klein VAE, Klein Consistency, or the Klein concept LoRA. Concept LoRA (`xaigen-klein_snofs_v1_4`) is xAIGen only; AIGen bypasses that node. On Krea it stays off unless `NUXT_KREA_CONCEPT_LORA` is set on xAIGen.
There is no `LoadImage` on Generate. If the executed Generate graph has a required LoadImage, the job fails. v1 node 145 (muted T2I with a baked prompt) is not used.
diff --git a/pages/index.vue b/pages/index.vue
index 9676ce4..4376512 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -273,10 +273,10 @@
/>
Paste a block, or load a saved Director script. Text before the first marker is shot 1. Put shot 2, shot 3 on their own lines to queue extensions — a line like Shot Type will not start a new shot. Every shot uses the duration slider. Global locks are inherited; each shot should add one beat.
- The Comfy identity prefix is prepended to every shot, and the same Picture 1 / extra stills are sent with each shot.
+ Character stills ride along as extra pictures. Picture 1 is the current frame (last frame after shot 1), not the original start still.
- Write camera, action, and audio. That is the dynamic action prompt. We concatenate the <Picture 1> identity prefix in front of it and send the result to Reference to Video, not Image to Video.
+ Write camera, action, and audio. Picture 1 is the current frame. Extra stills lock face and body only — they do not reset clothes. MiniMax still cannot hard-lock a last frame and character refs in one node, so this path is Reference to Video with the last frame as Picture 1.
- v2 · identity refs
- Optional lock with up to 4 stills
+ v2 · last frame + stills
+ Optional character stills. Last frame stays the start.
-
-
+
+
Character stills
+
Optional. Extra views of the same person — face, hair, body. Shot 2+ still starts from the last frame (clothes and pose stay). Empty slots are left out. A named extra person turns these off.
+
+
-
Identity refs stay off while a second named person is attached. MiniMax will not see the extra stills; shot 2+ starts from the parent clip’s last frame.
-
Picture 1 is the start still and stays Picture 1 on every shot, including queued extensions. Extra stills map to Picture 2–5 only if you add them — extra views of that same person, not a second character or a vehicle. Empty slots are left out of the graph, not filled with copies of Picture 1.
+
Character stills stay off while a second named person is attached. Shot 2+ starts from the last frame.
+
Picture 1 is the start still on shot 1 and the last frame after that. Extra stills are Picture 2–5. They do not put the start-still outfit back on.
@@ -2449,7 +2442,6 @@ const imageV2LoraOptions = computed(() => mergeLoraOptions(
filterLorasForImageEngine(imageLoras.value, v2Engine.value),
filterLorasForImageEngine(imageV2LoraStack.value.map(item => item.name), v2Engine.value)
))
-const useIdentityRefs = ref(false)
const identityRefs = ref<(File | null)[]>([null, null, null, null])
const identityPreviews = ref(['', '', '', ''])
const globalLocks = ref('')
@@ -2474,12 +2466,13 @@ const extraPersonLock = computed(() => shouldForceLastFrameI2V(
mergePermanenceRefs(familyPermanenceRefs.value, Object.values(shotPermanenceRefs.value).flat()),
globalLocks.value
))
+const useIdentityRefs = computed(() => !extraPersonLock.value && identityRefs.value.some(Boolean))
const permanenceComfyNote = computed(() => {
if (extraPersonLock.value) {
return 'A second named person keeps last-frame I2V on. MiniMax never sees the man/bench photos; those names only go in the inherit line.'
}
if (studioMode.value === 'video' && videoWorkflow.value === 'v2' && useIdentityRefs.value) {
- return 'Identity-ref still only maps extra views of the start-still person, never a second character. Permanence stills name the lock text; they are not Picture 2–5.'
+ return 'Character stills are extra views of the same person. Picture 1 is the last frame after shot 1. Permanence stills only name the lock text.'
}
return 'Last-frame I2V does not send these stills to MiniMax as extra pictures. Continuity is the previous clip’s last frame; the names only go in the lock line.'
})
@@ -2667,7 +2660,7 @@ const videoModelHint = computed(() => {
: 'PinkCherry image-to-video. Distilled LoRA stays on. Silent MP4 — LTX has no audio VAE here.'
}
if (textToVideo.value) return 'MiniMax H3 text-to-video with native stereo audio. Shot 2+ switches to last-frame image-to-video.'
- if (videoWorkflow.value === 'v2') return 'MiniMax H3 image-to-video. Identity refs stay on v2 with a start still.'
+ if (videoWorkflow.value === 'v2') return 'MiniMax H3 image-to-video. Shot 2+ starts from the last frame. Character stills are optional extras.'
return 'MiniMax H3 image-to-video with native stereo audio.'
})
const frameCount = computed(() => Math.max(5, Math.floor(duration.value * fps.value)))
@@ -3320,7 +3313,6 @@ onMounted(async () => {
applyEngineDefaults('ltx')
}
} catch { /* ignore */ }
- useIdentityRefs.value = localStorage.getItem('aigen-use-identity-refs') === 'true'
videoLoraStack.value = readStoredLoraStack('aigen-video-lora')
imageLoraStack.value = readStoredLoraStack('aigen-image-lora')
imageV2LoraStack.value = readStoredLoraStack('aigen-image-v2-lora')
@@ -3469,16 +3461,6 @@ watch(videoStart, (value) => {
} catch { /* ignore */ }
})
-watch(useIdentityRefs, (value) => {
- try {
- localStorage.setItem('aigen-use-identity-refs', String(value))
- } catch { /* ignore */ }
-})
-
-watch(extraPersonLock, (locked) => {
- if (locked) useIdentityRefs.value = false
-})
-
watch([globalLocks, familyPermanenceRefs, shotPermanenceRefs], () => {
try {
localStorage.setItem(LOCKS_STORE, globalLocks.value)
@@ -4493,7 +4475,6 @@ async function restoreStudioJob(id: string) {
}
if (typeof payload.sound === 'boolean') withSound.value = payload.sound
shotScriptMode.value = extensions.length > 0
- useIdentityRefs.value = payload.useIdentityRefs === true
identityRefs.value.forEach((_, index) => clearIdentityRef(index))
extensionQueue.value = extensions.map(item => ({
id: crypto.randomUUID(),
@@ -4749,7 +4730,6 @@ async function rerun(item: LibraryClip, collection = false) {
applyVideoWorkflow(target.workflow)
if (typeof target.sound === 'boolean') withSound.value = target.sound
shotScriptMode.value = restoreAll
- useIdentityRefs.value = false
identityRefs.value.forEach((_, index) => clearIdentityRef(index))
const ordered = chronologicalParts(parts)
globalLocks.value = ordered.find(part => part.globalLocks)?.globalLocks || target.globalLocks || ''
diff --git a/server/assets/klein_v2_compose.json b/server/assets/klein_v2_compose.json
index 7db4087..d270071 100644
--- a/server/assets/klein_v2_compose.json
+++ b/server/assets/klein_v2_compose.json
@@ -58,7 +58,7 @@
},
"7": {
"inputs": {
- "lora_name": "klein_snofs_v1_4.safetensors",
+ "lora_name": "xaigen-klein_snofs_v1_4.safetensors",
"strength_model": 0.65,
"strength_clip": 0.35,
"model": ["4", 0],
diff --git a/server/assets/klein_v2_edit.json b/server/assets/klein_v2_edit.json
index 800de36..68cbd00 100644
--- a/server/assets/klein_v2_edit.json
+++ b/server/assets/klein_v2_edit.json
@@ -43,7 +43,7 @@
},
"7": {
"inputs": {
- "lora_name": "klein_snofs_v1_4.safetensors",
+ "lora_name": "xaigen-klein_snofs_v1_4.safetensors",
"strength_model": 0.65,
"strength_clip": 0.35,
"model": ["4", 0],
diff --git a/server/assets/klein_v2_generate.json b/server/assets/klein_v2_generate.json
index 9d31c9b..7123b9c 100644
--- a/server/assets/klein_v2_generate.json
+++ b/server/assets/klein_v2_generate.json
@@ -23,7 +23,7 @@
},
"7": {
"inputs": {
- "lora_name": "klein_snofs_v1_4.safetensors",
+ "lora_name": "xaigen-klein_snofs_v1_4.safetensors",
"strength_model": 0.65,
"strength_clip": 0.35,
"model": ["4", 0],
diff --git a/server/assets/klein_v2_refine.json b/server/assets/klein_v2_refine.json
index 80c0f07..2981cd8 100644
--- a/server/assets/klein_v2_refine.json
+++ b/server/assets/klein_v2_refine.json
@@ -76,7 +76,7 @@
},
"7": {
"inputs": {
- "lora_name": "klein_snofs_v1_4.safetensors",
+ "lora_name": "xaigen-klein_snofs_v1_4.safetensors",
"strength_model": 0.65,
"strength_clip": 0.3,
"model": ["4", 0],
diff --git a/server/assets/workflow_minimax_video_v2.json b/server/assets/workflow_minimax_video_v2.json
index 4b87b34..5fea96d 100644
--- a/server/assets/workflow_minimax_video_v2.json
+++ b/server/assets/workflow_minimax_video_v2.json
@@ -97,7 +97,7 @@
},
"155": {
"inputs": {
- "prompt": " is the exact facial identity, hairstyle, body proportions and clothing of the main character. Preserve this identity with high fidelity even when the subject temporarily leaves the frame and returns.\n\n[describe the action, camera and audio here]",
+ "prompt": " is the current frame: keep this pose, clothes, place, and who is in shot. Extra stills are face, hair, and body only — not a wardrobe change.\n\n[describe the action, camera and audio here]",
"width": [
"127",
0
diff --git a/server/utils/videoChain.ts b/server/utils/videoChain.ts
index 9efe3c5..d5a865d 100644
--- a/server/utils/videoChain.ts
+++ b/server/utils/videoChain.ts
@@ -152,9 +152,9 @@ export async function queueMiniMax(
const hasImage = Boolean(params.image?.data?.length)
const uploading = !hasImage
? `Queueing ${engineName} text-to-video…`
- : params.useIdentityRefs
- ? (chainIndex > 0 ? 'Uploading identity stills for next shot...' : 'Uploading image to ComfyUI...')
- : (chainIndex > 0 ? 'Uploading last frame to ComfyUI...' : 'Uploading image to ComfyUI...')
+ : chainIndex > 0
+ ? (params.useIdentityRefs ? 'Uploading last frame and character stills...' : 'Uploading last frame to ComfyUI...')
+ : 'Uploading image to ComfyUI...'
const queueing = chainIndex > 0
? `Queueing extension on ${engineName}...`
: `Queueing ${engineName} job...`
@@ -393,9 +393,7 @@ export async function continueQueuedExtensions(
await ensureComfyReady(ready)
await queueMiniMax(job, {
prompt: ext.prompt,
- image: identity
- ? params.image
- : { filename: 'last_frame.png', data: frame, type: 'image/png' },
+ image: { filename: 'last_frame.png', data: frame, type: 'image/png' },
width: params.width,
height: params.height,
steps: params.steps,
diff --git a/utils/identityPrompt.ts b/utils/identityPrompt.ts
index c537d93..346e03e 100644
--- a/utils/identityPrompt.ts
+++ b/utils/identityPrompt.ts
@@ -11,13 +11,13 @@ export function identityBoilerplate(extraRefs: number | number[] = 0) {
const secondary = extras.length === 0
? ''
: extras.length === 1
- ? ` is another view of that same person for identity only, not a second character.`
- : ` ${extras.map(n => ``).join(' and ')} are extra views of that same person for identity only, not additional characters.`
- return ` is the identity lock for a single main subject: exact face, hair, body, and clothing.${secondary} Do not spawn extra people from reference stills. If this subject leaves the frame, they must return as the same person from .`
+ ? ` is that same person for face, hair, and body only — not a second character and not a wardrobe change.`
+ : ` ${extras.map(n => ``).join(' and ')} are extra views of that same person for face, hair, and body only — not additional characters and not a wardrobe change.`
+ return ` is the current frame: keep this pose, clothes, place, and who is in shot. Do not reset the outfit to an earlier still.${secondary} Do not spawn extra people from reference stills. If this subject leaves the frame, they return as the person from the extra stills, wearing whatever Picture 1 already shows.`
}
export function looksLikeIdentityPrompt(text: string) {
- return /^ is the (identity lock|primary identity anchor|exact facial identity)/i.test(String(text || '').trim())
+ return /^ is the (current frame|identity lock|primary identity anchor|exact facial identity)/i.test(String(text || '').trim())
}
export function buildIdentityPrompt(actionPrompt: string, extraRefs: number | number[] = 0) {
diff --git a/utils/imageV2.ts b/utils/imageV2.ts
index 31bae34..7cd28e8 100644
--- a/utils/imageV2.ts
+++ b/utils/imageV2.ts
@@ -26,7 +26,7 @@ export const IMAGE_V2_STRENGTH_MIN = 0
export const IMAGE_V2_STRENGTH_MAX = 2
export const IMAGE_V2_STRENGTH_STEP = 0.05
-export const IMAGE_V2_SNOFS_LORA = 'klein_snofs_v1_4.safetensors'
+export const IMAGE_V2_SNOFS_LORA = 'xaigen-klein_snofs_v1_4.safetensors'
export const IMAGE_V2_CONSISTENCY_LORA = 'Flux2-Klein-9B-consistency-V2.safetensors'
export const IMAGE_V2_DENOISE_DEFAULT = 0.35
export const IMAGE_V2_DENOISE_MIN = 0.15