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:
Towsty
2026-08-28 07:48:09 -05:00
co-authored by Cursor
parent 010ef6add2
commit efef409537
21 changed files with 1196 additions and 115 deletions
+53 -10
View File
@@ -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), {