Keep Output on the generating clip when another job is queued.
Queued generate/extend no longer hijacks the live progress bar, and Generate Extension stays clickable so another extension can wait in the job queue. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+79
-31
@@ -1290,6 +1290,9 @@
|
|||||||
<div class="h-2 overflow-hidden rounded-full bg-zinc-800">
|
<div class="h-2 overflow-hidden rounded-full bg-zinc-800">
|
||||||
<div class="h-full rounded-full bg-amber-400 progress-glow transition-all duration-300" :style="{ width: liveProgressPct + '%' }" />
|
<div class="h-full rounded-full bg-amber-400 progress-glow transition-all duration-300" :style="{ width: liveProgressPct + '%' }" />
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="studioWaitingCount" class="mt-2 text-xs text-zinc-500">
|
||||||
|
{{ studioWaitingCount }} waiting in the job queue — Output stays on the clip that is generating.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="outputPreviewKind === 'video'">
|
<div v-show="outputPreviewKind === 'video'">
|
||||||
@@ -1448,9 +1451,9 @@
|
|||||||
:disabled="!extendPrompt.trim()"
|
:disabled="!extendPrompt.trim()"
|
||||||
@click="generateExtension"
|
@click="generateExtension"
|
||||||
>
|
>
|
||||||
{{ videoBusy ? 'Queue extension' : 'Generate Extension' }}
|
{{ watchingLiveVideo() || videoBusy ? 'Queue extension' : 'Generate Extension' }}
|
||||||
</button>
|
</button>
|
||||||
<p v-if="videoBusy" class="text-xs text-zinc-500">The current job keeps running in Output. This extension waits in the job queue.</p>
|
<p v-if="watchingLiveVideo() || videoBusy" class="text-xs text-zinc-500">The current job keeps running in Output. This extension waits in the job queue.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2597,6 +2600,7 @@ const videoJobId = ref('')
|
|||||||
const editJobId = ref('')
|
const editJobId = ref('')
|
||||||
const currentClipId = ref('')
|
const currentClipId = ref('')
|
||||||
const extendOpen = ref(false)
|
const extendOpen = ref(false)
|
||||||
|
const extendClipId = ref('')
|
||||||
const extendPrompt = ref('')
|
const extendPrompt = ref('')
|
||||||
const extendDuration = ref(5)
|
const extendDuration = ref(5)
|
||||||
const lastFrameUrl = ref('')
|
const lastFrameUrl = ref('')
|
||||||
@@ -2818,6 +2822,25 @@ let videoListenGen = 0
|
|||||||
let editListenGen = 0
|
let editListenGen = 0
|
||||||
let activeDraftId = ''
|
let activeDraftId = ''
|
||||||
|
|
||||||
|
function watchingLiveVideo() {
|
||||||
|
return Boolean(videoJobId.value) && !videoSettledUi && (videoBusy.value || Boolean(videoSource))
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchingLiveEdit() {
|
||||||
|
return Boolean(editJobId.value) && !editSettledUi && (editBusy.value || Boolean(editSource))
|
||||||
|
}
|
||||||
|
|
||||||
|
function queuedOffLiveOutput(
|
||||||
|
started: { queued?: boolean; jobId?: string },
|
||||||
|
kind: 'video' | 'edit'
|
||||||
|
) {
|
||||||
|
if (started.queued) return true
|
||||||
|
if (kind === 'edit') {
|
||||||
|
return watchingLiveEdit() && Boolean(started.jobId) && started.jobId !== editJobId.value
|
||||||
|
}
|
||||||
|
return watchingLiveVideo() && Boolean(started.jobId) && started.jobId !== videoJobId.value
|
||||||
|
}
|
||||||
|
|
||||||
function formatElapsed(ms: number) {
|
function formatElapsed(ms: number) {
|
||||||
const seconds = Math.floor(ms / 1000)
|
const seconds = Math.floor(ms / 1000)
|
||||||
const mm = String(Math.floor(seconds / 60)).padStart(2, '0')
|
const mm = String(Math.floor(seconds / 60)).padStart(2, '0')
|
||||||
@@ -2875,7 +2898,7 @@ const liveStatusMessage = computed(() => {
|
|||||||
return idleInputStatus('edit')
|
return idleInputStatus('edit')
|
||||||
}
|
}
|
||||||
if (videoBusy.value || videoUrl.value || lockedSave.value) return statusMessage.value
|
if (videoBusy.value || videoUrl.value || lockedSave.value) return statusMessage.value
|
||||||
if (outputQueuedJob.value || videoAwaitingBeast.value) {
|
if (videoAwaitingBeast.value || outputQueuedJob.value) {
|
||||||
return statusMessage.value || 'Queued. Waiting for Beast to be free.'
|
return statusMessage.value || 'Queued. Waiting for Beast to be free.'
|
||||||
}
|
}
|
||||||
return idleInputStatus('video')
|
return idleInputStatus('video')
|
||||||
@@ -5046,16 +5069,15 @@ async function refreshStudioQueue() {
|
|||||||
}>('/api/studio-queue').catch(() => ({ jobs: [] }))
|
}>('/api/studio-queue').catch(() => ({ jobs: [] }))
|
||||||
studioJobs.value = data.jobs || []
|
studioJobs.value = data.jobs || []
|
||||||
const running = studioJobs.value.find(job => job.status === 'running' && job.liveJobId)
|
const running = studioJobs.value.find(job => job.status === 'running' && job.liveJobId)
|
||||||
const queuedMine = Boolean(
|
if (running?.liveJobId) videoAwaitingBeast.value = false
|
||||||
outputStudioJobId.value
|
if (watchingLiveVideo() && running?.liveJobId && running.liveJobId !== videoJobId.value) {
|
||||||
&& studioJobs.value.some(job => job.id === outputStudioJobId.value && (job.status === 'waiting' || job.status === 'held'))
|
return
|
||||||
)
|
}
|
||||||
if (queuedMine && running && running.id !== outputStudioJobId.value) {
|
if (watchingLiveEdit() && running?.liveJobId && running.liveJobId !== editJobId.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (running?.liveJobId) videoAwaitingBeast.value = false
|
|
||||||
if (running?.kind === 'edit') {
|
if (running?.kind === 'edit') {
|
||||||
if (running.liveJobId && running.liveJobId !== editJobId.value && !editBusy.value) {
|
if (running.liveJobId && running.liveJobId !== editJobId.value && !editBusy.value && !watchingLiveVideo()) {
|
||||||
outputStudioJobId.value = running.id
|
outputStudioJobId.value = running.id
|
||||||
editJobId.value = running.liveJobId
|
editJobId.value = running.liveJobId
|
||||||
persistActiveJob('edit', running.liveJobId, running.hideThumbnail === true, running.folderLocked === true)
|
persistActiveJob('edit', running.liveJobId, running.hideThumbnail === true, running.folderLocked === true)
|
||||||
@@ -5066,7 +5088,7 @@ async function refreshStudioQueue() {
|
|||||||
} else if (running.id && running.liveJobId === editJobId.value) {
|
} else if (running.id && running.liveJobId === editJobId.value) {
|
||||||
outputStudioJobId.value = running.id
|
outputStudioJobId.value = running.id
|
||||||
}
|
}
|
||||||
} else if (running?.liveJobId && running.liveJobId !== videoJobId.value && !videoBusy.value) {
|
} else if (running?.liveJobId && running.liveJobId !== videoJobId.value && !videoBusy.value && !watchingLiveVideo()) {
|
||||||
outputStudioJobId.value = running.id
|
outputStudioJobId.value = running.id
|
||||||
videoJobId.value = running.liveJobId
|
videoJobId.value = running.liveJobId
|
||||||
persistActiveJob('video', running.liveJobId, running.hideThumbnail === true, running.folderLocked === true)
|
persistActiveJob('video', running.liveJobId, running.hideThumbnail === true, running.folderLocked === true)
|
||||||
@@ -5696,6 +5718,7 @@ function clipVideoUrl(clipId: string) {
|
|||||||
function closeExtend() {
|
function closeExtend() {
|
||||||
lastFrameGen += 1
|
lastFrameGen += 1
|
||||||
extendOpen.value = false
|
extendOpen.value = false
|
||||||
|
extendClipId.value = ''
|
||||||
lastFrameLoading.value = false
|
lastFrameLoading.value = false
|
||||||
lastFrameRevealed.value = false
|
lastFrameRevealed.value = false
|
||||||
if (lastFrameObjectUrl) {
|
if (lastFrameObjectUrl) {
|
||||||
@@ -5739,6 +5762,7 @@ async function openExtend(item?: LibraryClip) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentClipId.value = id
|
currentClipId.value = id
|
||||||
|
extendClipId.value = id
|
||||||
const clip = clips.value.find(entry => entry.id === id)
|
const clip = clips.value.find(entry => entry.id === id)
|
||||||
if (clip) applyPromptRestore(clip.prompt, clip)
|
if (clip) applyPromptRestore(clip.prompt, clip)
|
||||||
extendPrompt.value = clip?.prompt || prompt.value
|
extendPrompt.value = clip?.prompt || prompt.value
|
||||||
@@ -5755,9 +5779,14 @@ async function openExtend(item?: LibraryClip) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function generateExtension() {
|
async function generateExtension() {
|
||||||
const id = clipIdFromPlayer()
|
const id = extendClipId.value || clipIdFromPlayer()
|
||||||
if (!id || !extendPrompt.value.trim()) return
|
if (!extendPrompt.value.trim()) return
|
||||||
|
if (!id) {
|
||||||
|
toast('Load a generated clip before extending it.')
|
||||||
|
return
|
||||||
|
}
|
||||||
const hideOut = hideThumbnail.value
|
const hideOut = hideThumbnail.value
|
||||||
|
const keepLiveOutput = watchingLiveVideo()
|
||||||
try {
|
try {
|
||||||
await ensureGenerationNotifyPermission()
|
await ensureGenerationNotifyPermission()
|
||||||
const started = await $fetch<{
|
const started = await $fetch<{
|
||||||
@@ -5780,16 +5809,22 @@ async function generateExtension() {
|
|||||||
loraStack: extendLoraStack.value
|
loraStack: extendLoraStack.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
closeExtend()
|
if (queuedOffLiveOutput(started, 'video')) {
|
||||||
if (started.queued) {
|
if (watchingLiveVideo()) {
|
||||||
|
toast('Queued behind the current job. Output stays on the clip that is generating.')
|
||||||
|
await refreshStudioQueue()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toast('Queued behind the current job.')
|
||||||
outputStudioJobId.value = started.studioJobId || ''
|
outputStudioJobId.value = started.studioJobId || ''
|
||||||
outputFocus.value = 'video'
|
outputFocus.value = 'video'
|
||||||
videoAwaitingBeast.value = true
|
videoAwaitingBeast.value = true
|
||||||
statusMessage.value = 'Queued. Waiting for Beast to be free.'
|
statusMessage.value = 'Queued. Waiting for Beast to be free.'
|
||||||
toast('Queued behind the current job.')
|
closeExtend()
|
||||||
await refreshStudioQueue()
|
await refreshStudioQueue()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
closeExtend()
|
||||||
statusBusy.value = false
|
statusBusy.value = false
|
||||||
videoBusy.value = true
|
videoBusy.value = true
|
||||||
watchingLibrary.value = false
|
watchingLibrary.value = false
|
||||||
@@ -5816,12 +5851,14 @@ async function generateExtension() {
|
|||||||
void pollComfyHealth()
|
void pollComfyHealth()
|
||||||
void refreshStudioQueue()
|
void refreshStudioQueue()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
videoBusy.value = false
|
|
||||||
statusBusy.value = false
|
|
||||||
stopTimer('video')
|
|
||||||
clearActiveJob('video')
|
|
||||||
const message = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Failed to start extension'
|
const message = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Failed to start extension'
|
||||||
statusMessage.value = message
|
if (!keepLiveOutput) {
|
||||||
|
videoBusy.value = false
|
||||||
|
statusBusy.value = false
|
||||||
|
stopTimer('video')
|
||||||
|
clearActiveJob('video')
|
||||||
|
statusMessage.value = message
|
||||||
|
}
|
||||||
toast(message)
|
toast(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6982,6 +7019,7 @@ async function generate() {
|
|||||||
: extensionQueue.value.map(item => ({ prompt: item.prompt.trim(), duration: item.duration, ...persistLoraFields(item.loraStack || []) }))
|
: extensionQueue.value.map(item => ({ prompt: item.prompt.trim(), duration: item.duration, ...persistLoraFields(item.loraStack || []) }))
|
||||||
const hideOut = hideThumbnail.value
|
const hideOut = hideThumbnail.value
|
||||||
const folderLockedPref = false
|
const folderLockedPref = false
|
||||||
|
const keepLiveOutput = watchingLiveVideo()
|
||||||
try {
|
try {
|
||||||
await ensureGenerationNotifyPermission()
|
await ensureGenerationNotifyPermission()
|
||||||
const body = new FormData()
|
const body = new FormData()
|
||||||
@@ -7059,15 +7097,23 @@ async function generate() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body
|
body
|
||||||
})
|
})
|
||||||
if (started.queued) {
|
const queuedShotCount = 1 + queued.length
|
||||||
|
const queuedBehindToast = queuedShotCount > 1
|
||||||
|
? `Queued ${queuedShotCount} shots behind the current job.`
|
||||||
|
: 'Queued behind the current job.'
|
||||||
|
if (queuedOffLiveOutput(started, 'video')) {
|
||||||
|
if (watchingLiveVideo()) {
|
||||||
|
toast(queuedShotCount > 1
|
||||||
|
? `Queued ${queuedShotCount} shots. Output stays on the clip that is generating.`
|
||||||
|
: 'Queued behind the current job. Output stays on the clip that is generating.')
|
||||||
|
await refreshStudioQueue()
|
||||||
|
return
|
||||||
|
}
|
||||||
outputStudioJobId.value = started.studioJobId || ''
|
outputStudioJobId.value = started.studioJobId || ''
|
||||||
outputFocus.value = 'video'
|
outputFocus.value = 'video'
|
||||||
videoAwaitingBeast.value = true
|
videoAwaitingBeast.value = true
|
||||||
statusMessage.value = 'Queued. Waiting for Beast to be free.'
|
statusMessage.value = 'Queued. Waiting for Beast to be free.'
|
||||||
const n = 1 + queued.length
|
toast(queuedBehindToast)
|
||||||
toast(n > 1
|
|
||||||
? `Queued ${n} shots behind the current job.`
|
|
||||||
: 'Queued behind the current job.')
|
|
||||||
await refreshStudioQueue()
|
await refreshStudioQueue()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -7107,12 +7153,14 @@ async function generate() {
|
|||||||
void pollComfyHealth()
|
void pollComfyHealth()
|
||||||
void refreshStudioQueue()
|
void refreshStudioQueue()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
videoBusy.value = false
|
|
||||||
statusBusy.value = false
|
|
||||||
stopTimer('video')
|
|
||||||
clearActiveJob('video')
|
|
||||||
const message = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Failed to start generation'
|
const message = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Failed to start generation'
|
||||||
statusMessage.value = message
|
if (!keepLiveOutput) {
|
||||||
|
videoBusy.value = false
|
||||||
|
statusBusy.value = false
|
||||||
|
stopTimer('video')
|
||||||
|
clearActiveJob('video')
|
||||||
|
statusMessage.value = message
|
||||||
|
}
|
||||||
toast(message)
|
toast(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user