Queue more video jobs while a long shot list is generating, and restore that job from Output.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+319
-33
@@ -25,9 +25,9 @@
|
||||
<NuxtLink
|
||||
to="/queue"
|
||||
class="rounded-full border px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white"
|
||||
:class="queuePendingCount ? 'border-amber-300/40 text-amber-100' : 'border-white/10'"
|
||||
:class="queueBadgeCount ? 'border-amber-300/40 text-amber-100' : 'border-white/10'"
|
||||
>
|
||||
Queue<span v-if="queuePendingCount" class="ml-1.5 text-amber-200">{{ queuePendingCount }}</span>
|
||||
Queue<span v-if="queueBadgeCount" class="ml-1.5 text-amber-200">{{ queueBadgeCount }}</span>
|
||||
</NuxtLink>
|
||||
<a v-if="authEnabled" href="/api/auth/logout" class="text-zinc-400 hover:text-white">Sign out</a>
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@
|
||||
<h2 class="font-display text-2xl font-bold">Input</h2>
|
||||
<p class="text-sm text-zinc-400">{{ studioMode === 'edit'
|
||||
? 'Drop the still to edit. An optional second still applies image 2 onto image 1. Files leave the desktop after save.'
|
||||
: 'Drop a still, describe the motion, then send the job to the desktop GPU.' }}</p>
|
||||
: 'Drop a still, describe the motion, then send the job. Generate stays available — a long shot list takes one queue slot, and you can line up more while it runs.' }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -660,8 +660,50 @@
|
||||
Cancel / Interrupt
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="studioMode === 'video' && videoBusy" class="text-xs text-zinc-400">The current job keeps running in Output. This submit waits in the job queue.</p>
|
||||
<p v-if="studioMode === 'edit' && editBlockReason" class="text-xs text-zinc-400">{{ editBlockReason }}</p>
|
||||
|
||||
<div v-if="studioMode === 'video' && studioJobs.length" class="rounded-2xl border border-white/10 bg-zinc-950/40 p-4">
|
||||
<h3 class="text-sm font-semibold text-zinc-100">Job queue</h3>
|
||||
<p class="mt-1 text-xs text-zinc-500">A long shot script is one slot. Run next inserts after the current shot as its own clip, then the original sequence continues.</p>
|
||||
<ul class="mt-3 space-y-2">
|
||||
<li
|
||||
v-for="job in studioJobs"
|
||||
:key="job.id"
|
||||
class="flex items-start gap-3 rounded-xl border px-3 py-2"
|
||||
:class="job.status === 'running'
|
||||
? 'border-amber-300/40 bg-amber-400/5'
|
||||
: job.cutIn
|
||||
? 'border-sky-300/30 bg-sky-400/5'
|
||||
: 'border-white/10 bg-zinc-950/50'"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium text-zinc-100">{{ job.name }}</p>
|
||||
<p class="mt-0.5 text-xs text-zinc-500">
|
||||
{{ studioJobStatusLabel(job) }}
|
||||
· {{ job.shotCount }} shot{{ job.shotCount === 1 ? '' : 's' }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="job.status === 'waiting' && canCutInStudioJob"
|
||||
type="button"
|
||||
class="shrink-0 rounded-full border border-sky-300/40 px-2.5 py-1 text-xs text-sky-100 hover:border-sky-200 hover:text-white"
|
||||
@click="cutInStudioJob(job.id)"
|
||||
>
|
||||
{{ job.cutIn ? 'Next' : 'Run next' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="job.status === 'waiting' || job.status === 'held'"
|
||||
type="button"
|
||||
class="shrink-0 text-xs text-zinc-400 hover:text-red-200"
|
||||
@click="dismissStudioJob(job.id)"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="drafts.length" class="rounded-2xl border border-amber-300/20 bg-amber-400/5 p-4">
|
||||
<h3 class="text-sm font-semibold text-amber-100">Held jobs</h3>
|
||||
<p class="mt-1 text-xs text-zinc-400">Comfy was still busy after 3 minutes. Click one to restore the still, prompt, and settings.</p>
|
||||
@@ -686,6 +728,17 @@
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="font-display text-2xl font-bold">Output</h2>
|
||||
<button
|
||||
v-if="canRestoreOutput"
|
||||
type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-full border border-white/15 text-zinc-300 hover:border-amber-300/50 hover:text-white"
|
||||
title="Load this job back into Input"
|
||||
@click="restoreOutputJob"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
|
||||
<path d="M13.586 3.586a2 2 0 1 1 2.828 2.828l-.793.793-2.828-2.828.793-.793ZM11.379 5.793 3 14.172V17h2.828l8.38-8.379-2.83-2.828Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="canClearPreview"
|
||||
type="button"
|
||||
@@ -1582,6 +1635,22 @@ const liveQueue = ref<{
|
||||
totalCount: number
|
||||
segments: Array<{ index: number; prompt: string; duration: number; status: string; clipId?: string }>
|
||||
} | null>(null)
|
||||
const studioJobs = ref<Array<{
|
||||
id: string
|
||||
status: string
|
||||
name: string
|
||||
prompt: string
|
||||
shotCount: number
|
||||
liveJobId?: string
|
||||
shotQueueId?: string
|
||||
stillId?: string
|
||||
hideThumbnail?: boolean
|
||||
folderLocked?: boolean
|
||||
cutIn?: boolean
|
||||
holdForCutIn?: boolean
|
||||
lastError?: string
|
||||
}>>([])
|
||||
const outputStudioJobId = ref('')
|
||||
const pickerOpen = ref(false)
|
||||
const pickerSlot = ref<'main' | 'editRef' | number>('main')
|
||||
const libraryFilter = ref<'all' | 'images' | 'videos'>('all')
|
||||
@@ -1758,7 +1827,7 @@ const queueReady = computed(() => {
|
||||
return extensionQueue.value.every(item => item.prompt.trim())
|
||||
})
|
||||
const videoGenerateDisabled = computed(() => {
|
||||
return videoBusy.value || (!textToVideo.value && !file.value) || !prompt.value.trim() || !folderId.value || !queueReady.value
|
||||
return (!textToVideo.value && !file.value) || !prompt.value.trim() || !folderId.value || !queueReady.value
|
||||
})
|
||||
const recommendDisabled = computed(() => {
|
||||
if (recommendBusy.value) return true
|
||||
@@ -1789,10 +1858,19 @@ const editBlockReason = computed(() => {
|
||||
})
|
||||
const generateLabel = computed(() => {
|
||||
const n = queuedExtensionCount.value
|
||||
if (videoBusy.value) return 'Generating…'
|
||||
if (videoBusy.value) return n ? `Queue ${n + 1} shots` : 'Queue job'
|
||||
if (!n) return 'Generate Video'
|
||||
return n === 1 ? 'Generate Initial + 1 Extension' : `Generate Initial + ${n} Extensions`
|
||||
})
|
||||
const studioWaitingCount = computed(() => studioJobs.value.filter(job => job.status === 'waiting').length)
|
||||
const queueBadgeCount = computed(() => studioWaitingCount.value + queuePendingCount.value)
|
||||
const canCutInStudioJob = computed(() => studioJobs.value.some(job => job.status === 'running' || job.status === 'held') || videoBusy.value)
|
||||
const outputRestoreId = computed(() => (
|
||||
outputStudioJobId.value
|
||||
|| studioJobs.value.find(job => job.liveJobId && job.liveJobId === videoJobId.value)?.id
|
||||
|| ''
|
||||
))
|
||||
const canRestoreOutput = computed(() => Boolean(outputRestoreId.value && outputProgressKind.value === 'video'))
|
||||
const queuedExtensionCount = computed(() => (
|
||||
shotScriptMode.value ? Math.max(0, parsedShots.value.length - 1) : extensionQueue.value.length
|
||||
))
|
||||
@@ -2153,6 +2231,7 @@ async function loadLibrary() {
|
||||
])
|
||||
applyLibrary(data)
|
||||
queuePendingCount.value = queued.pendingCount || 0
|
||||
await refreshStudioQueue()
|
||||
await refreshLiveQueue()
|
||||
}
|
||||
|
||||
@@ -2226,9 +2305,11 @@ onMounted(async () => {
|
||||
applyInstancePrivacy()
|
||||
healthTimer = setInterval(() => {
|
||||
void pollComfyHealth()
|
||||
void refreshStudioQueue()
|
||||
if (activeQueueId.value) void refreshLiveQueue()
|
||||
}, 5000)
|
||||
await resumeActiveJob()
|
||||
await refreshStudioQueue()
|
||||
await refreshLiveQueue()
|
||||
await loadPromptPresets()
|
||||
})
|
||||
@@ -2502,6 +2583,19 @@ function resetImage() {
|
||||
imageHeight.value = 0
|
||||
}
|
||||
|
||||
function clearVideoForm() {
|
||||
resetImage()
|
||||
prompt.value = ''
|
||||
clipName.value = ''
|
||||
extensionQueue.value = []
|
||||
shotScriptMode.value = false
|
||||
identityRefs.value.forEach((_, index) => clearIdentityRef(index))
|
||||
recommendText.value = ''
|
||||
recommendError.value = ''
|
||||
recommendApplied.value = false
|
||||
activeDraftId = ''
|
||||
}
|
||||
|
||||
function readFile(next: File, opts?: { keepThumbnailPref?: boolean }) {
|
||||
file.value = next
|
||||
preview.value = URL.createObjectURL(next)
|
||||
@@ -2635,6 +2729,166 @@ async function refreshLiveQueue() {
|
||||
}
|
||||
}
|
||||
|
||||
function studioJobStatusLabel(job: { status: string; cutIn?: boolean; holdForCutIn?: boolean; lastError?: string }) {
|
||||
if (job.status === 'running') return 'Generating'
|
||||
if (job.status === 'held') return job.holdForCutIn ? 'Paused for cut-in' : 'Paused'
|
||||
if (job.status === 'waiting') return job.cutIn ? 'Next after current shot' : 'Waiting'
|
||||
if (job.status === 'error') return job.lastError || 'Error'
|
||||
return job.status
|
||||
}
|
||||
|
||||
async function refreshStudioQueue() {
|
||||
const data = await $fetch<{
|
||||
jobs?: Array<{
|
||||
id: string
|
||||
status: string
|
||||
name: string
|
||||
prompt: string
|
||||
shotCount: number
|
||||
liveJobId?: string
|
||||
shotQueueId?: string
|
||||
stillId?: string
|
||||
hideThumbnail?: boolean
|
||||
folderLocked?: boolean
|
||||
cutIn?: boolean
|
||||
holdForCutIn?: boolean
|
||||
lastError?: string
|
||||
}>
|
||||
}>('/api/studio-queue').catch(() => ({ jobs: [] }))
|
||||
studioJobs.value = data.jobs || []
|
||||
const running = studioJobs.value.find(job => job.status === 'running' && job.liveJobId)
|
||||
if (running?.liveJobId && running.liveJobId !== videoJobId.value && !videoBusy.value) {
|
||||
outputStudioJobId.value = running.id
|
||||
videoJobId.value = running.liveJobId
|
||||
persistActiveJob('video', running.liveJobId, running.hideThumbnail === true, running.folderLocked === true)
|
||||
listen('video', running.liveJobId, running.hideThumbnail === true, running.folderLocked === true)
|
||||
if (running.shotQueueId) {
|
||||
persistActiveQueue(running.shotQueueId)
|
||||
void refreshLiveQueue()
|
||||
}
|
||||
videoBusy.value = true
|
||||
statusMessage.value = 'Starting queued job…'
|
||||
startTimer('video')
|
||||
} else if (running?.id && running.liveJobId === videoJobId.value) {
|
||||
outputStudioJobId.value = running.id
|
||||
}
|
||||
}
|
||||
|
||||
async function cutInStudioJob(id: string) {
|
||||
try {
|
||||
await $fetch(`/api/studio-queue/${id}/cut-in`, { method: 'POST' })
|
||||
toast('This job will run after the current shot, as its own clip.')
|
||||
await refreshStudioQueue()
|
||||
} catch (error: any) {
|
||||
toast(error?.data?.statusMessage || error?.statusMessage || 'Could not move that job next')
|
||||
}
|
||||
}
|
||||
|
||||
async function dismissStudioJob(id: string) {
|
||||
try {
|
||||
await $fetch(`/api/studio-queue/${id}`, { method: 'DELETE' })
|
||||
if (outputStudioJobId.value === id) outputStudioJobId.value = ''
|
||||
await refreshStudioQueue()
|
||||
} catch (error: any) {
|
||||
toast(error?.data?.statusMessage || error?.statusMessage || 'Could not remove that job')
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreStudioJob(id: string) {
|
||||
const job = await $fetch<{
|
||||
payload?: {
|
||||
prompt?: string
|
||||
name?: string
|
||||
folderId?: string
|
||||
aspect?: string
|
||||
width?: number
|
||||
height?: number
|
||||
turbo?: boolean
|
||||
seed?: number
|
||||
cfg?: number
|
||||
fps?: number
|
||||
samplerName?: string
|
||||
scheduler?: string
|
||||
duration?: number
|
||||
sound?: boolean
|
||||
workflow?: string
|
||||
useIdentityRefs?: boolean
|
||||
stillId?: string
|
||||
stillFilename?: string
|
||||
hideThumbnail?: boolean
|
||||
hideInput?: boolean
|
||||
referenceStillIds?: Array<string | null>
|
||||
extensions?: { prompt: string; duration: number }[]
|
||||
queueAutoRun?: boolean
|
||||
}
|
||||
}>(`/api/studio-queue/${id}`)
|
||||
const payload = job.payload
|
||||
if (!payload) throw new Error('That job has no saved inputs')
|
||||
studioMode.value = 'video'
|
||||
outputFocus.value = 'video'
|
||||
closeExtend()
|
||||
const extensions = payload.extensions || []
|
||||
applyVideoWorkflow(payload.workflow)
|
||||
prompt.value = extensions.length
|
||||
? [payload.prompt.trim(), ...extensions.map((item, index) => `shot ${index + 2}\n${item.prompt.trim()}`)].join('\n\n')
|
||||
: (payload.prompt || '')
|
||||
clipName.value = payload.name || ''
|
||||
seedInput.value = String(payload.seed || '')
|
||||
restoreAdvanced({
|
||||
turbo: payload.turbo === true,
|
||||
aspect: payload.aspect,
|
||||
width: payload.width,
|
||||
height: payload.height,
|
||||
cfg: payload.cfg,
|
||||
fps: payload.fps,
|
||||
samplerName: payload.samplerName,
|
||||
scheduler: payload.scheduler
|
||||
})
|
||||
duration.value = clampDuration(Number(payload.duration))
|
||||
hideThumbnail.value = showPrivacyToggles.value && payload.hideThumbnail === true
|
||||
if (showPrivacyToggles.value) hideInputPreview.value = payload.hideInput === true
|
||||
if (payload.folderId) {
|
||||
folderId.value = payload.folderId
|
||||
browseFolderId.value = payload.folderId
|
||||
}
|
||||
if (typeof payload.sound === 'boolean') withSound.value = payload.sound
|
||||
shotScriptMode.value = extensions.length > 0
|
||||
useIdentityRefs.value = payload.useIdentityRefs === true
|
||||
identityRefs.value.forEach((_, index) => clearIdentityRef(index))
|
||||
extensionQueue.value = extensions.map(item => ({
|
||||
id: crypto.randomUUID(),
|
||||
prompt: item.prompt,
|
||||
duration: clampDuration(Number(item.duration))
|
||||
}))
|
||||
queueAutoRun.value = payload.queueAutoRun === true
|
||||
const stillId = payload.stillId
|
||||
if (stillId) {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${stillId}`, { responseType: 'blob' })
|
||||
readFile(new File([blob], payload.stillFilename || 'still.png', { type: blob.type || 'image/png' }), { keepThumbnailPref: true })
|
||||
} else {
|
||||
resetImage()
|
||||
}
|
||||
for (const [index, refId] of (payload.referenceStillIds || []).entries()) {
|
||||
if (!refId || index >= 4) continue
|
||||
try {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${refId}`, { responseType: 'blob' })
|
||||
setIdentityRef(index, new File([blob], `identity-ref-${index + 2}.png`, { type: blob.type || 'image/png' }))
|
||||
} catch { /* skip missing identity stills */ }
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreOutputJob() {
|
||||
const id = outputRestoreId.value
|
||||
if (!id) return
|
||||
try {
|
||||
await restoreStudioJob(id)
|
||||
statusMessage.value = 'Job loaded into Input. Generate when you are ready.'
|
||||
toast('Inputs restored. Make any changes, then generate.')
|
||||
} catch (error: any) {
|
||||
toast(error?.data?.statusMessage || error?.message || 'Could not restore that job')
|
||||
}
|
||||
}
|
||||
|
||||
function clearActiveJob(kind?: 'video' | 'edit') {
|
||||
try {
|
||||
if (!kind || kind === 'video') localStorage.removeItem(JOB_STORE)
|
||||
@@ -3528,27 +3782,8 @@ async function generate() {
|
||||
const queued = shots
|
||||
? shots.slice(1).map(shot => ({ prompt: shot.prompt.trim(), duration: duration.value }))
|
||||
: extensionQueue.value.map(item => ({ prompt: item.prompt.trim(), duration: item.duration }))
|
||||
statusBusy.value = false
|
||||
videoBusy.value = true
|
||||
stopListen('video')
|
||||
videoSettledUi = false
|
||||
progress.value = 1
|
||||
videoUrl.value = ''
|
||||
currentClipId.value = ''
|
||||
closeExtend()
|
||||
awaitingReveal.value = false
|
||||
concealOutput.value = hideThumbnail.value
|
||||
lockedSave.value = false
|
||||
watchingLibrary.value = false
|
||||
playerSize.value = outputSize.value
|
||||
statusMessage.value = 'Checking ComfyUI...'
|
||||
chainStep.value = 1
|
||||
chainTotal.value = 1 + queued.length
|
||||
chainLabel.value = queued.length ? 'Initial' : ''
|
||||
overallProgress.value = 0
|
||||
completedChainStep.value = 0
|
||||
activeChainPlan.value = plannedChain.value.map(part => ({ ...part }))
|
||||
startTimer('video')
|
||||
const hideOut = hideThumbnail.value
|
||||
const folderLockedPref = false
|
||||
try {
|
||||
const body = new FormData()
|
||||
if (file.value) body.append('image', file.value)
|
||||
@@ -3581,22 +3816,68 @@ async function generate() {
|
||||
body.append('extensions', JSON.stringify(queued))
|
||||
body.append('queueAutoRun', String(queueAutoRun.value))
|
||||
}
|
||||
videoDownloadName.value = clipFileName(clipName.value.trim() || initialPrompt.split(/[.!?\n]/)[0] || 'aigen')
|
||||
const started = await $fetch<{ jobId: string; seed: number; width: number; height: number; steps: number; turbo: boolean; hideThumbnail?: boolean; folderLocked?: boolean; queueId?: string }>('/api/generate', {
|
||||
const downloadName = clipFileName(clipName.value.trim() || initialPrompt.split(/[.!?\n]/)[0] || 'aigen')
|
||||
const chainPlan = plannedChain.value.map(part => ({ ...part }))
|
||||
const started = await $fetch<{
|
||||
jobId: string
|
||||
studioJobId?: string
|
||||
queued?: boolean
|
||||
seed: number
|
||||
width: number
|
||||
height: number
|
||||
steps: number
|
||||
turbo: boolean
|
||||
hideThumbnail?: boolean
|
||||
folderLocked?: boolean
|
||||
queueId?: string
|
||||
chainTotal?: number
|
||||
}>('/api/generate', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
videoJobId.value = started.jobId
|
||||
maxStep.value = started.steps
|
||||
playerSize.value = { width: started.width, height: started.height }
|
||||
if (started.queued) {
|
||||
clearVideoForm()
|
||||
const n = 1 + queued.length
|
||||
toast(n > 1
|
||||
? `Queued ${n} shots behind the current job.`
|
||||
: 'Queued behind the current job.')
|
||||
await refreshStudioQueue()
|
||||
return
|
||||
}
|
||||
clearVideoForm()
|
||||
statusBusy.value = false
|
||||
videoBusy.value = true
|
||||
stopListen('video')
|
||||
videoSettledUi = false
|
||||
persistActiveJob('video', started.jobId, started.hideThumbnail === true || hideThumbnail.value, started.folderLocked === true)
|
||||
progress.value = 1
|
||||
videoUrl.value = ''
|
||||
currentClipId.value = ''
|
||||
closeExtend()
|
||||
awaitingReveal.value = false
|
||||
concealOutput.value = started.hideThumbnail === true || hideOut
|
||||
lockedSave.value = false
|
||||
watchingLibrary.value = false
|
||||
playerSize.value = { width: started.width, height: started.height }
|
||||
statusMessage.value = 'Checking ComfyUI...'
|
||||
chainStep.value = 1
|
||||
chainTotal.value = started.chainTotal || (1 + queued.length)
|
||||
chainLabel.value = queued.length ? 'Initial' : ''
|
||||
overallProgress.value = 0
|
||||
completedChainStep.value = 0
|
||||
activeChainPlan.value = chainPlan
|
||||
startTimer('video')
|
||||
videoDownloadName.value = downloadName
|
||||
videoJobId.value = started.jobId
|
||||
outputStudioJobId.value = started.studioJobId || ''
|
||||
maxStep.value = started.steps
|
||||
persistActiveJob('video', started.jobId, started.hideThumbnail === true || hideOut, started.folderLocked === true || folderLockedPref)
|
||||
if (started.queueId) {
|
||||
persistActiveQueue(started.queueId)
|
||||
void refreshLiveQueue()
|
||||
}
|
||||
listen('video', started.jobId, started.hideThumbnail === true || hideThumbnail.value, started.folderLocked === true)
|
||||
listen('video', started.jobId, started.hideThumbnail === true || hideOut, started.folderLocked === true)
|
||||
void pollComfyHealth()
|
||||
void refreshStudioQueue()
|
||||
} catch (error: any) {
|
||||
videoBusy.value = false
|
||||
statusBusy.value = false
|
||||
@@ -3761,6 +4042,7 @@ function applyVideoEvent(payload: Record<string, any>, hidden: boolean, folderLo
|
||||
clearActiveJob('video')
|
||||
loadLibrary().catch(() => null)
|
||||
void pollComfyHealth()
|
||||
void refreshStudioQueue()
|
||||
toast(payload.message || 'Comfy was still busy. This job was saved under Held jobs.')
|
||||
return
|
||||
}
|
||||
@@ -3794,6 +4076,7 @@ function applyVideoEvent(payload: Record<string, any>, hidden: boolean, folderLo
|
||||
clearActiveJob('video')
|
||||
activeDraftId = ''
|
||||
void pollComfyHealth()
|
||||
void refreshStudioQueue()
|
||||
videoSettledUi = true
|
||||
return
|
||||
}
|
||||
@@ -3812,6 +4095,7 @@ function applyVideoEvent(payload: Record<string, any>, hidden: boolean, folderLo
|
||||
activeDraftId = ''
|
||||
loadLibrary().catch(() => null)
|
||||
void pollComfyHealth()
|
||||
void refreshStudioQueue()
|
||||
statusMessage.value = currentClip.value?.hideThumbnail ? 'Hidden clip ready' : 'Loaded from library'
|
||||
toast(locked
|
||||
? 'Saved to the locked folder. Unlock it to view.'
|
||||
@@ -3846,6 +4130,7 @@ function applyVideoEvent(payload: Record<string, any>, hidden: boolean, folderLo
|
||||
activeDraftId = ''
|
||||
loadLibrary().catch(() => null)
|
||||
void pollComfyHealth()
|
||||
void refreshStudioQueue()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3858,6 +4143,7 @@ function applyVideoEvent(payload: Record<string, any>, hidden: boolean, folderLo
|
||||
clearActiveJob('video')
|
||||
loadLibrary().catch(() => null)
|
||||
void pollComfyHealth()
|
||||
void refreshStudioQueue()
|
||||
toast(payload.error || payload.message || 'Generation failed')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user