Keep pause across Coolify restarts, apply the chosen still from the image picker, and remux clips with Range/faststart so finished video can be scrubbed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 08:15:52 -05:00
co-authored by Cursor
parent 870150f052
commit 7d783bd97e
12 changed files with 338 additions and 49 deletions
+40 -3
View File
@@ -4,6 +4,7 @@ import { getJob, restoreJob } from '~/server/utils/jobs'
import type { PendingJob } from '~/server/utils/pending'
import { isLastChainShot, libraryFromPending, pendingFromJob, remainingAfterCurrentShot, writePendingJob, deletePendingJob } from '~/server/utils/pending'
import { syncQueueFromJob } from '~/server/utils/shotQueue'
import { isEncodingNode, NODE_LABELS } from '~/server/utils/workflow'
import { mergePermanenceRefs, type PermanenceRef } from '~/utils/globalLocks'
function clipRefsFromJob(library: NonNullable<Job['library']>): PermanenceRef[] | undefined {
@@ -52,6 +53,9 @@ export function mapChainProgress(job: Job, localPct: number) {
export function formatChainMessage(job: Job, message: string, samplePct?: number) {
const { chained, step, total, label } = chainMeta(job)
if (!chained) return message
if (/checkpoint saved|segment saved/i.test(message)) {
return `Shot ${step}/${total} saved`
}
if (typeof samplePct === 'number') {
return `Step ${step}/${total}: Generating ${label} (${samplePct}%)`
}
@@ -78,6 +82,13 @@ export function emitChainJob(job: Job, event: JobEvent, samplePct?: number) {
emitJob(job, next)
}
function isNonSamplerProgress(node: string) {
if (!node) return false
if (isEncodingNode(node)) return true
const label = NODE_LABELS[node] || ''
return /save|checkpoint|video combine|vhs/i.test(label) && !/sampler/i.test(label)
}
export function watchComfyJob(job: Job, options: { persist?: boolean } = {}): Promise<void> {
const persist = options.persist !== false
job.socketReady = false
@@ -350,6 +361,11 @@ export function watchComfyJob(job: Job, options: { persist?: boolean } = {}): Pr
if (job.promptId && data.prompt_id && data.prompt_id !== job.promptId) return
if (payload.type === 'progress') {
const node = data.node ? String(data.node) : ''
if (isNonSamplerProgress(node)) {
markActivity()
return
}
const value = Number(data.value || 0)
const max = Number(data.max || 1)
const pct = 12 + Math.round((value / Math.max(max, 1)) * 70)
@@ -360,7 +376,7 @@ export function watchComfyJob(job: Job, options: { persist?: boolean } = {}): Pr
step: value,
maxStep: max,
progress: pct,
node: data.node ? String(data.node) : null,
node: node || null,
message: `Sampling step ${value}/${max} (${samplePct}%)`
}, samplePct)
}
@@ -424,20 +440,41 @@ export function ensurePendingWatch(pending: PendingJob) {
pendingWatches.add(job.id)
const resume = async () => {
try {
const { applyPersistedPauseToJob, onLiveVideoSettled } = await import('~/server/utils/studioQueue')
applyPersistedPauseToJob(job)
const holdNow = () => job.library?.stopAfterCurrent === true
if (pending.promptId) {
await watchComfyJob(job, { persist: lastShot })
await watchComfyJob(job, { persist: lastShot || holdNow() })
}
applyPersistedPauseToJob(job)
if (job.status === 'error' || job.status === 'cancelled') {
await onLiveVideoSettled(job)
return
}
if (holdNow()) {
await onLiveVideoSettled(job)
return
}
if (job.status === 'complete') {
await onLiveVideoSettled(job)
return
}
if (job.status === 'error' || job.status === 'cancelled' || job.status === 'complete') return
if (!lastShot) {
const { continueQueuedExtensionsIfNeeded } = await import('~/server/utils/videoChain')
await continueQueuedExtensionsIfNeeded(job)
}
applyPersistedPauseToJob(job)
if (job.status === 'error' || job.status === 'cancelled' || job.status === 'complete' || holdNow()) {
await onLiveVideoSettled(job)
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
if (job.status === 'error' || job.status === 'cancelled') return
job.status = 'error'
job.error = message
emitChainJob(job, { type: 'error', error: message, message })
const { onLiveVideoSettled } = await import('~/server/utils/studioQueue')
await onLiveVideoSettled(job).catch(() => null)
} finally {
pendingWatches.delete(job.id)
}