Bind permanence stills to named lock keys instead of coded categories.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 09:52:27 -05:00
co-authored by Cursor
parent 727d895c10
commit 231c59a7a7
4 changed files with 89 additions and 44 deletions
+28 -15
View File
@@ -279,18 +279,19 @@
<div class="rounded-xl border border-white/10 bg-zinc-950/40 px-3 py-2.5">
<PermanenceRefStrip
:refs="familyPermanenceRefs"
:name-options="lockBindingKeys"
label="Permanence refs · all shots"
hint="Shared stills"
hint="Name + still"
cta="Add permanence refs"
add-title="Choose a still to label for every shot"
add-title="Choose a still to name for every shot"
:hidden="hideInputPreview"
@add-ref="openPermanencePicker()"
@remove="removeFamilyRef"
@update-label="updateFamilyRefLabel"
/>
<p class="mt-2 text-[11px] leading-relaxed text-zinc-500">
Add stills, then label each Subject / Vehicle / Interior / Object. Those names are injected into locks and
<span class="text-zinc-300">[INHERITS]</span>, so a shot can say “same Subject, same Vehicle.”
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.
</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>
@@ -311,10 +312,11 @@
<PermanenceRefStrip
class="mt-2"
:refs="shotPermanenceRefs[shot.n] || []"
:name-options="lockBindingKeys"
label="This shot only"
hint="Extra stills"
cta="Add still"
add-title="Choose a still for this shot only"
add-title="Choose a still to name for this shot only"
:hidden="hideInputPreview"
:max="4"
@add-ref="openPermanencePicker(shot.n)"
@@ -352,10 +354,11 @@
<PermanenceRefStrip
class="mt-3"
:refs="shotPermanenceRefs[index + 2] || []"
:name-options="lockBindingKeys"
label="This extension only"
hint="Extra stills"
cta="Add still"
add-title="Choose a still for this extension only"
add-title="Choose a still to name for this extension only"
:hidden="hideInputPreview"
:max="4"
@add-ref="openPermanencePicker(index + 2)"
@@ -1189,6 +1192,7 @@
v-if="entry.group.parts.length === 1 && !entry.group.head.hideThumbnail"
class="mt-2"
:refs="entry.group.head.permanenceRefs || []"
:name-options="clipBindingKeys(entry.group.head)"
cta="Add still"
:hidden="entry.group.head.hideInput || hideInputPreview"
@add-ref="openPermanencePicker(undefined, entry.group.head.id)"
@@ -1263,6 +1267,7 @@
v-if="!item.hideThumbnail"
class="mt-2"
:refs="item.permanenceRefs || []"
:name-options="clipBindingKeys(item, entry.group.head)"
cta="Add still"
:hidden="item.hideInput || hideInputPreview"
@add-ref="openPermanencePicker(undefined, item.id)"
@@ -1537,7 +1542,9 @@ import {
extractGlobalLocks,
isBlankGlobalLocks,
nextPermanenceLabel,
parseLockBindingKeys,
permanenceLabels,
sanitizeRefLabel,
type PermanenceRef
} from '~/utils/globalLocks'
import {
@@ -1871,12 +1878,13 @@ const shotPermanenceRefs = ref<Record<number, PermanenceRef[]>>({})
const LOCKS_STORE = 'aigen-global-locks'
const REFS_STORE = 'aigen-permanence-refs'
const pickerHeading = computed(() => {
if (pickerKind.value === 'permanence') return 'Choose a permanence still'
if (pickerKind.value === 'permanence') return 'Choose a still to name'
if (pickerSlot.value === 'main') return studioMode.value === 'edit' ? 'Choose image 1' : 'Choose an image'
if (pickerSlot.value === 'editRef') return 'Choose image 2'
return `Choose Picture ${Number(pickerSlot.value) + 2}`
})
const permanencePicker = computed(() => pickerKind.value === 'permanence')
const lockBindingKeys = computed(() => parseLockBindingKeys(globalLocks.value))
const familyRefLabelLine = computed(() => {
const labels = permanenceLabels(familyPermanenceRefs.value)
if (!labels.length) return ''
@@ -1884,9 +1892,9 @@ const familyRefLabelLine = computed(() => {
})
const permanenceComfyNote = computed(() => {
if (studioMode.value === 'video' && videoWorkflow.value === 'v2' && useIdentityRefs.value) {
return 'Identity-ref is on: Subject stills can map to Picture 2–5 as extra views of the same person only — never a second character or a vehicle.'
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: these refs are prompt anchors (and last-frame continuity), not extra MiniMax pictures. Turn on identity-ref only for one person.'
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.'
})
let videoTimer: ReturnType<typeof setInterval> | null = null
let editTimer: ReturnType<typeof setInterval> | null = null
@@ -2928,15 +2936,20 @@ function openPermanencePicker(shot?: number, clipId?: string) {
})
}
function clipBindingKeys(clip: LibraryClip, fallback?: LibraryClip) {
return parseLockBindingKeys(clip.globalLocks || fallback?.globalLocks || '')
}
function addPermanenceRef(stillId: string, shot?: number) {
const keys = lockBindingKeys.value
if (typeof shot === 'number') {
const current = shotPermanenceRefs.value[shot] || []
if (current.some(item => item.stillId === stillId) || current.length >= 4) return
shotPermanenceRefs.value = { ...shotPermanenceRefs.value, [shot]: [...current, { stillId, label: nextPermanenceLabel(current) }] }
shotPermanenceRefs.value = { ...shotPermanenceRefs.value, [shot]: [...current, { stillId, label: nextPermanenceLabel([...familyPermanenceRefs.value, ...current], keys) }] }
return
}
if (familyPermanenceRefs.value.some(item => item.stillId === stillId) || familyPermanenceRefs.value.length >= 6) return
familyPermanenceRefs.value = [...familyPermanenceRefs.value, { stillId, label: nextPermanenceLabel(familyPermanenceRefs.value) }]
familyPermanenceRefs.value = [...familyPermanenceRefs.value, { stillId, label: nextPermanenceLabel(familyPermanenceRefs.value, keys) }]
}
function removeFamilyRef(index: number) {
@@ -2944,7 +2957,7 @@ function removeFamilyRef(index: number) {
}
function updateFamilyRefLabel(index: number, label: string) {
familyPermanenceRefs.value = familyPermanenceRefs.value.map((item, i) => i === index ? { ...item, label } : item)
familyPermanenceRefs.value = familyPermanenceRefs.value.map((item, i) => i === index ? { ...item, label: sanitizeRefLabel(label) } : item)
}
function removeShotRef(shot: number, index: number) {
@@ -2956,7 +2969,7 @@ function removeShotRef(shot: number, index: number) {
}
function updateShotRefLabel(shot: number, index: number, label: string) {
const current = (shotPermanenceRefs.value[shot] || []).map((item, i) => i === index ? { ...item, label } : item)
const current = (shotPermanenceRefs.value[shot] || []).map((item, i) => i === index ? { ...item, label: sanitizeRefLabel(label) } : item)
shotPermanenceRefs.value = { ...shotPermanenceRefs.value, [shot]: current }
}
@@ -2976,7 +2989,7 @@ function removeClipRef(clip: LibraryClip, index: number) {
}
function updateClipRefLabel(clip: LibraryClip, index: number, label: string) {
void persistClipRefs(clip, (clip.permanenceRefs || []).map((item, i) => i === index ? { ...item, label } : item))
void persistClipRefs(clip, (clip.permanenceRefs || []).map((item, i) => i === index ? { ...item, label: sanitizeRefLabel(label) } : item))
}
function onPickedPermanence(still: { id: string }) {
@@ -2994,7 +3007,7 @@ function onPickedPermanence(still: { id: string }) {
pickerOpen.value = false
return
}
void persistClipRefs(clip, [...(clip.permanenceRefs || []), { stillId: still.id, label: nextPermanenceLabel(clip.permanenceRefs || []) }])
void persistClipRefs(clip, [...(clip.permanenceRefs || []), { stillId: still.id, label: nextPermanenceLabel(clip.permanenceRefs || [], parseLockBindingKeys(clip.globalLocks || '')) }])
pickerOpen.value = false
return
}