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()
|
||||
|
||||
+76
-1
@@ -124,14 +124,32 @@
|
||||
<p v-if="blockReason" class="text-xs text-zinc-400">{{ blockReason }}</p>
|
||||
<p v-else-if="queued" class="text-xs text-zinc-400">
|
||||
Waiting in the job queue.
|
||||
<button type="button" class="text-amber-200 hover:text-amber-100" @click="inspectJobId = studioJobId || liveMusicId">Inspect</button>
|
||||
·
|
||||
<NuxtLink to="/queue" class="text-amber-200 hover:text-amber-100">Open Queue</NuxtLink>
|
||||
</p>
|
||||
|
||||
<div v-if="musicPresets.length" class="rounded-2xl border border-white/10 bg-zinc-950/40 p-4">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium text-zinc-300">Templates</span>
|
||||
<select v-model="presetId" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm" @change="applyPreset">
|
||||
<option value="">Load a saved template</option>
|
||||
<option v-for="preset in musicPresets" :key="preset.id" :value="preset.id">{{ preset.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="glass min-w-0 max-w-full space-y-4 rounded-3xl p-5 sm:p-6">
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold">Output</h2>
|
||||
<p class="mt-1 text-sm text-zinc-400">{{ status || 'Waiting for a generate.' }}</p>
|
||||
<button
|
||||
v-if="studioJobId || liveMusicId"
|
||||
type="button"
|
||||
class="mt-2 text-xs text-amber-200 hover:text-amber-100"
|
||||
@click="inspectJobId = studioJobId || liveMusicId"
|
||||
>Inspect this job</button>
|
||||
</div>
|
||||
<div v-if="busy" class="h-2 overflow-hidden rounded-full bg-zinc-800">
|
||||
<div class="progress-glow h-full rounded-full bg-amber-400 transition-[width]" :style="{ width: `${Math.max(progress, 4)}%` }" />
|
||||
@@ -150,6 +168,7 @@
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
<QueuedJobModal :job-id="inspectJobId" @close="inspectJobId = ''" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -167,6 +186,8 @@ import {
|
||||
} from '~/utils/music'
|
||||
import { ensureGenerationNotifyPermission, notifyGenerationComplete } from '~/utils/generationNotify'
|
||||
import type { LibraryFolder } from '~/utils/libraryBrowse'
|
||||
import { LOAD_MUSIC_JOB_KEY } from '~/utils/queuedJob'
|
||||
import type { GenerationPreset, MusicPresetSettings } from '~/utils/generationPresets'
|
||||
|
||||
const tagsPlaceholder = DEFAULT_MUSIC_TAGS
|
||||
const lyricsPlaceholder = DEFAULT_MUSIC_LYRICS
|
||||
@@ -197,6 +218,10 @@ const studioJobId = ref('')
|
||||
const queued = ref(false)
|
||||
const queueCount = ref(0)
|
||||
const downloadName = ref('track.flac')
|
||||
const inspectJobId = ref('')
|
||||
const liveMusicId = ref('')
|
||||
const musicPresets = ref<GenerationPreset[]>([])
|
||||
const presetId = ref('')
|
||||
let source: EventSource | null = null
|
||||
let listenGen = 0
|
||||
let settled = false
|
||||
@@ -253,12 +278,15 @@ async function refreshStudioQueue() {
|
||||
jobs?: Array<{
|
||||
id: string
|
||||
status: string
|
||||
kind?: string
|
||||
liveJobId?: string
|
||||
lastError?: string
|
||||
}>
|
||||
waitingCount?: number
|
||||
}>('/api/studio-queue').catch(() => ({ jobs: [] as Array<{ id: string; status: string; liveJobId?: string; lastError?: string }>, waitingCount: 0 }))
|
||||
}>('/api/studio-queue').catch(() => ({ jobs: [] as Array<{ id: string; status: string; kind?: string; liveJobId?: string; lastError?: string }>, waitingCount: 0 }))
|
||||
const rows = data.jobs || []
|
||||
const musicRow = rows.find(job => job.kind === 'music' && (job.status === 'waiting' || job.status === 'running' || job.status === 'held'))
|
||||
liveMusicId.value = musicRow?.id || ''
|
||||
queueCount.value = data.waitingCount || rows.filter(job => job.status === 'waiting').length
|
||||
if (!studioJobId.value) return
|
||||
const row = rows.find(job => job.id === studioJobId.value)
|
||||
@@ -406,8 +434,55 @@ watch(instrumental, (on) => {
|
||||
if (on && !lyrics.value.trim()) lyrics.value = DEFAULT_MUSIC_LYRICS
|
||||
})
|
||||
|
||||
function applyIncomingMusic() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(LOAD_MUSIC_JOB_KEY)
|
||||
if (!raw) return
|
||||
sessionStorage.removeItem(LOAD_MUSIC_JOB_KEY)
|
||||
const loaded = JSON.parse(raw) as {
|
||||
name?: string
|
||||
tags?: string
|
||||
lyrics?: string
|
||||
instrumental?: boolean
|
||||
duration?: number
|
||||
steps?: number
|
||||
seed?: string
|
||||
folderId?: string
|
||||
}
|
||||
if (typeof loaded.tags === 'string') tags.value = loaded.tags
|
||||
if (typeof loaded.lyrics === 'string') lyrics.value = loaded.lyrics
|
||||
instrumental.value = loaded.instrumental === true
|
||||
if (typeof loaded.duration === 'number') duration.value = loaded.duration
|
||||
if (typeof loaded.steps === 'number') steps.value = loaded.steps
|
||||
if (typeof loaded.seed === 'string') seed.value = loaded.seed
|
||||
if (typeof loaded.name === 'string') name.value = loaded.name
|
||||
if (loaded.folderId && folders.value.some(folder => folder.id === loaded.folderId)) folderId.value = loaded.folderId
|
||||
status.value = 'Loaded from the queue'
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function loadMusicPresets() {
|
||||
const data = await $fetch<{ presets?: GenerationPreset[] }>('/api/generation-presets', { query: { kind: 'music' } }).catch(() => ({ presets: [] }))
|
||||
musicPresets.value = data.presets || []
|
||||
}
|
||||
|
||||
function applyPreset() {
|
||||
const preset = musicPresets.value.find(item => item.id === presetId.value)
|
||||
if (!preset) return
|
||||
const settings = preset.settings as MusicPresetSettings
|
||||
if (typeof settings.tags === 'string') tags.value = settings.tags
|
||||
if (typeof settings.lyrics === 'string') lyrics.value = settings.lyrics
|
||||
instrumental.value = settings.instrumental === true
|
||||
if (typeof settings.duration === 'number') duration.value = settings.duration
|
||||
if (typeof settings.steps === 'number') steps.value = settings.steps
|
||||
if (typeof settings.seed === 'number') seed.value = String(settings.seed)
|
||||
name.value = preset.name
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadLibrary()
|
||||
await loadMusicPresets()
|
||||
applyIncomingMusic()
|
||||
await refreshStudioQueue()
|
||||
stopQueuePoll()
|
||||
queuePoll = setInterval(() => { void refreshStudioQueue() }, 8000)
|
||||
|
||||
+21
-25
@@ -11,7 +11,7 @@
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold">Queue</h2>
|
||||
<p class="mt-1 max-w-2xl text-sm text-zinc-400">
|
||||
Jobs you submitted from Studio or Music. A long shot script is one job; its shots are the list underneath. Run next inserts a waiting job after the current shot as its own clip.
|
||||
Click a job to inspect it, fix a waiting one, copy it, or save a template. A long shot script is one job; its shots are the list underneath.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
@@ -57,7 +57,7 @@
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-start gap-3">
|
||||
<button type="button" class="flex min-w-0 items-start gap-3 text-left" @click="inspectJobId = job.id">
|
||||
<img
|
||||
v-if="jobThumb(job)"
|
||||
:src="jobThumb(job)"
|
||||
@@ -65,18 +65,18 @@
|
||||
class="h-12 w-12 shrink-0 rounded-lg bg-black/40 object-cover"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<p class="text-[11px] uppercase tracking-[0.18em] text-zinc-500">{{ jobKindLabel(job) }}</p>
|
||||
<h3 class="truncate font-display text-lg font-semibold text-zinc-100">{{ job.name }}</h3>
|
||||
<p class="mt-1 text-sm text-zinc-300">{{ jobLine(job) }}</p>
|
||||
<p v-if="job.shots && liveMessage[job.shots.id]" class="mt-1 text-xs text-amber-100">{{ liveMessage[job.shots.id] }}</p>
|
||||
<p v-else-if="liveMessage[job.id]" class="mt-1 text-xs text-amber-100">{{ liveMessage[job.id] }}</p>
|
||||
<p v-if="job.lastError" class="mt-1 text-xs text-red-300">{{ job.lastError }}</p>
|
||||
<p class="text-[11px] uppercase tracking-[0.18em] text-zinc-500">{{ jobKindLabel(job) }}</p>
|
||||
<h3 class="truncate font-display text-lg font-semibold text-zinc-100 hover:text-white">{{ job.name }}</h3>
|
||||
<p class="mt-1 text-sm text-zinc-300">{{ jobLine(job) }}</p>
|
||||
<p v-if="job.shots && liveMessage[job.shots.id]" class="mt-1 text-xs text-amber-100">{{ liveMessage[job.shots.id] }}</p>
|
||||
<p v-else-if="liveMessage[job.id]" class="mt-1 text-xs text-amber-100">{{ liveMessage[job.id] }}</p>
|
||||
<p v-if="job.lastError" class="mt-1 text-xs text-red-300">{{ job.lastError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-if="job.status === 'waiting'"
|
||||
v-if="job.status === 'waiting' && job.kind !== 'music'"
|
||||
type="button"
|
||||
class="rounded-full border border-sky-300/40 px-3 py-1.5 text-xs text-sky-100 hover:border-sky-200 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="Boolean(busyId)"
|
||||
@@ -410,6 +410,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
<QueuedJobModal
|
||||
:job-id="inspectJobId"
|
||||
@close="inspectJobId = ''"
|
||||
@saved="loadQueues"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -477,6 +482,7 @@ type PendingRemove = {
|
||||
const videoLoras = ref<string[]>([])
|
||||
const loraTriggers = ref<Record<string, string>>({})
|
||||
const loraAliases = ref<Record<string, string>>({})
|
||||
const inspectJobId = ref('')
|
||||
const jobs = ref<StudioJobRow[]>([])
|
||||
const orphans = ref<Queue[]>([])
|
||||
const waitingCount = ref(0)
|
||||
@@ -573,20 +579,10 @@ function progressLine(queue: Queue) {
|
||||
|
||||
function jobLine(job: StudioJobRow) {
|
||||
if (job.kind === 'music') {
|
||||
if (job.status === 'waiting') {
|
||||
return job.cutIn
|
||||
? 'Next after the current shot · ACE-Step'
|
||||
: 'Waiting · ACE-Step'
|
||||
}
|
||||
if (job.status === 'held') {
|
||||
if (jobIsHeldPaused(job)) return 'Paused. This track will not continue until you resume.'
|
||||
return job.holdForCutIn ? 'Paused so another job can cut in' : 'Paused'
|
||||
}
|
||||
if (job.status === 'running') {
|
||||
if (jobIsLatched(job)) return 'Generating track · will pause after this'
|
||||
return 'Generating track'
|
||||
}
|
||||
return job.status
|
||||
if (job.status === 'waiting') return job.cutIn ? 'Next after the current shot · ACE-Step' : 'Waiting · ACE-Step'
|
||||
if (job.status === 'held') return 'Paused'
|
||||
if (job.status === 'error') return job.lastError || 'Failed'
|
||||
return 'Generating track'
|
||||
}
|
||||
if (job.kind === 'edit') {
|
||||
const passes = job.shotCount || 1
|
||||
@@ -822,7 +818,7 @@ function jobKindLabel(job: StudioJobRow) {
|
||||
if (job.imagePipeline === 'v2') return 'IMAGE V2'
|
||||
return (job.shotCount || 1) > 1 ? 'EDIT' : 'IMAGE'
|
||||
}
|
||||
return 'Job'
|
||||
return 'VIDEO'
|
||||
}
|
||||
|
||||
function jobThumb(job: StudioJobRow) {
|
||||
|
||||
Reference in New Issue
Block a user