Bind permanence stills to named lock keys instead of coded categories.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -220,7 +220,7 @@ const emptyLabel = computed(() => tab.value === 'input'
|
||||
? 'No uploaded stills in this folder yet.'
|
||||
: 'No generated stills in this folder yet.')
|
||||
const pickerHint = computed(() => {
|
||||
if (props.stillOnly) return 'Pick a still, then label it Subject / Vehicle / Interior / Object on the strip. That name is what the shot prompt inherits.'
|
||||
if (props.stillOnly) return 'Pick a still, then type a short name on the strip (or pick a GLOBAL LOCKS key). That name is what the shot prompt inherits.'
|
||||
return tab.value === 'input'
|
||||
? 'Uploaded stills only. Generated frames and edits live under Output.'
|
||||
: 'Generated stills from image edits. Video frames stay with their clips.'
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div
|
||||
v-for="(item, index) in refs"
|
||||
:key="item.stillId + '-' + index"
|
||||
class="flex w-14 flex-col items-center gap-0.5"
|
||||
class="flex w-16 flex-col items-center gap-0.5"
|
||||
>
|
||||
<span class="relative h-12 w-12 overflow-hidden rounded-lg bg-zinc-900">
|
||||
<span v-if="hidden" class="sensitive-glow block h-full w-full" />
|
||||
@@ -30,15 +30,21 @@
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
<select
|
||||
class="w-full truncate rounded border border-white/10 bg-zinc-950 px-0.5 py-0 text-[10px] text-zinc-400"
|
||||
<input
|
||||
class="w-full truncate rounded border border-white/10 bg-zinc-950 px-0.5 py-0 text-center text-[10px] text-zinc-400 outline-none focus:border-amber-300/40"
|
||||
:disabled="disabled"
|
||||
:value="item.label"
|
||||
@change="emit('updateLabel', index, ($event.target as HTMLSelectElement).value)"
|
||||
:list="nameListId"
|
||||
:title="item.label || 'Name'"
|
||||
placeholder="Name"
|
||||
maxlength="24"
|
||||
autocomplete="off"
|
||||
@input="emit('updateLabel', index, sanitizeRefLabel(($event.target as HTMLInputElement).value))"
|
||||
>
|
||||
<option v-for="option in labelOptions(item.label)" :key="option" :value="option">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<datalist :id="nameListId">
|
||||
<option v-for="option in listNames" :key="option" :value="option" />
|
||||
</datalist>
|
||||
<button
|
||||
v-if="!disabled && refs.length < max"
|
||||
type="button"
|
||||
@@ -54,10 +60,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PERMANENCE_REF_LABELS, type PermanenceRef } from '~/utils/globalLocks'
|
||||
import { computed, useId } from 'vue'
|
||||
import { sanitizeRefLabel, type PermanenceRef } from '~/utils/globalLocks'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
refs: PermanenceRef[]
|
||||
nameOptions?: string[]
|
||||
label?: string
|
||||
hint?: string
|
||||
hidden?: boolean
|
||||
@@ -66,8 +74,9 @@ withDefaults(defineProps<{
|
||||
addTitle?: string
|
||||
cta?: string
|
||||
}>(), {
|
||||
nameOptions: () => [],
|
||||
max: 6,
|
||||
addTitle: 'Choose a still to label',
|
||||
addTitle: 'Choose a still to name',
|
||||
cta: 'Add still'
|
||||
})
|
||||
|
||||
@@ -77,8 +86,19 @@ const emit = defineEmits<{
|
||||
updateLabel: [index: number, label: string]
|
||||
}>()
|
||||
|
||||
function labelOptions(current: string) {
|
||||
if (PERMANENCE_REF_LABELS.includes(current as typeof PERMANENCE_REF_LABELS[number])) return [...PERMANENCE_REF_LABELS]
|
||||
return [...PERMANENCE_REF_LABELS, current]
|
||||
}
|
||||
const nameListId = useId()
|
||||
|
||||
const listNames = computed(() => {
|
||||
const seen = new Set<string>()
|
||||
const names: string[] = []
|
||||
for (const name of [...props.nameOptions, ...props.refs.map(item => item.label)]) {
|
||||
const trimmed = String(name || '').trim()
|
||||
if (!trimmed) continue
|
||||
const id = trimmed.toLowerCase()
|
||||
if (seen.has(id)) continue
|
||||
seen.add(id)
|
||||
names.push(trimmed)
|
||||
}
|
||||
return names
|
||||
})
|
||||
</script>
|
||||
|
||||
+28
-15
@@ -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
|
||||
}
|
||||
|
||||
+27
-15
@@ -1,7 +1,3 @@
|
||||
export const PERMANENCE_REF_LABELS = ['Subject', 'Vehicle', 'Interior', 'Object'] as const
|
||||
|
||||
export type PermanenceRefLabel = (typeof PERMANENCE_REF_LABELS)[number]
|
||||
|
||||
export interface PermanenceRef {
|
||||
stillId: string
|
||||
label: string
|
||||
@@ -9,8 +5,6 @@ export interface PermanenceRef {
|
||||
|
||||
export const DEFAULT_GLOBAL_LOCKS = `[GLOBAL LOCKS]
|
||||
Subject:
|
||||
Vehicle:
|
||||
Interior:
|
||||
Object:
|
||||
Extras:
|
||||
Camera:
|
||||
@@ -18,7 +12,28 @@ Speech:
|
||||
Never:
|
||||
State:`
|
||||
|
||||
const LOCK_FIELD = /^(Subject|Vehicle|Interior|Object|Extras|Camera|Speech|Never|State)\s*:?\s*/i
|
||||
const KNOWN_LOCK_KEYS = 'Subject|Vehicle|Interior|Object|Extras|Camera|Speech|Never|State'
|
||||
const LOCK_FIELD = new RegExp(`^(?:(?:${KNOWN_LOCK_KEYS})\\s*:?|[A-Za-z][A-Za-z0-9 _-]{0,22}\\s*:)\\s*`, 'i')
|
||||
const LOCK_KEY_LINE = /^([A-Za-z][A-Za-z0-9 _-]{0,22})\s*:/
|
||||
const SKIP_BINDING_KEYS = /^(speech|never|state)$/i
|
||||
|
||||
export function parseLockBindingKeys(text: string): string[] {
|
||||
const keys: string[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const line of String(text || '').replace(/\r\n/g, '\n').split('\n')) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('[')) continue
|
||||
const match = trimmed.match(LOCK_KEY_LINE)
|
||||
if (!match) continue
|
||||
const key = match[1].replace(/\s+/g, ' ').trim()
|
||||
if (!key || SKIP_BINDING_KEYS.test(key)) continue
|
||||
const id = key.toLowerCase()
|
||||
if (seen.has(id)) continue
|
||||
seen.add(id)
|
||||
keys.push(key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
export function normalizePermanenceRefs(raw: unknown, limit = 8): PermanenceRef[] {
|
||||
if (!Array.isArray(raw)) return []
|
||||
@@ -36,15 +51,12 @@ export function normalizePermanenceRefs(raw: unknown, limit = 8): PermanenceRef[
|
||||
}
|
||||
|
||||
export function sanitizeRefLabel(raw: unknown) {
|
||||
const label = String(raw || '').trim().slice(0, 24)
|
||||
if (!label) return 'Object'
|
||||
const match = PERMANENCE_REF_LABELS.find(item => item.toLowerCase() === label.toLowerCase())
|
||||
return match || label
|
||||
return String(raw || '').replace(/\s+/g, ' ').trim().slice(0, 24)
|
||||
}
|
||||
|
||||
export function nextPermanenceLabel(existing: PermanenceRef[]) {
|
||||
const used = new Set(existing.map(item => item.label))
|
||||
return PERMANENCE_REF_LABELS.find(label => !used.has(label)) || 'Object'
|
||||
export function nextPermanenceLabel(existing: PermanenceRef[], lockKeys: string[] = []) {
|
||||
const used = new Set(existing.map(item => item.label.trim().toLowerCase()).filter(Boolean))
|
||||
return lockKeys.find(key => !used.has(key.trim().toLowerCase())) || ''
|
||||
}
|
||||
|
||||
export function mergePermanenceRefs(family?: PermanenceRef[] | null, shot?: PermanenceRef[] | null) {
|
||||
@@ -90,7 +102,7 @@ export function resolveGlobalLocks(locks: string, firstPrompt: string) {
|
||||
}
|
||||
|
||||
function extractState(locks: string) {
|
||||
const match = String(locks || '').match(/(?:^|\n)\s*State:\s*(.+?)(?=\n\s*(?:Subject|Vehicle|Interior|Object|Extras|Camera|Speech|Never)\s*:|\n\s*\[|$)/is)
|
||||
const match = String(locks || '').match(/(?:^|\n)\s*State:\s*(.+?)(?=\n\s*[A-Za-z][A-Za-z0-9 _-]{0,22}\s*:|\n\s*\[|$)/is)
|
||||
return String(match?.[1] || '').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user