Let queued video, image, and music jobs open in an inspect modal.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+154
-7
@@ -1177,7 +1177,7 @@
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-zinc-100">Job queue</h3>
|
||||
<p class="mt-1 text-xs text-zinc-500">Each submit is one job. Shots inside a job live on the Queue page.</p>
|
||||
<p class="mt-1 text-xs text-zinc-500">Click a job to inspect it. Shots inside a job live on the Queue page.</p>
|
||||
</div>
|
||||
<NuxtLink to="/queue" class="shrink-0 text-xs text-amber-200 hover:text-white">Open Queue</NuxtLink>
|
||||
</div>
|
||||
@@ -1192,17 +1192,17 @@
|
||||
? '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>
|
||||
<button type="button" class="min-w-0 flex-1 text-left" @click="inspectJobId = job.id">
|
||||
<p class="truncate text-sm font-medium text-zinc-100 hover:text-white">{{ job.name }}</p>
|
||||
<p class="mt-0.5 text-xs text-zinc-500">
|
||||
<span v-if="job.kind === 'music'" class="mr-1 text-amber-200/80">MUSIC ·</span>
|
||||
<span v-else-if="job.kind === 'edit'" class="mr-1 text-amber-200/80">{{ job.imagePipeline === 'v2' ? 'IMAGE V2' : ((job.shotCount || 1) > 1 ? 'EDIT' : 'IMAGE') }} ·</span>
|
||||
{{ studioJobStatusLabel(job) }}
|
||||
· {{ studioJobShotLine(job) }}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
v-if="job.status === 'waiting'"
|
||||
v-if="job.status === 'waiting' && job.kind !== 'music'"
|
||||
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)"
|
||||
@@ -1210,7 +1210,7 @@
|
||||
{{ job.cutIn ? 'Next' : (canCutInStudioJob ? 'Run next' : 'Start now') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="job.status === 'error' || (job.status === 'held' && !job.holdForCutIn)"
|
||||
v-if="job.status === 'error' || (job.status === 'held' && !job.holdForCutIn && job.kind !== 'music')"
|
||||
type="button"
|
||||
class="shrink-0 rounded-full border border-amber-300/40 px-2.5 py-1 text-xs text-amber-100 hover:border-amber-200 hover:text-white"
|
||||
@click="retryStudioJob(job.id)"
|
||||
@@ -2193,6 +2193,13 @@
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<QueuedJobModal
|
||||
:job-id="inspectJobId"
|
||||
@close="inspectJobId = ''"
|
||||
@saved="refreshStudioQueue"
|
||||
@loaded="onQueuedJobLoaded"
|
||||
/>
|
||||
|
||||
<div class="pointer-events-none fixed bottom-4 right-4 z-50 space-y-2">
|
||||
<div
|
||||
v-for="toast in toasts"
|
||||
@@ -2258,6 +2265,7 @@ import {
|
||||
VIDEO_CFG_STEP,
|
||||
type GenerationPreset
|
||||
} from '~/utils/generationPresets'
|
||||
import { LOAD_QUEUED_JOB_KEY } from '~/utils/queuedJob'
|
||||
import {
|
||||
clipSettingsRows,
|
||||
inferStillPipeline,
|
||||
@@ -2781,6 +2789,7 @@ const studioJobs = ref<Array<{
|
||||
pendingCount?: number
|
||||
imagePipeline?: string
|
||||
}>>([])
|
||||
const inspectJobId = ref('')
|
||||
const outputStudioJobId = ref('')
|
||||
const videoAwaitingBeast = ref(false)
|
||||
const pickerOpen = ref(false)
|
||||
@@ -3980,6 +3989,7 @@ onMounted(async () => {
|
||||
await loadPromptPresets()
|
||||
if (route.query.clip || route.query.still) applyIncomingLibraryItem()
|
||||
else applyIncomingDirectorScript()
|
||||
await applyIncomingQueuedJob()
|
||||
window.addEventListener('keydown', onStudioKey)
|
||||
})
|
||||
|
||||
@@ -4416,6 +4426,27 @@ function applyIncomingDirectorScript() {
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function applyIncomingQueuedJob() {
|
||||
let id = ''
|
||||
try {
|
||||
id = sessionStorage.getItem(LOAD_QUEUED_JOB_KEY) || ''
|
||||
if (id) sessionStorage.removeItem(LOAD_QUEUED_JOB_KEY)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!id) return
|
||||
try {
|
||||
await restoreStudioJob(id)
|
||||
toast('Queued job loaded into Input.')
|
||||
} catch (error: any) {
|
||||
toast(error?.data?.statusMessage || error?.message || 'Could not load that queued job')
|
||||
}
|
||||
}
|
||||
|
||||
function onQueuedJobLoaded() {
|
||||
void applyIncomingQueuedJob()
|
||||
}
|
||||
|
||||
async function loadGenerationPresets() {
|
||||
const [video, image, imagev2] = await Promise.all([
|
||||
$fetch<{ presets?: GenerationPreset[] }>('/api/generation-presets', { query: { kind: 'video' } }).catch(() => ({ presets: [] })),
|
||||
@@ -5205,7 +5236,7 @@ async function refreshStudioQueue() {
|
||||
}>
|
||||
}>('/api/studio-queue').catch(() => ({ 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 && job.kind !== 'music')
|
||||
if (running?.liveJobId) videoAwaitingBeast.value = false
|
||||
if (watchingLiveVideo() && running?.liveJobId && running.liveJobId !== videoJobId.value) {
|
||||
return
|
||||
@@ -5274,6 +5305,102 @@ async function dismissStudioJob(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreStudioEditJob(payload: {
|
||||
prompt?: string
|
||||
name?: string
|
||||
folderId?: string
|
||||
aspect?: string
|
||||
width?: number
|
||||
height?: number
|
||||
stillId?: string
|
||||
stillFilename?: string
|
||||
hideInput?: boolean
|
||||
promptPre?: string
|
||||
promptPost?: string
|
||||
loraName?: string
|
||||
loraStack?: LoraStackItem[]
|
||||
negative?: string
|
||||
imagePipeline?: 'v1' | 'v2'
|
||||
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate'
|
||||
v2Task?: 'scene' | 'identity' | 'outfit' | 'face_lock' | 'refine' | 't2i'
|
||||
engine?: 'flux' | 'krea'
|
||||
snofsModel?: number
|
||||
snofsClip?: number
|
||||
consistencyModel?: number
|
||||
consistencyClip?: number
|
||||
refineStrength?: number
|
||||
scaleToTotalPixels?: boolean
|
||||
scaleMegapixels?: number
|
||||
steps?: number
|
||||
cfg?: number
|
||||
turbo?: boolean
|
||||
referenceStillId?: string
|
||||
}) {
|
||||
closeExtend()
|
||||
applyPromptRestore(payload.prompt || '', payload)
|
||||
clipName.value = payload.name || ''
|
||||
if (payload.folderId) {
|
||||
folderId.value = payload.folderId
|
||||
browseFolderId.value = payload.folderId
|
||||
}
|
||||
const v2 = payload.imagePipeline === 'v2'
|
||||
if (v2) {
|
||||
studioMode.value = 'editv2'
|
||||
outputFocus.value = 'edit'
|
||||
const mode = payload.v2Mode === 'compose' || payload.v2Mode === 'refine' || payload.v2Mode === 'generate'
|
||||
? payload.v2Mode
|
||||
: 'edit'
|
||||
setV2Mode(mode)
|
||||
persistImageV2LoraStack(imageV2LoraStack.value, v2Engine.value)
|
||||
v2Engine.value = parseImageV2Engine(payload.engine, 'flux')
|
||||
if (typeof payload.negative === 'string') v2Negative.value = payload.negative
|
||||
if (payload.steps != null) v2Steps.value = clampImageSteps(payload.steps, v2Engine.value === 'krea' ? IMAGE_V2_KREA_STEPS : IMAGE_V2_STEPS_DEFAULT)
|
||||
if (payload.cfg != null) v2Cfg.value = clampImageCfg(payload.cfg, v2Engine.value === 'krea' ? IMAGE_V2_KREA_CFG : IMAGE_V2_CFG_DEFAULT)
|
||||
if (payload.scaleMegapixels != null) v2Megapixels.value = clampImageScaleMegapixels(payload.scaleMegapixels, 1)
|
||||
v2ScaleOn.value = mode !== 'generate' && payload.scaleToTotalPixels !== false
|
||||
v2Turbo.value = payload.turbo === true
|
||||
if ((mode === 'refine' || (v2Engine.value === 'krea' && mode !== 'generate')) && payload.refineStrength != null) {
|
||||
v2Denoise.value = clampImageV2Denoise(payload.refineStrength)
|
||||
}
|
||||
aspect.value = parseImageAspect(payload.aspect)
|
||||
if (mode === 'generate') {
|
||||
const size = generateSizeForAspect(aspect.value)
|
||||
v2Width.value = size.width
|
||||
v2Height.value = size.height
|
||||
}
|
||||
if (mode === 'compose' && payload.v2Task) v2Task.value = payload.v2Task
|
||||
imageV2LoraStack.value = mergeLegacyImageV2Loras(readLoraStack(payload), {
|
||||
engine: v2Engine.value,
|
||||
mode,
|
||||
xaigen: v2ConceptLoraEnabled.value,
|
||||
snofsModel: payload.snofsModel,
|
||||
snofsClip: payload.snofsClip,
|
||||
consistencyModel: payload.consistencyModel,
|
||||
consistencyClip: payload.consistencyClip,
|
||||
kreaConcept: String(runtimeConfig.public.kreaConceptLora || IMAGE_V2_KREA_CONCEPT_LORA || '').trim()
|
||||
})
|
||||
} else {
|
||||
studioMode.value = 'edit'
|
||||
outputFocus.value = 'edit'
|
||||
if (payload.steps != null) editSteps.value = clampImageSteps(payload.steps)
|
||||
if (payload.cfg != null) editCfg.value = clampImageCfg(payload.cfg)
|
||||
if (typeof payload.negative === 'string') editNegative.value = payload.negative
|
||||
editScaleToTotalPixels.value = payload.scaleToTotalPixels === true
|
||||
if (payload.scaleMegapixels != null) editScaleMegapixels.value = clampImageScaleMegapixels(payload.scaleMegapixels)
|
||||
imageLoraStack.value = filterLoraStackForStudio(readLoraStack(payload), ltxEnabled.value)
|
||||
aspect.value = parseImageAspect(payload.aspect)
|
||||
}
|
||||
const generate = v2 && payload.v2Mode === 'generate'
|
||||
if (!generate && payload.stillId) {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${payload.stillId}`, { responseType: 'blob' })
|
||||
if (payload.hideInput && showPrivacyToggles.value) hideInputPreview.value = true
|
||||
readFile(new File([blob], stillFileName(stills.value.find(item => item.id === payload.stillId)) || payload.stillFilename || 'still.png', { type: blob.type || 'image/png' }), { stillId: payload.stillId })
|
||||
}
|
||||
if (v2 && payload.v2Mode === 'compose' && payload.referenceStillId) {
|
||||
await loadStillAsEditRef(payload.referenceStillId).catch(() => null)
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreStudioJob(id: string) {
|
||||
const job = await $fetch<{
|
||||
payload?: {
|
||||
@@ -5309,10 +5436,30 @@ async function restoreStudioJob(id: string) {
|
||||
loraStack?: LoraStackItem[]
|
||||
shotLoras?: string[]
|
||||
shotLoraStacks?: LoraStackItem[][]
|
||||
negative?: string
|
||||
imagePipeline?: 'v1' | 'v2'
|
||||
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate'
|
||||
v2Task?: 'scene' | 'identity' | 'outfit' | 'face_lock' | 'refine' | 't2i'
|
||||
engine?: 'flux' | 'krea'
|
||||
snofsModel?: number
|
||||
snofsClip?: number
|
||||
consistencyModel?: number
|
||||
consistencyClip?: number
|
||||
refineStrength?: number
|
||||
scaleToTotalPixels?: boolean
|
||||
scaleMegapixels?: number
|
||||
steps?: number
|
||||
referenceStillId?: string
|
||||
}
|
||||
kind?: string
|
||||
imagePipeline?: string
|
||||
}>(`/api/studio-queue/${id}`)
|
||||
const payload = job.payload
|
||||
if (!payload) throw new Error('That job has no saved inputs')
|
||||
if (job.kind === 'edit' || payload.imagePipeline === 'v2') {
|
||||
await restoreStudioEditJob(payload)
|
||||
return
|
||||
}
|
||||
studioMode.value = 'video'
|
||||
outputFocus.value = 'video'
|
||||
closeExtend()
|
||||
|
||||
Reference in New Issue
Block a user