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' }}
+
+ {{ forceClearing ? 'Force clearing…' : 'Force clear stuck queue' }}
+
{{ 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 @@
- Clear queue
+ Clear waiting
+
+
+ {{ forceClearing ? 'Force clearing…' : 'Force clear' }}
{{ 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)
}
}