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