diff --git a/components/ImagePickerModal.vue b/components/ImagePickerModal.vue
index 294a442..6730a6b 100644
--- a/components/ImagePickerModal.vue
+++ b/components/ImagePickerModal.vue
@@ -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.'
diff --git a/components/PermanenceRefStrip.vue b/components/PermanenceRefStrip.vue
index 5699d99..21cd47b 100644
--- a/components/PermanenceRefStrip.vue
+++ b/components/PermanenceRefStrip.vue
@@ -8,7 +8,7 @@
@@ -30,15 +30,21 @@
×
-
- {{ option }}
-
+
+
+
diff --git a/pages/index.vue b/pages/index.vue
index 901b419..97bca69 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -279,18 +279,19 @@
- Add stills, then label each Subject / Vehicle / Interior / Object. Those names are injected into locks and
- [INHERITS] , 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.
{{ permanenceComfyNote }}
{{ familyRefLabelLine }}
@@ -311,10 +312,11 @@
>({})
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 | null = null
let editTimer: ReturnType | 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
}
diff --git a/utils/globalLocks.ts b/utils/globalLocks.ts
index 1616b4e..99f4139 100644
--- a/utils/globalLocks.ts
+++ b/utils/globalLocks.ts
@@ -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()
+ 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()
}