Give video extensions the same pre-text, prompt, and post-text fields as the main form.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+91
-51
@@ -522,11 +522,12 @@
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
<PromptField
|
||||
v-model="item.prompt"
|
||||
:rows="3"
|
||||
:title="`Extension #${index + 1}`"
|
||||
placeholder="Motion prompt for this extension…"
|
||||
<PromptPartsEditor
|
||||
v-model:pre="item.promptPre"
|
||||
v-model:prompt="item.prompt"
|
||||
v-model:post="item.promptPost"
|
||||
prompt-placeholder="Motion prompt for this extension…"
|
||||
:prompt-rows="4"
|
||||
:keep-items="keepPrompts.video"
|
||||
keep-heading="Video prompts"
|
||||
/>
|
||||
@@ -1417,18 +1418,15 @@
|
||||
{{ lastFrameLoading ? 'Reading last frame…' : 'Last frame preview unavailable' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="block text-sm">
|
||||
<span class="mb-1 block text-zinc-400">Extension prompt</span>
|
||||
<PromptField
|
||||
v-model="extendPrompt"
|
||||
:rows="4"
|
||||
title="Extension prompt"
|
||||
placeholder="Continue the motion from this frame…"
|
||||
:keep-items="keepPrompts.video"
|
||||
keep-heading="Video prompts"
|
||||
textarea-class=""
|
||||
/>
|
||||
</div>
|
||||
<PromptPartsEditor
|
||||
v-model:pre="extendPromptPre"
|
||||
v-model:prompt="extendPrompt"
|
||||
v-model:post="extendPromptPost"
|
||||
prompt-placeholder="Continue the motion from this frame…"
|
||||
:prompt-rows="4"
|
||||
:keep-items="keepPrompts.video"
|
||||
keep-heading="Video prompts"
|
||||
/>
|
||||
<DurationField
|
||||
v-model="extendDuration"
|
||||
label="Extension duration"
|
||||
@@ -1448,7 +1446,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-2xl bg-amber-400 px-4 py-2.5 text-sm font-semibold text-zinc-950 transition hover:bg-amber-300 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="!extendPrompt.trim()"
|
||||
:disabled="!composePromptParts(extendPromptPre, extendPrompt, extendPromptPost)"
|
||||
@click="generateExtension"
|
||||
>
|
||||
{{ watchingLiveVideo() || videoBusy ? 'Queue extension' : 'Generate Extension' }}
|
||||
@@ -2364,7 +2362,7 @@ interface RetryDraft {
|
||||
fps?: number
|
||||
samplerName?: string
|
||||
scheduler?: string
|
||||
extensions?: { prompt: string; duration: number; permanenceRefs?: PermanenceRef[]; loraName?: string; loraStack?: LoraStackItem[] }[]
|
||||
extensions?: { prompt: string; promptPre?: string; promptPost?: string; duration: number; permanenceRefs?: PermanenceRef[]; loraName?: string; loraStack?: LoraStackItem[] }[]
|
||||
workflow?: string
|
||||
globalLocks?: string
|
||||
permanenceRefs?: PermanenceRef[]
|
||||
@@ -2378,6 +2376,8 @@ interface RetryDraft {
|
||||
interface QueuedExtension {
|
||||
id: string
|
||||
prompt: string
|
||||
promptPre: string
|
||||
promptPost: string
|
||||
duration: number
|
||||
loraName?: string
|
||||
loraStack?: LoraStackItem[]
|
||||
@@ -2602,6 +2602,8 @@ const currentClipId = ref('')
|
||||
const extendOpen = ref(false)
|
||||
const extendClipId = ref('')
|
||||
const extendPrompt = ref('')
|
||||
const extendPromptPre = ref('')
|
||||
const extendPromptPost = ref('')
|
||||
const extendDuration = ref(5)
|
||||
const lastFrameUrl = ref('')
|
||||
const lastFrameLoading = ref(false)
|
||||
@@ -2998,7 +3000,7 @@ const canExtend = computed(() => Boolean(
|
||||
const extendSaveName = computed(() => nextClipPartName(currentClip.value?.name || currentClip.value?.prompt || ''))
|
||||
const queueReady = computed(() => {
|
||||
if (shotScriptMode.value) return parsedShots.value.length > 0 && parsedShots.value.every(shot => shot.prompt.trim())
|
||||
return extensionQueue.value.every(item => item.prompt.trim())
|
||||
return extensionQueue.value.every(item => composePromptParts(item.promptPre, item.prompt, item.promptPost))
|
||||
})
|
||||
const canUseGeneratedStill = computed(() => Boolean(
|
||||
editResultUrl.value && !editLockedSave.value && !editAwaitingReveal.value
|
||||
@@ -3204,10 +3206,10 @@ const plannedChain = computed(() => {
|
||||
}))
|
||||
}
|
||||
return [
|
||||
{ label: 'Initial', prompt: prompt.value, duration: duration.value },
|
||||
{ label: 'Initial', prompt: composePromptParts(promptPre.value, prompt.value, promptPost.value), duration: duration.value },
|
||||
...extensionQueue.value.map((item, index) => ({
|
||||
label: `Extension ${index + 1}`,
|
||||
prompt: item.prompt,
|
||||
prompt: composePromptParts(item.promptPre, item.prompt, item.promptPost),
|
||||
duration: item.duration
|
||||
}))
|
||||
]
|
||||
@@ -4128,6 +4130,26 @@ function applyPromptRestore(saved: string, stored?: { prompt?: string; promptPre
|
||||
promptPost.value = parts.post
|
||||
}
|
||||
|
||||
function queuedExtensionFrom(
|
||||
item: { prompt?: string; promptPre?: string; promptPost?: string; duration?: number; loraName?: string; loraStack?: LoraStackItem[] },
|
||||
duration: number,
|
||||
loraStack: LoraStackItem[]
|
||||
): QueuedExtension {
|
||||
const parts = restorePromptParts(item.prompt || '', {
|
||||
pre: item.promptPre,
|
||||
prompt: item.prompt,
|
||||
post: item.promptPost
|
||||
})
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
prompt: parts.prompt,
|
||||
promptPre: parts.pre,
|
||||
promptPost: parts.post,
|
||||
duration,
|
||||
loraStack
|
||||
}
|
||||
}
|
||||
|
||||
function insertKeepExtend(item: KeepPrompt) {
|
||||
extendPrompt.value = insertKeepSnippet(extendPrompt.value, item.text)
|
||||
}
|
||||
@@ -5155,7 +5177,7 @@ async function restoreStudioJob(id: string) {
|
||||
hideThumbnail?: boolean
|
||||
hideInput?: boolean
|
||||
referenceStillIds?: Array<string | null>
|
||||
extensions?: { prompt: string; duration: number; permanenceRefs?: PermanenceRef[]; loraName?: string; loraStack?: LoraStackItem[] }[]
|
||||
extensions?: { prompt: string; promptPre?: string; promptPost?: string; duration: number; permanenceRefs?: PermanenceRef[]; loraName?: string; loraStack?: LoraStackItem[] }[]
|
||||
queueAutoRun?: boolean
|
||||
promptPre?: string
|
||||
promptPost?: string
|
||||
@@ -5174,10 +5196,14 @@ async function restoreStudioJob(id: string) {
|
||||
outputFocus.value = 'video'
|
||||
closeExtend()
|
||||
const extensions = payload.extensions || []
|
||||
const queuedAsCards = extensions.some(item =>
|
||||
Object.prototype.hasOwnProperty.call(item, 'promptPre')
|
||||
|| Object.prototype.hasOwnProperty.call(item, 'promptPost')
|
||||
)
|
||||
applyVideoWorkflow(payload.workflow)
|
||||
applyPromptRestore(extensions.length
|
||||
? [payload.prompt.trim(), ...extensions.map((item, index) => `shot ${index + 2}\n${item.prompt.trim()}`)].join('\n\n')
|
||||
: (payload.prompt || ''), payload)
|
||||
applyPromptRestore(queuedAsCards || !extensions.length
|
||||
? (payload.prompt || '')
|
||||
: [payload.prompt.trim(), ...extensions.map((item, index) => `shot ${index + 2}\n${item.prompt.trim()}`)].join('\n\n'), payload)
|
||||
clipName.value = payload.name || ''
|
||||
seedInput.value = String(payload.seed || '')
|
||||
restoreAdvanced({
|
||||
@@ -5198,14 +5224,13 @@ async function restoreStudioJob(id: string) {
|
||||
browseFolderId.value = payload.folderId
|
||||
}
|
||||
if (typeof payload.sound === 'boolean') withSound.value = payload.sound
|
||||
shotScriptMode.value = extensions.length > 0
|
||||
shotScriptMode.value = extensions.length > 0 && !queuedAsCards
|
||||
identityRefs.value.forEach((_, index) => clearIdentityRef(index))
|
||||
extensionQueue.value = extensions.map(item => ({
|
||||
id: crypto.randomUUID(),
|
||||
prompt: item.prompt,
|
||||
duration: clampDuration(Number(item.duration)),
|
||||
loraStack: readLoraStack(item)
|
||||
}))
|
||||
extensionQueue.value = extensions.map(item => queuedExtensionFrom(
|
||||
item,
|
||||
clampDuration(Number(item.duration)),
|
||||
readLoraStack(item)
|
||||
))
|
||||
queueAutoRun.value = payload.queueAutoRun === true
|
||||
globalLocks.value = payload.globalLocks || ''
|
||||
familyPermanenceRefs.value = payload.permanenceRefs || []
|
||||
@@ -5590,12 +5615,11 @@ async function rerun(item: LibraryClip, collection = false) {
|
||||
restoreAll ? ordered : undefined
|
||||
)
|
||||
extensionQueue.value = restoreAll
|
||||
? parts.slice(1).map(part => ({
|
||||
id: crypto.randomUUID(),
|
||||
prompt: part.prompt,
|
||||
duration: clipGenerateDuration(part),
|
||||
loraStack: loraStacksEqual(readLoraStack(part), readLoraStack(ordered[0] || target)) ? [] : readLoraStack(part)
|
||||
}))
|
||||
? parts.slice(1).map(part => queuedExtensionFrom(
|
||||
part,
|
||||
clipGenerateDuration(part),
|
||||
loraStacksEqual(readLoraStack(part), readLoraStack(ordered[0] || target)) ? [] : readLoraStack(part)
|
||||
))
|
||||
: []
|
||||
try {
|
||||
const initialTextToVideo = textToVideo.value && !target.parentClipId && !(target.chainIndex)
|
||||
@@ -5636,12 +5660,11 @@ async function loadDraft(draft: RetryDraft) {
|
||||
})
|
||||
shotPermanenceRefs.value = draftShots
|
||||
applyLoraSelection(draft, draft.shotLoraStacks || draft.shotLoras)
|
||||
extensionQueue.value = (draft.extensions || []).map(item => ({
|
||||
id: crypto.randomUUID(),
|
||||
prompt: String(item.prompt || ''),
|
||||
duration: clampDuration(Number(item.duration)),
|
||||
loraStack: readLoraStack(item)
|
||||
}))
|
||||
extensionQueue.value = (draft.extensions || []).map(item => queuedExtensionFrom(
|
||||
item,
|
||||
clampDuration(Number(item.duration)),
|
||||
readLoraStack(item)
|
||||
))
|
||||
try {
|
||||
const blob = await $fetch<Blob>(`/api/library/drafts/${draft.id}/still`, { responseType: 'blob' })
|
||||
const filename = draft.stillFilename || 'held-still.png'
|
||||
@@ -5715,6 +5738,9 @@ function closeExtend() {
|
||||
lastFrameGen += 1
|
||||
extendOpen.value = false
|
||||
extendClipId.value = ''
|
||||
extendPromptPre.value = ''
|
||||
extendPrompt.value = ''
|
||||
extendPromptPost.value = ''
|
||||
lastFrameLoading.value = false
|
||||
lastFrameRevealed.value = false
|
||||
if (lastFrameObjectUrl) {
|
||||
@@ -5760,8 +5786,14 @@ async function openExtend(item?: LibraryClip) {
|
||||
currentClipId.value = id
|
||||
extendClipId.value = id
|
||||
const clip = clips.value.find(entry => entry.id === id)
|
||||
if (clip) applyPromptRestore(clip.prompt, clip)
|
||||
extendPrompt.value = clip?.prompt || prompt.value
|
||||
const parts = restorePromptParts(clip?.prompt || prompt.value, {
|
||||
pre: clip ? (clip.promptPre || '') : promptPre.value,
|
||||
prompt: clip?.prompt || prompt.value,
|
||||
post: clip ? (clip.promptPost || '') : promptPost.value
|
||||
})
|
||||
extendPromptPre.value = parts.pre
|
||||
extendPrompt.value = parts.prompt
|
||||
extendPromptPost.value = parts.post
|
||||
extendDuration.value = clampDuration(duration.value)
|
||||
extendLoraStack.value = readLoraStack(clip)
|
||||
lastFrameRevealed.value = false
|
||||
@@ -5776,7 +5808,7 @@ async function openExtend(item?: LibraryClip) {
|
||||
|
||||
async function generateExtension() {
|
||||
const id = extendClipId.value || clipIdFromPlayer()
|
||||
if (!extendPrompt.value.trim()) return
|
||||
if (!composePromptParts(extendPromptPre.value, extendPrompt.value, extendPromptPost.value)) return
|
||||
if (!id) {
|
||||
toast('Load a generated clip before extending it.')
|
||||
return
|
||||
@@ -5799,8 +5831,8 @@ async function generateExtension() {
|
||||
body: {
|
||||
clipId: id,
|
||||
prompt: extendPrompt.value.trim(),
|
||||
promptPre: promptPre.value.trim() || undefined,
|
||||
promptPost: promptPost.value.trim() || undefined,
|
||||
promptPre: extendPromptPre.value,
|
||||
promptPost: extendPromptPost.value,
|
||||
duration: extendDuration.value,
|
||||
loraStack: extendLoraStack.value
|
||||
}
|
||||
@@ -6475,6 +6507,8 @@ function queueExtension() {
|
||||
extensionQueue.value.push({
|
||||
id: crypto.randomUUID(),
|
||||
prompt: prompt.value.trim(),
|
||||
promptPre: promptPre.value,
|
||||
promptPost: promptPost.value,
|
||||
duration: duration.value,
|
||||
loraStack: []
|
||||
})
|
||||
@@ -7012,7 +7046,13 @@ async function generate() {
|
||||
const initialPrompt = (shots?.[0]?.prompt || prompt.value).trim()
|
||||
const queued = shots
|
||||
? shots.slice(1).map(shot => ({ prompt: shot.prompt.trim(), duration: duration.value, ...persistLoraFields(shotLoraStacks.value[shot.n] || []) }))
|
||||
: extensionQueue.value.map(item => ({ prompt: item.prompt.trim(), duration: item.duration, ...persistLoraFields(item.loraStack || []) }))
|
||||
: extensionQueue.value.map(item => ({
|
||||
prompt: item.prompt.trim(),
|
||||
promptPre: item.promptPre,
|
||||
promptPost: item.promptPost,
|
||||
duration: item.duration,
|
||||
...persistLoraFields(item.loraStack || [])
|
||||
}))
|
||||
const hideOut = hideThumbnail.value
|
||||
const folderLockedPref = false
|
||||
const keepLiveOutput = watchingLiveVideo()
|
||||
|
||||
Reference in New Issue
Block a user