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:
Towsty
2026-08-28 17:19:42 -05:00
co-authored by Cursor
parent 099b62edd7
commit b48531d3c2
+56 -13
View File
@@ -811,6 +811,7 @@
</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-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>
<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">
Playing the last saved part. The shot still encoding cannot be scrubbed until it saves.
</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
v-if="!awaitingReveal"
v-if="videoUrl && !lockedSave && !awaitingReveal"
:href="videoUrl"
:download="`${videoDownloadName}.mp4`"
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
</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 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">
@@ -2097,9 +2106,24 @@ const liveElapsedLabel = computed(() => (
const playingLastSavedWhileBusy = computed(() => (
videoBusy.value && Boolean(videoUrl.value) && !watchingLibrary.value && !lockedSave.value
))
const liveStatusMessage = computed(() => (
outputProgressKind.value === 'edit' ? editStatusMessage.value : statusMessage.value
))
function idleInputStatus(kind: 'video' | 'edit') {
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(() => (
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())
return extensionQueue.value.every(item => item.prompt.trim())
})
const videoGenerateDisabled = computed(() => {
return (!textToVideo.value && !file.value) || !prompt.value.trim() || !folderId.value || !queueReady.value
const canUseGeneratedStill = computed(() => Boolean(
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(() => {
if (recommendBusy.value) return true
if (!file.value || !prompt.value.trim()) return true
@@ -2236,6 +2270,10 @@ const editBlocked = computed(() => {
return false
})
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 (!comfyOk.value && !imageComfyOk.value) return 'Desktop GPU is offline.'
return ''
@@ -3287,6 +3325,7 @@ async function copyPrompt(text: string) {
}
function resetImage() {
if (preview.value) URL.revokeObjectURL(preview.value)
file.value = null
preview.value = ''
imageWidth.value = 0
@@ -3311,6 +3350,7 @@ function clearVideoForm() {
}
function readFile(next: File, opts?: { keepThumbnailPref?: boolean }) {
if (preview.value) URL.revokeObjectURL(preview.value)
file.value = next
preview.value = URL.createObjectURL(next)
if (!opts?.keepThumbnailPref) applyThumbnailDefault()
@@ -4421,11 +4461,11 @@ function clearPreview() {
concealEditOutput.value = false
editLockedSave.value = false
if (!videoBusy.value) {
statusMessage.value = 'Waiting for a still.'
statusMessage.value = idleInputStatus('video')
progress.value = 0
}
if (!editBusy.value) {
editStatusMessage.value = 'Waiting for a still.'
editStatusMessage.value = idleInputStatus('edit')
editProgress.value = 0
}
}
@@ -4665,7 +4705,10 @@ function removeEditPass(id: string) {
}
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())) {
toast('Each extra pass needs a prompt.')
return
@@ -4757,6 +4800,7 @@ async function useEditAsInput() {
if (!editResultUrl.value) return
try {
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' }))
toast('Edited still is now the input')
} catch {
@@ -4796,9 +4840,8 @@ async function generate() {
toast('LTX Pink Cherry is not available on this instance')
return
}
if ((!file.value && !textToVideo.value) || !prompt.value.trim()) return
if (!queueReady.value) {
toast(shotScriptMode.value ? 'Each shot in the script needs a prompt.' : 'Each queued extension needs a prompt.')
if (videoBlockReason.value) {
toast(videoBlockReason.value)
return
}
if (!folderId.value) {