From b48531d3c29eb6b80282b259b66d2e311ee7879a Mon Sep 17 00:00:00 2001 From: Towsty Date: Fri, 28 Aug 2026 17:19:42 -0500 Subject: [PATCH] Fix Use as input and uploaded stills so I2V is not stuck waiting for a still. Co-authored-by: Cursor --- pages/index.vue | 69 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/pages/index.vue b/pages/index.vue index 070039e..57cdb93 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -811,6 +811,7 @@

The current job keeps running in Output. This submit waits in the job queue.

The current job keeps running in Output. This submit waits in the job queue.

+

{{ videoBlockReason }}

{{ editBlockReason }}

@@ -1015,9 +1016,9 @@

Playing the last saved part. The shot still encoding cannot be scrubbed until it saves.

-
+
@@ -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(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) {