Let queued video, image, and music jobs open in an inspect modal.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,440 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="jobId"
|
||||||
|
class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/70 p-4 sm:p-8"
|
||||||
|
@click.self="close"
|
||||||
|
>
|
||||||
|
<div class="glass w-full max-w-3xl rounded-3xl p-5 sm:p-6">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-[11px] uppercase tracking-[0.18em] text-zinc-500">{{ kindLabel }}</p>
|
||||||
|
<h2 class="font-display text-2xl font-bold text-zinc-100">{{ draft.name || 'Queued job' }}</h2>
|
||||||
|
<p class="mt-1 text-sm text-zinc-400">{{ statusLine }}</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="rounded-full border border-white/10 px-3 py-1 text-sm text-zinc-400 hover:text-white" @click="close">Close</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="error" class="mt-4 rounded-2xl border border-red-400/30 bg-red-400/10 px-4 py-3 text-sm text-red-200">{{ error }}</p>
|
||||||
|
<p v-else-if="notice" class="mt-4 rounded-2xl border border-amber-300/30 bg-amber-400/10 px-4 py-3 text-sm text-amber-100">{{ notice }}</p>
|
||||||
|
<p v-else-if="loading" class="mt-4 text-sm text-zinc-400">Loading job…</p>
|
||||||
|
|
||||||
|
<div v-if="job" class="mt-5 space-y-4">
|
||||||
|
<div v-if="thumbUrl" class="overflow-hidden rounded-2xl border border-white/10 bg-black/40">
|
||||||
|
<img :src="thumbUrl" alt="" class="max-h-56 w-full object-contain">
|
||||||
|
</div>
|
||||||
|
<p v-if="job.lastError" class="rounded-2xl border border-red-400/30 bg-red-400/10 px-4 py-3 text-sm text-red-200">{{ job.lastError }}</p>
|
||||||
|
<p v-if="!editable && job.kind !== 'music'" class="text-xs text-zinc-500">This one is generating. You can copy it or save a template. Waiting jobs can be edited here.</p>
|
||||||
|
<p v-else-if="!editable && job.kind === 'music'" class="text-xs text-zinc-500">Music that’s already on the GPU can’t be rewritten. Copy it, save a template, or load it into Music.</p>
|
||||||
|
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">Name</span>
|
||||||
|
<input
|
||||||
|
v-model="draft.name"
|
||||||
|
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2 disabled:opacity-70"
|
||||||
|
:disabled="!editable"
|
||||||
|
maxlength="80"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<template v-if="job.kind === 'music'">
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">Tags</span>
|
||||||
|
<textarea v-model="draft.tags" rows="3" class="max-h-32 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 text-sm text-zinc-300">
|
||||||
|
<input v-model="draft.instrumental" type="checkbox" class="accent-amber-400" :disabled="!editable">
|
||||||
|
Instrumental
|
||||||
|
</label>
|
||||||
|
<label v-if="!draft.instrumental" class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">Lyrics</span>
|
||||||
|
<textarea v-model="draft.lyrics" rows="8" class="max-h-64 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<label v-if="draft.promptPre" class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">Prompt pre</span>
|
||||||
|
<textarea v-model="draft.promptPre" rows="2" class="max-h-28 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">{{ shots.length > 1 ? (job.kind === 'edit' ? 'Pass 1' : 'Shot 1') : 'Prompt' }}</span>
|
||||||
|
<textarea v-model="draft.prompt" rows="5" class="max-h-48 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||||||
|
</label>
|
||||||
|
<label v-if="draft.promptPost" class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">Prompt post</span>
|
||||||
|
<textarea v-model="draft.promptPost" rows="2" class="max-h-28 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||||||
|
</label>
|
||||||
|
<label v-if="job.kind === 'edit'" class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">Negative</span>
|
||||||
|
<textarea v-model="draft.negative" rows="2" class="max-h-28 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="extraShots.length" class="space-y-3">
|
||||||
|
<p class="text-xs text-zinc-500">{{ job.kind === 'edit' ? 'Later passes' : 'Later shots' }} queued with this job.</p>
|
||||||
|
<div v-for="(shot, index) in extraShots" :key="index" class="rounded-2xl border border-white/10 bg-zinc-950/50 px-3 py-3">
|
||||||
|
<p class="text-sm font-medium text-zinc-200">{{ job.kind === 'edit' ? `Pass ${index + 2}` : `Shot ${index + 2}` }}</p>
|
||||||
|
<textarea v-model="shot.prompt" rows="3" class="mt-2 max-h-36 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||||||
|
<label v-if="job.kind !== 'edit'" class="mt-2 block text-xs text-zinc-400">
|
||||||
|
Duration
|
||||||
|
<input v-model.number="shot.duration" type="number" min="0.5" max="120" step="0.1" class="mt-1 w-28 rounded-lg border border-white/10 bg-zinc-950 px-2 py-1 text-sm disabled:opacity-70" :disabled="!editable">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<label v-if="job.kind !== 'edit' || draft.duration" class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">Duration</span>
|
||||||
|
<input v-model.number="draft.duration" type="number" min="0.5" max="180" step="0.1" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm disabled:opacity-70" :disabled="!editable">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">Seed</span>
|
||||||
|
<input v-model="draft.seed" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm disabled:opacity-70" :disabled="!editable">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">Steps</span>
|
||||||
|
<input v-model.number="draft.steps" type="number" min="1" max="100" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm disabled:opacity-70" :disabled="!editable">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">CFG</span>
|
||||||
|
<input v-model.number="draft.cfg" type="number" min="0" max="20" step="0.1" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm disabled:opacity-70" :disabled="!editable">
|
||||||
|
</label>
|
||||||
|
<label v-if="job.kind === 'video'" class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">FPS</span>
|
||||||
|
<input v-model.number="draft.fps" type="number" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm disabled:opacity-70" :disabled="!editable">
|
||||||
|
</label>
|
||||||
|
<p v-if="metaLine" class="sm:col-span-2 text-xs text-zinc-500">{{ metaLine }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label v-if="job.kind === 'video'" class="flex items-center gap-2 text-sm text-zinc-300">
|
||||||
|
<input v-model="draft.turbo" type="checkbox" class="accent-amber-400" :disabled="!editable">
|
||||||
|
Turbo
|
||||||
|
</label>
|
||||||
|
<label v-if="job.kind === 'video'" class="flex items-center gap-2 text-sm text-zinc-300">
|
||||||
|
<input v-model="draft.sound" type="checkbox" class="accent-amber-400" :disabled="!editable">
|
||||||
|
Sound
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div v-if="job.kind !== 'music'">
|
||||||
|
<LoraSelect
|
||||||
|
v-if="editable"
|
||||||
|
:model-value="draft.loraStack"
|
||||||
|
:names="loraNames"
|
||||||
|
:triggers="loraTriggers"
|
||||||
|
:aliases="loraAliases"
|
||||||
|
:engine="loraEngine"
|
||||||
|
label="LoRA"
|
||||||
|
compact
|
||||||
|
@update:model-value="draft.loraStack = $event"
|
||||||
|
/>
|
||||||
|
<p v-else-if="loraLine" class="text-xs text-zinc-500">{{ loraLine }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-zinc-950/40 p-3">
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium text-zinc-300">Template name</span>
|
||||||
|
<input v-model="presetName" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2" maxlength="80" placeholder="Save these settings as a preset">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button type="button" class="rounded-full border border-white/15 px-3 py-1.5 text-xs text-zinc-200 hover:border-amber-300/40" @click="copyJob">Copy</button>
|
||||||
|
<button type="button" class="rounded-full border border-white/15 px-3 py-1.5 text-xs text-zinc-200 hover:border-amber-300/40" :disabled="busy" @click="saveTemplate">Save as template</button>
|
||||||
|
<button v-if="editable" type="button" class="rounded-full bg-amber-400 px-3 py-1.5 text-xs font-semibold text-zinc-950 disabled:opacity-50" :disabled="busy" @click="saveChanges">Save changes</button>
|
||||||
|
<button type="button" class="rounded-full border border-sky-300/40 px-3 py-1.5 text-xs text-sky-100 hover:border-sky-200" @click="loadAway">{{ job.kind === 'music' ? 'Load into Music' : 'Load into Studio' }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
applyQueuedShots,
|
||||||
|
LOAD_MUSIC_JOB_KEY,
|
||||||
|
LOAD_QUEUED_JOB_KEY,
|
||||||
|
queuedJobCopyText,
|
||||||
|
queuedJobEditable,
|
||||||
|
queuedJobKindLabel,
|
||||||
|
queuedJobPresetBody,
|
||||||
|
queuedJobShots,
|
||||||
|
type QueuedInspectJob,
|
||||||
|
type QueuedInspectPayload,
|
||||||
|
type QueuedShotDraft
|
||||||
|
} from '~/utils/queuedJob'
|
||||||
|
import { loraMatchesUse, loraStackLabel, persistLoraFields, readLoraStack, type LoraStackItem } from '~/utils/loras'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
jobId?: string | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
saved: []
|
||||||
|
loaded: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const job = ref<QueuedInspectJob | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const busy = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const notice = ref('')
|
||||||
|
const presetName = ref('')
|
||||||
|
const loraNames = ref<string[]>([])
|
||||||
|
const loraTriggers = ref<Record<string, string>>({})
|
||||||
|
const loraAliases = ref<Record<string, string>>({})
|
||||||
|
const draft = reactive({
|
||||||
|
name: '',
|
||||||
|
prompt: '',
|
||||||
|
promptPre: '',
|
||||||
|
promptPost: '',
|
||||||
|
negative: '',
|
||||||
|
tags: '',
|
||||||
|
lyrics: '',
|
||||||
|
instrumental: false,
|
||||||
|
duration: 5,
|
||||||
|
seed: '',
|
||||||
|
steps: 20,
|
||||||
|
cfg: 1,
|
||||||
|
fps: 24,
|
||||||
|
turbo: true,
|
||||||
|
sound: true,
|
||||||
|
loraStack: [] as LoraStackItem[]
|
||||||
|
})
|
||||||
|
const extraShots = ref<QueuedShotDraft[]>([])
|
||||||
|
|
||||||
|
const kindLabel = computed(() => job.value ? queuedJobKindLabel(job.value) : 'Job')
|
||||||
|
const editable = computed(() => job.value ? queuedJobEditable(job.value) : false)
|
||||||
|
const shots = computed(() => job.value ? queuedJobShots(currentPayload(), job.value.kind) : [])
|
||||||
|
const thumbUrl = computed(() => {
|
||||||
|
const stillId = job.value?.payload?.stillId
|
||||||
|
if (!stillId || job.value?.payload?.hideThumbnail || job.value?.payload?.folderLocked) return ''
|
||||||
|
return `/api/library/stills/${stillId}`
|
||||||
|
})
|
||||||
|
const loraLine = computed(() => loraStackLabel(draft.loraStack, loraAliases.value))
|
||||||
|
const loraEngine = computed(() => {
|
||||||
|
const engine = job.value?.payload?.engine
|
||||||
|
return engine === 'flux' || engine === 'krea' ? engine : ''
|
||||||
|
})
|
||||||
|
const metaLine = computed(() => {
|
||||||
|
const payload = job.value?.payload
|
||||||
|
if (!payload) return ''
|
||||||
|
return [
|
||||||
|
payload.workflow,
|
||||||
|
payload.v2Mode,
|
||||||
|
payload.engine,
|
||||||
|
payload.aspect,
|
||||||
|
payload.samplerName,
|
||||||
|
payload.scheduler
|
||||||
|
].filter(Boolean).join(' · ')
|
||||||
|
})
|
||||||
|
const statusLine = computed(() => {
|
||||||
|
if (!job.value) return ''
|
||||||
|
if (job.value.status === 'waiting') return 'Waiting in the queue'
|
||||||
|
if (job.value.status === 'held') return 'Paused'
|
||||||
|
if (job.value.status === 'running') return 'Generating'
|
||||||
|
if (job.value.status === 'error') return 'Failed'
|
||||||
|
return job.value.status
|
||||||
|
})
|
||||||
|
|
||||||
|
function currentPayload(): QueuedInspectPayload {
|
||||||
|
const base = { ...(job.value?.payload || {}) }
|
||||||
|
base.name = draft.name
|
||||||
|
base.prompt = draft.prompt
|
||||||
|
base.promptPre = draft.promptPre
|
||||||
|
base.promptPost = draft.promptPost
|
||||||
|
base.negative = draft.negative
|
||||||
|
base.tags = draft.tags
|
||||||
|
base.lyrics = draft.lyrics
|
||||||
|
base.instrumental = draft.instrumental
|
||||||
|
base.duration = draft.duration
|
||||||
|
base.steps = draft.steps
|
||||||
|
base.cfg = draft.cfg
|
||||||
|
base.fps = draft.fps
|
||||||
|
base.turbo = draft.turbo
|
||||||
|
base.sound = draft.sound
|
||||||
|
const seed = Number(draft.seed)
|
||||||
|
if (Number.isFinite(seed)) base.seed = seed
|
||||||
|
const fields = persistLoraFields(draft.loraStack)
|
||||||
|
base.loraName = fields.loraName
|
||||||
|
base.loraStack = fields.loraStack
|
||||||
|
if (!job.value) return base
|
||||||
|
return applyQueuedShots(job.value.kind, [
|
||||||
|
{ prompt: draft.prompt, duration: draft.duration, loraName: fields.loraName, loraStack: fields.loraStack },
|
||||||
|
...extraShots.value
|
||||||
|
], base)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrate(next: QueuedInspectJob) {
|
||||||
|
job.value = next
|
||||||
|
const payload = next.payload || {}
|
||||||
|
draft.name = payload.name || next.name || ''
|
||||||
|
draft.prompt = payload.prompt || next.prompt || ''
|
||||||
|
draft.promptPre = payload.promptPre || ''
|
||||||
|
draft.promptPost = payload.promptPost || ''
|
||||||
|
draft.negative = payload.negative || ''
|
||||||
|
draft.tags = payload.tags || (next.kind === 'music' ? (payload.prompt || next.prompt) : '') || ''
|
||||||
|
draft.lyrics = payload.lyrics || ''
|
||||||
|
draft.instrumental = payload.instrumental === true
|
||||||
|
draft.duration = Number(payload.duration) || 5
|
||||||
|
draft.seed = payload.seed != null ? String(payload.seed) : ''
|
||||||
|
draft.steps = Number(payload.steps) || 20
|
||||||
|
draft.cfg = Number(payload.cfg) || 1
|
||||||
|
draft.fps = Number(payload.fps) || 24
|
||||||
|
draft.turbo = payload.turbo !== false
|
||||||
|
draft.sound = payload.sound !== false
|
||||||
|
draft.loraStack = readLoraStack(payload)
|
||||||
|
extraShots.value = queuedJobShots(payload, next.kind).slice(1).map(item => ({ ...item }))
|
||||||
|
presetName.value = draft.name || kindLabel.value
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadJob(id: string) {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
notice.value = ''
|
||||||
|
job.value = null
|
||||||
|
try {
|
||||||
|
const data = await $fetch<QueuedInspectJob>(`/api/studio-queue/${id}`)
|
||||||
|
hydrate(data)
|
||||||
|
const listed = await $fetch<{ video?: string[]; image?: string[] }>('/api/loras').catch(() => ({ video: [], image: [] }))
|
||||||
|
const library = await $fetch<{ loraTriggers?: Record<string, string>; loraAliases?: Record<string, string> }>('/api/library?light=1').catch(() => ({}))
|
||||||
|
if (library.loraTriggers) loraTriggers.value = library.loraTriggers
|
||||||
|
if (library.loraAliases) loraAliases.value = library.loraAliases
|
||||||
|
const video = listed.video || []
|
||||||
|
const image = listed.image || []
|
||||||
|
if (data.kind === 'edit') {
|
||||||
|
const engine = data.payload?.engine === 'krea' ? 'krea' : data.payload?.engine === 'flux' ? 'flux' : ''
|
||||||
|
loraNames.value = engine
|
||||||
|
? image.filter(name => loraMatchesUse(name, engine))
|
||||||
|
: image
|
||||||
|
} else {
|
||||||
|
loraNames.value = video.filter(name => loraMatchesUse(name, 'minimax') || loraMatchesUse(name, 'ltx'))
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not load that job'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyJob() {
|
||||||
|
if (!job.value) return
|
||||||
|
const text = queuedJobCopyText({ ...job.value, name: draft.name, prompt: draft.prompt, payload: currentPayload() })
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
notice.value = 'Copied to the clipboard.'
|
||||||
|
} catch {
|
||||||
|
error.value = 'Could not copy. Select the fields and copy them yourself.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTemplate() {
|
||||||
|
if (!job.value) return
|
||||||
|
const name = presetName.value.trim()
|
||||||
|
if (!name) {
|
||||||
|
error.value = 'Name the template first.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
busy.value = true
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
await $fetch('/api/generation-presets', {
|
||||||
|
method: 'POST',
|
||||||
|
body: queuedJobPresetBody({ ...job.value, name: draft.name, payload: currentPayload() }, name)
|
||||||
|
})
|
||||||
|
notice.value = `Saved template “${name}”.`
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not save that template'
|
||||||
|
} finally {
|
||||||
|
busy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChanges() {
|
||||||
|
if (!job.value || !editable.value) return
|
||||||
|
busy.value = true
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
const payload = currentPayload()
|
||||||
|
const saved = await $fetch<QueuedInspectJob>(`/api/studio-queue/${job.value.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
name: payload.name,
|
||||||
|
prompt: job.value.kind === 'music' ? payload.tags : payload.prompt,
|
||||||
|
tags: payload.tags,
|
||||||
|
lyrics: payload.lyrics,
|
||||||
|
instrumental: payload.instrumental,
|
||||||
|
promptPre: payload.promptPre,
|
||||||
|
promptPost: payload.promptPost,
|
||||||
|
negative: payload.negative,
|
||||||
|
duration: payload.duration,
|
||||||
|
seed: payload.seed,
|
||||||
|
steps: payload.steps,
|
||||||
|
cfg: payload.cfg,
|
||||||
|
fps: payload.fps,
|
||||||
|
turbo: payload.turbo,
|
||||||
|
sound: payload.sound,
|
||||||
|
loraStack: payload.loraStack,
|
||||||
|
extensions: payload.extensions,
|
||||||
|
passes: payload.passes
|
||||||
|
}
|
||||||
|
})
|
||||||
|
hydrate({ ...job.value, ...saved, payload: saved.payload || payload, source: 'studio' })
|
||||||
|
notice.value = 'Saved. The queue will use this the next time it runs.'
|
||||||
|
emit('saved')
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not save those changes'
|
||||||
|
} finally {
|
||||||
|
busy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAway() {
|
||||||
|
if (!job.value) return
|
||||||
|
try {
|
||||||
|
if (job.value.kind === 'music') {
|
||||||
|
sessionStorage.setItem(LOAD_MUSIC_JOB_KEY, JSON.stringify({
|
||||||
|
name: draft.name,
|
||||||
|
tags: draft.tags,
|
||||||
|
lyrics: draft.lyrics,
|
||||||
|
instrumental: draft.instrumental,
|
||||||
|
duration: draft.duration,
|
||||||
|
steps: draft.steps,
|
||||||
|
seed: draft.seed,
|
||||||
|
folderId: job.value.payload.folderId || ''
|
||||||
|
}))
|
||||||
|
if (window.location.pathname !== '/music') {
|
||||||
|
void navigateTo('/music')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sessionStorage.setItem(LOAD_QUEUED_JOB_KEY, job.value.id)
|
||||||
|
if (window.location.pathname !== '/') {
|
||||||
|
void navigateTo('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
emit('loaded')
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKey(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape' && props.jobId) {
|
||||||
|
event.preventDefault()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.jobId, (id) => {
|
||||||
|
if (id) void loadJob(id)
|
||||||
|
else job.value = null
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
onMounted(() => window.addEventListener('keydown', onKey))
|
||||||
|
onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
||||||
|
</script>
|
||||||
+154
-7
@@ -1177,7 +1177,7 @@
|
|||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold text-zinc-100">Job queue</h3>
|
<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>
|
</div>
|
||||||
<NuxtLink to="/queue" class="shrink-0 text-xs text-amber-200 hover:text-white">Open Queue</NuxtLink>
|
<NuxtLink to="/queue" class="shrink-0 text-xs text-amber-200 hover:text-white">Open Queue</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
@@ -1192,17 +1192,17 @@
|
|||||||
? 'border-sky-300/30 bg-sky-400/5'
|
? 'border-sky-300/30 bg-sky-400/5'
|
||||||
: 'border-white/10 bg-zinc-950/50'"
|
: 'border-white/10 bg-zinc-950/50'"
|
||||||
>
|
>
|
||||||
<div class="min-w-0 flex-1">
|
<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">{{ job.name }}</p>
|
<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">
|
<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-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>
|
<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) }}
|
{{ studioJobStatusLabel(job) }}
|
||||||
· {{ studioJobShotLine(job) }}
|
· {{ studioJobShotLine(job) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="job.status === 'waiting'"
|
v-if="job.status === 'waiting' && job.kind !== 'music'"
|
||||||
type="button"
|
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"
|
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)"
|
@click="cutInStudioJob(job.id)"
|
||||||
@@ -1210,7 +1210,7 @@
|
|||||||
{{ job.cutIn ? 'Next' : (canCutInStudioJob ? 'Run next' : 'Start now') }}
|
{{ job.cutIn ? 'Next' : (canCutInStudioJob ? 'Run next' : 'Start now') }}
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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"
|
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)"
|
@click="retryStudioJob(job.id)"
|
||||||
@@ -2193,6 +2193,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</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 class="pointer-events-none fixed bottom-4 right-4 z-50 space-y-2">
|
||||||
<div
|
<div
|
||||||
v-for="toast in toasts"
|
v-for="toast in toasts"
|
||||||
@@ -2258,6 +2265,7 @@ import {
|
|||||||
VIDEO_CFG_STEP,
|
VIDEO_CFG_STEP,
|
||||||
type GenerationPreset
|
type GenerationPreset
|
||||||
} from '~/utils/generationPresets'
|
} from '~/utils/generationPresets'
|
||||||
|
import { LOAD_QUEUED_JOB_KEY } from '~/utils/queuedJob'
|
||||||
import {
|
import {
|
||||||
clipSettingsRows,
|
clipSettingsRows,
|
||||||
inferStillPipeline,
|
inferStillPipeline,
|
||||||
@@ -2781,6 +2789,7 @@ const studioJobs = ref<Array<{
|
|||||||
pendingCount?: number
|
pendingCount?: number
|
||||||
imagePipeline?: string
|
imagePipeline?: string
|
||||||
}>>([])
|
}>>([])
|
||||||
|
const inspectJobId = ref('')
|
||||||
const outputStudioJobId = ref('')
|
const outputStudioJobId = ref('')
|
||||||
const videoAwaitingBeast = ref(false)
|
const videoAwaitingBeast = ref(false)
|
||||||
const pickerOpen = ref(false)
|
const pickerOpen = ref(false)
|
||||||
@@ -3980,6 +3989,7 @@ onMounted(async () => {
|
|||||||
await loadPromptPresets()
|
await loadPromptPresets()
|
||||||
if (route.query.clip || route.query.still) applyIncomingLibraryItem()
|
if (route.query.clip || route.query.still) applyIncomingLibraryItem()
|
||||||
else applyIncomingDirectorScript()
|
else applyIncomingDirectorScript()
|
||||||
|
await applyIncomingQueuedJob()
|
||||||
window.addEventListener('keydown', onStudioKey)
|
window.addEventListener('keydown', onStudioKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -4416,6 +4426,27 @@ function applyIncomingDirectorScript() {
|
|||||||
} catch { /* ignore */ }
|
} 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() {
|
async function loadGenerationPresets() {
|
||||||
const [video, image, imagev2] = await Promise.all([
|
const [video, image, imagev2] = await Promise.all([
|
||||||
$fetch<{ presets?: GenerationPreset[] }>('/api/generation-presets', { query: { kind: 'video' } }).catch(() => ({ presets: [] })),
|
$fetch<{ presets?: GenerationPreset[] }>('/api/generation-presets', { query: { kind: 'video' } }).catch(() => ({ presets: [] })),
|
||||||
@@ -5205,7 +5236,7 @@ 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 && job.kind !== 'music')
|
||||||
if (running?.liveJobId) videoAwaitingBeast.value = false
|
if (running?.liveJobId) videoAwaitingBeast.value = false
|
||||||
if (watchingLiveVideo() && running?.liveJobId && running.liveJobId !== videoJobId.value) {
|
if (watchingLiveVideo() && running?.liveJobId && running.liveJobId !== videoJobId.value) {
|
||||||
return
|
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) {
|
async function restoreStudioJob(id: string) {
|
||||||
const job = await $fetch<{
|
const job = await $fetch<{
|
||||||
payload?: {
|
payload?: {
|
||||||
@@ -5309,10 +5436,30 @@ async function restoreStudioJob(id: string) {
|
|||||||
loraStack?: LoraStackItem[]
|
loraStack?: LoraStackItem[]
|
||||||
shotLoras?: string[]
|
shotLoras?: string[]
|
||||||
shotLoraStacks?: LoraStackItem[][]
|
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}`)
|
}>(`/api/studio-queue/${id}`)
|
||||||
const payload = job.payload
|
const payload = job.payload
|
||||||
if (!payload) throw new Error('That job has no saved inputs')
|
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'
|
studioMode.value = 'video'
|
||||||
outputFocus.value = 'video'
|
outputFocus.value = 'video'
|
||||||
closeExtend()
|
closeExtend()
|
||||||
|
|||||||
+76
-1
@@ -124,14 +124,32 @@
|
|||||||
<p v-if="blockReason" class="text-xs text-zinc-400">{{ blockReason }}</p>
|
<p v-if="blockReason" class="text-xs text-zinc-400">{{ blockReason }}</p>
|
||||||
<p v-else-if="queued" class="text-xs text-zinc-400">
|
<p v-else-if="queued" class="text-xs text-zinc-400">
|
||||||
Waiting in the job queue.
|
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>
|
<NuxtLink to="/queue" class="text-amber-200 hover:text-amber-100">Open Queue</NuxtLink>
|
||||||
</p>
|
</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>
|
||||||
|
|
||||||
<section class="glass min-w-0 max-w-full space-y-4 rounded-3xl p-5 sm:p-6">
|
<section class="glass min-w-0 max-w-full space-y-4 rounded-3xl p-5 sm:p-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-display text-2xl font-bold">Output</h2>
|
<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>
|
<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>
|
||||||
<div v-if="busy" class="h-2 overflow-hidden rounded-full bg-zinc-800">
|
<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)}%` }" />
|
<div class="progress-glow h-full rounded-full bg-amber-400 transition-[width]" :style="{ width: `${Math.max(progress, 4)}%` }" />
|
||||||
@@ -150,6 +168,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
<QueuedJobModal :job-id="inspectJobId" @close="inspectJobId = ''" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -167,6 +186,8 @@ import {
|
|||||||
} from '~/utils/music'
|
} from '~/utils/music'
|
||||||
import { ensureGenerationNotifyPermission, notifyGenerationComplete } from '~/utils/generationNotify'
|
import { ensureGenerationNotifyPermission, notifyGenerationComplete } from '~/utils/generationNotify'
|
||||||
import type { LibraryFolder } from '~/utils/libraryBrowse'
|
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 tagsPlaceholder = DEFAULT_MUSIC_TAGS
|
||||||
const lyricsPlaceholder = DEFAULT_MUSIC_LYRICS
|
const lyricsPlaceholder = DEFAULT_MUSIC_LYRICS
|
||||||
@@ -197,6 +218,10 @@ const studioJobId = ref('')
|
|||||||
const queued = ref(false)
|
const queued = ref(false)
|
||||||
const queueCount = ref(0)
|
const queueCount = ref(0)
|
||||||
const downloadName = ref('track.flac')
|
const downloadName = ref('track.flac')
|
||||||
|
const inspectJobId = ref('')
|
||||||
|
const liveMusicId = ref('')
|
||||||
|
const musicPresets = ref<GenerationPreset[]>([])
|
||||||
|
const presetId = ref('')
|
||||||
let source: EventSource | null = null
|
let source: EventSource | null = null
|
||||||
let listenGen = 0
|
let listenGen = 0
|
||||||
let settled = false
|
let settled = false
|
||||||
@@ -253,12 +278,15 @@ async function refreshStudioQueue() {
|
|||||||
jobs?: Array<{
|
jobs?: Array<{
|
||||||
id: string
|
id: string
|
||||||
status: string
|
status: string
|
||||||
|
kind?: string
|
||||||
liveJobId?: string
|
liveJobId?: string
|
||||||
lastError?: string
|
lastError?: string
|
||||||
}>
|
}>
|
||||||
waitingCount?: number
|
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 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
|
queueCount.value = data.waitingCount || rows.filter(job => job.status === 'waiting').length
|
||||||
if (!studioJobId.value) return
|
if (!studioJobId.value) return
|
||||||
const row = rows.find(job => job.id === studioJobId.value)
|
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
|
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 () => {
|
onMounted(async () => {
|
||||||
await loadLibrary()
|
await loadLibrary()
|
||||||
|
await loadMusicPresets()
|
||||||
|
applyIncomingMusic()
|
||||||
await refreshStudioQueue()
|
await refreshStudioQueue()
|
||||||
stopQueuePoll()
|
stopQueuePoll()
|
||||||
queuePoll = setInterval(() => { void refreshStudioQueue() }, 8000)
|
queuePoll = setInterval(() => { void refreshStudioQueue() }, 8000)
|
||||||
|
|||||||
+21
-25
@@ -11,7 +11,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="font-display text-2xl font-bold">Queue</h2>
|
<h2 class="font-display text-2xl font-bold">Queue</h2>
|
||||||
<p class="mt-1 max-w-2xl text-sm text-zinc-400">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
|
<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="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div class="min-w-0">
|
<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
|
<img
|
||||||
v-if="jobThumb(job)"
|
v-if="jobThumb(job)"
|
||||||
:src="jobThumb(job)"
|
:src="jobThumb(job)"
|
||||||
@@ -65,18 +65,18 @@
|
|||||||
class="h-12 w-12 shrink-0 rounded-lg bg-black/40 object-cover"
|
class="h-12 w-12 shrink-0 rounded-lg bg-black/40 object-cover"
|
||||||
>
|
>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="text-[11px] uppercase tracking-[0.18em] text-zinc-500">{{ jobKindLabel(job) }}</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">{{ job.name }}</h3>
|
<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 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-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-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 v-if="job.lastError" class="mt-1 text-xs text-red-300">{{ job.lastError }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
v-if="job.status === 'waiting'"
|
v-if="job.status === 'waiting' && job.kind !== 'music'"
|
||||||
type="button"
|
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"
|
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)"
|
:disabled="Boolean(busyId)"
|
||||||
@@ -410,6 +410,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
<QueuedJobModal
|
||||||
|
:job-id="inspectJobId"
|
||||||
|
@close="inspectJobId = ''"
|
||||||
|
@saved="loadQueues"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -477,6 +482,7 @@ type PendingRemove = {
|
|||||||
const videoLoras = ref<string[]>([])
|
const videoLoras = ref<string[]>([])
|
||||||
const loraTriggers = ref<Record<string, string>>({})
|
const loraTriggers = ref<Record<string, string>>({})
|
||||||
const loraAliases = ref<Record<string, string>>({})
|
const loraAliases = ref<Record<string, string>>({})
|
||||||
|
const inspectJobId = ref('')
|
||||||
const jobs = ref<StudioJobRow[]>([])
|
const jobs = ref<StudioJobRow[]>([])
|
||||||
const orphans = ref<Queue[]>([])
|
const orphans = ref<Queue[]>([])
|
||||||
const waitingCount = ref(0)
|
const waitingCount = ref(0)
|
||||||
@@ -573,20 +579,10 @@ function progressLine(queue: Queue) {
|
|||||||
|
|
||||||
function jobLine(job: StudioJobRow) {
|
function jobLine(job: StudioJobRow) {
|
||||||
if (job.kind === 'music') {
|
if (job.kind === 'music') {
|
||||||
if (job.status === 'waiting') {
|
if (job.status === 'waiting') return job.cutIn ? 'Next after the current shot · ACE-Step' : 'Waiting · ACE-Step'
|
||||||
return job.cutIn
|
if (job.status === 'held') return 'Paused'
|
||||||
? 'Next after the current shot · ACE-Step'
|
if (job.status === 'error') return job.lastError || 'Failed'
|
||||||
: 'Waiting · ACE-Step'
|
return 'Generating track'
|
||||||
}
|
|
||||||
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.kind === 'edit') {
|
if (job.kind === 'edit') {
|
||||||
const passes = job.shotCount || 1
|
const passes = job.shotCount || 1
|
||||||
@@ -822,7 +818,7 @@ function jobKindLabel(job: StudioJobRow) {
|
|||||||
if (job.imagePipeline === 'v2') return 'IMAGE V2'
|
if (job.imagePipeline === 'v2') return 'IMAGE V2'
|
||||||
return (job.shotCount || 1) > 1 ? 'EDIT' : 'IMAGE'
|
return (job.shotCount || 1) > 1 ? 'EDIT' : 'IMAGE'
|
||||||
}
|
}
|
||||||
return 'Job'
|
return 'VIDEO'
|
||||||
}
|
}
|
||||||
|
|
||||||
function jobThumb(job: StudioJobRow) {
|
function jobThumb(job: StudioJobRow) {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export default defineEventHandler((event) => {
|
|||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
...summarizeStudioJob(job),
|
...summarizeStudioJob(job),
|
||||||
|
source: 'studio' as const,
|
||||||
...(full ? {
|
...(full ? {
|
||||||
shots: shotSummary,
|
shots: shotSummary,
|
||||||
plannedShots: plannedShots && plannedShots.length > 1 ? plannedShots : undefined
|
plannedShots: plannedShots && plannedShots.length > 1 ? plannedShots : undefined
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
|
import { cancelLiveMusicJob, findInspectJob } from '~/server/utils/queuedInspect'
|
||||||
import { cancelStudioJob } from '~/server/utils/studioQueue'
|
import { cancelStudioJob } from '~/server/utils/studioQueue'
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { owner } = assertLibraryOwner(event)
|
const { owner } = assertLibraryOwner(event)
|
||||||
const id = String(getRouterParam(event, 'id') || '')
|
const id = String(getRouterParam(event, 'id') || '')
|
||||||
|
const job = findInspectJob(owner, id)
|
||||||
|
if (!job) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: 'Queued job not found' })
|
||||||
|
}
|
||||||
|
if (job.source === 'live' && job.kind === 'music') {
|
||||||
|
await cancelLiveMusicJob(owner, id)
|
||||||
|
return { ok: true }
|
||||||
|
}
|
||||||
await cancelStudioJob(owner, id)
|
await cancelStudioJob(owner, id)
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { listStudioJobs } from '~/server/utils/studioQueue'
|
import { findInspectJob } from '~/server/utils/queuedInspect'
|
||||||
|
|
||||||
export default defineEventHandler((event) => {
|
export default defineEventHandler((event) => {
|
||||||
const { owner } = assertLibraryOwner(event)
|
const { owner } = assertLibraryOwner(event)
|
||||||
const id = String(getRouterParam(event, 'id') || '')
|
const id = String(getRouterParam(event, 'id') || '')
|
||||||
const job = listStudioJobs(owner).find(item => item.id === id)
|
const job = findInspectJob(owner, id)
|
||||||
if (!job) {
|
if (!job) {
|
||||||
throw createError({ statusCode: 404, statusMessage: 'Queued job not found' })
|
throw createError({ statusCode: 404, statusMessage: 'Queued job not found' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { persistLoraFields, normalizeLoraStack } from '~/utils/loras'
|
||||||
|
import { listStudioJobs, patchStudioJob, type StudioJobPayload } from '~/server/utils/studioQueue'
|
||||||
|
|
||||||
|
const EDITABLE = new Set(['waiting', 'held', 'error'])
|
||||||
|
|
||||||
|
function text(raw: unknown, max = 8000) {
|
||||||
|
return String(raw ?? '').replace(/\r\n/g, '\n').slice(0, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionalNumber(raw: unknown) {
|
||||||
|
if (raw == null || raw === '') return undefined
|
||||||
|
const value = Number(raw)
|
||||||
|
return Number.isFinite(value) ? value : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionalBool(raw: unknown) {
|
||||||
|
if (raw === true || raw === 'true' || raw === 1 || raw === '1') return true
|
||||||
|
if (raw === false || raw === 'false' || raw === 0 || raw === '0') return false
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { owner } = assertLibraryOwner(event)
|
||||||
|
const id = String(getRouterParam(event, 'id') || '')
|
||||||
|
const current = listStudioJobs(owner).find(item => item.id === id)
|
||||||
|
if (!current) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: 'Queued job not found' })
|
||||||
|
}
|
||||||
|
if (!EDITABLE.has(current.status)) {
|
||||||
|
throw createError({ statusCode: 409, statusMessage: 'That job is already generating. Copy it or save a template.' })
|
||||||
|
}
|
||||||
|
const body = await readBody<Record<string, unknown>>(event).catch(() => ({}))
|
||||||
|
|
||||||
|
const job = await patchStudioJob(owner, id, (row) => {
|
||||||
|
const payload = row.payload
|
||||||
|
if (body.name != null) {
|
||||||
|
const name = text(body.name, 80).trim()
|
||||||
|
payload.name = name
|
||||||
|
row.name = name || payload.prompt.slice(0, 80)
|
||||||
|
}
|
||||||
|
if (body.prompt != null) {
|
||||||
|
payload.prompt = text(body.prompt)
|
||||||
|
row.prompt = payload.prompt
|
||||||
|
}
|
||||||
|
if (body.tags != null && row.kind === 'music') {
|
||||||
|
payload.prompt = text(body.tags, 2000)
|
||||||
|
row.prompt = payload.prompt
|
||||||
|
}
|
||||||
|
if (body.lyrics != null) payload.lyrics = text(body.lyrics)
|
||||||
|
const instrumental = optionalBool(body.instrumental)
|
||||||
|
if (instrumental != null) payload.instrumental = instrumental
|
||||||
|
if (body.promptPre != null) payload.promptPre = text(body.promptPre)
|
||||||
|
if (body.promptMid != null) payload.promptMid = text(body.promptMid)
|
||||||
|
if (body.promptPost != null) payload.promptPost = text(body.promptPost)
|
||||||
|
if (body.negative != null) payload.negative = text(body.negative, 2000)
|
||||||
|
const duration = optionalNumber(body.duration)
|
||||||
|
if (duration != null) payload.duration = Math.min(120, Math.max(0.5, duration))
|
||||||
|
const steps = optionalNumber(body.steps)
|
||||||
|
if (steps != null) payload.steps = Math.min(100, Math.max(1, Math.round(steps)))
|
||||||
|
const cfg = optionalNumber(body.cfg)
|
||||||
|
if (cfg != null) payload.cfg = Math.min(20, Math.max(0, Math.round(cfg * 10) / 10))
|
||||||
|
const fps = optionalNumber(body.fps)
|
||||||
|
if (fps === 12 || fps === 24 || fps === 30) payload.fps = fps
|
||||||
|
const seed = optionalNumber(body.seed)
|
||||||
|
if (seed != null) payload.seed = Math.max(0, Math.min(2_147_483_647, Math.floor(seed)))
|
||||||
|
const turbo = optionalBool(body.turbo)
|
||||||
|
if (turbo != null) payload.turbo = turbo
|
||||||
|
const sound = optionalBool(body.sound)
|
||||||
|
if (sound != null) payload.sound = sound
|
||||||
|
if (body.samplerName != null) payload.samplerName = text(body.samplerName, 40)
|
||||||
|
if (body.scheduler != null) payload.scheduler = text(body.scheduler, 40)
|
||||||
|
if (Array.isArray(body.loraStack) || body.loraName != null) {
|
||||||
|
const fields = persistLoraFields(body.loraStack ?? payload.loraStack ?? body.loraName)
|
||||||
|
payload.loraName = fields.loraName
|
||||||
|
payload.loraStack = fields.loraStack
|
||||||
|
}
|
||||||
|
if (Array.isArray(body.passes) && row.kind === 'edit') {
|
||||||
|
payload.passes = body.passes.map((item) => ({
|
||||||
|
prompt: text((item as { prompt?: unknown })?.prompt)
|
||||||
|
}))
|
||||||
|
row.shotCount = 1 + payload.passes.length
|
||||||
|
}
|
||||||
|
if (Array.isArray(body.extensions) && row.kind !== 'edit') {
|
||||||
|
payload.extensions = body.extensions.map((item, index) => {
|
||||||
|
const rec = (item && typeof item === 'object' ? item : {}) as {
|
||||||
|
prompt?: unknown
|
||||||
|
duration?: unknown
|
||||||
|
loraName?: unknown
|
||||||
|
loraStack?: unknown
|
||||||
|
}
|
||||||
|
const prev = payload.extensions?.[index]
|
||||||
|
const fields = persistLoraFields(rec.loraStack || rec.loraName || prev?.loraStack || prev?.loraName)
|
||||||
|
const shotDuration = optionalNumber(rec.duration)
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
prompt: text(rec.prompt),
|
||||||
|
duration: shotDuration != null ? Math.min(120, Math.max(0.5, shotDuration)) : (prev?.duration || payload.duration),
|
||||||
|
loraName: fields.loraName,
|
||||||
|
loraStack: fields.loraStack
|
||||||
|
}
|
||||||
|
}) as StudioJobPayload['extensions']
|
||||||
|
row.shotCount = 1 + payload.extensions.length
|
||||||
|
}
|
||||||
|
if (normalizeLoraStack(payload.loraStack).length && !payload.loraName) {
|
||||||
|
payload.loraName = payload.loraStack?.[0]?.name
|
||||||
|
}
|
||||||
|
if (body.prompt != null && !row.name) row.name = payload.prompt.slice(0, 80)
|
||||||
|
})
|
||||||
|
|
||||||
|
return job
|
||||||
|
})
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
type GenerationPreset,
|
type GenerationPreset,
|
||||||
type GenerationPresetKind,
|
type GenerationPresetKind,
|
||||||
type ImagePresetSettings,
|
type ImagePresetSettings,
|
||||||
|
type MusicPresetSettings,
|
||||||
type VideoPresetSettings
|
type VideoPresetSettings
|
||||||
} from '~/utils/generationPresets'
|
} from '~/utils/generationPresets'
|
||||||
import {
|
import {
|
||||||
@@ -35,6 +36,13 @@ import {
|
|||||||
parseImageV2Task,
|
parseImageV2Task,
|
||||||
type ImageV2PresetSettings
|
type ImageV2PresetSettings
|
||||||
} from '~/utils/imageV2'
|
} from '~/utils/imageV2'
|
||||||
|
import {
|
||||||
|
clampMusicCfg,
|
||||||
|
clampMusicDuration,
|
||||||
|
clampMusicSteps,
|
||||||
|
sanitizeMusicLyrics,
|
||||||
|
sanitizeMusicTags
|
||||||
|
} from '~/utils/music'
|
||||||
import { coerceVideoWorkflow, isXaigenStudio, parseVideoWorkflow } from '~/utils/videoModels'
|
import { coerceVideoWorkflow, isXaigenStudio, parseVideoWorkflow } from '~/utils/videoModels'
|
||||||
|
|
||||||
const writeChains = new Map<string, Promise<unknown>>()
|
const writeChains = new Map<string, Promise<unknown>>()
|
||||||
@@ -155,9 +163,24 @@ function sanitizeImageV2Settings(raw: unknown): ImageV2PresetSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeMusicSettings(raw: unknown): MusicPresetSettings {
|
||||||
|
const rec = raw && typeof raw === 'object' ? raw as Record<string, unknown> : {}
|
||||||
|
const instrumental = rec.instrumental === true
|
||||||
|
return {
|
||||||
|
tags: sanitizeMusicTags(rec.tags),
|
||||||
|
lyrics: sanitizeMusicLyrics(rec.lyrics, instrumental),
|
||||||
|
instrumental,
|
||||||
|
duration: clampMusicDuration(rec.duration),
|
||||||
|
steps: clampMusicSteps(rec.steps),
|
||||||
|
seed: typeof rec.seed === 'number' && Number.isFinite(rec.seed) ? Math.max(0, Math.floor(rec.seed)) : undefined,
|
||||||
|
cfg: rec.cfg == null ? undefined : clampMusicCfg(rec.cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeSettings(kind: GenerationPresetKind, raw: unknown) {
|
function sanitizeSettings(kind: GenerationPresetKind, raw: unknown) {
|
||||||
if (kind === 'image') return sanitizeImageSettings(raw)
|
if (kind === 'image') return sanitizeImageSettings(raw)
|
||||||
if (kind === 'imagev2') return sanitizeImageV2Settings(raw)
|
if (kind === 'imagev2') return sanitizeImageV2Settings(raw)
|
||||||
|
if (kind === 'music') return sanitizeMusicSettings(raw)
|
||||||
return sanitizeVideoSettings(raw)
|
return sanitizeVideoSettings(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { emitJob, getJob, listJobs, type Job } from '~/server/utils/jobs'
|
||||||
|
import { listStudioJobs, type StudioJob } from '~/server/utils/studioQueue'
|
||||||
|
|
||||||
|
const LIVE_ERROR_KEEP_MS = 60 * 60 * 1000
|
||||||
|
|
||||||
|
function liveMusicActive(job: Job) {
|
||||||
|
if (job.kind !== 'music') return false
|
||||||
|
if (job.status === 'queued' || job.status === 'uploading' || job.status === 'running') return true
|
||||||
|
if (job.status === 'error' && Date.now() - job.startedAt < LIVE_ERROR_KEEP_MS) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listLiveMusicJobs(owner: string) {
|
||||||
|
return listJobs()
|
||||||
|
.filter(job => liveMusicActive(job) && job.library?.ownerKey === owner)
|
||||||
|
.sort((a, b) => b.startedAt - a.startedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summarizeLiveMusicJob(job: Job) {
|
||||||
|
const lib = job.library
|
||||||
|
const tags = String(lib?.tags || lib?.prompt || '').trim()
|
||||||
|
return {
|
||||||
|
id: job.id,
|
||||||
|
createdAt: job.startedAt,
|
||||||
|
updatedAt: job.startedAt,
|
||||||
|
status: job.status === 'error' ? 'error' : 'running',
|
||||||
|
kind: 'music' as const,
|
||||||
|
name: (lib?.name || '').trim() || tags.slice(0, 80) || 'Music',
|
||||||
|
prompt: tags,
|
||||||
|
shotCount: 1,
|
||||||
|
liveJobId: job.id,
|
||||||
|
source: 'live' as const,
|
||||||
|
lastError: job.error,
|
||||||
|
hideThumbnail: true,
|
||||||
|
folderLocked: lib?.folderLocked === true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inspectStudioJob(job: StudioJob) {
|
||||||
|
return {
|
||||||
|
source: 'studio' as const,
|
||||||
|
id: job.id,
|
||||||
|
createdAt: job.createdAt,
|
||||||
|
updatedAt: job.updatedAt,
|
||||||
|
status: job.status,
|
||||||
|
kind: job.kind === 'edit' ? 'edit' as const : 'video' as const,
|
||||||
|
name: job.name,
|
||||||
|
prompt: job.prompt,
|
||||||
|
shotCount: job.shotCount,
|
||||||
|
familyId: job.familyId,
|
||||||
|
shotQueueId: job.shotQueueId,
|
||||||
|
liveJobId: job.liveJobId,
|
||||||
|
lastError: job.lastError,
|
||||||
|
imagePipeline: job.payload.imagePipeline || 'v1',
|
||||||
|
cutIn: job.cutIn === true,
|
||||||
|
holdForCutIn: job.holdForCutIn === true,
|
||||||
|
pauseAfterCurrent: job.pauseAfterCurrent === true,
|
||||||
|
pausedByUser: job.pausedByUser === true,
|
||||||
|
payload: job.kind === 'music'
|
||||||
|
? { ...job.payload, tags: job.payload.prompt }
|
||||||
|
: job.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inspectLiveMusicJob(job: Job) {
|
||||||
|
const lib = job.library
|
||||||
|
const tags = String(lib?.tags || lib?.prompt || '').trim()
|
||||||
|
return {
|
||||||
|
source: 'live' as const,
|
||||||
|
id: job.id,
|
||||||
|
createdAt: job.startedAt,
|
||||||
|
updatedAt: job.startedAt,
|
||||||
|
status: job.status === 'error' ? 'error' : job.status === 'cancelled' ? 'cancelled' : 'running',
|
||||||
|
kind: 'music' as const,
|
||||||
|
name: (lib?.name || '').trim() || tags.slice(0, 80) || 'Music',
|
||||||
|
prompt: tags,
|
||||||
|
shotCount: 1,
|
||||||
|
liveJobId: job.id,
|
||||||
|
lastError: job.error,
|
||||||
|
imagePipeline: undefined,
|
||||||
|
payload: {
|
||||||
|
name: lib?.name || '',
|
||||||
|
prompt: tags,
|
||||||
|
tags,
|
||||||
|
lyrics: lib?.lyrics || '',
|
||||||
|
instrumental: lib?.instrumental === true,
|
||||||
|
folderId: lib?.folderId || '',
|
||||||
|
duration: lib?.duration,
|
||||||
|
steps: lib?.steps,
|
||||||
|
seed: lib?.seed,
|
||||||
|
cfg: lib?.cfg,
|
||||||
|
engine: lib?.engine,
|
||||||
|
lyricsStrength: lib?.lyricsStrength,
|
||||||
|
hideThumbnail: true,
|
||||||
|
folderLocked: lib?.folderLocked === true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findInspectJob(owner: string, id: string) {
|
||||||
|
const studio = listStudioJobs(owner).find(item => item.id === id)
|
||||||
|
if (studio) return inspectStudioJob(studio)
|
||||||
|
const live = getJob(id)
|
||||||
|
if (live?.kind === 'music' && live.library?.ownerKey === owner) return inspectLiveMusicJob(live)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancelLiveMusicJob(owner: string, id: string) {
|
||||||
|
const live = getJob(id)
|
||||||
|
if (!live || live.kind !== 'music' || live.library?.ownerKey !== owner) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: 'Queued job not found' })
|
||||||
|
}
|
||||||
|
live.status = 'cancelled'
|
||||||
|
if (live.library) live.library.stopAfterCurrent = true
|
||||||
|
emitJob(live, { type: 'error', error: 'Job interrupted.', message: 'Job interrupted.' })
|
||||||
|
const { interruptComfy } = await import('~/server/utils/comfy')
|
||||||
|
await interruptComfy().catch(() => false)
|
||||||
|
}
|
||||||
@@ -2,7 +2,17 @@ import type { LoraStackItem } from '~/utils/loras'
|
|||||||
import type { VideoWorkflowId } from '~/utils/videoModels'
|
import type { VideoWorkflowId } from '~/utils/videoModels'
|
||||||
import type { ImageV2PresetSettings } from '~/utils/imageV2'
|
import type { ImageV2PresetSettings } from '~/utils/imageV2'
|
||||||
|
|
||||||
export type GenerationPresetKind = 'video' | 'image' | 'imagev2'
|
export type GenerationPresetKind = 'video' | 'image' | 'imagev2' | 'music'
|
||||||
|
|
||||||
|
export type MusicPresetSettings = {
|
||||||
|
tags?: string
|
||||||
|
lyrics?: string
|
||||||
|
instrumental?: boolean
|
||||||
|
duration?: number
|
||||||
|
steps?: number
|
||||||
|
seed?: number
|
||||||
|
cfg?: number
|
||||||
|
}
|
||||||
|
|
||||||
export type VideoPresetSettings = {
|
export type VideoPresetSettings = {
|
||||||
workflow?: VideoWorkflowId
|
workflow?: VideoWorkflowId
|
||||||
@@ -86,12 +96,13 @@ export type GenerationPreset = {
|
|||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
loraStack: LoraStackItem[]
|
loraStack: LoraStackItem[]
|
||||||
settings: VideoPresetSettings | ImagePresetSettings | ImageV2PresetSettings
|
settings: VideoPresetSettings | ImagePresetSettings | ImageV2PresetSettings | MusicPresetSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseGenerationPresetKind(raw: unknown): GenerationPresetKind {
|
export function parseGenerationPresetKind(raw: unknown): GenerationPresetKind {
|
||||||
if (raw === 'image') return 'image'
|
if (raw === 'image') return 'image'
|
||||||
if (raw === 'imagev2') return 'imagev2'
|
if (raw === 'imagev2') return 'imagev2'
|
||||||
|
if (raw === 'music') return 'music'
|
||||||
return 'video'
|
return 'video'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
import { persistLoraFields, readLoraStack, type LoraStackItem } from '~/utils/loras'
|
||||||
|
import {
|
||||||
|
clampMusicDuration,
|
||||||
|
clampMusicSteps,
|
||||||
|
sanitizeMusicLyrics,
|
||||||
|
sanitizeMusicTags
|
||||||
|
} from '~/utils/music'
|
||||||
|
|
||||||
|
export const LOAD_QUEUED_JOB_KEY = 'aigen-load-queued-job'
|
||||||
|
export const LOAD_MUSIC_JOB_KEY = 'aigen-load-music-job'
|
||||||
|
|
||||||
|
export type QueuedInspectKind = 'video' | 'edit' | 'music'
|
||||||
|
export type QueuedInspectSource = 'studio' | 'live'
|
||||||
|
|
||||||
|
export type QueuedShotDraft = {
|
||||||
|
prompt: string
|
||||||
|
duration: number
|
||||||
|
loraName?: string
|
||||||
|
loraStack?: LoraStackItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueuedInspectPayload = {
|
||||||
|
prompt?: string
|
||||||
|
promptMid?: string
|
||||||
|
promptPre?: string
|
||||||
|
promptPost?: string
|
||||||
|
name?: string
|
||||||
|
folderId?: string
|
||||||
|
aspect?: string
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
steps?: number
|
||||||
|
turbo?: boolean
|
||||||
|
seed?: number
|
||||||
|
cfg?: number
|
||||||
|
fps?: number
|
||||||
|
samplerName?: string
|
||||||
|
scheduler?: string
|
||||||
|
duration?: number
|
||||||
|
sound?: boolean
|
||||||
|
workflow?: string
|
||||||
|
stillId?: string
|
||||||
|
stillFilename?: string
|
||||||
|
hideThumbnail?: boolean
|
||||||
|
folderLocked?: boolean
|
||||||
|
loraName?: string
|
||||||
|
loraStack?: LoraStackItem[]
|
||||||
|
negative?: string
|
||||||
|
passes?: { prompt: string }[]
|
||||||
|
imagePipeline?: 'v1' | 'v2'
|
||||||
|
v2Mode?: string
|
||||||
|
v2Task?: string
|
||||||
|
engine?: string
|
||||||
|
snofsModel?: number
|
||||||
|
snofsClip?: number
|
||||||
|
consistencyModel?: number
|
||||||
|
consistencyClip?: number
|
||||||
|
refineStrength?: number
|
||||||
|
scaleToTotalPixels?: boolean
|
||||||
|
scaleMegapixels?: number
|
||||||
|
extensions?: QueuedShotDraft[]
|
||||||
|
tags?: string
|
||||||
|
lyrics?: string
|
||||||
|
instrumental?: boolean
|
||||||
|
lyricsStrength?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueuedInspectJob = {
|
||||||
|
source: QueuedInspectSource
|
||||||
|
id: string
|
||||||
|
status: string
|
||||||
|
kind: QueuedInspectKind
|
||||||
|
name: string
|
||||||
|
prompt: string
|
||||||
|
shotCount: number
|
||||||
|
liveJobId?: string
|
||||||
|
shotQueueId?: string
|
||||||
|
lastError?: string
|
||||||
|
imagePipeline?: string
|
||||||
|
payload: QueuedInspectPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queuedJobKindLabel(job: { kind?: string; imagePipeline?: string; shotCount?: number }) {
|
||||||
|
if (job.kind === 'music') return 'Music'
|
||||||
|
if (job.kind === 'edit') {
|
||||||
|
if (job.imagePipeline === 'v2') return 'Image v2'
|
||||||
|
return (job.shotCount || 1) > 1 ? 'Image edit' : 'Image'
|
||||||
|
}
|
||||||
|
return 'Video'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queuedJobEditable(job: { source?: string; status?: string }) {
|
||||||
|
return job.source === 'studio' && (job.status === 'waiting' || job.status === 'held' || job.status === 'error')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queuedJobShots(payload: QueuedInspectPayload, kind: string): QueuedShotDraft[] {
|
||||||
|
if (kind === 'edit') {
|
||||||
|
const first = { prompt: payload.prompt || '', duration: 0 }
|
||||||
|
const rest = (payload.passes || []).map(item => ({ prompt: item.prompt || '', duration: 0 }))
|
||||||
|
return rest.length ? [first, ...rest] : [first]
|
||||||
|
}
|
||||||
|
if (kind === 'music') return []
|
||||||
|
const first = {
|
||||||
|
prompt: payload.prompt || '',
|
||||||
|
duration: Number(payload.duration) || 5,
|
||||||
|
loraName: payload.loraName,
|
||||||
|
loraStack: payload.loraStack
|
||||||
|
}
|
||||||
|
const rest = (payload.extensions || []).map(item => ({
|
||||||
|
prompt: item.prompt || '',
|
||||||
|
duration: Number(item.duration) || 5,
|
||||||
|
loraName: item.loraName,
|
||||||
|
loraStack: item.loraStack
|
||||||
|
}))
|
||||||
|
return [first, ...rest]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyQueuedShots(kind: string, shots: QueuedShotDraft[], payload: QueuedInspectPayload) {
|
||||||
|
const next = { ...payload }
|
||||||
|
if (kind === 'music') {
|
||||||
|
next.prompt = payload.tags || payload.prompt || ''
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
if (kind === 'edit') {
|
||||||
|
next.prompt = shots[0]?.prompt || ''
|
||||||
|
next.passes = shots.slice(1).map(item => ({ prompt: item.prompt || '' }))
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
next.prompt = shots[0]?.prompt || ''
|
||||||
|
next.duration = Number(shots[0]?.duration) || next.duration || 5
|
||||||
|
next.extensions = shots.slice(1).map((item) => {
|
||||||
|
const fields = persistLoraFields(item.loraStack || item.loraName)
|
||||||
|
return {
|
||||||
|
prompt: item.prompt || '',
|
||||||
|
duration: Number(item.duration) || 5,
|
||||||
|
loraName: fields.loraName,
|
||||||
|
loraStack: fields.loraStack
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
function line(label: string, value: unknown) {
|
||||||
|
const text = String(value ?? '').trim()
|
||||||
|
if (!text) return ''
|
||||||
|
return `${label}: ${text}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queuedJobCopyText(job: QueuedInspectJob) {
|
||||||
|
const payload = job.payload || {}
|
||||||
|
const kind = queuedJobKindLabel(job)
|
||||||
|
const lines = [
|
||||||
|
line('Kind', kind),
|
||||||
|
line('Name', job.name || payload.name),
|
||||||
|
line('Status', job.status),
|
||||||
|
job.lastError ? line('Error', job.lastError) : '',
|
||||||
|
line('Workflow', payload.workflow),
|
||||||
|
line('Mode', payload.v2Mode),
|
||||||
|
line('Engine', payload.engine),
|
||||||
|
line('Task', payload.v2Task),
|
||||||
|
line('Aspect', payload.aspect),
|
||||||
|
payload.duration != null ? line('Duration', `${payload.duration}s`) : '',
|
||||||
|
payload.steps != null ? line('Steps', payload.steps) : '',
|
||||||
|
payload.cfg != null ? line('CFG', payload.cfg) : '',
|
||||||
|
payload.fps != null ? line('FPS', payload.fps) : '',
|
||||||
|
payload.seed != null ? line('Seed', payload.seed) : '',
|
||||||
|
payload.turbo != null ? line('Turbo', payload.turbo ? 'yes' : 'no') : '',
|
||||||
|
payload.sound != null && job.kind !== 'music' ? line('Sound', payload.sound ? 'on' : 'off') : '',
|
||||||
|
line('Sampler', payload.samplerName),
|
||||||
|
line('Scheduler', payload.scheduler),
|
||||||
|
line('Negative', payload.negative)
|
||||||
|
].filter(Boolean)
|
||||||
|
|
||||||
|
const stack = readLoraStack(payload)
|
||||||
|
if (stack.length) {
|
||||||
|
lines.push(`LoRAs: ${stack.map(item => `${item.name} ${item.strengthModel}/${item.strengthClip}`).join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job.kind === 'music') {
|
||||||
|
lines.push(line('Tags', payload.tags || job.prompt))
|
||||||
|
if (payload.instrumental) lines.push('Instrumental: yes')
|
||||||
|
else lines.push(line('Lyrics', payload.lyrics))
|
||||||
|
} else {
|
||||||
|
if (payload.promptPre) lines.push(line('Prompt pre', payload.promptPre))
|
||||||
|
lines.push(line('Prompt', payload.prompt || job.prompt))
|
||||||
|
if (payload.promptPost) lines.push(line('Prompt post', payload.promptPost))
|
||||||
|
const shots = queuedJobShots(payload, job.kind)
|
||||||
|
if (shots.length > 1) {
|
||||||
|
lines.push('')
|
||||||
|
shots.forEach((shot, index) => {
|
||||||
|
const label = job.kind === 'edit' ? `Pass ${index + 1}` : `Shot ${index + 1}`
|
||||||
|
lines.push(`${label} (${shot.duration || 0}s): ${shot.prompt}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.filter(Boolean).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queuedJobPresetKind(job: { kind?: string; imagePipeline?: string }): 'video' | 'image' | 'imagev2' | 'music' {
|
||||||
|
if (job.kind === 'music') return 'music'
|
||||||
|
if (job.kind === 'edit') return job.imagePipeline === 'v2' ? 'imagev2' : 'image'
|
||||||
|
return 'video'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queuedJobPresetBody(job: QueuedInspectJob, name: string) {
|
||||||
|
const payload = job.payload || {}
|
||||||
|
const kind = queuedJobPresetKind(job)
|
||||||
|
const loraStack = readLoraStack(payload)
|
||||||
|
if (kind === 'music') {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
overwrite: true,
|
||||||
|
loraStack: [],
|
||||||
|
settings: {
|
||||||
|
tags: sanitizeMusicTags(payload.tags || job.prompt),
|
||||||
|
lyrics: sanitizeMusicLyrics(payload.lyrics, payload.instrumental === true),
|
||||||
|
instrumental: payload.instrumental === true,
|
||||||
|
duration: clampMusicDuration(payload.duration),
|
||||||
|
steps: clampMusicSteps(payload.steps),
|
||||||
|
seed: payload.seed,
|
||||||
|
cfg: payload.cfg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kind === 'imagev2') {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
overwrite: true,
|
||||||
|
loraStack,
|
||||||
|
settings: {
|
||||||
|
mode: payload.v2Mode,
|
||||||
|
engine: payload.engine,
|
||||||
|
task: payload.v2Task,
|
||||||
|
prompt: payload.prompt || job.prompt,
|
||||||
|
negative: payload.negative,
|
||||||
|
snofsModel: payload.snofsModel,
|
||||||
|
snofsClip: payload.snofsClip,
|
||||||
|
consistencyModel: payload.consistencyModel,
|
||||||
|
consistencyClip: payload.consistencyClip,
|
||||||
|
steps: payload.steps,
|
||||||
|
cfg: payload.cfg,
|
||||||
|
megapixels: payload.scaleMegapixels,
|
||||||
|
scaleToTotalPixels: payload.scaleToTotalPixels,
|
||||||
|
turbo: payload.turbo === true,
|
||||||
|
strength: payload.refineStrength,
|
||||||
|
aspect: payload.aspect,
|
||||||
|
width: payload.width,
|
||||||
|
height: payload.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kind === 'image') {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
overwrite: true,
|
||||||
|
loraStack,
|
||||||
|
settings: {
|
||||||
|
prompt: payload.prompt || job.prompt,
|
||||||
|
steps: payload.steps,
|
||||||
|
cfg: payload.cfg,
|
||||||
|
negative: payload.negative,
|
||||||
|
scaleToTotalPixels: payload.scaleToTotalPixels,
|
||||||
|
scaleMegapixels: payload.scaleMegapixels,
|
||||||
|
aspect: payload.aspect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
overwrite: true,
|
||||||
|
loraStack,
|
||||||
|
settings: {
|
||||||
|
workflow: payload.workflow,
|
||||||
|
turbo: payload.turbo !== false,
|
||||||
|
cfg: payload.cfg,
|
||||||
|
fps: payload.fps,
|
||||||
|
samplerName: payload.samplerName,
|
||||||
|
scheduler: payload.scheduler,
|
||||||
|
duration: payload.duration,
|
||||||
|
aspect: payload.aspect,
|
||||||
|
sound: payload.sound !== false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user