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:
@@ -1,5 +1,6 @@
|
||||
import { copyFileSync, existsSync, readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { composeShotPrompt } from '~/utils/globalLocks'
|
||||
|
||||
const SAMPLERS = new Set(['res_multistep', 'euler', 'dpmpp_2m'])
|
||||
const SCHEDULERS = new Set(['simple', 'ddim_uniform', 'sgm_uniform'])
|
||||
@@ -59,6 +60,12 @@ export default defineEventHandler(async (event) => {
|
||||
const destFolder = publicLibrary(event).folders.find(folder => folder.id === source.folderId)
|
||||
const folderLocked = Boolean(destFolder?.protected && !destFolder.unlocked)
|
||||
const durationSeconds = parseExtendDuration(body?.duration)
|
||||
const composedPrompt = composeShotPrompt({
|
||||
globalLocks: source.globalLocks,
|
||||
prompt,
|
||||
shotIndex: (source.chainIndex || 0) + 1,
|
||||
familyRefs: source.permanenceRefs
|
||||
})
|
||||
const turbo = source.turbo !== false
|
||||
const fps = parseFps(source.fps)
|
||||
const cfg = parseCfg(source.cfg, turbo)
|
||||
@@ -84,7 +91,7 @@ export default defineEventHandler(async (event) => {
|
||||
hideThumbnail: source.hideThumbnail,
|
||||
folderLocked,
|
||||
name: clipName,
|
||||
prompt,
|
||||
prompt: composedPrompt,
|
||||
aspect: source.aspect || '16:9',
|
||||
width,
|
||||
height,
|
||||
@@ -102,7 +109,9 @@ export default defineEventHandler(async (event) => {
|
||||
familyId: source.familyId || crypto.randomUUID(),
|
||||
parentClipId: source.id,
|
||||
chainIndex: (source.chainIndex || 0) + 1,
|
||||
workflow: parseVideoWorkflow(source.workflow)
|
||||
workflow: parseVideoWorkflow(source.workflow),
|
||||
globalLocks: source.globalLocks,
|
||||
permanenceRefs: source.permanenceRefs
|
||||
}
|
||||
|
||||
emitJob(job, { type: 'status', message: 'Extracting last frame & stitching extension...', progress: 2 })
|
||||
@@ -111,7 +120,7 @@ export default defineEventHandler(async (event) => {
|
||||
sourcePath,
|
||||
part1Path,
|
||||
framePath,
|
||||
prompt,
|
||||
prompt: composedPrompt,
|
||||
width,
|
||||
height,
|
||||
steps,
|
||||
@@ -236,7 +245,13 @@ async function runExtend(
|
||||
extendPart1Path: job.library.extendPart1Path,
|
||||
familyId: job.library.familyId,
|
||||
parentClipId: job.library.parentClipId,
|
||||
chainIndex: job.library.chainIndex
|
||||
chainIndex: job.library.chainIndex,
|
||||
stillId: job.library.stillId,
|
||||
sound: job.library.sound,
|
||||
workflow: job.library.workflow,
|
||||
duration: job.library.duration,
|
||||
globalLocks: job.library.globalLocks,
|
||||
permanenceRefs: job.library.permanenceRefs
|
||||
})
|
||||
}
|
||||
emitJob(job, { type: 'status', message: 'Job queued on ComfyUI', progress: 8 })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { addStudioJob, kickStudioQueue, listStudioJobs } from '~/server/utils/studioQueue'
|
||||
import { defaultVideoSteps, isLtxWorkflow, isTextToVideo, parseVideoWorkflow } from '~/utils/videoModels'
|
||||
import { normalizePermanenceRefs, resolveGlobalLocks, type PermanenceRef } from '~/utils/globalLocks'
|
||||
|
||||
function parseDuration(raw: unknown) {
|
||||
const seconds = Number(raw)
|
||||
@@ -12,14 +13,15 @@ function parseExtendDuration(raw: unknown) {
|
||||
}
|
||||
|
||||
function parseExtensions(raw: string | undefined) {
|
||||
if (!raw) return [] as { prompt: string; duration: number }[]
|
||||
if (!raw) return [] as { prompt: string; duration: number; permanenceRefs?: PermanenceRef[] }[]
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!Array.isArray(parsed)) return []
|
||||
return parsed
|
||||
.map((item: { prompt?: unknown; duration?: unknown }) => ({
|
||||
.map((item: { prompt?: unknown; duration?: unknown; permanenceRefs?: unknown }) => ({
|
||||
prompt: String(item?.prompt || '').trim(),
|
||||
duration: parseExtendDuration(item?.duration)
|
||||
duration: parseExtendDuration(item?.duration),
|
||||
permanenceRefs: normalizePermanenceRefs(item?.permanenceRefs)
|
||||
}))
|
||||
.filter(item => item.prompt)
|
||||
} catch {
|
||||
@@ -27,6 +29,26 @@ function parseExtensions(raw: string | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseFamilyPermanenceRefs(raw: string | undefined) {
|
||||
if (!raw) return [] as PermanenceRef[]
|
||||
try {
|
||||
return normalizePermanenceRefs(JSON.parse(raw))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function parseShotPermanenceRefs(raw: string | undefined, shotCount: number) {
|
||||
if (!raw) return Array.from({ length: shotCount }, () => [] as PermanenceRef[])
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!Array.isArray(parsed)) return Array.from({ length: shotCount }, () => [] as PermanenceRef[])
|
||||
return Array.from({ length: shotCount }, (_, index) => normalizePermanenceRefs(parsed[index]))
|
||||
} catch {
|
||||
return Array.from({ length: shotCount }, () => [] as PermanenceRef[])
|
||||
}
|
||||
}
|
||||
|
||||
const SAMPLERS = new Set(['res_multistep', 'euler', 'dpmpp_2m'])
|
||||
const SCHEDULERS = new Set(['simple', 'ddim_uniform', 'sgm_uniform'])
|
||||
|
||||
@@ -84,11 +106,21 @@ export default defineEventHandler(async (event) => {
|
||||
if (!image && !textToVideo) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'An initial image is required' })
|
||||
}
|
||||
const prompt = (fields.prompt || '').trim()
|
||||
if (!prompt) {
|
||||
const promptRaw = (fields.prompt || '').trim()
|
||||
if (!promptRaw) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'A motion prompt is required' })
|
||||
}
|
||||
const resolvedLocks = resolveGlobalLocks(fields.globalLocks || '', promptRaw)
|
||||
const prompt = resolvedLocks.prompt
|
||||
const globalLocks = resolvedLocks.locks
|
||||
const extensions = parseExtensions(fields.extensions)
|
||||
const permanenceRefs = parseFamilyPermanenceRefs(fields.permanenceRefs)
|
||||
const shotPermanenceRefs = parseShotPermanenceRefs(fields.shotPermanenceRefs, 1 + extensions.length)
|
||||
for (const [index, item] of extensions.entries()) {
|
||||
if (!shotPermanenceRefs[index + 1]?.length && item.permanenceRefs?.length) {
|
||||
shotPermanenceRefs[index + 1] = item.permanenceRefs
|
||||
}
|
||||
}
|
||||
const useIdentityRefs = workflow === 'v2' && !textToVideo && !isLtxWorkflow(workflow) && fields.useIdentityRefs === 'true'
|
||||
const { width, height } = resolveOutputSize(
|
||||
fields.aspect,
|
||||
@@ -180,7 +212,10 @@ export default defineEventHandler(async (event) => {
|
||||
folderLocked,
|
||||
referenceStillIds,
|
||||
extensions,
|
||||
queueAutoRun
|
||||
queueAutoRun,
|
||||
globalLocks: globalLocks || undefined,
|
||||
permanenceRefs: permanenceRefs.length ? permanenceRefs : undefined,
|
||||
shotPermanenceRefs: shotPermanenceRefs.some(item => item.length) ? shotPermanenceRefs : undefined
|
||||
}
|
||||
})
|
||||
await kickStudioQueue()
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const id = String(getRouterParam(event, 'id') || '')
|
||||
const body = await readBody<{ folderId?: string; name?: string; family?: boolean }>(event).catch(() => ({} as { folderId?: string; name?: string; family?: boolean }))
|
||||
const body = await readBody<{ folderId?: string; name?: string; family?: boolean; globalLocks?: string; permanenceRefs?: unknown }>(event).catch(() => ({} as { folderId?: string; name?: string; family?: boolean; globalLocks?: string; permanenceRefs?: unknown }))
|
||||
const folderId = String(body?.folderId || '')
|
||||
const hasName = typeof body?.name === 'string'
|
||||
const family = body?.family === true
|
||||
if (!folderId && !hasName) {
|
||||
const hasLocks = typeof body?.globalLocks === 'string'
|
||||
const hasRefs = Array.isArray(body?.permanenceRefs)
|
||||
if (!folderId && !hasName && !hasLocks && !hasRefs) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Choose a library folder or a new name' })
|
||||
}
|
||||
const clip = getClip(owner, id)
|
||||
@@ -19,5 +21,12 @@ export default defineEventHandler(async (event) => {
|
||||
if (family) await renameClipFamily(owner, id, body.name || '')
|
||||
else await renameClip(owner, id, body.name || '')
|
||||
}
|
||||
if (hasLocks || hasRefs) {
|
||||
await patchClipMeta(owner, id, {
|
||||
globalLocks: hasLocks ? body.globalLocks : undefined,
|
||||
permanenceRefs: hasRefs ? body.permanenceRefs as import('~/utils/globalLocks').PermanenceRef[] : undefined,
|
||||
family: family || hasLocks
|
||||
})
|
||||
}
|
||||
return publicLibrary(event)
|
||||
})
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
import { pauseShotQueue, summarizeQueue, getShotQueue } from '~/server/utils/shotQueue'
|
||||
import { setShotQueuePause, summarizeQueue, getShotQueue } from '~/server/utils/shotQueue'
|
||||
import { toggleStudioQueuePause } from '~/server/utils/studioQueue'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const id = String(getRouterParam(event, 'id') || '')
|
||||
await pauseShotQueue(owner, id)
|
||||
const body = await readBody<{ resume?: boolean }>().catch(() => ({} as { resume?: boolean }))
|
||||
const queue = getShotQueue(owner, id)
|
||||
if (!queue) throw createError({ statusCode: 404, statusMessage: 'Queue not found' })
|
||||
return summarizeQueue(queue)
|
||||
const result = await toggleStudioQueuePause(owner, {
|
||||
resume: body?.resume,
|
||||
queueId: id
|
||||
})
|
||||
const next = getShotQueue(owner, id)
|
||||
if (!next) throw createError({ statusCode: 404, statusMessage: 'Queue not found' })
|
||||
if (result.willPause && !next.stopAfterCurrent) {
|
||||
await setShotQueuePause(owner, id, true)
|
||||
}
|
||||
if (!result.willPause && next.stopAfterCurrent) {
|
||||
await setShotQueuePause(owner, id, false)
|
||||
}
|
||||
return {
|
||||
...summarizeQueue(getShotQueue(owner, id) || next),
|
||||
paused: result.paused,
|
||||
willPause: result.willPause
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { listShotQueues, summarizeQueue } from '~/server/utils/shotQueue'
|
||||
import { listStudioJobs, summarizeStudioJob } from '~/server/utils/studioQueue'
|
||||
import { isStudioQueuePaused, listStudioJobs, summarizeStudioJob } from '~/server/utils/studioQueue'
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
@@ -35,6 +35,7 @@ export default defineEventHandler((event) => {
|
||||
.filter(queue => !claimed.has(queue.id) && queue.status !== 'complete')
|
||||
.map(summarizeQueue)
|
||||
: [],
|
||||
waitingCount: jobs.filter(job => job.status === 'waiting').length
|
||||
waitingCount: jobs.filter(job => job.status === 'waiting').length,
|
||||
paused: isStudioQueuePaused(owner)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { toggleStudioQueuePause } from '~/server/utils/studioQueue'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const id = String(getRouterParam(event, 'id') || '')
|
||||
const body = await readBody<{ resume?: boolean }>().catch(() => ({} as { resume?: boolean }))
|
||||
return toggleStudioQueuePause(owner, {
|
||||
resume: body?.resume,
|
||||
jobId: id
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import { toggleStudioQueuePause } from '~/server/utils/studioQueue'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const body = await readBody<{ resume?: boolean; jobId?: string }>().catch(() => ({} as { resume?: boolean; jobId?: string }))
|
||||
return toggleStudioQueuePause(owner, {
|
||||
resume: body?.resume,
|
||||
jobId: body?.jobId ? String(body.jobId) : undefined
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user