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