|
|
|
@@ -1,14 +1,6 @@
|
|
|
|
|
import { frameLength, runGeneration } from '~/server/utils/videoChain'
|
|
|
|
|
import { createShotQueue, finishQueueBurst, setQueueJob } from '~/server/utils/shotQueue'
|
|
|
|
|
import { addStudioJob, kickStudioQueue, listStudioJobs } from '~/server/utils/studioQueue'
|
|
|
|
|
import { defaultVideoSteps, isLtxWorkflow, isTextToVideo, parseVideoWorkflow } from '~/utils/videoModels'
|
|
|
|
|
|
|
|
|
|
function isComfyBusyTimeout(error: unknown) {
|
|
|
|
|
const err = error as { statusCode?: number; status?: number; data?: { code?: string }; message?: string; statusMessage?: string }
|
|
|
|
|
if (err?.statusCode === 409 || err?.status === 409 || err?.data?.code === 'COMFY_BUSY') return true
|
|
|
|
|
const text = `${err?.statusMessage || ''} ${err?.message || ''}`
|
|
|
|
|
return text.includes('still busy after 3 minutes')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseDuration(raw: unknown) {
|
|
|
|
|
const seconds = Number(raw)
|
|
|
|
|
if (!Number.isFinite(seconds)) return 5
|
|
|
|
@@ -114,7 +106,6 @@ export default defineEventHandler(async (event) => {
|
|
|
|
|
const cfg = parseCfg(fields.cfg, turbo, isLtxWorkflow(workflow))
|
|
|
|
|
const samplerName = parseSampler(fields.sampler_name, isLtxWorkflow(workflow))
|
|
|
|
|
const scheduler = parseScheduler(fields.scheduler)
|
|
|
|
|
const length = frameLength(durationSeconds, fps)
|
|
|
|
|
const hideThumbnail = fields.hideThumbnail === 'true'
|
|
|
|
|
const hideInput = fields.hideInput === 'true'
|
|
|
|
|
const sound = !isLtxWorkflow(workflow) && fields.sound !== 'false'
|
|
|
|
@@ -159,156 +150,49 @@ export default defineEventHandler(async (event) => {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const chainTotal = 1 + extensions.length
|
|
|
|
|
const job = createJob()
|
|
|
|
|
job.kind = 'video'
|
|
|
|
|
job.maxStep = steps
|
|
|
|
|
job.hideThumbnail = hideThumbnail
|
|
|
|
|
const familyId = crypto.randomUUID()
|
|
|
|
|
const queueAutoRun = fields.queueAutoRun === 'true'
|
|
|
|
|
job.library = {
|
|
|
|
|
const studio = await addStudioJob({
|
|
|
|
|
ownerKey,
|
|
|
|
|
folderId,
|
|
|
|
|
hideThumbnail,
|
|
|
|
|
hideInput,
|
|
|
|
|
folderLocked,
|
|
|
|
|
name: clipName,
|
|
|
|
|
prompt,
|
|
|
|
|
aspect: fields.aspect || 'auto',
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
steps,
|
|
|
|
|
turbo,
|
|
|
|
|
seed,
|
|
|
|
|
cfg,
|
|
|
|
|
fps,
|
|
|
|
|
samplerName,
|
|
|
|
|
scheduler,
|
|
|
|
|
thumb: image && !isPipelineFrameFilename(image.filename) ? image.data : undefined,
|
|
|
|
|
stillId: still?.id,
|
|
|
|
|
stillFilename: still?.filename,
|
|
|
|
|
referenceStillIds,
|
|
|
|
|
duration: durationSeconds,
|
|
|
|
|
sound,
|
|
|
|
|
draftId: (fields.draftId || '').trim() || undefined,
|
|
|
|
|
extensions,
|
|
|
|
|
chainIndex: 0,
|
|
|
|
|
chainStep: 1,
|
|
|
|
|
chainTotal,
|
|
|
|
|
chainLabel: extensions.length ? 'Initial' : undefined,
|
|
|
|
|
familyId,
|
|
|
|
|
workflow,
|
|
|
|
|
useIdentityRefs,
|
|
|
|
|
queueAutoRun: extensions.length ? queueAutoRun : false,
|
|
|
|
|
queueBudget: extensions.length && queueAutoRun ? extensions.length : 0
|
|
|
|
|
}
|
|
|
|
|
if (extensions.length) {
|
|
|
|
|
const queue = await createShotQueue({
|
|
|
|
|
ownerKey,
|
|
|
|
|
payload: {
|
|
|
|
|
prompt,
|
|
|
|
|
name: clipName,
|
|
|
|
|
familyId,
|
|
|
|
|
folderId,
|
|
|
|
|
autoRun: queueAutoRun,
|
|
|
|
|
stillId: still?.id,
|
|
|
|
|
stillFilename: still?.filename,
|
|
|
|
|
hideThumbnail,
|
|
|
|
|
hideInput,
|
|
|
|
|
aspect: fields.aspect || 'auto',
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
steps,
|
|
|
|
|
turbo,
|
|
|
|
|
seed,
|
|
|
|
|
cfg,
|
|
|
|
|
fps,
|
|
|
|
|
samplerName,
|
|
|
|
|
scheduler,
|
|
|
|
|
workflow,
|
|
|
|
|
duration: durationSeconds,
|
|
|
|
|
sound,
|
|
|
|
|
workflow,
|
|
|
|
|
useIdentityRefs,
|
|
|
|
|
stillId: still?.id,
|
|
|
|
|
stillFilename: still?.filename,
|
|
|
|
|
hideThumbnail,
|
|
|
|
|
hideInput,
|
|
|
|
|
folderLocked,
|
|
|
|
|
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 })
|
|
|
|
|
|
|
|
|
|
void runGeneration(job, {
|
|
|
|
|
prompt,
|
|
|
|
|
image,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
steps,
|
|
|
|
|
seed,
|
|
|
|
|
turbo,
|
|
|
|
|
length,
|
|
|
|
|
sound,
|
|
|
|
|
cfg,
|
|
|
|
|
fps,
|
|
|
|
|
samplerName,
|
|
|
|
|
scheduler,
|
|
|
|
|
extensions,
|
|
|
|
|
workflow,
|
|
|
|
|
duration: durationSeconds,
|
|
|
|
|
useIdentityRefs,
|
|
|
|
|
referenceImages
|
|
|
|
|
}).catch(async (error) => {
|
|
|
|
|
removeExtendTemp(job.library?.extendTmpDir)
|
|
|
|
|
if (isComfyBusyTimeout(error) && job.library) {
|
|
|
|
|
try {
|
|
|
|
|
const draft = await saveRetryDraft({
|
|
|
|
|
ownerKey: job.library.ownerKey,
|
|
|
|
|
id: job.library.draftId,
|
|
|
|
|
folderId: job.library.folderId,
|
|
|
|
|
stillId: job.library.stillId,
|
|
|
|
|
stillFilename: job.library.stillFilename,
|
|
|
|
|
name: job.library.name,
|
|
|
|
|
prompt: job.library.prompt,
|
|
|
|
|
aspect: job.library.aspect,
|
|
|
|
|
width: job.library.width,
|
|
|
|
|
height: job.library.height,
|
|
|
|
|
steps: job.library.steps,
|
|
|
|
|
turbo: job.library.turbo,
|
|
|
|
|
seed: job.library.seed,
|
|
|
|
|
duration: job.library.duration || durationSeconds,
|
|
|
|
|
hideThumbnail: job.library.hideThumbnail,
|
|
|
|
|
hideInput: job.library.hideInput === true,
|
|
|
|
|
sound: job.library.sound !== false,
|
|
|
|
|
cfg: job.library.cfg,
|
|
|
|
|
fps: job.library.fps,
|
|
|
|
|
samplerName: job.library.samplerName,
|
|
|
|
|
scheduler: job.library.scheduler,
|
|
|
|
|
extensions: job.library.extensions,
|
|
|
|
|
workflow: job.library.workflow
|
|
|
|
|
})
|
|
|
|
|
job.library.draftId = draft.id
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (job.status === 'error' || job.status === 'cancelled' || job.status === 'deferred') return
|
|
|
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
|
|
|
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)
|
|
|
|
|
queueAutoRun
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
await kickStudioQueue()
|
|
|
|
|
const latest = listStudioJobs(ownerKey).find(item => item.id === studio.id)
|
|
|
|
|
const liveJobId = latest?.liveJobId || ''
|
|
|
|
|
const queueId = latest?.shotQueueId
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
jobId: job.id,
|
|
|
|
|
clientId: job.clientId,
|
|
|
|
|
jobId: liveJobId || studio.id,
|
|
|
|
|
studioJobId: studio.id,
|
|
|
|
|
queued: !liveJobId,
|
|
|
|
|
clientId: '',
|
|
|
|
|
seed,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
@@ -323,7 +207,7 @@ export default defineEventHandler(async (event) => {
|
|
|
|
|
hideThumbnail,
|
|
|
|
|
folderLocked,
|
|
|
|
|
chainTotal,
|
|
|
|
|
queueId: job.library?.queueId,
|
|
|
|
|
queueId,
|
|
|
|
|
queuedRemaining: extensions.length
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|