Always prepend the Comfy identity prefix and keep refs on every shot.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-27 07:19:56 -05:00
co-authored by Cursor
parent 4840b2719d
commit d540b8c283
3 changed files with 17 additions and 15 deletions
+3 -3
View File
@@ -109,7 +109,7 @@
</div> </div>
<p v-if="shotScriptMode && studioMode === 'video'" class="mb-2 text-xs text-zinc-500"> <p v-if="shotScriptMode && studioMode === 'video'" class="mb-2 text-xs text-zinc-500">
Paste a block. Text before the first marker is shot 1. Put <span class="font-medium text-zinc-300">shot 2</span>, <span class="font-medium text-zinc-300">shot 3</span> on their own lines to queue extensions — a line like <span class="font-medium text-zinc-300">Shot Type</span> will not start a new shot. Every shot uses the duration slider. Paste a block. Text before the first marker is shot 1. Put <span class="font-medium text-zinc-300">shot 2</span>, <span class="font-medium text-zinc-300">shot 3</span> on their own lines to queue extensions — a line like <span class="font-medium text-zinc-300">Shot Type</span> will not start a new shot. Every shot uses the duration slider.
<span v-if="identityPrompting"> Identity boilerplate is prepended to the first shot only, then concatenated into Reference to Video.</span> <span v-if="identityPrompting"> The Comfy identity prefix is prepended to every shot, and the same Picture 1 / extra stills are sent with each shot.</span>
</p> </p>
<p v-else-if="identityPrompting" class="mb-2 text-xs text-zinc-500"> <p v-else-if="identityPrompting" class="mb-2 text-xs text-zinc-500">
Write camera, action, and audio. That is the dynamic action prompt. We concatenate the <span class="font-medium text-zinc-300">&lt;Picture 1&gt;</span> identity prefix in front of it and send the result to <span class="font-medium text-zinc-300">Reference to Video</span>, not Image to Video. Write camera, action, and audio. That is the dynamic action prompt. We concatenate the <span class="font-medium text-zinc-300">&lt;Picture 1&gt;</span> identity prefix in front of it and send the result to <span class="font-medium text-zinc-300">Reference to Video</span>, not Image to Video.
@@ -140,7 +140,7 @@
{{ index === 0 ? 'Initial' : `Extension ${index}` }} {{ index === 0 ? 'Initial' : `Extension ${index}` }}
<span class="font-normal text-zinc-500"> · shot {{ shot.n }} · {{ formatClipDuration(duration) }}</span> <span class="font-normal text-zinc-500"> · shot {{ shot.n }} · {{ formatClipDuration(duration) }}</span>
</p> </p>
<p class="mt-1 whitespace-pre-wrap text-xs text-zinc-300">{{ shot.prompt }}</p> <p class="mt-1 whitespace-pre-wrap text-xs text-zinc-300">{{ identityPrompting ? buildIdentityPrompt(shot.prompt, extraIdentityPictures) : shot.prompt }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -321,7 +321,7 @@
</button> </button>
</div> </div>
</div> </div>
<p v-if="useIdentityRefs" class="text-xs text-zinc-500">Picture 1 is the start still. Extra stills map to Picture 2–5. Empty extra slots reuse the start still so Comfy does not error. Identity refs are not reused on queued extensions.</p> <p v-if="useIdentityRefs" class="text-xs text-zinc-500">Picture 1 is the start still and stays Picture 1 on every shot, including queued extensions. Extra stills map to Picture 2–5. Empty extra slots reuse the start still.</p>
</div> </div>
</div> </div>
+12 -10
View File
@@ -309,17 +309,17 @@ async function queueMiniMax(
const done = watchComfyJob(job, { persist: params.persist }) const done = watchComfyJob(job, { persist: params.persist })
job.status = 'uploading' job.status = 'uploading'
const uploading = (job.library?.chainIndex || 0) > 0 const chainIndex = job.library?.chainIndex || 0
? 'Uploading last frame to ComfyUI...' const uploading = params.useIdentityRefs
: 'Uploading image to ComfyUI...' ? (chainIndex > 0 ? 'Uploading identity stills for next shot...' : 'Uploading image to ComfyUI...')
: (chainIndex > 0 ? 'Uploading last frame to ComfyUI...' : 'Uploading image to ComfyUI...')
const queueing = (job.library?.chainIndex || 0) > 0 const queueing = (job.library?.chainIndex || 0) > 0
? 'Queueing extension on MiniMax H3...' ? 'Queueing extension on MiniMax H3...'
: 'Queueing MiniMax H3 job...' : 'Queueing MiniMax H3 job...'
emitChainJob(job, { type: 'status', message: uploading, progress: 4 }) emitChainJob(job, { type: 'status', message: uploading, progress: 4 })
const uploaded = await uploadImage(params.image, job.id) const uploaded = await uploadImage(params.image, job.id)
const referenceNames: string[] = ['', '', '', ''] const referenceNames: string[] = ['', '', '', '']
const chainIndex = job.library?.chainIndex || 0 if (params.useIdentityRefs) {
if (chainIndex === 0) {
for (const [index, ref] of (params.referenceImages || []).entries()) { for (const [index, ref] of (params.referenceImages || []).entries()) {
if (!ref?.data?.length) continue if (!ref?.data?.length) continue
const next = await uploadImage({ const next = await uploadImage({
@@ -353,8 +353,8 @@ async function queueMiniMax(
sound: params.sound, sound: params.sound,
workflow: params.workflow, workflow: params.workflow,
duration: params.duration, duration: params.duration,
useIdentityRefs: chainIndex === 0 && params.useIdentityRefs, useIdentityRefs: params.useIdentityRefs,
referenceImageNames: chainIndex === 0 ? referenceNames : [] referenceImageNames: params.useIdentityRefs ? referenceNames : []
}) })
const queued = await queuePrompt(graph, job.clientId) const queued = await queuePrompt(graph, job.clientId)
@@ -466,7 +466,9 @@ async function runGeneration(
await ensureComfyReady(ready) await ensureComfyReady(ready)
await queueMiniMax(job, { await queueMiniMax(job, {
prompt: ext.prompt, prompt: ext.prompt,
image: { filename: 'last_frame.png', data: frame, type: 'image/png' }, image: params.useIdentityRefs
? params.image
: { filename: 'last_frame.png', data: frame, type: 'image/png' },
width: params.width, width: params.width,
height: params.height, height: params.height,
steps: params.steps, steps: params.steps,
@@ -481,8 +483,8 @@ async function runGeneration(
persist: isLast, persist: isLast,
workflow: params.workflow, workflow: params.workflow,
duration: ext.duration, duration: ext.duration,
useIdentityRefs: false, useIdentityRefs: params.useIdentityRefs,
referenceImages: [] referenceImages: params.useIdentityRefs ? params.referenceImages : []
}) })
if (!isLast) { if (!isLast) {
+2 -2
View File
@@ -13,11 +13,11 @@ export function identityBoilerplate(extraRefs: number | number[] = 0) {
: extras.length === 1 : extras.length === 1
? ` <Picture ${extras[0]}> is a secondary reference angle for profile details.` ? ` <Picture ${extras[0]}> is a secondary reference angle for profile details.`
: ` ${extras.map(n => `<Picture ${n}>`).join(' and ')} are secondary reference angles for profile features only.` : ` ${extras.map(n => `<Picture ${n}>`).join(' and ')} are secondary reference angles for profile features only.`
return `<Picture 1> 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.${secondary}` return `<Picture 1> is the primary identity anchor defining exact facial geometry, hairstyle, and body proportions.${secondary} Strictly maintain the structural proportions of <Picture 1> with high fidelity when moving.`
} }
export function looksLikeIdentityPrompt(text: string) { export function looksLikeIdentityPrompt(text: string) {
return /primary identity anchor|exact facial identity/i.test(String(text || '')) return /^<Picture 1> is the (primary identity anchor|exact facial identity)/i.test(String(text || '').trim())
} }
export function buildIdentityPrompt(actionPrompt: string, extraRefs: number | number[] = 0) { export function buildIdentityPrompt(actionPrompt: string, extraRefs: number | number[] = 0) {