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:
Towsty
2026-08-25 18:43:15 -05:00
co-authored by Cursor
parent f0f80b5873
commit b7196ecde3
10 changed files with 342 additions and 77 deletions
+62 -9
View File
@@ -27,6 +27,7 @@ export interface LibraryClip {
seed: number
hideThumbnail: boolean
createdAt: number
comfyFilename?: string
}
export interface LibraryStill {
@@ -460,10 +461,14 @@ export async function saveClip(params: {
hideThumbnail: boolean
video: Buffer
thumb?: Buffer | null
comfyFilename?: string
}) {
const catalog = readCatalog(params.ownerKey)
const folder = catalog.folders.find(item => item.id === params.folderId) || catalog.folders[0]
if (!folder) throw createError({ statusCode: 400, statusMessage: 'No library folder available' })
if (params.comfyFilename && catalog.clips.some(clip => clip.comfyFilename === params.comfyFilename)) {
return catalog.clips.find(clip => clip.comfyFilename === params.comfyFilename) as LibraryClip
}
const clip: LibraryClip = {
id: crypto.randomUUID(),
folderId: folder.id,
@@ -475,7 +480,8 @@ export async function saveClip(params: {
turbo: params.turbo,
seed: params.seed,
hideThumbnail: params.hideThumbnail,
createdAt: Date.now()
createdAt: Date.now(),
comfyFilename: params.comfyFilename
}
mkdirSync(clipDir(params.ownerKey, clip.id), { recursive: true })
writeFileSync(clipVideoPath(params.ownerKey, clip.id), params.video)
@@ -515,12 +521,59 @@ export function deleteClip(owner: string, id: string) {
}
export async function downloadComfyVideo(video: { filename: string; subfolder: string; type: string }) {
const params = new URLSearchParams({
filename: video.filename,
subfolder: video.subfolder,
type: video.type
})
const res = await comfyFetch(`/view?${params.toString()}`)
if (!res.ok) throw new Error('Failed to fetch completed video from ComfyUI')
return Buffer.from(await res.arrayBuffer())
const subfolders = [...new Set([video.subfolder, 'video', ''])]
let lastStatus = 0
for (const subfolder of subfolders) {
const params = new URLSearchParams({
filename: video.filename,
subfolder,
type: video.type || 'output'
})
const res = await comfyFetch(`/view?${params.toString()}`)
lastStatus = res.status
if (res.ok) return Buffer.from(await res.arrayBuffer())
}
throw new Error(`Failed to fetch completed video from ComfyUI (${lastStatus})`)
}
export async function importMissingComfyVideos(owner: string, folderId?: string) {
const catalog = readCatalog(owner)
const folder = catalog.folders.find(item => item.id === folderId) || catalog.folders[0]
if (!folder) return []
const known = new Set(catalog.clips.map(clip => clip.comfyFilename).filter(Boolean) as string[])
const unlabeled = catalog.clips.filter(clip => !clip.comfyFilename).length
const history = await fetchHistoryAll()
const found: { promptId: string; video: { filename: string; subfolder: string; type: string }; prompt: string }[] = []
for (const [promptId, entry] of Object.entries(history)) {
const video = extractVideo({ [promptId]: entry as Record<string, unknown> }, promptId)
if (!video || known.has(video.filename)) continue
found.push({
promptId,
video,
prompt: extractPromptFromHistory(entry) || 'Recovered from ComfyUI'
})
}
found.sort((a, b) => a.video.filename.localeCompare(b.video.filename, undefined, { numeric: true }))
const toImport = found.slice(unlabeled)
const imported = []
for (const item of toImport) {
const buffer = await downloadComfyVideo(item.video)
const clip = await saveClip({
ownerKey: owner,
folderId: folder.id,
prompt: item.prompt,
aspect: 'auto',
width: 0,
height: 0,
steps: 0,
turbo: true,
seed: 0,
hideThumbnail: false,
video: buffer,
thumb: null,
comfyFilename: item.video.filename
})
imported.push(clip)
}
return imported
}