From 9f82377b5290856db9028bc7eb837acaa5aef6c4 Mon Sep 17 00:00:00 2001 From: Towsty Date: Sun, 30 Aug 2026 13:07:17 -0500 Subject: [PATCH] Stop a deleted video chain from keeping the GPU. Interrupt now clears Comfy, and a missing shot list no longer continues to the next extension. Co-authored-by: Cursor --- pages/index.vue | 7 +++++ server/api/interrupt.post.ts | 59 +++++++++++++++++++++++++++--------- server/utils/comfy.ts | 9 ++++-- server/utils/videoChain.ts | 18 +++++++++++ 4 files changed, 77 insertions(+), 16 deletions(-) diff --git a/pages/index.vue b/pages/index.vue index 29c643b..f9b29f2 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -6481,8 +6481,12 @@ async function cancelJob() { ? 'video' : studioMode.value === 'edit' ? 'edit' : 'video' const id = kind === 'edit' ? editJobId.value : videoJobId.value + const studioId = outputStudioJobId.value try { await $fetch('/api/interrupt', { method: 'POST', body: { jobId: id, kind } }) + if (studioId) { + await $fetch(`/api/studio-queue/${studioId}`, { method: 'DELETE' }).catch(() => null) + } if (kind === 'edit') editStatusMessage.value = 'Edit cancelled. GPU is idle — edit again.' else statusMessage.value = 'Job cancelled. GPU is idle — generate again.' } catch (error: any) { @@ -6504,6 +6508,9 @@ async function cancelJob() { stopTimer('video') stopListen('video') clearActiveJob('video') + outputStudioJobId.value = '' + activeQueueId.value = '' + try { localStorage.removeItem(QUEUE_STORE) } catch { /* ignore */ } } } diff --git a/server/api/interrupt.post.ts b/server/api/interrupt.post.ts index 78a9cc4..cd3f9f6 100644 --- a/server/api/interrupt.post.ts +++ b/server/api/interrupt.post.ts @@ -1,24 +1,55 @@ -import { pauseShotQueue, setQueueJob } from '~/server/utils/shotQueue' +import { emitJob, getJob, listJobs } from '~/server/utils/jobs' +import { interruptComfy } from '~/server/utils/comfy' +import { deletePendingJob } from '~/server/utils/pending' +import { assertLibraryOwner, removeExtendTemp } from '~/server/utils/library' +import { markShotQueueCancelled, pauseShotQueue, setQueueJob } from '~/server/utils/shotQueue' +import { cancelStudioJob, listStudioJobs } from '~/server/utils/studioQueue' + +function stopLiveJob(job: NonNullable>) { + job.status = 'cancelled' + job.error = 'Job interrupted.' + emitJob(job, { type: 'status', message: 'Cancelling job...' }) + removeExtendTemp(job.library?.extendTmpDir) + if (job.library) { + job.library.stopAfterCurrent = true + job.library.queueAutoRun = false + job.library.chainContinuing = false + if (job.library.queueId) { + markShotQueueCancelled(job.library.queueId) + setQueueJob(job.library.queueId, null) + } + } + deletePendingJob(job.id) + emitJob(job, { type: 'error', error: 'Job interrupted.', message: 'Job interrupted.' }) +} export default defineEventHandler(async (event) => { const body = await readBody<{ jobId?: string; kind?: string }>(event).catch(() => ({})) const jobId = String(body?.jobId || '') - const job = jobId ? getJob(jobId) : undefined - if (job) { - job.status = 'cancelled' - emitJob(job, { type: 'status', message: 'Cancelling job...' }) - removeExtendTemp(job.library?.extendTmpDir) - if (job.library?.queueId) { - job.library.stopAfterCurrent = true - job.library.queueAutoRun = false + const kind = String(body?.kind || '') + const named = jobId ? getJob(jobId) : undefined + const targets = named + ? [named] + : listJobs().filter((job) => { + if (job.status === 'complete' || job.status === 'error' || job.status === 'cancelled') return false + if (kind === 'edit') return job.kind === 'edit' + if (kind === 'video') return job.kind !== 'edit' + return true + }) + for (const job of targets) { + stopLiveJob(job) + if (job.library?.queueId && job.library.ownerKey) { await pauseShotQueue(job.library.ownerKey, job.library.queueId).catch(() => null) - setQueueJob(job.library.queueId, null) } } if (jobId) deletePendingJob(jobId) - const ok = await interruptComfy() - if (job) { - emitJob(job, { type: 'error', error: 'Job interrupted.', message: 'Job interrupted.' }) + const { owner } = assertLibraryOwner(event) + for (const row of listStudioJobs(owner)) { + if (row.status !== 'running' && row.status !== 'held') continue + if (kind === 'edit' && row.kind !== 'edit') continue + if (kind === 'video' && row.kind === 'edit') continue + await cancelStudioJob(owner, row.id).catch(() => null) } - return { ok } + const ok = await interruptComfy() + return { ok, cancelled: targets.length } }) diff --git a/server/utils/comfy.ts b/server/utils/comfy.ts index 747e633..7981bf6 100644 --- a/server/utils/comfy.ts +++ b/server/utils/comfy.ts @@ -144,8 +144,13 @@ export async function queuePrompt(graph: unknown, clientId: string) { } export async function interruptComfy() { - const res = await comfyFetch('/interrupt', { method: 'POST' }) - return res.ok + const res = await comfyFetch('/interrupt', { method: 'POST' }).catch(() => null) + await comfyFetch('/queue', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ clear: true }) + }).catch(() => null) + return Boolean(res?.ok) } export async function fetchHistory(promptId: string) { diff --git a/server/utils/videoChain.ts b/server/utils/videoChain.ts index 7140b79..b097c6c 100644 --- a/server/utils/videoChain.ts +++ b/server/utils/videoChain.ts @@ -259,10 +259,13 @@ function seedCurrentVideo(job: Job) { function chainShouldHold(job: Job) { if (!job.library) return false + if (job.status === 'cancelled' || job.status === 'error') return true if (job.library.stopAfterCurrent) return true + if (job.library.queueId && isShotQueueCancelled(job.library.queueId)) return true const queue = job.library.queueId ? getShotQueue(job.library.ownerKey, job.library.queueId) : null + if (job.library.queueId && !queue) return true if (queue?.stopAfterCurrent || queue?.status === 'paused') return true return false } @@ -371,6 +374,11 @@ export async function continueQueuedExtensions( ? detail : `Could not extract the last frame for the next extension: ${detail}`) } + assertJobActive(job) + if (chainShouldHold(job)) { + job.library.stopAfterCurrent = true + break + } const frame = readFileSync(framePath) if (!frame.length || frame.length < 64) { throw new Error('Could not extract the last frame for the next extension: the frame file was empty') @@ -391,7 +399,17 @@ export async function continueQueuedExtensions( job.library.shotPermanenceRefs || params.shotPermanenceRefs ) + assertJobActive(job) + if (chainShouldHold(job)) { + job.library.stopAfterCurrent = true + break + } await ensureComfyReady(ready) + assertJobActive(job) + if (chainShouldHold(job)) { + job.library.stopAfterCurrent = true + break + } await queueMiniMax(job, { prompt: ext.prompt, image: { filename: 'last_frame.png', data: frame, type: 'image/png' },