Keep remaining multi-shot clips on a Queue page so prompts can be edited between shots, with process-next or overnight run-all.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { frameLength, runGeneration } from '~/server/utils/videoChain'
|
||||
import { createShotQueue, finishQueueBurst, setQueueJob } from '~/server/utils/shotQueue'
|
||||
|
||||
function isComfyBusyTimeout(error: unknown) {
|
||||
const err = error as { statusCode?: number; status?: number; data?: { code?: string }; message?: string; statusMessage?: string }
|
||||
@@ -154,6 +155,8 @@ export default defineEventHandler(async (event) => {
|
||||
job.kind = 'video'
|
||||
job.maxStep = steps
|
||||
job.hideThumbnail = hideThumbnail
|
||||
const familyId = crypto.randomUUID()
|
||||
const queueAutoRun = fields.queueAutoRun === 'true'
|
||||
job.library = {
|
||||
ownerKey,
|
||||
folderId,
|
||||
@@ -184,9 +187,42 @@ export default defineEventHandler(async (event) => {
|
||||
chainStep: 1,
|
||||
chainTotal,
|
||||
chainLabel: extensions.length ? 'Initial' : undefined,
|
||||
familyId: crypto.randomUUID(),
|
||||
familyId,
|
||||
workflow,
|
||||
useIdentityRefs
|
||||
useIdentityRefs,
|
||||
queueAutoRun: extensions.length ? queueAutoRun : false,
|
||||
queueBudget: extensions.length && queueAutoRun ? extensions.length : 0
|
||||
}
|
||||
if (extensions.length) {
|
||||
const queue = await createShotQueue({
|
||||
ownerKey,
|
||||
name: clipName,
|
||||
familyId,
|
||||
folderId,
|
||||
autoRun: queueAutoRun,
|
||||
stillId: still?.id,
|
||||
stillFilename: still?.filename,
|
||||
hideThumbnail,
|
||||
hideInput,
|
||||
aspect: fields.aspect || 'auto',
|
||||
width,
|
||||
height,
|
||||
steps,
|
||||
turbo,
|
||||
cfg,
|
||||
fps,
|
||||
samplerName,
|
||||
scheduler,
|
||||
workflow,
|
||||
sound,
|
||||
useIdentityRefs,
|
||||
referenceStillIds,
|
||||
initial: { prompt, duration: durationSeconds },
|
||||
extensions,
|
||||
jobId: job.id
|
||||
})
|
||||
job.library.queueId = queue.id
|
||||
setQueueJob(queue.id, job.id)
|
||||
}
|
||||
emitChainJob(job, { type: 'status', message: 'Checking ComfyUI...', progress: 1 })
|
||||
|
||||
@@ -242,6 +278,10 @@ export default defineEventHandler(async (event) => {
|
||||
job.status = 'deferred'
|
||||
const message = 'ComfyUI was still busy after 3 minutes. Saved this job — click it under Held jobs to restore the form.'
|
||||
emitJob(job, { type: 'deferred', draftId: draft.id, message, progress: job.progress })
|
||||
if (job.library.queueId) {
|
||||
await finishQueueBurst(job.library.ownerKey, job.library.queueId, true, 'Held because ComfyUI was busy').catch(() => null)
|
||||
setQueueJob(job.library.queueId, null)
|
||||
}
|
||||
return
|
||||
} catch {
|
||||
// fall through to a normal error if the held job could not be saved
|
||||
@@ -252,6 +292,10 @@ export default defineEventHandler(async (event) => {
|
||||
job.status = 'error'
|
||||
job.error = message
|
||||
emitJob(job, { type: 'error', error: message, message })
|
||||
if (job.library?.queueId) {
|
||||
await finishQueueBurst(job.library.ownerKey, job.library.queueId, false, message).catch(() => null)
|
||||
setQueueJob(job.library.queueId, null)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -270,6 +314,8 @@ export default defineEventHandler(async (event) => {
|
||||
folderId,
|
||||
hideThumbnail,
|
||||
folderLocked,
|
||||
chainTotal
|
||||
chainTotal,
|
||||
queueId: job.library?.queueId,
|
||||
queuedRemaining: extensions.length
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { pauseShotQueue, setQueueJob } from '~/server/utils/shotQueue'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<{ jobId?: string; kind?: string }>(event).catch(() => ({}))
|
||||
const jobId = String(body?.jobId || '')
|
||||
@@ -6,6 +8,12 @@ export default defineEventHandler(async (event) => {
|
||||
job.status = 'cancelled'
|
||||
emitJob(job, { type: 'status', message: 'Cancelling job...' })
|
||||
removeExtendTemp(job.library?.extendTmpDir)
|
||||
if (job.library?.queueId) {
|
||||
job.library.stopAfterCurrent = true
|
||||
job.library.queueAutoRun = false
|
||||
await pauseShotQueue(job.library.ownerKey, job.library.queueId).catch(() => null)
|
||||
setQueueJob(job.library.queueId, null)
|
||||
}
|
||||
}
|
||||
if (jobId) deletePendingJob(jobId)
|
||||
const ok = await interruptComfy()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { deleteShotQueue } from '~/server/utils/shotQueue'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const id = String(getRouterParam(event, 'id') || '')
|
||||
await deleteShotQueue(owner, id)
|
||||
return { ok: true }
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { getShotQueue, summarizeQueue } from '~/server/utils/shotQueue'
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const id = String(getRouterParam(event, 'id') || '')
|
||||
const queue = getShotQueue(owner, id)
|
||||
if (!queue) throw createError({ statusCode: 404, statusMessage: 'Queue not found' })
|
||||
return summarizeQueue(queue)
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { getShotQueue, patchShotQueueSegments, updateShotQueue, summarizeQueue } from '~/server/utils/shotQueue'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const id = String(getRouterParam(event, 'id') || '')
|
||||
const body = await readBody<{
|
||||
name?: string
|
||||
autoRun?: boolean
|
||||
segments?: Array<{ index: number; prompt?: string; duration?: number }>
|
||||
}>().catch(() => ({}))
|
||||
|
||||
if (Array.isArray(body?.segments) && body.segments.length) {
|
||||
await patchShotQueueSegments(owner, id, body.segments)
|
||||
}
|
||||
if (typeof body?.name === 'string' || typeof body?.autoRun === 'boolean') {
|
||||
await updateShotQueue(owner, id, (queue) => {
|
||||
if (typeof body.name === 'string') queue.name = body.name.trim().slice(0, 80) || queue.name
|
||||
if (typeof body.autoRun === 'boolean' && queue.status !== 'running') queue.autoRun = body.autoRun
|
||||
})
|
||||
}
|
||||
const queue = getShotQueue(owner, id)
|
||||
if (!queue) throw createError({ statusCode: 404, statusMessage: 'Queue not found' })
|
||||
return summarizeQueue(queue)
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import { pauseShotQueue, summarizeQueue, getShotQueue } from '~/server/utils/shotQueue'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const id = String(getRouterParam(event, 'id') || '')
|
||||
await pauseShotQueue(owner, id)
|
||||
const queue = getShotQueue(owner, id)
|
||||
if (!queue) throw createError({ statusCode: 404, statusMessage: 'Queue not found' })
|
||||
return summarizeQueue(queue)
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import { startQueueBurst } from '~/server/utils/videoChain'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const id = String(getRouterParam(event, 'id') || '')
|
||||
const body = await readBody<{ count?: number | 'all' }>().catch(() => ({}))
|
||||
const raw = body?.count
|
||||
const count = raw === 'all' || raw === 'remaining' ? 'all' as const : Math.max(1, Math.min(50, Number(raw) || 1))
|
||||
return startQueueBurst(owner, id, count)
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import { listShotQueues, summarizeQueue } from '~/server/utils/shotQueue'
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const queues = listShotQueues(owner).map(summarizeQueue)
|
||||
const pendingCount = queues.reduce((sum, queue) => (
|
||||
queue.status === 'complete' ? sum : sum + queue.pendingCount
|
||||
), 0)
|
||||
return { queues, pendingCount }
|
||||
})
|
||||
Reference in New Issue
Block a user