Fix Use as input and uploaded stills so I2V is not stuck waiting for a still.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+56
-13
@@ -811,6 +811,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="studioMode === 'video' && videoBusy" class="text-xs text-zinc-400">The current job keeps running in Output. This submit waits in the job queue.</p>
|
<p v-if="studioMode === 'video' && videoBusy" class="text-xs text-zinc-400">The current job keeps running in Output. This submit waits in the job queue.</p>
|
||||||
<p v-else-if="studioMode === 'edit' && (videoBusy || editBusy)" class="text-xs text-zinc-400">The current job keeps running in Output. This submit waits in the job queue.</p>
|
<p v-else-if="studioMode === 'edit' && (videoBusy || editBusy)" class="text-xs text-zinc-400">The current job keeps running in Output. This submit waits in the job queue.</p>
|
||||||
|
<p v-if="studioMode === 'video' && videoBlockReason" class="text-xs text-zinc-400">{{ videoBlockReason }}</p>
|
||||||
<p v-if="studioMode === 'edit' && editBlockReason" class="text-xs text-zinc-400">{{ editBlockReason }}</p>
|
<p v-if="studioMode === 'edit' && editBlockReason" class="text-xs text-zinc-400">{{ editBlockReason }}</p>
|
||||||
|
|
||||||
<div v-if="studioJobs.length" class="rounded-2xl border border-white/10 bg-zinc-950/40 p-4">
|
<div v-if="studioJobs.length" class="rounded-2xl border border-white/10 bg-zinc-950/40 p-4">
|
||||||
@@ -1015,9 +1016,9 @@
|
|||||||
<p v-else-if="playingLastSavedWhileBusy" class="mt-3 text-xs text-zinc-500">
|
<p v-else-if="playingLastSavedWhileBusy" class="mt-3 text-xs text-zinc-500">
|
||||||
Playing the last saved part. The shot still encoding cannot be scrubbed until it saves.
|
Playing the last saved part. The shot still encoding cannot be scrubbed until it saves.
|
||||||
</p>
|
</p>
|
||||||
<div v-if="videoUrl && !lockedSave" class="mt-4 flex flex-wrap gap-3">
|
<div v-if="(videoUrl && !lockedSave) || canUseGeneratedStill" class="mt-4 flex flex-wrap gap-3">
|
||||||
<a
|
<a
|
||||||
v-if="!awaitingReveal"
|
v-if="videoUrl && !lockedSave && !awaitingReveal"
|
||||||
:href="videoUrl"
|
:href="videoUrl"
|
||||||
:download="`${videoDownloadName}.mp4`"
|
:download="`${videoDownloadName}.mp4`"
|
||||||
class="inline-flex rounded-2xl border border-white/15 px-4 py-2 text-sm hover:border-amber-300/50"
|
class="inline-flex rounded-2xl border border-white/15 px-4 py-2 text-sm hover:border-amber-300/50"
|
||||||
@@ -1032,6 +1033,14 @@
|
|||||||
>
|
>
|
||||||
Extend Video
|
Extend Video
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="canUseGeneratedStill"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex rounded-2xl border border-amber-300/40 px-4 py-2 text-sm text-amber-100 hover:border-amber-300/70"
|
||||||
|
@click="useEditAsInput"
|
||||||
|
>
|
||||||
|
Use the image I just generated as input
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="extendOpen" class="mt-4 space-y-4 rounded-2xl border border-white/10 bg-zinc-950/50 p-4">
|
<div v-if="extendOpen" class="mt-4 space-y-4 rounded-2xl border border-white/10 bg-zinc-950/50 p-4">
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
@@ -2097,9 +2106,24 @@ const liveElapsedLabel = computed(() => (
|
|||||||
const playingLastSavedWhileBusy = computed(() => (
|
const playingLastSavedWhileBusy = computed(() => (
|
||||||
videoBusy.value && Boolean(videoUrl.value) && !watchingLibrary.value && !lockedSave.value
|
videoBusy.value && Boolean(videoUrl.value) && !watchingLibrary.value && !lockedSave.value
|
||||||
))
|
))
|
||||||
const liveStatusMessage = computed(() => (
|
function idleInputStatus(kind: 'video' | 'edit') {
|
||||||
outputProgressKind.value === 'edit' ? editStatusMessage.value : statusMessage.value
|
if (kind === 'edit') return file.value ? 'Still ready. Edit when you are ready.' : 'Waiting for a still.'
|
||||||
))
|
if (textToVideo.value) {
|
||||||
|
return file.value
|
||||||
|
? 'Still loaded (optional). Generate when you are ready.'
|
||||||
|
: 'Ready for text-to-video.'
|
||||||
|
}
|
||||||
|
return file.value ? 'Still ready. Generate when you are ready.' : 'Waiting for a still.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveStatusMessage = computed(() => {
|
||||||
|
if (outputProgressKind.value === 'edit') {
|
||||||
|
if (editBusy.value || editResultUrl.value || editLockedSave.value) return editStatusMessage.value
|
||||||
|
return idleInputStatus('edit')
|
||||||
|
}
|
||||||
|
if (videoBusy.value || videoUrl.value || lockedSave.value) return statusMessage.value
|
||||||
|
return idleInputStatus('video')
|
||||||
|
})
|
||||||
const liveStatusBusy = computed(() => (
|
const liveStatusBusy = computed(() => (
|
||||||
outputProgressKind.value === 'edit' ? editStatusBusy.value : statusBusy.value
|
outputProgressKind.value === 'edit' ? editStatusBusy.value : statusBusy.value
|
||||||
))
|
))
|
||||||
@@ -2212,9 +2236,19 @@ const queueReady = computed(() => {
|
|||||||
if (shotScriptMode.value) return parsedShots.value.length > 0 && parsedShots.value.every(shot => shot.prompt.trim())
|
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 => item.prompt.trim())
|
||||||
})
|
})
|
||||||
const videoGenerateDisabled = computed(() => {
|
const canUseGeneratedStill = computed(() => Boolean(
|
||||||
return (!textToVideo.value && !file.value) || !prompt.value.trim() || !folderId.value || !queueReady.value
|
editResultUrl.value && !editLockedSave.value && !editAwaitingReveal.value
|
||||||
|
))
|
||||||
|
const videoBlockReason = computed(() => {
|
||||||
|
if (!textToVideo.value && !file.value) return 'Load a still first. Image-to-video needs an input still.'
|
||||||
|
if (!prompt.value.trim()) return 'Write a motion prompt first.'
|
||||||
|
if (!folderId.value) return 'Choose a library folder before generating.'
|
||||||
|
if (!queueReady.value) {
|
||||||
|
return shotScriptMode.value ? 'Each shot in the script needs a prompt.' : 'Each queued extension needs a prompt.'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
})
|
})
|
||||||
|
const videoGenerateDisabled = computed(() => Boolean(videoBlockReason.value))
|
||||||
const recommendDisabled = computed(() => {
|
const recommendDisabled = computed(() => {
|
||||||
if (recommendBusy.value) return true
|
if (recommendBusy.value) return true
|
||||||
if (!file.value || !prompt.value.trim()) return true
|
if (!file.value || !prompt.value.trim()) return true
|
||||||
@@ -2236,6 +2270,10 @@ const editBlocked = computed(() => {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
const editBlockReason = computed(() => {
|
const editBlockReason = computed(() => {
|
||||||
|
if (!file.value) return 'Load a still first.'
|
||||||
|
if (!prompt.value.trim()) return 'Write an edit prompt first.'
|
||||||
|
if (!folderId.value) return 'Choose a library folder before editing.'
|
||||||
|
if (editPassQueue.value.some(item => !item.prompt.trim())) return 'Each extra pass needs a prompt.'
|
||||||
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image edit is not configured. Set COMFY_HOST.'
|
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image edit is not configured. Set COMFY_HOST.'
|
||||||
if (!comfyOk.value && !imageComfyOk.value) return 'Desktop GPU is offline.'
|
if (!comfyOk.value && !imageComfyOk.value) return 'Desktop GPU is offline.'
|
||||||
return ''
|
return ''
|
||||||
@@ -3287,6 +3325,7 @@ async function copyPrompt(text: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetImage() {
|
function resetImage() {
|
||||||
|
if (preview.value) URL.revokeObjectURL(preview.value)
|
||||||
file.value = null
|
file.value = null
|
||||||
preview.value = ''
|
preview.value = ''
|
||||||
imageWidth.value = 0
|
imageWidth.value = 0
|
||||||
@@ -3311,6 +3350,7 @@ function clearVideoForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readFile(next: File, opts?: { keepThumbnailPref?: boolean }) {
|
function readFile(next: File, opts?: { keepThumbnailPref?: boolean }) {
|
||||||
|
if (preview.value) URL.revokeObjectURL(preview.value)
|
||||||
file.value = next
|
file.value = next
|
||||||
preview.value = URL.createObjectURL(next)
|
preview.value = URL.createObjectURL(next)
|
||||||
if (!opts?.keepThumbnailPref) applyThumbnailDefault()
|
if (!opts?.keepThumbnailPref) applyThumbnailDefault()
|
||||||
@@ -4421,11 +4461,11 @@ function clearPreview() {
|
|||||||
concealEditOutput.value = false
|
concealEditOutput.value = false
|
||||||
editLockedSave.value = false
|
editLockedSave.value = false
|
||||||
if (!videoBusy.value) {
|
if (!videoBusy.value) {
|
||||||
statusMessage.value = 'Waiting for a still.'
|
statusMessage.value = idleInputStatus('video')
|
||||||
progress.value = 0
|
progress.value = 0
|
||||||
}
|
}
|
||||||
if (!editBusy.value) {
|
if (!editBusy.value) {
|
||||||
editStatusMessage.value = 'Waiting for a still.'
|
editStatusMessage.value = idleInputStatus('edit')
|
||||||
editProgress.value = 0
|
editProgress.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4665,7 +4705,10 @@ function removeEditPass(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function editImage() {
|
async function editImage() {
|
||||||
if (!file.value || !prompt.value.trim()) return
|
if (!file.value || !prompt.value.trim()) {
|
||||||
|
toast(editBlockReason.value || 'Load a still first.')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (editPassQueue.value.some(item => !item.prompt.trim())) {
|
if (editPassQueue.value.some(item => !item.prompt.trim())) {
|
||||||
toast('Each extra pass needs a prompt.')
|
toast('Each extra pass needs a prompt.')
|
||||||
return
|
return
|
||||||
@@ -4757,6 +4800,7 @@ async function useEditAsInput() {
|
|||||||
if (!editResultUrl.value) return
|
if (!editResultUrl.value) return
|
||||||
try {
|
try {
|
||||||
const blob = await $fetch<Blob>(editResultUrl.value, { responseType: 'blob' })
|
const blob = await $fetch<Blob>(editResultUrl.value, { responseType: 'blob' })
|
||||||
|
if (!blob || !blob.size) throw new Error('empty still')
|
||||||
readFile(new File([blob], `${editDownloadName.value || 'edited'}.png`, { type: blob.type || 'image/png' }))
|
readFile(new File([blob], `${editDownloadName.value || 'edited'}.png`, { type: blob.type || 'image/png' }))
|
||||||
toast('Edited still is now the input')
|
toast('Edited still is now the input')
|
||||||
} catch {
|
} catch {
|
||||||
@@ -4796,9 +4840,8 @@ async function generate() {
|
|||||||
toast('LTX Pink Cherry is not available on this instance')
|
toast('LTX Pink Cherry is not available on this instance')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ((!file.value && !textToVideo.value) || !prompt.value.trim()) return
|
if (videoBlockReason.value) {
|
||||||
if (!queueReady.value) {
|
toast(videoBlockReason.value)
|
||||||
toast(shotScriptMode.value ? 'Each shot in the script needs a prompt.' : 'Each queued extension needs a prompt.')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!folderId.value) {
|
if (!folderId.value) {
|
||||||
|
|||||||
Reference in New Issue
Block a user