Stop ghost extension clips and recover last-frame extract after an interrupt.
Only one Comfy finish path can save a clip, hide catalog entries whose MP4 is missing, and retry last-frame extract so a truncated file does not dump FFmpeg noise into the UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+4
-3
@@ -702,6 +702,7 @@ interface LibraryClip {
|
|||||||
seed: number
|
seed: number
|
||||||
hideThumbnail: boolean
|
hideThumbnail: boolean
|
||||||
hasThumbnail?: boolean
|
hasThumbnail?: boolean
|
||||||
|
hasVideo?: boolean
|
||||||
createdAt: number
|
createdAt: number
|
||||||
cfg?: number
|
cfg?: number
|
||||||
fps?: number
|
fps?: number
|
||||||
@@ -950,7 +951,7 @@ const plannedChain = computed(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const visibleClips = computed(() => clips.value.filter(clip => clip.folderId === browseFolderId.value))
|
const visibleClips = computed(() => clips.value.filter(clip => clip.folderId === browseFolderId.value && clip.hasVideo !== false))
|
||||||
const libraryGroups = computed(() => {
|
const libraryGroups = computed(() => {
|
||||||
const groups = new Map<string, LibraryClip[]>()
|
const groups = new Map<string, LibraryClip[]>()
|
||||||
for (const clip of visibleClips.value) {
|
for (const clip of visibleClips.value) {
|
||||||
@@ -974,9 +975,9 @@ function clipsIn(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clipFamilyKey(clip: LibraryClip) {
|
function clipFamilyKey(clip: LibraryClip) {
|
||||||
if (clip.familyId) return clip.familyId
|
|
||||||
const base = String(clip.name || '').trim().replace(/\s+p\d+$/i, '')
|
const base = String(clip.name || '').trim().replace(/\s+p\d+$/i, '')
|
||||||
if (base) return `name:${clip.folderId}:${base.toLowerCase()}`
|
if (base) return `name:${clip.folderId}:${base.toLowerCase()}`
|
||||||
|
if (clip.familyId) return clip.familyId
|
||||||
return `solo:${clip.id}`
|
return `solo:${clip.id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,7 +986,7 @@ function sortClipParts(a: LibraryClip, b: LibraryClip) {
|
|||||||
if (durationDelta) return durationDelta
|
if (durationDelta) return durationDelta
|
||||||
const chainDelta = (b.chainIndex || 0) - (a.chainIndex || 0)
|
const chainDelta = (b.chainIndex || 0) - (a.chainIndex || 0)
|
||||||
if (chainDelta) return chainDelta
|
if (chainDelta) return chainDelta
|
||||||
return b.createdAt - a.createdAt
|
return a.createdAt - b.createdAt
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatClipDuration(value?: number) {
|
function formatClipDuration(value?: number) {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
extendTmpDir: tmpDir,
|
extendTmpDir: tmpDir,
|
||||||
extendPart1Path: part1Path,
|
extendPart1Path: part1Path,
|
||||||
extendSourceClipId: source.id,
|
extendSourceClipId: source.id,
|
||||||
familyId: crypto.randomUUID(),
|
familyId: source.familyId || crypto.randomUUID(),
|
||||||
parentClipId: source.id,
|
parentClipId: source.id,
|
||||||
chainIndex: (source.chainIndex || 0) + 1
|
chainIndex: (source.chainIndex || 0) + 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -403,7 +403,14 @@ async function runGeneration(
|
|||||||
assertJobActive(job)
|
assertJobActive(job)
|
||||||
|
|
||||||
copyFileSync(currentPath, part1Path)
|
copyFileSync(currentPath, part1Path)
|
||||||
await extractLastFrame(part1Path, framePath)
|
try {
|
||||||
|
await extractLastFrame(part1Path, framePath)
|
||||||
|
} catch (error) {
|
||||||
|
const detail = error instanceof Error ? error.message : String(error)
|
||||||
|
throw new Error(detail.includes('last frame')
|
||||||
|
? detail
|
||||||
|
: `Could not extract the last frame for the next extension: ${detail}`)
|
||||||
|
}
|
||||||
const frame = readFileSync(framePath)
|
const frame = readFileSync(framePath)
|
||||||
job.library.extendPart1Path = part1Path
|
job.library.extendPart1Path = part1Path
|
||||||
job.library.prompt = ext.prompt
|
job.library.prompt = ext.prompt
|
||||||
|
|||||||
+23
-11
@@ -63,18 +63,30 @@ export async function extractLastFrame(inputPath: string, outputPath: string) {
|
|||||||
if (!existsSync(inputPath)) {
|
if (!existsSync(inputPath)) {
|
||||||
throw new Error('Source video is missing')
|
throw new Error('Source video is missing')
|
||||||
}
|
}
|
||||||
await runFfmpeg([
|
|
||||||
'-y',
|
const attempts: string[][] = []
|
||||||
'-sseof', '-1',
|
try {
|
||||||
'-i', inputPath,
|
const duration = await probeDuration(inputPath)
|
||||||
'-update', '1',
|
const seek = Math.max(0, duration - 0.08)
|
||||||
'-frames:v', '1',
|
attempts.push(['-y', '-ss', seek.toFixed(3), '-i', inputPath, '-frames:v', '1', '-an', '-q:v', '1', outputPath])
|
||||||
'-q:v', '1',
|
} catch {
|
||||||
outputPath
|
// Truncated files often have no duration index; fall through to decode attempts.
|
||||||
], 30000)
|
|
||||||
if (!existsSync(outputPath)) {
|
|
||||||
throw new Error('FFmpeg did not write the last frame')
|
|
||||||
}
|
}
|
||||||
|
attempts.push(
|
||||||
|
['-y', '-sseof', '-0.04', '-i', inputPath, '-update', '1', '-frames:v', '1', '-an', '-q:v', '1', outputPath],
|
||||||
|
['-y', '-i', inputPath, '-update', '1', '-an', '-q:v', '1', outputPath]
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const args of attempts) {
|
||||||
|
try {
|
||||||
|
await runFfmpeg(args, 60000)
|
||||||
|
if (existsSync(outputPath)) return
|
||||||
|
} catch {
|
||||||
|
// Try the next seek strategy.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Could not extract the last frame. The video looks incomplete — interrupting Comfy mid-encode usually causes this. Earlier saved parts are still in the library.')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function probeHasAudio(filePath: string) {
|
export async function probeHasAudio(filePath: string) {
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ export interface Job {
|
|||||||
}
|
}
|
||||||
error?: string
|
error?: string
|
||||||
socketReady?: boolean
|
socketReady?: boolean
|
||||||
|
saving?: boolean
|
||||||
|
savedPromptId?: string
|
||||||
segmentBuffer?: Buffer
|
segmentBuffer?: Buffer
|
||||||
events: JobEvent[]
|
events: JobEvent[]
|
||||||
listeners: Set<(event: JobEvent) => void>
|
listeners: Set<(event: JobEvent) => void>
|
||||||
|
|||||||
@@ -317,7 +317,8 @@ export function publicLibrary(event: H3Event) {
|
|||||||
clips: catalog.clips.filter(clip => unlockedIds.has(clip.folderId)).map(clip => ({
|
clips: catalog.clips.filter(clip => unlockedIds.has(clip.folderId)).map(clip => ({
|
||||||
...clip,
|
...clip,
|
||||||
name: clipTitle(clip),
|
name: clipTitle(clip),
|
||||||
hasThumbnail: !clip.hideThumbnail && existsSync(clipThumbPath(owner, clip.id))
|
hasThumbnail: !clip.hideThumbnail && existsSync(clipThumbPath(owner, clip.id)),
|
||||||
|
hasVideo: existsSync(clipVideoPath(owner, clip.id))
|
||||||
})),
|
})),
|
||||||
stills: catalog.stills.filter(still => unlockedIds.has(still.folderId)),
|
stills: catalog.stills.filter(still => unlockedIds.has(still.folderId)),
|
||||||
drafts: catalog.drafts
|
drafts: catalog.drafts
|
||||||
|
|||||||
@@ -70,10 +70,42 @@ export function deletePendingJob(jobId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function completePendingIfReady(pending: PendingJob) {
|
export async function completePendingIfReady(pending: PendingJob) {
|
||||||
|
const alreadySaved = () => {
|
||||||
|
const live = getJob(pending.jobId)
|
||||||
|
if (live?.savedPromptId === pending.promptId) {
|
||||||
|
deletePendingJob(pending.jobId)
|
||||||
|
return {
|
||||||
|
type: 'complete' as const,
|
||||||
|
status: 'complete' as const,
|
||||||
|
message: pending.folderLocked
|
||||||
|
? 'Saved to the locked folder. Unlock it to view.'
|
||||||
|
: (pending.extendPart1Path ? 'Extended video ready' : 'Video ready'),
|
||||||
|
progress: 100,
|
||||||
|
jobId: pending.jobId,
|
||||||
|
clipId: live.clipId,
|
||||||
|
hideThumbnail: pending.hideThumbnail,
|
||||||
|
folderLocked: pending.folderLocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const inFlight = () => {
|
||||||
|
const current = getJob(pending.jobId)
|
||||||
|
return Boolean(current && current.promptId === pending.promptId && current.saving)
|
||||||
|
}
|
||||||
|
if (inFlight()) return null
|
||||||
|
const saved = alreadySaved()
|
||||||
|
if (saved) return saved
|
||||||
const history = await fetchHistory(pending.promptId)
|
const history = await fetchHistory(pending.promptId)
|
||||||
|
if (inFlight()) return null
|
||||||
|
const afterHistory = alreadySaved()
|
||||||
|
if (afterHistory) return afterHistory
|
||||||
const video = extractVideo(history, pending.promptId)
|
const video = extractVideo(history, pending.promptId)
|
||||||
if (!video) return null
|
if (!video) return null
|
||||||
let buffer = await downloadComfyVideo(video)
|
let buffer = await downloadComfyVideo(video)
|
||||||
|
if (inFlight()) return null
|
||||||
|
const afterDownload = alreadySaved()
|
||||||
|
if (afterDownload) return afterDownload
|
||||||
if (pending.extendPart1Path && pending.extendTmpDir) {
|
if (pending.extendPart1Path && pending.extendTmpDir) {
|
||||||
if (!existsSync(pending.extendPart1Path)) {
|
if (!existsSync(pending.extendPart1Path)) {
|
||||||
removeExtendTemp(pending.extendTmpDir)
|
removeExtendTemp(pending.extendTmpDir)
|
||||||
@@ -91,6 +123,8 @@ export async function completePendingIfReady(pending: PendingJob) {
|
|||||||
}
|
}
|
||||||
removeExtendTemp(pending.extendTmpDir)
|
removeExtendTemp(pending.extendTmpDir)
|
||||||
}
|
}
|
||||||
|
const afterStitch = alreadySaved()
|
||||||
|
if (afterStitch) return afterStitch
|
||||||
const clip = await saveClip({
|
const clip = await saveClip({
|
||||||
ownerKey: pending.ownerKey,
|
ownerKey: pending.ownerKey,
|
||||||
folderId: pending.folderId,
|
folderId: pending.folderId,
|
||||||
|
|||||||
+127
-108
@@ -71,8 +71,10 @@ export function emitChainJob(job: Job, event: JobEvent, samplePct?: number) {
|
|||||||
export function watchComfyJob(job: Job, options: { persist?: boolean } = {}): Promise<void> {
|
export function watchComfyJob(job: Job, options: { persist?: boolean } = {}): Promise<void> {
|
||||||
const persist = options.persist !== false
|
const persist = options.persist !== false
|
||||||
job.socketReady = false
|
job.socketReady = false
|
||||||
|
job.saving = false
|
||||||
const ws = new WebSocket(comfyWsUrl(job.clientId))
|
const ws = new WebSocket(comfyWsUrl(job.clientId))
|
||||||
let settled = false
|
let settled = false
|
||||||
|
let finishing = false
|
||||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
@@ -111,7 +113,7 @@ export function watchComfyJob(job: Job, options: { persist?: boolean } = {}): Pr
|
|||||||
}
|
}
|
||||||
const idleLeft = IDLE_MS - (Date.now() - lastActivity)
|
const idleLeft = IDLE_MS - (Date.now() - lastActivity)
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
if (settled) return
|
if (settled || finishing) return
|
||||||
if (Date.now() - lastActivity >= IDLE_MS) {
|
if (Date.now() - lastActivity >= IDLE_MS) {
|
||||||
void fail('Timed out waiting for ComfyUI progress (30 minutes with no updates). The desktop job may still be running.')
|
void fail('Timed out waiting for ComfyUI progress (30 minutes with no updates). The desktop job may still be running.')
|
||||||
return
|
return
|
||||||
@@ -121,7 +123,7 @@ export function watchComfyJob(job: Job, options: { persist?: boolean } = {}): Pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fail = async (error: string) => {
|
const fail = async (error: string) => {
|
||||||
if (settled) return
|
if (settled || finishing) return
|
||||||
settled = true
|
settled = true
|
||||||
cleanup()
|
cleanup()
|
||||||
job.status = job.status === 'cancelled' ? 'cancelled' : 'error'
|
job.status = job.status === 'cancelled' ? 'cancelled' : 'error'
|
||||||
@@ -133,119 +135,136 @@ export function watchComfyJob(job: Job, options: { persist?: boolean } = {}): Pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
const succeed = async () => {
|
const succeed = async () => {
|
||||||
if (settled || !job.promptId) return false
|
if (settled || finishing || !job.promptId) return false
|
||||||
const history = await fetchHistory(job.promptId)
|
finishing = true
|
||||||
const video = extractVideo(history, job.promptId)
|
job.saving = true
|
||||||
if (!video) return false
|
|
||||||
settled = true
|
|
||||||
cleanup()
|
|
||||||
try {
|
try {
|
||||||
job.video = video
|
const history = await fetchHistory(job.promptId)
|
||||||
const stitching = Boolean(job.library?.extendPart1Path && job.library.extendTmpDir)
|
const video = extractVideo(history, job.promptId)
|
||||||
emitLocal({
|
if (!video) {
|
||||||
type: 'status',
|
finishing = false
|
||||||
message: stitching ? 'Extracting last frame & stitching extension...' : (persist ? 'Saving to library...' : 'Downloading segment...'),
|
job.saving = false
|
||||||
progress: stitching ? 97 : 98
|
return false
|
||||||
})
|
|
||||||
if (job.library) {
|
|
||||||
let buffer = await downloadComfyVideo(video)
|
|
||||||
if (stitching) {
|
|
||||||
if (!existsSync(job.library.extendPart1Path!)) {
|
|
||||||
throw new Error('Extension source clip was missing during stitch')
|
|
||||||
}
|
|
||||||
buffer = await stitchExtension({
|
|
||||||
part1Path: job.library.extendPart1Path!,
|
|
||||||
part2: buffer,
|
|
||||||
tmpDir: job.library.extendTmpDir!
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const clip = await saveClip({
|
|
||||||
ownerKey: job.library.ownerKey,
|
|
||||||
folderId: job.library.folderId,
|
|
||||||
name: job.library.name,
|
|
||||||
prompt: job.library.prompt,
|
|
||||||
aspect: job.library.aspect,
|
|
||||||
width: job.library.width,
|
|
||||||
height: job.library.height,
|
|
||||||
steps: job.library.steps,
|
|
||||||
turbo: job.library.turbo,
|
|
||||||
seed: job.library.seed,
|
|
||||||
hideThumbnail: job.library.hideThumbnail,
|
|
||||||
hideInput: job.library.hideInput,
|
|
||||||
video: buffer,
|
|
||||||
thumb: job.library.thumb,
|
|
||||||
comfyFilename: video.filename,
|
|
||||||
cfg: job.library.cfg,
|
|
||||||
fps: job.library.fps,
|
|
||||||
samplerName: job.library.samplerName,
|
|
||||||
scheduler: job.library.scheduler,
|
|
||||||
familyId: job.library.familyId || (job.library.familyId = crypto.randomUUID()),
|
|
||||||
parentClipId: job.library.parentClipId,
|
|
||||||
chainIndex: job.library.chainIndex
|
|
||||||
})
|
|
||||||
job.clipId = clip.id
|
|
||||||
job.hideThumbnail = clip.hideThumbnail
|
|
||||||
job.library.thumb = undefined
|
|
||||||
job.library.name = nextClipPartName(clip.name)
|
|
||||||
job.library.parentClipId = clip.id
|
|
||||||
await purgeComfyArtifacts({
|
|
||||||
video,
|
|
||||||
imageName: job.library.imageName,
|
|
||||||
imageSubfolder: job.library.imageSubfolder,
|
|
||||||
promptId: job.promptId
|
|
||||||
})
|
|
||||||
if (!persist) {
|
|
||||||
job.segmentBuffer = buffer
|
|
||||||
deletePendingJob(job.id)
|
|
||||||
emitLocal({
|
|
||||||
type: 'checkpoint',
|
|
||||||
message: stitching ? 'Extension checkpoint saved' : 'Initial segment saved',
|
|
||||||
progress: 99,
|
|
||||||
clipId: clip.id,
|
|
||||||
hideThumbnail: job.hideThumbnail,
|
|
||||||
folderLocked: job.library.folderLocked
|
|
||||||
})
|
|
||||||
resolve()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
removeExtendTemp(job.library.extendTmpDir)
|
|
||||||
job.library.extendTmpDir = undefined
|
|
||||||
}
|
}
|
||||||
job.status = 'complete'
|
if (settled) return false
|
||||||
emitJob(job, {
|
settled = true
|
||||||
type: 'complete',
|
cleanup()
|
||||||
message: job.library?.folderLocked
|
try {
|
||||||
? 'Saved to the locked folder. Unlock it to view.'
|
job.video = video
|
||||||
: (job.library?.extendSourceClipId || (job.library?.chainTotal || 1) > 1 ? 'Extended video ready' : 'Video ready'),
|
const stitching = Boolean(job.library?.extendPart1Path && job.library.extendTmpDir)
|
||||||
progress: 100,
|
emitLocal({
|
||||||
filename: job.library?.folderLocked ? undefined : video.filename,
|
type: 'status',
|
||||||
subfolder: job.library?.folderLocked ? undefined : video.subfolder,
|
message: stitching ? 'Extracting last frame & stitching extension...' : (persist ? 'Saving to library...' : 'Downloading segment...'),
|
||||||
mediaType: job.library?.folderLocked ? undefined : video.type,
|
progress: stitching ? 97 : 98
|
||||||
clipId: job.clipId,
|
})
|
||||||
hideThumbnail: job.hideThumbnail,
|
if (job.library) {
|
||||||
folderLocked: job.library?.folderLocked,
|
let buffer = await downloadComfyVideo(video)
|
||||||
chainStep: job.library?.chainStep,
|
if (stitching) {
|
||||||
chainTotal: job.library?.chainTotal,
|
if (!existsSync(job.library.extendPart1Path!)) {
|
||||||
chainLabel: job.library?.chainLabel
|
throw new Error('Extension source clip was missing during stitch')
|
||||||
})
|
}
|
||||||
deletePendingJob(job.id)
|
buffer = await stitchExtension({
|
||||||
} catch (saveError) {
|
part1Path: job.library.extendPart1Path!,
|
||||||
removeExtendTemp(job.library?.extendTmpDir)
|
part2: buffer,
|
||||||
const message = saveError instanceof Error ? saveError.message : String(saveError)
|
tmpDir: job.library.extendTmpDir!
|
||||||
job.status = 'error'
|
})
|
||||||
job.error = persist
|
}
|
||||||
? `Video generated but library save failed: ${message}`
|
const clip = await saveClip({
|
||||||
: `Segment finished but could not be prepared: ${message}`
|
ownerKey: job.library.ownerKey,
|
||||||
emitChainJob(job, { type: 'error', error: job.error, message: job.error })
|
folderId: job.library.folderId,
|
||||||
|
name: job.library.name,
|
||||||
|
prompt: job.library.prompt,
|
||||||
|
aspect: job.library.aspect,
|
||||||
|
width: job.library.width,
|
||||||
|
height: job.library.height,
|
||||||
|
steps: job.library.steps,
|
||||||
|
turbo: job.library.turbo,
|
||||||
|
seed: job.library.seed,
|
||||||
|
hideThumbnail: job.library.hideThumbnail,
|
||||||
|
hideInput: job.library.hideInput,
|
||||||
|
video: buffer,
|
||||||
|
thumb: job.library.thumb,
|
||||||
|
comfyFilename: video.filename,
|
||||||
|
cfg: job.library.cfg,
|
||||||
|
fps: job.library.fps,
|
||||||
|
samplerName: job.library.samplerName,
|
||||||
|
scheduler: job.library.scheduler,
|
||||||
|
familyId: job.library.familyId || (job.library.familyId = crypto.randomUUID()),
|
||||||
|
parentClipId: job.library.parentClipId,
|
||||||
|
chainIndex: job.library.chainIndex
|
||||||
|
})
|
||||||
|
job.clipId = clip.id
|
||||||
|
job.savedPromptId = job.promptId
|
||||||
|
job.hideThumbnail = clip.hideThumbnail
|
||||||
|
job.library.thumb = undefined
|
||||||
|
job.library.name = nextClipPartName(clip.name)
|
||||||
|
job.library.parentClipId = clip.id
|
||||||
|
await purgeComfyArtifacts({
|
||||||
|
video,
|
||||||
|
imageName: job.library.imageName,
|
||||||
|
imageSubfolder: job.library.imageSubfolder,
|
||||||
|
promptId: job.promptId
|
||||||
|
})
|
||||||
|
if (!persist) {
|
||||||
|
job.segmentBuffer = buffer
|
||||||
|
deletePendingJob(job.id)
|
||||||
|
job.saving = false
|
||||||
|
emitLocal({
|
||||||
|
type: 'checkpoint',
|
||||||
|
message: stitching ? 'Extension checkpoint saved' : 'Initial segment saved',
|
||||||
|
progress: 99,
|
||||||
|
clipId: clip.id,
|
||||||
|
hideThumbnail: job.hideThumbnail,
|
||||||
|
folderLocked: job.library.folderLocked
|
||||||
|
})
|
||||||
|
resolve()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
removeExtendTemp(job.library.extendTmpDir)
|
||||||
|
job.library.extendTmpDir = undefined
|
||||||
|
}
|
||||||
|
job.status = 'complete'
|
||||||
|
emitJob(job, {
|
||||||
|
type: 'complete',
|
||||||
|
message: job.library?.folderLocked
|
||||||
|
? 'Saved to the locked folder. Unlock it to view.'
|
||||||
|
: (job.library?.extendSourceClipId || (job.library?.chainTotal || 1) > 1 ? 'Extended video ready' : 'Video ready'),
|
||||||
|
progress: 100,
|
||||||
|
filename: job.library?.folderLocked ? undefined : video.filename,
|
||||||
|
subfolder: job.library?.folderLocked ? undefined : video.subfolder,
|
||||||
|
mediaType: job.library?.folderLocked ? undefined : video.type,
|
||||||
|
clipId: job.clipId,
|
||||||
|
hideThumbnail: job.hideThumbnail,
|
||||||
|
folderLocked: job.library?.folderLocked,
|
||||||
|
chainStep: job.library?.chainStep,
|
||||||
|
chainTotal: job.library?.chainTotal,
|
||||||
|
chainLabel: job.library?.chainLabel
|
||||||
|
})
|
||||||
|
deletePendingJob(job.id)
|
||||||
|
job.saving = false
|
||||||
|
} catch (saveError) {
|
||||||
|
job.saving = false
|
||||||
|
removeExtendTemp(job.library?.extendTmpDir)
|
||||||
|
const message = saveError instanceof Error ? saveError.message : String(saveError)
|
||||||
|
job.status = 'error'
|
||||||
|
job.error = persist
|
||||||
|
? `Video generated but library save failed: ${message}`
|
||||||
|
: `Segment finished but could not be prepared: ${message}`
|
||||||
|
emitChainJob(job, { type: 'error', error: job.error, message: job.error })
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
finishing = false
|
||||||
|
job.saving = false
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
resolve()
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let dropMisses = 0
|
let dropMisses = 0
|
||||||
|
|
||||||
const pollHistory = async () => {
|
const pollHistory = async () => {
|
||||||
if (settled || !job.promptId) return
|
if (settled || finishing || !job.promptId) return
|
||||||
try {
|
try {
|
||||||
const inspected = inspectHistory(await fetchHistory(job.promptId), job.promptId)
|
const inspected = inspectHistory(await fetchHistory(job.promptId), job.promptId)
|
||||||
if (inspected.error) {
|
if (inspected.error) {
|
||||||
@@ -282,7 +301,7 @@ export function watchComfyJob(job: Job, options: { persist?: boolean } = {}): Pr
|
|||||||
})
|
})
|
||||||
|
|
||||||
ws.addEventListener('close', () => {
|
ws.addEventListener('close', () => {
|
||||||
if (settled || !job.promptId) return
|
if (settled || finishing || !job.promptId) return
|
||||||
void pollHistory()
|
void pollHistory()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user