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 <cursoragent@cursor.com>
This commit is contained in:
@@ -6481,8 +6481,12 @@ async function cancelJob() {
|
|||||||
? 'video'
|
? 'video'
|
||||||
: studioMode.value === 'edit' ? 'edit' : 'video'
|
: studioMode.value === 'edit' ? 'edit' : 'video'
|
||||||
const id = kind === 'edit' ? editJobId.value : videoJobId.value
|
const id = kind === 'edit' ? editJobId.value : videoJobId.value
|
||||||
|
const studioId = outputStudioJobId.value
|
||||||
try {
|
try {
|
||||||
await $fetch('/api/interrupt', { method: 'POST', body: { jobId: id, kind } })
|
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.'
|
if (kind === 'edit') editStatusMessage.value = 'Edit cancelled. GPU is idle — edit again.'
|
||||||
else statusMessage.value = 'Job cancelled. GPU is idle — generate again.'
|
else statusMessage.value = 'Job cancelled. GPU is idle — generate again.'
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -6504,6 +6508,9 @@ async function cancelJob() {
|
|||||||
stopTimer('video')
|
stopTimer('video')
|
||||||
stopListen('video')
|
stopListen('video')
|
||||||
clearActiveJob('video')
|
clearActiveJob('video')
|
||||||
|
outputStudioJobId.value = ''
|
||||||
|
activeQueueId.value = ''
|
||||||
|
try { localStorage.removeItem(QUEUE_STORE) } catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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<ReturnType<typeof getJob>>) {
|
||||||
|
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) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody<{ jobId?: string; kind?: string }>(event).catch(() => ({}))
|
const body = await readBody<{ jobId?: string; kind?: string }>(event).catch(() => ({}))
|
||||||
const jobId = String(body?.jobId || '')
|
const jobId = String(body?.jobId || '')
|
||||||
const job = jobId ? getJob(jobId) : undefined
|
const kind = String(body?.kind || '')
|
||||||
if (job) {
|
const named = jobId ? getJob(jobId) : undefined
|
||||||
job.status = 'cancelled'
|
const targets = named
|
||||||
emitJob(job, { type: 'status', message: 'Cancelling job...' })
|
? [named]
|
||||||
removeExtendTemp(job.library?.extendTmpDir)
|
: listJobs().filter((job) => {
|
||||||
if (job.library?.queueId) {
|
if (job.status === 'complete' || job.status === 'error' || job.status === 'cancelled') return false
|
||||||
job.library.stopAfterCurrent = true
|
if (kind === 'edit') return job.kind === 'edit'
|
||||||
job.library.queueAutoRun = false
|
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)
|
await pauseShotQueue(job.library.ownerKey, job.library.queueId).catch(() => null)
|
||||||
setQueueJob(job.library.queueId, null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (jobId) deletePendingJob(jobId)
|
if (jobId) deletePendingJob(jobId)
|
||||||
const ok = await interruptComfy()
|
const { owner } = assertLibraryOwner(event)
|
||||||
if (job) {
|
for (const row of listStudioJobs(owner)) {
|
||||||
emitJob(job, { type: 'error', error: 'Job interrupted.', message: 'Job interrupted.' })
|
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 }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -144,8 +144,13 @@ export async function queuePrompt(graph: unknown, clientId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function interruptComfy() {
|
export async function interruptComfy() {
|
||||||
const res = await comfyFetch('/interrupt', { method: 'POST' })
|
const res = await comfyFetch('/interrupt', { method: 'POST' }).catch(() => null)
|
||||||
return res.ok
|
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) {
|
export async function fetchHistory(promptId: string) {
|
||||||
|
|||||||
@@ -259,10 +259,13 @@ function seedCurrentVideo(job: Job) {
|
|||||||
|
|
||||||
function chainShouldHold(job: Job) {
|
function chainShouldHold(job: Job) {
|
||||||
if (!job.library) return false
|
if (!job.library) return false
|
||||||
|
if (job.status === 'cancelled' || job.status === 'error') return true
|
||||||
if (job.library.stopAfterCurrent) return true
|
if (job.library.stopAfterCurrent) return true
|
||||||
|
if (job.library.queueId && isShotQueueCancelled(job.library.queueId)) return true
|
||||||
const queue = job.library.queueId
|
const queue = job.library.queueId
|
||||||
? getShotQueue(job.library.ownerKey, job.library.queueId)
|
? getShotQueue(job.library.ownerKey, job.library.queueId)
|
||||||
: null
|
: null
|
||||||
|
if (job.library.queueId && !queue) return true
|
||||||
if (queue?.stopAfterCurrent || queue?.status === 'paused') return true
|
if (queue?.stopAfterCurrent || queue?.status === 'paused') return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -371,6 +374,11 @@ export async function continueQueuedExtensions(
|
|||||||
? detail
|
? detail
|
||||||
: `Could not extract the last frame for the next extension: ${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)
|
const frame = readFileSync(framePath)
|
||||||
if (!frame.length || frame.length < 64) {
|
if (!frame.length || frame.length < 64) {
|
||||||
throw new Error('Could not extract the last frame for the next extension: the frame file was empty')
|
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
|
job.library.shotPermanenceRefs || params.shotPermanenceRefs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assertJobActive(job)
|
||||||
|
if (chainShouldHold(job)) {
|
||||||
|
job.library.stopAfterCurrent = true
|
||||||
|
break
|
||||||
|
}
|
||||||
await ensureComfyReady(ready)
|
await ensureComfyReady(ready)
|
||||||
|
assertJobActive(job)
|
||||||
|
if (chainShouldHold(job)) {
|
||||||
|
job.library.stopAfterCurrent = true
|
||||||
|
break
|
||||||
|
}
|
||||||
await queueMiniMax(job, {
|
await queueMiniMax(job, {
|
||||||
prompt: ext.prompt,
|
prompt: ext.prompt,
|
||||||
image: { filename: 'last_frame.png', data: frame, type: 'image/png' },
|
image: { filename: 'last_frame.png', data: frame, type: 'image/png' },
|
||||||
|
|||||||
Reference in New Issue
Block a user