Use the parent clip last frame and new-segment join for extensions, not the stitched file's first frame. Show this account's library storage in Settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+37
-18
@@ -290,8 +290,7 @@
|
||||
@update-label="updateFamilyRefLabel"
|
||||
/>
|
||||
<p class="mt-2 text-[11px] leading-relaxed text-zinc-500">
|
||||
Name each still, or pick a GLOBAL LOCKS key. Shot prompts inherit those names.
|
||||
The generate start still stays the first frame — attach it here only if you want a named alias.
|
||||
MiniMax will not see these photos. Continuity is the last frame of the previous clip. The stills only name the lock lines.
|
||||
</p>
|
||||
<p class="mt-1 text-[11px] leading-relaxed text-zinc-500">{{ permanenceComfyNote }}</p>
|
||||
<p v-if="familyRefLabelLine" class="mt-1 text-[11px] text-amber-200/80">{{ familyRefLabelLine }}</p>
|
||||
@@ -515,18 +514,18 @@
|
||||
</div>
|
||||
|
||||
<div v-if="videoEngine === 'minimax' && videoStart === 'still' && minimaxGraph === 'v2'" class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3 space-y-3">
|
||||
<label class="flex cursor-pointer items-center justify-between gap-3 text-sm">
|
||||
<label class="flex cursor-pointer items-center justify-between gap-3 text-sm" :class="extraPersonLock ? 'cursor-not-allowed opacity-60' : ''">
|
||||
<span>
|
||||
<span class="block font-medium text-zinc-200">Use identity references</span>
|
||||
<span class="mt-0.5 block text-xs text-zinc-500">Off uses the start still as first-frame only. On keeps that still as Picture 1. Extra stills are optional identity views of the same person, not extra characters.</span>
|
||||
<span class="mt-0.5 block text-xs text-zinc-500">Off uses last-frame I2V after shot 1. On keeps the start still as Picture 1 — extra stills are more views of that same person, never a second character. A named extra person forces last-frame I2V.</span>
|
||||
</span>
|
||||
<span class="relative inline-flex h-6 w-11 shrink-0 items-center">
|
||||
<input v-model="useIdentityRefs" type="checkbox" class="peer sr-only" role="switch">
|
||||
<input v-model="useIdentityRefs" type="checkbox" class="peer sr-only" role="switch" :disabled="extraPersonLock">
|
||||
<span class="absolute inset-0 rounded-full bg-zinc-700 transition peer-checked:bg-amber-400" />
|
||||
<span class="absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white transition peer-checked:translate-x-5" />
|
||||
</span>
|
||||
</label>
|
||||
<div v-if="useIdentityRefs" class="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
<div v-if="useIdentityRefs && !extraPersonLock" class="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
<div
|
||||
v-for="slot in 4"
|
||||
:key="slot"
|
||||
@@ -564,7 +563,8 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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 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.</p>
|
||||
<p v-if="extraPersonLock" class="text-xs text-zinc-500">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.</p>
|
||||
<p v-else-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 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.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1244,9 +1244,9 @@
|
||||
:hidden="item.hideThumbnail"
|
||||
:revealed="!!stitchReveal[item.id]"
|
||||
:still-id="partIndex === 0 ? item.stillId : undefined"
|
||||
:from-clip-id="previousChronoClipId(entry.group.parts, partIndex)"
|
||||
:from-clip-id="previousClipIdFor(item, entry.group.parts, partIndex)"
|
||||
:clip-id="item.id"
|
||||
:show-first-frame="partIndex > 0 || !item.stillId"
|
||||
:show-first-frame="partIndex > 0 || !item.stillId || Boolean(item.parentClipId)"
|
||||
@update:revealed="stitchReveal[item.id] = $event"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
@@ -1541,10 +1541,12 @@ import {
|
||||
composeShotPrompt,
|
||||
extractGlobalLocks,
|
||||
isBlankGlobalLocks,
|
||||
mergePermanenceRefs,
|
||||
nextPermanenceLabel,
|
||||
parseLockBindingKeys,
|
||||
permanenceLabels,
|
||||
sanitizeRefLabel,
|
||||
shouldForceLastFrameI2V,
|
||||
type PermanenceRef
|
||||
} from '~/utils/globalLocks'
|
||||
import {
|
||||
@@ -1890,11 +1892,18 @@ const familyRefLabelLine = computed(() => {
|
||||
if (!labels.length) return ''
|
||||
return `In the prompt: Permanence: ${labels.join(', ')} stay the same.`
|
||||
})
|
||||
const extraPersonLock = computed(() => shouldForceLastFrameI2V(
|
||||
mergePermanenceRefs(familyPermanenceRefs.value, Object.values(shotPermanenceRefs.value).flat()),
|
||||
globalLocks.value
|
||||
))
|
||||
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 'Last-frame I2V does not send these stills to MiniMax as extra pictures; they name what the lock text is talking about. Identity-ref still only maps extra views of the start-still person, never a second character.'
|
||||
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.'
|
||||
})
|
||||
let videoTimer: ReturnType<typeof setInterval> | null = null
|
||||
let editTimer: ReturnType<typeof setInterval> | null = null
|
||||
@@ -2117,7 +2126,7 @@ const recommendedShots = computed(() => {
|
||||
if (!recommendText.value || recommendBusy.value) return []
|
||||
return parseShotScript(recommendText.value)
|
||||
})
|
||||
const identityPrompting = computed(() => studioMode.value === 'video' && videoWorkflow.value === 'v2' && useIdentityRefs.value)
|
||||
const identityPrompting = computed(() => studioMode.value === 'video' && videoWorkflow.value === 'v2' && useIdentityRefs.value && !extraPersonLock.value)
|
||||
const extraIdentityPictures = computed(() => identityRefs.value.flatMap((item, index) => item ? [index + 2] : []))
|
||||
function composedShotPreview(text: string, index: number, shotN?: number) {
|
||||
const composed = composeShotPrompt({
|
||||
@@ -2322,9 +2331,16 @@ function chronologicalParts(parts: LibraryClip[]) {
|
||||
})
|
||||
}
|
||||
|
||||
function previousChronoClipId(parts: LibraryClip[], index: number) {
|
||||
function previousClipIdFor(item: LibraryClip, parts: LibraryClip[], index: number) {
|
||||
if (item.parentClipId) return item.parentClipId
|
||||
if (index <= 0) return undefined
|
||||
return chronologicalParts(parts)[index - 1]?.id
|
||||
const ordered = chronologicalParts(parts)
|
||||
const current = ordered[index]
|
||||
const currentChain = current?.chainIndex || 0
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
if ((ordered[i].chainIndex || 0) < currentChain) return ordered[i].id
|
||||
}
|
||||
return ordered[index - 1]?.id
|
||||
}
|
||||
|
||||
function collectionTitle(group: { head: LibraryClip; parts: LibraryClip[] }) {
|
||||
@@ -2688,6 +2704,10 @@ watch(useIdentityRefs, (value) => {
|
||||
} catch { /* ignore */ }
|
||||
})
|
||||
|
||||
watch(extraPersonLock, (locked) => {
|
||||
if (locked) useIdentityRefs.value = false
|
||||
})
|
||||
|
||||
watch([globalLocks, familyPermanenceRefs, shotPermanenceRefs], () => {
|
||||
try {
|
||||
localStorage.setItem(LOCKS_STORE, globalLocks.value)
|
||||
@@ -3502,9 +3522,8 @@ async function loadRerunImage(item: LibraryClip, parts: LibraryClip[]) {
|
||||
const index = Math.max(0, parts.findIndex(part => part.id === item.id))
|
||||
const isExtension = index > 0 || Boolean(item.parentClipId) || (item.chainIndex || 0) > 0
|
||||
if (isExtension) {
|
||||
const previous = index > 0
|
||||
? parts[index - 1]
|
||||
: clips.value.find(clip => clip.id === item.parentClipId)
|
||||
const previous = (item.parentClipId && clips.value.find(clip => clip.id === item.parentClipId))
|
||||
|| (index > 0 ? parts[index - 1] : undefined)
|
||||
if (previous) {
|
||||
const blob = await $fetch<Blob>(`/api/library/clips/${previous.id}/last-frame`, { responseType: 'blob' })
|
||||
readFile(new File([blob], 'last_frame.png', { type: blob.type || 'image/png' }), { keepThumbnailPref: true })
|
||||
@@ -4401,8 +4420,8 @@ async function generate() {
|
||||
body.append('sound', String(withSound.value))
|
||||
body.append('name', clipName.value.trim())
|
||||
body.append('workflow', videoWorkflow.value)
|
||||
body.append('useIdentityRefs', String(videoWorkflow.value === 'v2' && useIdentityRefs.value))
|
||||
if (videoWorkflow.value === 'v2' && useIdentityRefs.value) {
|
||||
body.append('useIdentityRefs', String(videoWorkflow.value === 'v2' && useIdentityRefs.value && !extraPersonLock.value))
|
||||
if (videoWorkflow.value === 'v2' && useIdentityRefs.value && !extraPersonLock.value) {
|
||||
identityRefs.value.forEach((ref, index) => {
|
||||
if (ref) body.append(`refImage${index}`, ref)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user