Recover finished Comfy videos after relay restarts.
Persist queued jobs to disk, import leftover MP4s from Comfy history, and do not mark a run complete until the saved video exists. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
export interface PendingJob {
|
||||
jobId: string
|
||||
promptId: string
|
||||
clientId: string
|
||||
ownerKey: string
|
||||
folderId: string
|
||||
hideThumbnail: boolean
|
||||
prompt: string
|
||||
aspect: string
|
||||
width: number
|
||||
height: number
|
||||
steps: number
|
||||
turbo: boolean
|
||||
seed: number
|
||||
startedAt: number
|
||||
}
|
||||
|
||||
function pendingRoot() {
|
||||
const config = useRuntimeConfig()
|
||||
return join((config.libraryDir || process.env.LIBRARY_DIR || '/data/library').replace(/\/$/, ''), 'pending')
|
||||
}
|
||||
|
||||
function pendingPath(jobId: string) {
|
||||
return join(pendingRoot(), `${jobId}.json`)
|
||||
}
|
||||
|
||||
function ensurePending() {
|
||||
mkdirSync(pendingRoot(), { recursive: true })
|
||||
}
|
||||
|
||||
export function writePendingJob(job: PendingJob) {
|
||||
ensurePending()
|
||||
const tmp = pendingPath(job.jobId) + '.tmp'
|
||||
writeFileSync(tmp, JSON.stringify(job, null, 2))
|
||||
renameSync(tmp, pendingPath(job.jobId))
|
||||
}
|
||||
|
||||
export function readPendingJob(jobId: string): PendingJob | null {
|
||||
const path = pendingPath(jobId)
|
||||
if (!existsSync(path)) return null
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, 'utf8')) as PendingJob
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function listPendingJobs(): PendingJob[] {
|
||||
ensurePending()
|
||||
return readdirSync(pendingRoot())
|
||||
.filter(name => name.endsWith('.json'))
|
||||
.map(name => readPendingJob(name.replace(/\.json$/, '')))
|
||||
.filter((job): job is PendingJob => Boolean(job))
|
||||
}
|
||||
|
||||
export function deletePendingJob(jobId: string) {
|
||||
rmSync(pendingPath(jobId), { force: true })
|
||||
}
|
||||
|
||||
export async function completePendingIfReady(pending: PendingJob) {
|
||||
const history = await fetchHistory(pending.promptId)
|
||||
const video = extractVideo(history, pending.promptId)
|
||||
if (!video) return null
|
||||
const buffer = await downloadComfyVideo(video)
|
||||
const clip = await saveClip({
|
||||
ownerKey: pending.ownerKey,
|
||||
folderId: pending.folderId,
|
||||
prompt: pending.prompt,
|
||||
aspect: pending.aspect,
|
||||
width: pending.width,
|
||||
height: pending.height,
|
||||
steps: pending.steps,
|
||||
turbo: pending.turbo,
|
||||
seed: pending.seed,
|
||||
hideThumbnail: pending.hideThumbnail,
|
||||
video: buffer,
|
||||
thumb: null,
|
||||
comfyFilename: video.filename
|
||||
})
|
||||
deletePendingJob(pending.jobId)
|
||||
return {
|
||||
type: 'complete' as const,
|
||||
status: 'complete' as const,
|
||||
message: 'Video ready',
|
||||
progress: 100,
|
||||
clipId: clip.id,
|
||||
filename: video.filename,
|
||||
subfolder: video.subfolder,
|
||||
mediaType: video.type,
|
||||
hideThumbnail: pending.hideThumbnail
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user