Post stillId with video generate and do not queue I2V behind a phantom busy job.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+68
-13
@@ -1825,6 +1825,7 @@ const outputPanel = ref<HTMLElement | null>(null)
|
||||
const editLightbox = ref<HTMLElement | null>(null)
|
||||
const editLightboxUrl = ref('')
|
||||
const file = ref<File | null>(null)
|
||||
const inputStillId = ref('')
|
||||
const preview = ref('')
|
||||
const dragging = ref(false)
|
||||
const prompt = ref('')
|
||||
@@ -2106,14 +2107,24 @@ const liveElapsedLabel = computed(() => (
|
||||
const playingLastSavedWhileBusy = computed(() => (
|
||||
videoBusy.value && Boolean(videoUrl.value) && !watchingLibrary.value && !lockedSave.value
|
||||
))
|
||||
const hasInputStill = computed(() => {
|
||||
if (file.value || inputStillId.value) return true
|
||||
const selected = selectedLibraryEntries().filter(entry => entry.kind === 'still')
|
||||
return selected.length === 1 && Boolean(selected[0].stillGroup?.head.id)
|
||||
})
|
||||
const outputQueuedJob = computed(() => {
|
||||
const id = outputStudioJobId.value
|
||||
if (!id) return null
|
||||
return studioJobs.value.find(job => job.id === id && (job.status === 'waiting' || job.status === 'held')) || null
|
||||
})
|
||||
function idleInputStatus(kind: 'video' | 'edit') {
|
||||
if (kind === 'edit') return file.value ? 'Still ready. Edit when you are ready.' : 'Waiting for a still.'
|
||||
if (kind === 'edit') return hasInputStill.value ? 'Still ready. Edit when you are ready.' : 'Waiting for a still.'
|
||||
if (textToVideo.value) {
|
||||
return file.value
|
||||
return hasInputStill.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.'
|
||||
return hasInputStill.value ? 'Still ready. Generate when you are ready.' : 'Waiting for a still.'
|
||||
}
|
||||
|
||||
const liveStatusMessage = computed(() => {
|
||||
@@ -2122,6 +2133,7 @@ const liveStatusMessage = computed(() => {
|
||||
return idleInputStatus('edit')
|
||||
}
|
||||
if (videoBusy.value || videoUrl.value || lockedSave.value) return statusMessage.value
|
||||
if (outputQueuedJob.value) return statusMessage.value || 'Queued. Waiting for Beast to be free.'
|
||||
return idleInputStatus('video')
|
||||
})
|
||||
const liveStatusBusy = computed(() => (
|
||||
@@ -2240,7 +2252,7 @@ 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 (!textToVideo.value && !hasInputStill.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) {
|
||||
@@ -3327,6 +3339,7 @@ async function copyPrompt(text: string) {
|
||||
function resetImage() {
|
||||
if (preview.value) URL.revokeObjectURL(preview.value)
|
||||
file.value = null
|
||||
inputStillId.value = ''
|
||||
preview.value = ''
|
||||
imageWidth.value = 0
|
||||
imageHeight.value = 0
|
||||
@@ -3349,9 +3362,15 @@ function clearVideoForm() {
|
||||
activeDraftId = ''
|
||||
}
|
||||
|
||||
function readFile(next: File, opts?: { keepThumbnailPref?: boolean }) {
|
||||
function stillIdFromLibraryUrl(url: string) {
|
||||
const match = String(url || '').match(/\/api\/library\/stills\/([^/?#]+)/)
|
||||
return match ? decodeURIComponent(match[1]) : ''
|
||||
}
|
||||
|
||||
function readFile(next: File, opts?: { keepThumbnailPref?: boolean; stillId?: string }) {
|
||||
if (preview.value) URL.revokeObjectURL(preview.value)
|
||||
file.value = next
|
||||
inputStillId.value = opts?.stillId || ''
|
||||
preview.value = URL.createObjectURL(next)
|
||||
if (!opts?.keepThumbnailPref) applyThumbnailDefault()
|
||||
const img = new Image()
|
||||
@@ -3514,7 +3533,7 @@ function onPickedImage(next: File, hidden = false, stillId?: string) {
|
||||
return
|
||||
}
|
||||
if (hidden && showPrivacyToggles.value) hideInputPreview.value = true
|
||||
readFile(next)
|
||||
readFile(next, stillId ? { stillId } : undefined)
|
||||
}
|
||||
|
||||
async function persistDroppedStill(next: File) {
|
||||
@@ -3524,7 +3543,9 @@ async function persistDroppedStill(next: File) {
|
||||
body.append('folderId', folderId.value)
|
||||
body.append('hideInput', String(hideInputPreview.value))
|
||||
try {
|
||||
applyLibrary(await $fetch('/api/library/stills', { method: 'POST', body }))
|
||||
const result = await $fetch<{ still?: { id: string } }>('/api/library/stills', { method: 'POST', body })
|
||||
applyLibrary(result)
|
||||
if (result.still?.id) inputStillId.value = result.still.id
|
||||
} catch (error: any) {
|
||||
toast(error?.data?.statusMessage || 'Could not save still to the library')
|
||||
}
|
||||
@@ -3765,7 +3786,7 @@ async function restoreStudioJob(id: string) {
|
||||
const stillId = payload.stillId
|
||||
if (stillId) {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${stillId}`, { responseType: 'blob' })
|
||||
readFile(new File([blob], payload.stillFilename || 'still.png', { type: blob.type || 'image/png' }), { keepThumbnailPref: true })
|
||||
readFile(new File([blob], payload.stillFilename || 'still.png', { type: blob.type || 'image/png' }), { keepThumbnailPref: true, stillId })
|
||||
} else {
|
||||
resetImage()
|
||||
}
|
||||
@@ -3969,7 +3990,7 @@ async function loadRerunImage(item: LibraryClip, parts: LibraryClip[]) {
|
||||
if (stillId) {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${stillId}`, { responseType: 'blob' })
|
||||
if (still?.hideInput && showPrivacyToggles.value) hideInputPreview.value = true
|
||||
readFile(new File([blob], still?.filename || 'still.png', { type: blob.type || 'image/png' }), { keepThumbnailPref: true })
|
||||
readFile(new File([blob], still?.filename || 'still.png', { type: blob.type || 'image/png' }), { keepThumbnailPref: true, stillId })
|
||||
return true
|
||||
}
|
||||
const blob = await $fetch<Blob>(`/api/library/clips/${item.id}/thumb`, { responseType: 'blob' })
|
||||
@@ -4070,7 +4091,7 @@ async function loadDraft(draft: RetryDraft) {
|
||||
try {
|
||||
const blob = await $fetch<Blob>(`/api/library/drafts/${draft.id}/still`, { responseType: 'blob' })
|
||||
const filename = draft.stillFilename || 'held-still.png'
|
||||
readFile(new File([blob], filename, { type: blob.type || 'image/png' }), { keepThumbnailPref: true })
|
||||
readFile(new File([blob], filename, { type: blob.type || 'image/png' }), { keepThumbnailPref: true, stillId: draft.stillId })
|
||||
statusMessage.value = 'Held job restored. Generate when ComfyUI is free.'
|
||||
} catch {
|
||||
if (textToVideo.value) {
|
||||
@@ -4266,7 +4287,7 @@ async function useStillAsInput(still: LibraryStill) {
|
||||
try {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${still.id}`, { responseType: 'blob' })
|
||||
if (still.hideInput && showPrivacyToggles.value) hideInputPreview.value = true
|
||||
readFile(new File([blob], still.filename, { type: blob.type || 'image/png' }))
|
||||
readFile(new File([blob], still.filename, { type: blob.type || 'image/png' }), { stillId: still.id })
|
||||
toast('Loaded as input. It stays in Output, not the upload library.')
|
||||
} catch (error: any) {
|
||||
toast(error?.data?.statusMessage || 'Could not load that still')
|
||||
@@ -4801,7 +4822,9 @@ async function useEditAsInput() {
|
||||
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' }))
|
||||
readFile(new File([blob], `${editDownloadName.value || 'edited'}.png`, { type: blob.type || 'image/png' }), {
|
||||
stillId: stillIdFromLibraryUrl(editResultUrl.value)
|
||||
})
|
||||
toast('Edited still is now the input')
|
||||
} catch {
|
||||
toast('Could not load that still as input')
|
||||
@@ -4833,6 +4856,25 @@ function applyVideoWorkflow(raw?: string) {
|
||||
minimaxGraph.value = minimaxGraphOf(parsed)
|
||||
}
|
||||
|
||||
async function ensureVideoInputStill() {
|
||||
if (inputStillId.value && file.value) return
|
||||
let stillId = inputStillId.value
|
||||
if (!stillId) {
|
||||
const selected = selectedLibraryEntries().filter(entry => entry.kind === 'still')
|
||||
if (selected.length === 1) stillId = selected[0].stillGroup?.head.id || ''
|
||||
}
|
||||
if (!stillId) return
|
||||
inputStillId.value = stillId
|
||||
if (file.value) return
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${stillId}`, { responseType: 'blob' })
|
||||
if (!blob?.size) throw new Error('Could not load that still')
|
||||
const still = stills.value.find(item => item.id === stillId)
|
||||
readFile(new File([blob], still?.filename || 'still.png', { type: blob.type || 'image/png' }), {
|
||||
keepThumbnailPref: true,
|
||||
stillId
|
||||
})
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
if (!ltxEnabled.value && isLtxWorkflow(videoWorkflow.value)) {
|
||||
videoEngine.value = 'minimax'
|
||||
@@ -4848,6 +4890,16 @@ async function generate() {
|
||||
toast('Choose a library folder before generating.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ensureVideoInputStill()
|
||||
} catch {
|
||||
toast('Could not load the selected still for image-to-video.')
|
||||
return
|
||||
}
|
||||
if (!textToVideo.value && !file.value && !inputStillId.value) {
|
||||
toast('Load a still first. Image-to-video needs an input still.')
|
||||
return
|
||||
}
|
||||
const shots = shotScriptMode.value ? parsedShots.value : null
|
||||
const initialPrompt = (shots?.[0]?.prompt || prompt.value).trim()
|
||||
const queued = shots
|
||||
@@ -4858,6 +4910,7 @@ async function generate() {
|
||||
try {
|
||||
const body = new FormData()
|
||||
if (file.value) body.append('image', file.value)
|
||||
if (inputStillId.value) body.append('stillId', inputStillId.value)
|
||||
body.append('prompt', initialPrompt)
|
||||
body.append('aspect', aspect.value)
|
||||
body.append('width', String(outputSize.value.width))
|
||||
@@ -4922,7 +4975,9 @@ async function generate() {
|
||||
body
|
||||
})
|
||||
if (started.queued) {
|
||||
clearVideoForm()
|
||||
outputStudioJobId.value = started.studioJobId || ''
|
||||
outputFocus.value = 'video'
|
||||
statusMessage.value = 'Queued. Waiting for Beast to be free.'
|
||||
const n = 1 + queued.length
|
||||
toast(n > 1
|
||||
? `Queued ${n} shots behind the current job.`
|
||||
|
||||
Reference in New Issue
Block a user