Keep queued extension chains running after refresh instead of saving the first clip as final.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-27 08:42:12 -05:00
co-authored by Cursor
parent 72f4909815
commit 6fec10febd
6 changed files with 524 additions and 300 deletions
+131
View File
@@ -1,5 +1,6 @@
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import type { Job } from '~/server/utils/jobs'
export interface PendingJob {
jobId: string
@@ -25,9 +26,25 @@ export interface PendingJob {
familyId?: string
parentClipId?: string
chainIndex?: number
chainStep?: number
chainTotal?: number
chainLabel?: string
stillId?: string
stillFilename?: string
sound?: boolean
referenceImageNames?: string[]
referenceStillIds?: Array<string | null>
useIdentityRefs?: boolean
workflow?: 'v1' | 'v2'
duration?: number
cfg?: number
fps?: number
samplerName?: string
scheduler?: string
hideInput?: boolean
extensions?: { prompt: string; duration: number }[]
remainingExtensions?: { prompt: string; duration: number }[]
currentClipId?: string
}
function pendingRoot() {
@@ -43,6 +60,118 @@ function ensurePending() {
mkdirSync(pendingRoot(), { recursive: true })
}
export function remainingAfterCurrentShot(job: {
chainIndex?: number
extensions?: { prompt: string; duration: number }[]
}) {
const extensions = job.extensions || []
const index = job.chainIndex || 0
return index === 0 ? extensions : extensions.slice(index)
}
export function isLastChainShot(job: {
chainIndex?: number
extensions?: { prompt: string; duration: number }[]
remainingExtensions?: { prompt: string; duration: number }[]
}) {
if (job.remainingExtensions) return job.remainingExtensions.length === 0
return remainingAfterCurrentShot(job).length === 0
}
export function pendingFromJob(job: Job, overrides: Partial<PendingJob> = {}): PendingJob {
const library = job.library
if (!library) {
throw new Error('Cannot persist a job without library metadata')
}
const remaining = remainingAfterCurrentShot(library)
return {
jobId: job.id,
promptId: job.promptId || '',
clientId: job.clientId,
ownerKey: library.ownerKey,
folderId: library.folderId,
hideThumbnail: library.hideThumbnail,
folderLocked: library.folderLocked,
name: library.name,
prompt: library.prompt,
aspect: library.aspect,
width: library.width,
height: library.height,
steps: library.steps,
turbo: library.turbo,
seed: library.seed,
startedAt: job.startedAt,
imageName: library.imageName,
imageSubfolder: library.imageSubfolder,
extendTmpDir: library.extendTmpDir,
extendPart1Path: library.extendPart1Path,
familyId: library.familyId,
parentClipId: library.parentClipId,
chainIndex: library.chainIndex,
chainStep: library.chainStep,
chainTotal: library.chainTotal,
chainLabel: library.chainLabel,
stillId: library.stillId,
stillFilename: library.stillFilename,
sound: library.sound,
referenceImageNames: library.referenceImageNames,
referenceStillIds: library.referenceStillIds,
useIdentityRefs: library.useIdentityRefs,
workflow: library.workflow,
duration: library.duration,
cfg: library.cfg,
fps: library.fps,
samplerName: library.samplerName,
scheduler: library.scheduler,
hideInput: library.hideInput,
extensions: library.extensions,
remainingExtensions: remaining,
currentClipId: job.clipId,
...overrides
}
}
export function libraryFromPending(pending: PendingJob): NonNullable<Job['library']> {
return {
ownerKey: pending.ownerKey,
folderId: pending.folderId,
hideThumbnail: pending.hideThumbnail,
hideInput: pending.hideInput,
folderLocked: pending.folderLocked,
name: pending.name,
prompt: pending.prompt,
aspect: pending.aspect,
width: pending.width,
height: pending.height,
steps: pending.steps,
turbo: pending.turbo,
seed: pending.seed,
cfg: pending.cfg,
fps: pending.fps,
samplerName: pending.samplerName,
scheduler: pending.scheduler,
imageName: pending.imageName,
imageSubfolder: pending.imageSubfolder,
extendTmpDir: pending.extendTmpDir,
extendPart1Path: pending.extendPart1Path,
familyId: pending.familyId,
parentClipId: pending.parentClipId,
chainIndex: pending.chainIndex,
chainStep: pending.chainStep,
chainTotal: pending.chainTotal,
chainLabel: pending.chainLabel,
stillId: pending.stillId,
stillFilename: pending.stillFilename,
sound: pending.sound,
referenceImageNames: pending.referenceImageNames,
referenceStillIds: pending.referenceStillIds,
useIdentityRefs: pending.useIdentityRefs,
workflow: pending.workflow,
duration: pending.duration,
extensions: pending.extensions || pending.remainingExtensions
}
}
export function writePendingJob(job: PendingJob) {
ensurePending()
const tmp = pendingPath(job.jobId) + '.tmp'
@@ -73,6 +202,8 @@ export function deletePendingJob(jobId: string) {
}
export async function completePendingIfReady(pending: PendingJob) {
if (!isLastChainShot(pending)) return null
if (!pending.promptId) return null
const alreadySaved = () => {
const live = getJob(pending.jobId)
if (live?.savedPromptId === pending.promptId) {