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>
|
||||
<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) {
|
||||
|
||||
Reference in New Issue
Block a user