Finish the current video shot list before starting the next queued video.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-30 07:24:47 -05:00
co-authored by Cursor
parent 61d711f989
commit 18f350c32a
5 changed files with 95 additions and 32 deletions
+66 -20
View File
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFile
import { join } from 'node:path'
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 { getShotQueue, pendingSegmentCount } from '~/server/utils/shotQueue'
import { fetchLiveQueue } from '~/server/utils/comfy'
import { isLtxWorkflow, isTextToVideo, isXaigenStudio, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow, type VideoWorkflowId } from '~/utils/videoModels'
import { persistLoraFields } from '~/server/utils/loras'
@@ -262,17 +262,24 @@ function sweepStaleLiveJobs() {
}
}
function liveJobOwnsGpu(job: Job) {
if (job.library?.stopAfterCurrent) return false
if (job.library?.chainContinuing) return true
if (job.status === 'uploading' || job.status === 'running') return true
if (job.status === 'queued') return Boolean(job.promptId) || jobAgeMs(job) < SUBMIT_WINDOW_MS
return false
}
export async function videoJobsBusy() {
sweepStaleLiveJobs()
if (listJobs().some(jobIsLocallySubmitting)) return true
// An empty Comfy queue is not idle: the 3s buffer + last-frame extract between
// shots leaves Comfy empty while the current video still owns the GPU.
if (listJobs().some(liveJobOwnsGpu)) return true
if (listOwnersWithStudioQueues().some(owner => listStudioJobs(owner).some(job => job.status === 'running'))) return true
const queue = await fetchLiveQueue()
if (queue) return queue.running > 0 || queue.pending > 0
return listJobs().some((job) => {
if (job.library?.stopAfterCurrent) return false
if (job.status === 'uploading' || job.status === 'running') return true
if (job.status === 'queued') return jobAgeMs(job) < SUBMIT_WINDOW_MS
return false
}) || listPendingJobs().some((pending) => {
return listPendingJobs().some((pending) => {
if (!pending.promptId || pending.stopAfterCurrent) return false
const live = getJob(pending.jobId)
if (live) return jobIsLocallySubmitting(live) || live.status === 'running' || live.status === 'uploading'
@@ -525,10 +532,10 @@ function pickHeld(jobs: StudioJob[]) {
.filter(job => (
job.status === 'held'
&& job.shotQueueId
&& job.holdForCutIn
&& !job.pausedByUser
&& (job.holdForCutIn === true || job.resumeAutoRun === true)
))
.sort((a, b) => b.updatedAt - a.updatedAt)[0] || null
.sort((a, b) => a.createdAt - b.createdAt)[0] || null
}
function pickWaiting(jobs: StudioJob[]) {
@@ -628,16 +635,17 @@ async function dispatchStudioQueue() {
await startStudioJob(cutIn)
return
}
const held = pickHeld(store.jobs)
if (held?.shotQueueId) {
const resumed = await resumeHeldStudioJob(held)
if (!resumed) scheduleKickRetry()
return
}
const waiting = pickWaiting(store.jobs)
if (waiting) {
await startStudioJob(waiting)
return
}
const held = pickHeld(store.jobs)
if (held?.shotQueueId) {
const resumed = await resumeHeldStudioJob(held)
if (resumed) return
}
}
}
@@ -649,6 +657,10 @@ async function resumeHeldStudioJob(item: StudioJob) {
})
return false
}
await patchStudioJob(item.ownerKey, item.id, (job) => {
job.status = 'running'
job.lastError = undefined
})
const { startQueueBurst } = await import('~/server/utils/videoChain')
const count = item.resumeAutoRun ? 'all' as const : 1
try {
@@ -662,10 +674,12 @@ async function resumeHeldStudioJob(item: StudioJob) {
return true
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
if (/already processing/i.test(message)) return true
const transient = /offline|never answered|host agent|unreachable|asleep|Starting Comfy|ComfyUI \(1\)|instance picker/i.test(message)
await patchStudioJob(item.ownerKey, item.id, (job) => {
job.status = 'error'
job.status = transient ? 'held' : 'error'
job.lastError = message
job.holdForCutIn = false
job.holdForCutIn = transient
})
return false
}
@@ -1031,16 +1045,29 @@ export async function startStudioJob(item: StudioJob) {
}
}
function remainingStudioShots(job: Job) {
const library = job.library
if (!library) return 0
if (library.queueId) {
const queue = getShotQueue(library.ownerKey, library.queueId)
if (queue) return pendingSegmentCount(queue)
}
return Math.max(0, (library.chainTotal || 1) - (library.chainStep || 1))
}
function isTransientComfyError(error?: string) {
return /still busy|COMFY_BUSY|offline|never answered|host agent|unreachable|asleep|Starting Comfy|ComfyUI \(1\)|instance picker/i.test(String(error || ''))
}
export async function onLiveVideoSettled(job: Job) {
const owner = job.library?.ownerKey
if (!owner) {
kickStudioQueue()
return
}
const remaining = job.library
? Math.max(0, (job.library.chainTotal || 1) - (job.library.chainStep || 1))
: 0
const failed = job.status === 'error' || job.status === 'cancelled'
const remaining = remainingStudioShots(job)
const wakeFail = job.status === 'error' && remaining > 0 && isTransientComfyError(job.error)
const failed = (job.status === 'error' || job.status === 'cancelled') && !wakeFail
const snapshot = readStore(owner)
const rowNow = snapshot.jobs.find(item => item.liveJobId === job.id)
|| (job.library?.queueId
@@ -1085,6 +1112,25 @@ export async function onLiveVideoSettled(job: Job) {
row.resumeAutoRun = row.payload.queueAutoRun === true || job.library?.queueAutoRun === true || row.resumeAutoRun === true
row.updatedAt = Date.now()
})
} else if (!failed && remaining > 0) {
await mutateStore(owner, (store) => {
const row = store.jobs.find(item => item.liveJobId === job.id)
|| (job.library?.queueId ? store.jobs.find(item => item.shotQueueId === job.library?.queueId) : undefined)
if (!row) {
syncPausedFlag(store)
return
}
const auto = row.payload.queueAutoRun === true || job.library?.queueAutoRun === true || row.resumeAutoRun === true
row.status = 'held'
row.liveJobId = undefined
row.holdForCutIn = auto
row.pauseAfterCurrent = false
row.pausedByUser = !auto
row.resumeAutoRun = auto
row.lastError = wakeFail ? job.error : undefined
row.updatedAt = Date.now()
syncPausedFlag(store)
})
} else if (userPause) {
await mutateStore(owner, (store) => {
const row = store.jobs.find(item => item.liveJobId === job.id)