From 780b407b3c27af9008371fb29cfe4f2782c3ea03 Mon Sep 17 00:00:00 2001 From: Towsty Date: Thu, 3 Sep 2026 23:03:05 -0500 Subject: [PATCH] Add Force clear for stuck queues including running YuE jobs. Queue page and Music get a nuclear clear that cancels studio/live work, wipes shot batches, and force-stops Comfy so images/video/other music can run again. Co-authored-by: Cursor --- components/AppHeader.vue | 4 +-- components/SettingsForm.vue | 2 +- pages/music.vue | 39 ++++++++++++++++++++++++ pages/queue.vue | 44 +++++++++++++++++++++++++--- server/api/comfy/force-reset.post.ts | 2 +- server/utils/shotQueue.ts | 14 +++++++++ server/utils/studioQueue.ts | 10 +++++-- 7 files changed, 105 insertions(+), 10 deletions(-) diff --git a/components/AppHeader.vue b/components/AppHeader.vue index ac373c9..ec8c599 100644 --- a/components/AppHeader.vue +++ b/components/AppHeader.vue @@ -84,7 +84,7 @@ type="button" class="block w-full rounded-xl px-3 py-2 text-left text-sm text-red-200 hover:bg-white/5 hover:text-red-100 disabled:cursor-not-allowed disabled:opacity-50" :disabled="gpuForceResetting || gpuSleeping || gpuWaking" - title="Clear stuck jobs and kill Comfy even if a generation is wedged" + title="Clear stuck Studio/music/shot jobs and kill Comfy even if a generation is wedged" role="menuitem" @click="forceResetGpu" >{{ gpuForceResetting ? 'Force resetting…' : 'Force reset' }} @@ -275,7 +275,7 @@ async function sleepGpu() { async function forceResetGpu() { if (gpuForceResetting.value || gpuSleeping.value || gpuWaking.value) return const ok = window.confirm( - 'Force reset clears stuck Studio/queue jobs and kills Comfy even if a generation is wedged. Continue?' + 'Force reset cancels every running/waiting Studio and music job, wipes shot batches, and kills Comfy even if wedged. Continue?' ) if (!ok) return menuOpen.value = false diff --git a/components/SettingsForm.vue b/components/SettingsForm.vue index fe9a0e8..6db16a9 100644 --- a/components/SettingsForm.vue +++ b/components/SettingsForm.vue @@ -735,7 +735,7 @@ async function wakeGpu() { async function forceResetGpu() { if (gpuForceResetting.value || gpuSleeping.value || gpuWaking.value) return const ok = window.confirm( - 'Force reset clears stuck Studio/queue jobs and kills Comfy even if a generation is wedged. Continue?' + 'Force reset cancels every running/waiting Studio and music job, wipes shot batches, and kills Comfy even if wedged. Continue?' ) if (!ok) return gpuForceResetting.value = true diff --git a/pages/music.vue b/pages/music.vue index 5e4082f..7a7830b 100644 --- a/pages/music.vue +++ b/pages/music.vue @@ -173,6 +173,16 @@ /> {{ busy ? (status || 'Generating…') : 'Generate' }} +

{{ blockReason }}

Waiting in the job queue. @@ -270,6 +280,7 @@ const lyricsEl = ref(null) const ace15 = ref(false) const engineFamily = ref<'ace' | 'yue'>('ace') const busy = ref(false) +const forceClearing = ref(false) const error = ref('') const status = ref('') const progress = ref(0) @@ -568,6 +579,34 @@ function listen(id: string) { } } +async function forceClearQueue() { + if (forceClearing.value) return + if (!confirm( + 'Force clear cancels every running and waiting job (including this track), wipes shot batches, and kills Comfy if wedged. Continue?' + )) return + forceClearing.value = true + error.value = '' + try { + await $fetch('/api/comfy/force-reset', { method: 'POST' }) + settled = true + busy.value = false + queued.value = false + progress.value = 0 + status.value = 'Queue force-cleared. Poke Comfy when you want the GPU again.' + studioJobId.value = '' + liveMusicId.value = '' + jobId.value = '' + if (source) { + source.close() + source = null + } + } catch (err: any) { + error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not force-clear the queue' + } finally { + forceClearing.value = false + } +} + async function generate() { if (blocked.value) { error.value = blockReason.value diff --git a/pages/queue.vue b/pages/queue.vue index 0a1dfb2..e731f66 100644 --- a/pages/queue.vue +++ b/pages/queue.vue @@ -4,6 +4,7 @@ subtitle="Queue" current="queue" :queue-count="waitingCount || pendingCount" + @gpu="loadQueues" />

@@ -31,15 +32,25 @@ +

{{ error }}

+

{{ forceClearNote }}

Will pause after this video. The current clip keeps generating; this batch will stop after it finishes.

@@ -557,6 +568,8 @@ const pendingCount = ref(0) const queuePaused = ref(false) const error = ref('') const busyId = ref('') +const forceClearing = ref(false) +const forceClearNote = ref('') const liveMessage = ref>({}) const dirty = new Set() const sources = new Map() @@ -1194,11 +1207,11 @@ async function remove(queue: Queue) { async function clearAll() { const n = clearableCount.value if (!n) { - error.value = 'Pause the job that’s generating before clearing the queue.' + error.value = 'Nothing waiting to clear. Use Force clear if a running job is stuck.' return } const running = jobs.value.some(job => job.status === 'running') || orphans.value.some(queue => queue.status === 'running') - const extra = running ? ' The job that’s generating will be left until you stop it.' : '' + const extra = running ? ' The job that’s generating will be left until you Force clear or cancel it.' : '' if (!confirm(`Clear waiting and paused jobs from the queue? Saved clips stay in the library.${extra}`)) return error.value = '' try { @@ -1212,6 +1225,29 @@ async function clearAll() { } } +async function forceClearAll() { + if (forceClearing.value) return + if (!confirm( + 'Force clear cancels every running and waiting job (including stuck YuE/music), wipes shot batches, and kills Comfy if it is wedged. Saved library clips stay. Continue?' + )) return + forceClearing.value = true + error.value = '' + forceClearNote.value = '' + try { + const result = await $fetch<{ + message?: string + cleared?: { cancelledStudio?: number; clearedPending?: number; cancelledLive?: number; clearedShotQueues?: number } + }>('/api/comfy/force-reset', { method: 'POST' }) + const c = result.cleared || {} + await loadQueues() + forceClearNote.value = `Force cleared · studio ${c.cancelledStudio || 0}, live ${c.cancelledLive || 0}, shots ${c.clearedShotQueues || 0}. Comfy is stopped — Poke when ready.` + } catch (err: any) { + error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not force-clear the queue' + } finally { + forceClearing.value = false + } +} + let saveTimer: ReturnType | null = null function saveSegment(queue: Queue, segment: Segment) { markDirty(queue.id, segment.index) diff --git a/server/api/comfy/force-reset.post.ts b/server/api/comfy/force-reset.post.ts index 2280384..5c0c828 100644 --- a/server/api/comfy/force-reset.post.ts +++ b/server/api/comfy/force-reset.post.ts @@ -8,7 +8,7 @@ export default defineEventHandler(async (event) => { const comfy = await requestComfyForceReset() return { ok: true, - message: 'Cleared stuck jobs and force-stopped Comfy. Use Wake / Poke when you want the GPU again.', + message: 'Cleared every stuck Studio/music/shot job and force-stopped Comfy. Use Wake / Poke when you want the GPU again.', cleared, comfy } diff --git a/server/utils/shotQueue.ts b/server/utils/shotQueue.ts index 5daf799..4edc0b6 100644 --- a/server/utils/shotQueue.ts +++ b/server/utils/shotQueue.ts @@ -484,6 +484,20 @@ export async function clearShotQueues(owner: string) { }) } +/** Nuclear clear: wipe every shot batch for this owner, including ones marked running. */ +export async function forceClearAllShotQueues(owner: string) { + const before = listShotQueues(owner) + for (const queue of before) { + markShotQueueCancelled(queue.id) + activeJobs.delete(queue.id) + } + return mutate(owner, (queues) => { + const removed = queues.length + queues.splice(0, queues.length) + return { removed } + }) +} + export function listOwnersWithQueues() { const root = join(libraryRoot(), 'users') if (!existsSync(root)) return [] diff --git a/server/utils/studioQueue.ts b/server/utils/studioQueue.ts index 47bab60..c1f5c41 100644 --- a/server/utils/studioQueue.ts +++ b/server/utils/studioQueue.ts @@ -483,7 +483,7 @@ export async function cancelStudioJob(owner: string, id: string) { return result } -/** Wipe waiting/running/held studio rows + live/pending jobs for this owner (stuck recovery). */ +/** Wipe waiting/running/held studio rows + live/pending/shot queues for this owner (stuck recovery). */ export async function clearStuckStudioWork(owner: string) { const liveIds = new Set() const cancelledStudio: string[] = [] @@ -530,10 +530,16 @@ export async function clearStuckStudioWork(owner: string) { for (const id of liveIds) deletePendingJob(id) + const { forceClearAllShotQueues } = await import('~/server/utils/shotQueue') + const shots = await forceClearAllShotQueues(owner).catch(() => ({ removed: 0 })) + + kickStudioQueue() + return { cancelledStudio: cancelledStudio.length, clearedPending: clearedPending.length, - cancelledLive: liveIds.size + cancelledLive: liveIds.size, + clearedShotQueues: Number(shots?.removed || 0) } }