From 3ed1d6aa9d532d4cd1f31e37dad8fcf840d84bdf Mon Sep 17 00:00:00 2001 From: Towsty Date: Fri, 28 Aug 2026 17:58:28 -0500 Subject: [PATCH] Do not mark a waiting I2V complete when the image job finishes. Co-authored-by: Cursor --- pages/index.vue | 29 ++++++++++++++++++++- server/api/generate.post.ts | 13 ++++++++++ server/api/studio-queue.get.ts | 7 +++++- server/utils/studioQueue.ts | 46 ++++++++++++++++------------------ 4 files changed, 69 insertions(+), 26 deletions(-) diff --git a/pages/index.vue b/pages/index.vue index 80f5635..9467f15 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -3127,6 +3127,13 @@ function applyGenerationPreset(preset: GenerationPreset) { toast(skipped ? `Loaded “${preset.name}”. Skipped ${skipped} xAIGen-only LoRA${skipped === 1 ? '' : 's'}.` : `Loaded “${preset.name}”.`) + console.info('[aigen preset apply]', { + kind: preset.kind, + name: preset.name, + hasFile: Boolean(file.value), + inputStillId: inputStillId.value, + hasPreview: Boolean(preview.value) + }) } function loadGenerationPreset(id: string) { @@ -4895,6 +4902,19 @@ async function generate() { toast('LTX Pink Cherry is not available on this instance') return } + console.info('[aigen generate click]', { + hasFile: Boolean(file.value), + fileName: file.value?.name || '', + fileSize: file.value?.size || 0, + fileType: file.value?.type || '', + inputStillId: inputStillId.value, + hasPreview: Boolean(preview.value), + hasInputStill: hasInputStill.value, + mode: studioMode.value, + workflow: videoWorkflow.value, + block: videoBlockReason.value || '', + promptChars: prompt.value.trim().length + }) if (videoBlockReason.value) { toast(videoBlockReason.value) return @@ -4970,6 +4990,13 @@ async function generate() { } const downloadName = clipFileName(clipName.value.trim() || initialPrompt.split(/[.!?\n]/)[0] || 'aigen') const chainPlan = plannedChain.value.map(part => ({ ...part })) + console.info('[aigen generate payload]', { + keys: [...body.keys()], + hasImage: body.has('image'), + stillId: inputStillId.value, + promptChars: initialPrompt.length, + workflow: videoWorkflow.value + }) const started = await $fetch<{ jobId: string studioJobId?: string @@ -5142,6 +5169,7 @@ function applyEditEvent(payload: Record, hidden: boolean, folderLoc clearActiveJob('edit') scheduleLibraryRefresh() void pollComfyHealth() + void refreshStudioQueue() return } @@ -5174,7 +5202,6 @@ function applyVideoEvent(payload: Record, hidden: boolean, folderLo const complete = payload.type === 'complete' || payload.status === 'complete' const deferred = payload.type === 'deferred' || payload.status === 'deferred' if (videoSettledUi) return - if (payload.stillId) return if (payload.type === 'checkpoint' && payload.clipId) { completedChainStep.value = Math.max(completedChainStep.value, Number(payload.chainStep || chainStep.value) || 0) diff --git a/server/api/generate.post.ts b/server/api/generate.post.ts index 3a86979..3b26d28 100644 --- a/server/api/generate.post.ts +++ b/server/api/generate.post.ts @@ -201,6 +201,19 @@ export default defineEventHandler(async (event) => { hideInput }) : null) + if (!textToVideo && !still?.id) { + throw createError({ statusCode: 400, statusMessage: 'An initial image is required' }) + } + console.info('[aigen generate]', { + hasImage: Boolean(image?.data?.length), + imageBytes: image?.data?.length || 0, + imageName: image?.filename || '', + stillId: still?.id || '', + postedStillId, + workflow, + promptChars: promptRaw.length, + textToVideo + }) const referenceStillIds: Array = [null, null, null, null] if (useIdentityRefs) { for (const [index, ref] of referenceImages.entries()) { diff --git a/server/api/studio-queue.get.ts b/server/api/studio-queue.get.ts index 6cb52dc..1b4b0c3 100644 --- a/server/api/studio-queue.get.ts +++ b/server/api/studio-queue.get.ts @@ -5,7 +5,12 @@ export default defineEventHandler((event) => { const { owner } = assertLibraryOwner(event) const full = String(getQuery(event).full || '') === '1' const active = listStudioJobs(owner) - .filter(job => job.status === 'waiting' || job.status === 'running' || job.status === 'held') + .filter(job => ( + job.status === 'waiting' + || job.status === 'running' + || job.status === 'held' + || (job.status === 'error' && Date.now() - job.updatedAt < 60 * 60 * 1000) + )) const claimed = new Set(active.map(job => job.shotQueueId).filter(Boolean) as string[]) const queues = listShotQueues(owner) const byId = new Map(queues.map(queue => [queue.id, queue])) diff --git a/server/utils/studioQueue.ts b/server/utils/studioQueue.ts index 0eb0554..4d3b7cf 100644 --- a/server/utils/studioQueue.ts +++ b/server/utils/studioQueue.ts @@ -4,7 +4,7 @@ import { getJob, listJobs, emitJob, type Job } from '~/server/utils/jobs' import { listPendingJobs, patchPendingJob, readPendingJob, deletePendingJob } from '~/server/utils/pending' import { getShotQueue } from '~/server/utils/shotQueue' import { fetchLiveQueue } from '~/server/utils/comfy' -import { isLtxWorkflow, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow, type VideoWorkflowId } from '~/utils/videoModels' +import { isLtxWorkflow, isTextToVideo, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow, type VideoWorkflowId } from '~/utils/videoModels' import { persistLoraFields } from '~/server/utils/loras' import { resolveLoraStack } from '~/utils/loras' @@ -484,7 +484,7 @@ export async function markStudioHeld(owner: string, id: string) { export async function markStudioSettled(owner: string, liveJobId: string, status: 'complete' | 'error' | 'cancelled', error?: string) { return mutate(owner, (jobs) => { - const job = jobs.find(item => item.liveJobId === liveJobId) || jobs.find(item => item.status === 'running') + const job = jobs.find(item => item.liveJobId === liveJobId) if (!job) return null if (job.holdForCutIn && status === 'complete') { job.status = 'held' @@ -750,6 +750,16 @@ export async function startStudioJob(item: StudioJob) { payload.globalLocks, payload.shotPermanenceRefs ) + const stillFile = payload.stillId && existsSync(stillPath(item.ownerKey, payload.stillId)) + ? { + filename: payload.stillFilename || 'still.png', + data: readFileSync(stillPath(item.ownerKey, payload.stillId)), + type: 'image/png' + } + : null + if (!isTextToVideo(workflow) && !stillFile) { + throw new Error('The input still is missing from the library') + } const job = createJob() live = job job.kind = 'video' @@ -840,13 +850,6 @@ export async function startStudioJob(item: StudioJob) { await markStudioLive(item.ownerKey, item.id, job.id, job.library.queueId) - const stillFile = payload.stillId && existsSync(stillPath(item.ownerKey, payload.stillId)) - ? { - filename: payload.stillFilename || 'still.png', - data: readFileSync(stillPath(item.ownerKey, payload.stillId)), - type: 'image/png' - } - : null const referenceImages: Array<{ filename: string; data: Buffer; type?: string } | null> = [null, null, null, null] for (const [index, stillId] of (payload.referenceStillIds || []).entries()) { if (!stillId || index >= 4) continue @@ -914,17 +917,12 @@ export async function onLiveVideoSettled(job: Job) { const failed = job.status === 'error' || job.status === 'cancelled' const snapshot = readStore(owner) const rowNow = snapshot.jobs.find(item => item.liveJobId === job.id) - || snapshot.jobs.find(item => ( - item.status === 'running' - && Boolean(item.shotQueueId) - && item.shotQueueId === job.library?.queueId - )) - || snapshot.jobs.find(item => ( - item.status === 'running' - && !item.shotQueueId - && !job.library?.queueId - && (!item.liveJobId || item.liveJobId === job.id) - )) + || (job.library?.queueId + ? snapshot.jobs.find(item => ( + item.status === 'running' + && item.shotQueueId === job.library?.queueId + )) + : undefined) const userPause = !failed && rowNow?.holdForCutIn !== true && ( rowNow?.pauseAfterCurrent === true || rowNow?.pausedByUser === true @@ -937,7 +935,7 @@ export async function onLiveVideoSettled(job: Job) { } if (userPause && remaining > 0) { await mutateStore(owner, (store) => { - const row = store.jobs.find(item => item.liveJobId === job.id) || store.jobs.find(item => item.status === 'running') + const row = store.jobs.find(item => item.liveJobId === job.id) if (!row) { syncPausedFlag(store) return @@ -953,7 +951,7 @@ export async function onLiveVideoSettled(job: Job) { }) } else if (cutInHold) { await mutate(owner, (jobs) => { - const row = jobs.find(item => item.liveJobId === job.id) || jobs.find(item => item.status === 'running') + const row = jobs.find(item => item.liveJobId === job.id) if (!row) return row.status = 'held' row.liveJobId = undefined @@ -963,7 +961,7 @@ export async function onLiveVideoSettled(job: Job) { }) } else if (userPause) { await mutateStore(owner, (store) => { - const row = store.jobs.find(item => item.liveJobId === job.id) || store.jobs.find(item => item.status === 'running') + const row = store.jobs.find(item => item.liveJobId === job.id) if (!row) { syncPausedFlag(store) return @@ -990,7 +988,7 @@ export async function onLiveVideoSettled(job: Job) { : 'error' await markStudioSettled(owner, job.id, status, job.error) } - kickStudioQueue() + await kickStudioQueue() } function applyLiveJobPause(live: Job | undefined, pause: boolean, restoreAutoRun?: boolean) {