Pause the queue after the current clip for real, and carry global locks plus labeled permanence stills through generate and shot prompts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+53
-10
@@ -9,7 +9,7 @@ import { isLtxWorkflow, isTextToVideo, parseVideoWorkflow, workflowForExtension,
|
||||
import { clipVideoPath, clipTitle, deleteRetryDraft, extendTempDir, getClip, nextClipPartName, removeExtendTemp, stillPath } from '~/server/utils/library'
|
||||
import { extractLastFrame, probeHasAudio } from '~/server/utils/ffmpeg'
|
||||
import { ensureComfyReady } from '~/server/utils/comfyLifecycle'
|
||||
import { onLiveVideoSettled } from '~/server/utils/studioQueue'
|
||||
import { onLiveVideoSettled, onStudioQueueBurstStarted } from '~/server/utils/studioQueue'
|
||||
import {
|
||||
finishQueueBurst,
|
||||
getShotQueue,
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
setQueueJob,
|
||||
updateShotQueue
|
||||
} from '~/server/utils/shotQueue'
|
||||
import { composeShotPrompt, type PermanenceRef } from '~/utils/globalLocks'
|
||||
|
||||
export type ChainImage = { filename: string; data: Buffer; type?: string }
|
||||
|
||||
@@ -42,6 +43,9 @@ type VideoChainParams = {
|
||||
duration: number
|
||||
useIdentityRefs: boolean
|
||||
referenceImages: Array<ChainImage | null>
|
||||
globalLocks?: string
|
||||
permanenceRefs?: PermanenceRef[]
|
||||
shotPermanenceRefs?: PermanenceRef[][]
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
@@ -107,7 +111,10 @@ function paramsFromJob(job: Job): VideoChainParams {
|
||||
workflow,
|
||||
duration,
|
||||
useIdentityRefs: library.useIdentityRefs === true,
|
||||
referenceImages
|
||||
referenceImages,
|
||||
globalLocks: library.globalLocks,
|
||||
permanenceRefs: library.permanenceRefs,
|
||||
shotPermanenceRefs: library.shotPermanenceRefs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,8 +165,17 @@ export async function queueMiniMax(
|
||||
emitChainJob(job, { type: 'status', message: queueing, progress: 6 })
|
||||
await waitForComfySocket(job, 4000)
|
||||
|
||||
const graph = buildWorkflow({
|
||||
const shotIndex = job.library?.chainIndex || 0
|
||||
const composedPrompt = composeShotPrompt({
|
||||
globalLocks: job.library?.globalLocks || params.globalLocks,
|
||||
prompt: params.prompt,
|
||||
shotIndex,
|
||||
familyRefs: job.library?.permanenceRefs || params.permanenceRefs,
|
||||
shotRefs: job.library?.shotPermanenceRefs?.[shotIndex] || params.shotPermanenceRefs?.[shotIndex]
|
||||
})
|
||||
|
||||
const graph = buildWorkflow({
|
||||
prompt: composedPrompt,
|
||||
imageName: uploaded.name,
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
@@ -258,6 +274,9 @@ export async function continueQueuedExtensions(
|
||||
const liveQueue = job.library.queueId
|
||||
? getShotQueue(job.library.ownerKey, job.library.queueId)
|
||||
: null
|
||||
if (liveQueue?.stopAfterCurrent) job.library.stopAfterCurrent = true
|
||||
if (liveQueue?.autoRun === false) job.library.queueAutoRun = false
|
||||
if (job.library.stopAfterCurrent) break
|
||||
const live = liveQueue ? liveSegmentPrompt(liveQueue, i) : extensions[i]
|
||||
const ext = {
|
||||
prompt: live.prompt || extensions[i].prompt,
|
||||
@@ -267,6 +286,17 @@ export async function continueQueuedExtensions(
|
||||
throw new Error(`Shot ${i + 2} needs a prompt`)
|
||||
}
|
||||
|
||||
emitChainJob(job, { type: 'status', message: 'Waiting 3s buffer...', progress: 1 })
|
||||
await sleep(3000)
|
||||
assertJobActive(job)
|
||||
const pausedQueue = job.library.queueId
|
||||
? getShotQueue(job.library.ownerKey, job.library.queueId)
|
||||
: null
|
||||
if (job.library.stopAfterCurrent || pausedQueue?.stopAfterCurrent) {
|
||||
job.library.stopAfterCurrent = true
|
||||
break
|
||||
}
|
||||
|
||||
const remainingAfter = extensions.length - (i + 1)
|
||||
const lastOfChain = remainingAfter === 0
|
||||
const lastOfBurst = !autoRun && shotsLeft === 1
|
||||
@@ -290,10 +320,6 @@ export async function continueQueuedExtensions(
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
emitChainJob(job, { type: 'status', message: 'Waiting 3s buffer...', progress: 1 })
|
||||
await sleep(3000)
|
||||
assertJobActive(job)
|
||||
|
||||
copyFileSync(currentPath, part1Path)
|
||||
try {
|
||||
await extractLastFrame(part1Path, framePath)
|
||||
@@ -306,6 +332,11 @@ export async function continueQueuedExtensions(
|
||||
const frame = readFileSync(framePath)
|
||||
job.library.extendPart1Path = part1Path
|
||||
job.library.prompt = ext.prompt
|
||||
if (live.permanenceRefs?.length) {
|
||||
const refs = [...(job.library.shotPermanenceRefs || [])]
|
||||
refs[i + 1] = live.permanenceRefs
|
||||
job.library.shotPermanenceRefs = refs
|
||||
}
|
||||
const sound = await probeHasAudio(currentPath)
|
||||
const seed = Math.floor(Math.random() * 2_147_483_647)
|
||||
|
||||
@@ -367,11 +398,13 @@ export async function continueQueuedExtensions(
|
||||
export async function continueQueuedExtensionsIfNeeded(job: Job) {
|
||||
if (!job.library) return
|
||||
if (job.status === 'error' || job.status === 'cancelled' || job.status === 'complete') return
|
||||
if (job.library.stopAfterCurrent) return
|
||||
const remaining = remainingAfterCurrentShot(job.library)
|
||||
if (!remaining.length) return
|
||||
const queue = job.library.queueId
|
||||
? getShotQueue(job.library.ownerKey, job.library.queueId)
|
||||
: null
|
||||
if (queue?.stopAfterCurrent) return
|
||||
const autoRun = queue ? queue.autoRun === true : job.library.queueAutoRun === true
|
||||
const budget = job.library.queueBudget || 0
|
||||
if (queue && !autoRun && budget <= 0) return
|
||||
@@ -407,12 +440,17 @@ export async function runGeneration(job: Job, params: VideoChainParams) {
|
||||
persist: extensions.length === 0 || !autoRun
|
||||
})
|
||||
|
||||
if (job.library?.queueId && !autoRun) {
|
||||
const stopNow = job.library?.stopAfterCurrent === true
|
||||
|| (job.library?.queueId
|
||||
? getShotQueue(job.library.ownerKey, job.library.queueId)?.stopAfterCurrent === true
|
||||
: false)
|
||||
const keepGoing = Boolean(autoRun && !stopNow && extensions.length && job.library)
|
||||
if (job.library?.queueId && !keepGoing) {
|
||||
await finishQueueBurst(job.library.ownerKey, job.library.queueId, true).catch(() => null)
|
||||
setQueueJob(job.library.queueId, null)
|
||||
}
|
||||
|
||||
if (!extensions.length || !job.library || !autoRun) return
|
||||
if (!keepGoing) return
|
||||
await continueQueuedExtensions(job, params, { maxShots: extensions.length, autoRun: true })
|
||||
} finally {
|
||||
await onLiveVideoSettled(job)
|
||||
@@ -467,13 +505,18 @@ export async function startQueueBurst(owner: string, queueId: string, count: num
|
||||
useIdentityRefs: queue.useIdentityRefs,
|
||||
queueId: queue.id,
|
||||
queueAutoRun: count === 'all',
|
||||
queueBudget: n
|
||||
queueBudget: n,
|
||||
globalLocks: queue.globalLocks,
|
||||
permanenceRefs: queue.permanenceRefs,
|
||||
shotPermanenceRefs: queue.segments.map(segment => segment.permanenceRefs || [])
|
||||
}
|
||||
setQueueJob(queue.id, job.id)
|
||||
await updateShotQueue(owner, queue.id, (next) => {
|
||||
next.currentJobId = job.id
|
||||
next.status = 'running'
|
||||
next.stopAfterCurrent = false
|
||||
}).catch(() => null)
|
||||
await onStudioQueueBurstStarted(owner, queue.id, job.id).catch(() => null)
|
||||
|
||||
emitChainJob(job, { type: 'status', message: 'Checking ComfyUI...', progress: 1 })
|
||||
void continueQueuedExtensions(job, paramsFromJob(job), {
|
||||
|
||||
Reference in New Issue
Block a user