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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user