Add per-image iteration settings and steps/CFG sweeps
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="mt-3 space-y-3 border-t border-white/10 pt-3">
|
||||
<p class="text-xs text-zinc-400">Blank values use the shared settings. A blank prompt uses the first prompt.</p>
|
||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<label class="text-sm text-zinc-300">Steps
|
||||
<input :value="modelValue.steps" type="number" min="1" max="50" step="1" :placeholder="String(baseSteps)" class="iteration-input" :disabled="disabled" @input="numberField('steps', $event)">
|
||||
</label>
|
||||
<label class="text-sm text-zinc-300">CFG
|
||||
<input :value="modelValue.cfg" type="number" min="0" max="20" step="0.1" :placeholder="String(baseCfg)" class="iteration-input" :disabled="disabled" @input="numberField('cfg', $event)">
|
||||
</label>
|
||||
<label class="text-sm text-zinc-300">Seed
|
||||
<select :value="seedMode" class="iteration-input" :disabled="disabled" @change="setSeedMode($event)">
|
||||
<option value="random">Random</option><option value="same">Same as first</option><option value="custom">Custom</option>
|
||||
</select>
|
||||
</label>
|
||||
<label v-if="seedMode === 'custom'" class="text-sm text-zinc-300">Seed number
|
||||
<input :value="modelValue.seed" type="number" min="0" max="2147483647" step="1" class="iteration-input" :disabled="disabled" @input="numberField('seed', $event)">
|
||||
</label>
|
||||
<label v-if="allowStrength" class="text-sm text-zinc-300">Denoise
|
||||
<input :value="modelValue.strength" type="number" min="0.15" max="0.75" step="0.01" placeholder="Shared" class="iteration-input" :disabled="disabled" @input="numberField('strength', $event)">
|
||||
</label>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm text-zinc-300">
|
||||
<input type="checkbox" :checked="modelValue.loraStack === undefined" :disabled="disabled" @change="inheritLoras($event)"> Use shared LoRAs
|
||||
</label>
|
||||
<LoraSelect v-if="modelValue.loraStack !== undefined" :model-value="modelValue.loraStack" :names="loraNames" :engine="engine" :disabled="disabled" label="This variation's LoRAs" empty-label="No LoRAs" @update:model-value="update({ loraStack: $event })" />
|
||||
<p v-if="modelValue.steps !== undefined || modelValue.cfg !== undefined" class="text-xs text-zinc-400">Custom steps or CFG override the shared Turbo preset for this variation.</p>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { ImageIteration } from '~/utils/imageIterations'
|
||||
const props = withDefaults(defineProps<{ modelValue: ImageIteration; baseSteps?: number; baseCfg?: number; loraNames?: string[]; engine?: 'flux' | 'krea'; allowStrength?: boolean; disabled?: boolean }>(), { baseSteps: 20, baseCfg: 1, loraNames: () => [], engine: 'flux', allowStrength: false, disabled: false })
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: ImageIteration] }>()
|
||||
const seedMode = computed(() => typeof props.modelValue.seed === 'number' ? 'custom' : props.modelValue.seed || 'random')
|
||||
function update(patch: Partial<ImageIteration>) { emit('update:modelValue', { ...props.modelValue, ...patch }) }
|
||||
function numberField(key: 'steps' | 'cfg' | 'seed' | 'strength', event: Event) {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
update({ [key]: value === '' ? undefined : Number(value) })
|
||||
}
|
||||
function setSeedMode(event: Event) {
|
||||
const mode = (event.target as HTMLSelectElement).value
|
||||
update({ seed: mode === 'custom' ? 0 : mode === 'same' ? 'same' : 'random' })
|
||||
}
|
||||
function inheritLoras(event: Event) { update({ loraStack: (event.target as HTMLInputElement).checked ? undefined : [] }) }
|
||||
</script>
|
||||
<style scoped>
|
||||
.iteration-input { @apply mt-1 w-full rounded-lg border border-white/15 bg-zinc-950 px-3 py-2 text-sm text-zinc-100 outline-none focus:ring-2 focus:ring-amber-300/40 disabled:opacity-50; }
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<details class="rounded-2xl border border-amber-300/25 bg-amber-400/5 p-4">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-amber-100">Compare settings · build a sweep</summary>
|
||||
<p class="mt-3 text-sm text-zinc-300">Use one prompt with several step counts or CFG values. Each combination becomes an editable variation.</p>
|
||||
<div class="mt-3 grid gap-3 sm:grid-cols-2">
|
||||
<label class="text-sm text-zinc-300">Steps, separated by commas
|
||||
<input v-model="steps" class="mt-1 w-full rounded-lg border border-white/15 bg-zinc-950 px-3 py-2" placeholder="24, 20, 18, 19">
|
||||
</label>
|
||||
<label class="text-sm text-zinc-300">CFG values
|
||||
<input v-model="cfg" class="mt-1 w-full rounded-lg border border-white/15 bg-zinc-950 px-3 py-2" placeholder="1.5">
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-zinc-300">{{ preview }}</p>
|
||||
<p class="mt-1 text-xs text-zinc-400">Uses the same seed for comparison and the original input for every image. Replaces the current variations; nothing renders until you queue.</p>
|
||||
<button type="button" class="mt-3 rounded-lg bg-amber-400 px-4 py-2 text-sm font-semibold text-zinc-950 disabled:opacity-50" :disabled="!rows.length" @click="emit('apply', rows)">Build {{ rows.length || '' }} variations</button>
|
||||
</details>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { imageIterationSweep, type ImageIteration } from '~/utils/imageIterations'
|
||||
const emit = defineEmits<{ apply: [rows: ImageIteration[]] }>()
|
||||
const steps = ref('24, 20, 18, 19')
|
||||
const cfg = ref('1.5')
|
||||
const result = computed(() => {
|
||||
try { return { rows: imageIterationSweep(steps.value, cfg.value), error: '' } }
|
||||
catch (error) { return { rows: [] as ImageIteration[], error: error instanceof Error ? error.message : String(error) } }
|
||||
})
|
||||
const rows = computed(() => result.value.rows)
|
||||
const preview = computed(() => result.value.error || `${rows.value.length} images · ${rows.value.map(row => `${row.steps} steps / CFG ${row.cfg}`).join(' · ')}`)
|
||||
</script>
|
||||
@@ -76,6 +76,17 @@
|
||||
<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" />
|
||||
<ImageIterationSettings
|
||||
v-if="job.kind === 'edit' && job.payload.v2Mode === 'iterate'"
|
||||
:model-value="shot"
|
||||
:base-steps="draft.steps"
|
||||
:base-cfg="draft.cfg"
|
||||
:lora-names="loraNames"
|
||||
:engine="job.payload.engine === 'krea' ? 'krea' : 'flux'"
|
||||
:allow-strength="job.payload.engine === 'krea' && Boolean(job.payload.stillId)"
|
||||
:disabled="!editable"
|
||||
@update:model-value="extraShots[index] = { ...$event, duration: shot.duration }"
|
||||
/>
|
||||
<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">
|
||||
|
||||
+43
-8
@@ -326,7 +326,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<p v-if="studioMode === 'editv2' && v2Mode === 'generate'" class="mt-3 text-xs text-zinc-500">No reference image. Prompt only.</p>
|
||||
<p v-else-if="studioMode === 'editv2' && v2Mode === 'iterate'" class="mt-3 text-xs text-zinc-500">Add more iterations below. Everything saves as one nested library collection.</p>
|
||||
<p v-else-if="studioMode === 'editv2' && v2Mode === 'iterate'" class="mt-3 text-xs text-zinc-500">Use one prompt with different steps, CFG, seeds, and LoRAs below. Everything saves as one library collection.</p>
|
||||
<div v-if="studioMode === 'editv2' && v2Mode === 'refine'" class="mt-3 space-y-2">
|
||||
<p class="text-xs text-zinc-500">Prompt only what should change in the painted area.</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@@ -380,6 +380,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="studioMode === 'edit' || v2PassChaining" class="mt-3 space-y-3">
|
||||
<ImageIterationSweep v-if="studioMode === 'editv2' && v2Mode === 'iterate'" @apply="applyIterationSweep" />
|
||||
<div v-if="!(v2Mode === 'iterate' && !file)" class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -435,6 +436,16 @@
|
||||
:keep-items="activeKeepPrompts"
|
||||
:keep-heading="keepPromptHeading"
|
||||
/>
|
||||
<ImageIterationSettings
|
||||
v-if="studioMode === 'editv2' && v2Mode === 'iterate'"
|
||||
:model-value="item"
|
||||
:base-steps="v2Steps"
|
||||
:base-cfg="v2Cfg"
|
||||
:lora-names="imageV2LoraOptions"
|
||||
:engine="v2Engine"
|
||||
:allow-strength="v2Engine === 'krea' && Boolean(file)"
|
||||
@update:model-value="editPassQueue[index] = { ...$event, id: item.id }"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -2242,6 +2253,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeImageIterations, type ImageIteration } from '~/utils/imageIterations'
|
||||
import { composePromptParts, formatPromptParts, resolvePromptWrappers, restorePromptParts } from '~/utils/promptParts'
|
||||
import { parseEditPasses, looksLikeShotScript } from '~/utils/parseRecommend'
|
||||
import { parseImageBatchScript, parseShotScript } from '~/utils/parseShots'
|
||||
@@ -2496,7 +2508,7 @@ interface QueuedExtension {
|
||||
loraStack?: LoraStackItem[]
|
||||
}
|
||||
|
||||
interface QueuedEditPass {
|
||||
interface QueuedEditPass extends ImageIteration {
|
||||
id: string
|
||||
prompt: string
|
||||
}
|
||||
@@ -3213,7 +3225,7 @@ const editV2Blocked = computed(() => {
|
||||
if (v2Mode.value !== 'generate' && v2Mode.value !== 'iterate' && !file.value) return true
|
||||
if (v2Mode.value === 'compose' && !editRefFile.value) return true
|
||||
if (v2Mode.value === 'refine' && !refineMaskDirty.value) return true
|
||||
if (v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) return true
|
||||
if (v2Mode.value !== 'iterate' && v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) return true
|
||||
if (v2Mode.value === 'iterate' && !editPassQueue.value.length) return true
|
||||
return false
|
||||
})
|
||||
@@ -3226,8 +3238,8 @@ const editV2BlockReason = computed(() => {
|
||||
? 'Write a prompt first.'
|
||||
: 'Write a prompt, or pick Landscape / Portrait / Square to fill the frame.'
|
||||
}
|
||||
if (v2Mode.value === 'iterate' && !editPassQueue.value.length) return 'Add at least one more iteration prompt.'
|
||||
if (v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) return 'Each extra pass needs a prompt.'
|
||||
if (v2Mode.value === 'iterate' && !editPassQueue.value.length) return 'Add a variation or build a steps/CFG sweep.'
|
||||
if (v2Mode.value !== 'iterate' && v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) return 'Each extra pass needs a prompt.'
|
||||
if (!folderId.value) return 'Choose a library folder before generating.'
|
||||
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image v2 is not configured. Set COMFY_HOST.'
|
||||
return ''
|
||||
@@ -3265,7 +3277,7 @@ const extraPassHint = computed(() => {
|
||||
return 'Each extra pass is another Edit on the last output. You cannot switch graphs between passes.'
|
||||
})
|
||||
const extraPassPlaceholder = computed(() => {
|
||||
if (v2Mode.value === 'iterate') return 'Next iteration prompt…'
|
||||
if (v2Mode.value === 'iterate') return 'Leave blank to use the first prompt…'
|
||||
if (imagePassMode.value === 'batch') return 'Another prompt for the same start still…'
|
||||
return studioMode.value === 'editv2' && v2Mode.value === 'compose'
|
||||
? 'Next compose prompt, applied to the previous pass output…'
|
||||
@@ -5481,6 +5493,9 @@ async function restoreStudioEditJob(payload: {
|
||||
cfg?: number
|
||||
turbo?: boolean
|
||||
referenceStillId?: string
|
||||
passes?: ImageIteration[]
|
||||
passMode?: ImagePassMode
|
||||
seed?: number
|
||||
}) {
|
||||
closeExtend()
|
||||
applyPromptRestore(payload.prompt || '', payload)
|
||||
@@ -5489,6 +5504,10 @@ async function restoreStudioEditJob(payload: {
|
||||
folderId.value = payload.folderId
|
||||
browseFolderId.value = payload.folderId
|
||||
}
|
||||
editPassQueue.value = (payload.passes || []).map(pass => ({ ...pass, id: crypto.randomUUID() }))
|
||||
imagePassMode.value = payload.passMode || 'batch'
|
||||
imageBatchScript.value = ''
|
||||
seedInput.value = payload.seed == null ? '' : String(payload.seed)
|
||||
const v2 = payload.imagePipeline === 'v2'
|
||||
if (v2) {
|
||||
studioMode.value = 'editv2'
|
||||
@@ -5612,6 +5631,8 @@ async function restoreStudioPayload(kind: string, payload: {
|
||||
scaleMegapixels?: number
|
||||
steps?: number
|
||||
referenceStillId?: string
|
||||
passes?: ImageIteration[]
|
||||
passMode?: ImagePassMode
|
||||
}) {
|
||||
if (kind === 'edit' || payload.imagePipeline === 'v2') {
|
||||
await restoreStudioEditJob(payload)
|
||||
@@ -6965,6 +6986,18 @@ function removeQueuedExtension(id: string) {
|
||||
extensionQueue.value = extensionQueue.value.filter(item => item.id !== id)
|
||||
}
|
||||
|
||||
function applyIterationSweep(rows: ImageIteration[]) {
|
||||
if (rows.length < 2) { toast('Use at least two variations for Iterate.'); return }
|
||||
v2Steps.value = rows[0].steps!
|
||||
v2Cfg.value = rows[0].cfg!
|
||||
v2Turbo.value = false
|
||||
imagePassMode.value = 'batch'
|
||||
if (!seedInput.value || seedInput.value === 'random') seedInput.value = String(Math.floor(Math.random() * 2_147_483_647))
|
||||
editPassQueue.value = rows.slice(1).map(row => ({ ...row, id: crypto.randomUUID() }))
|
||||
imageBatchScript.value = ''
|
||||
toast(rows.length + ' variations ready. The first uses the shared settings; each following variation can be edited.')
|
||||
}
|
||||
|
||||
function queueEditPass() {
|
||||
editPassQueue.value.push({
|
||||
id: crypto.randomUUID(),
|
||||
@@ -7271,12 +7304,14 @@ async function editImageV2() {
|
||||
toast('Paint a mask on Still A first. Refine will not fall back to Edit.')
|
||||
return
|
||||
}
|
||||
if (v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) {
|
||||
if (v2Mode.value !== 'iterate' && v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) {
|
||||
toast('Each extra pass needs a prompt.')
|
||||
return
|
||||
}
|
||||
absorbPastedImageBatch()
|
||||
const extraPasses = v2PassChaining.value ? editPassQueue.value.map(item => ({ prompt: item.prompt.trim() })) : []
|
||||
let extraPasses: ImageIteration[]
|
||||
try { extraPasses = v2PassChaining.value ? normalizeImageIterations(editPassQueue.value) : [] }
|
||||
catch (error) { toast(error instanceof Error ? error.message : String(error)); return }
|
||||
const hideOut = hideThumbnail.value
|
||||
try {
|
||||
await ensureGenerationNotifyPermission()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { normalizeImageIterations } from '~/utils/imageIterations'
|
||||
import { assertImageV2LoraStack, parsePostedLoraStack } from '~/server/utils/loras'
|
||||
import { persistLoraFields, normalizeLoraStack } from '~/utils/loras'
|
||||
import { listStudioJobs, patchStudioJob, type StudioJobPayload } from '~/server/utils/studioQueue'
|
||||
|
||||
@@ -32,6 +34,7 @@ export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<Record<string, unknown>>(event).catch(() => ({}))
|
||||
|
||||
const job = await patchStudioJob(owner, id, (row) => {
|
||||
if (!EDITABLE.has(row.status)) throw createError({ statusCode: 409, statusMessage: 'That job has started generating.' })
|
||||
const payload = row.payload
|
||||
if (body.name != null) {
|
||||
const name = text(body.name, 80).trim()
|
||||
@@ -75,9 +78,16 @@ export default defineEventHandler(async (event) => {
|
||||
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)
|
||||
}))
|
||||
try {
|
||||
payload.passes = normalizeImageIterations(body.passes)
|
||||
if (payload.imagePipeline === 'v2') {
|
||||
for (const pass of payload.passes) {
|
||||
if (pass.loraStack !== undefined) pass.loraStack = assertImageV2LoraStack(parsePostedLoraStack(pass.loraStack, 'image'), payload.engine || 'flux')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw createError({ statusCode: 400, statusMessage: error instanceof Error ? error.message : String(error) })
|
||||
}
|
||||
row.shotCount = 1 + payload.passes.length
|
||||
}
|
||||
if (Array.isArray(body.extensions) && row.kind !== 'edit') {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeImageIterations } from '~/utils/imageIterations'
|
||||
import { addStudioJob, kickStudioQueue, listStudioJobs, videoJobsBusy } from '~/server/utils/studioQueue'
|
||||
import { comfyConfigured } from '~/server/utils/comfy'
|
||||
import { parseImageAspect, generateSizeForAspect } from '~/utils/imageAspect'
|
||||
@@ -35,15 +36,9 @@ import { existsSync, readFileSync } from 'node:fs'
|
||||
type ImageFile = { filename: string; data: Buffer; type?: string }
|
||||
|
||||
function parsePasses(raw: unknown) {
|
||||
if (!raw) return [] as { prompt: string }[]
|
||||
let parsed = raw
|
||||
if (typeof raw === 'string') {
|
||||
try { parsed = JSON.parse(raw) } catch { return [] }
|
||||
try { return normalizeImageIterations(raw) } catch (error) {
|
||||
throw createError({ statusCode: 400, statusMessage: error instanceof Error ? error.message : String(error) })
|
||||
}
|
||||
if (!Array.isArray(parsed)) return []
|
||||
return parsed
|
||||
.map((item: { prompt?: unknown }) => ({ prompt: String(item?.prompt || '').trim() }))
|
||||
.filter(item => item.prompt)
|
||||
}
|
||||
|
||||
function parseBool(raw: unknown) {
|
||||
@@ -157,10 +152,10 @@ export default defineEventHandler(async (event) => {
|
||||
if (mode === 'iterate' && !extraPasses.length) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Iterate needs at least two prompts. Add more iterations, or use Generate / Edit for a single run.'
|
||||
statusMessage: 'Iterate needs at least two images. Add a variation or build a steps/CFG sweep.'
|
||||
})
|
||||
}
|
||||
if (extraPasses.some(item => !item.prompt)) {
|
||||
if (mode !== 'iterate' && extraPasses.some(item => !item.prompt)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Each extra pass needs a prompt' })
|
||||
}
|
||||
if (!comfyConfigured()) {
|
||||
@@ -278,6 +273,9 @@ export default defineEventHandler(async (event) => {
|
||||
const generateSnofsClipFallback = 0
|
||||
await listStudioLoras()
|
||||
const loraStack = assertImageV2LoraStack(parsePostedLoraStack(fields.loraStack || fields.lora, 'image'), engine)
|
||||
for (const pass of extraPasses) {
|
||||
if (pass.loraStack !== undefined) pass.loraStack = assertImageV2LoraStack(parsePostedLoraStack(pass.loraStack, 'image'), engine)
|
||||
}
|
||||
const stackSpecials = imageV2StackSpecials(loraStack, engine)
|
||||
const snofsModel = conceptAllowed
|
||||
? (stackSpecials.snofs?.strengthModel ?? clampImageV2Strength(fields.snofs_model, generateSnofsFallback))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeImageIterations, resolveImageIteration } from '~/utils/imageIterations'
|
||||
import { createJob, emitJob, type Job } from '~/server/utils/jobs'
|
||||
import { ensureComfyReady } from '~/server/utils/comfyLifecycle'
|
||||
import { assertImageScaleToTotalPixelsNode, comfyImagePrefix, getComfyHost, uploadImage, queuePrompt } from '~/server/utils/comfy'
|
||||
@@ -46,7 +47,7 @@ export type EditV2RunParams = {
|
||||
sourceStillId?: string
|
||||
referenceStillId?: string
|
||||
loraStack?: import('~/utils/loras').LoraStackItem[]
|
||||
passes?: { prompt: string }[]
|
||||
passes?: import('~/utils/imageIterations').ImageIteration[]
|
||||
passMode?: 'batch' | 'chain'
|
||||
}
|
||||
|
||||
@@ -72,7 +73,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
|
||||
const canChain = params.mode === 'iterate' || params.mode === 'edit' || params.mode === 'compose'
|
||||
const extraPasses = canChain
|
||||
? (params.passes || []).map(item => ({ prompt: String(item.prompt || '').trim() })).filter(item => item.prompt)
|
||||
? normalizeImageIterations(params.passes)
|
||||
: []
|
||||
const prompts = [
|
||||
library.promptMid || restorePromptParts(params.prompt, {
|
||||
@@ -80,7 +81,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
prompt: params.prompt,
|
||||
post: library.promptPost
|
||||
}).prompt,
|
||||
...extraPasses.map(item => item.prompt)
|
||||
...extraPasses.map(item => item.prompt || library.promptMid || library.prompt || params.prompt)
|
||||
]
|
||||
const chainTotal = prompts.length
|
||||
// Text-only iterate is always independent generations. With a still, honor batch vs chain.
|
||||
@@ -90,6 +91,8 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
library.familyId = chainTotal > 1 ? (library.familyId || crypto.randomUUID()) : library.familyId
|
||||
|
||||
try {
|
||||
// Keep the studio slot across all iterations, including upload/save gaps.
|
||||
library.chainContinuing = true
|
||||
await ensureComfyReady((status) => {
|
||||
emitChainJob(job, {
|
||||
type: status.state === 'busy' ? 'busy' : 'status',
|
||||
@@ -117,22 +120,6 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
await ensureComfyLoraNames('image')
|
||||
if (!generate && params.megapixels > 0) await assertImageScaleToTotalPixelsNode()
|
||||
const krea = engine === 'krea' ? await resolveKreaGenerateAssets() : null
|
||||
const appliedStack = mergeLegacyImageV2Loras(params.loraStack?.length ? params.loraStack : library.loraStack, {
|
||||
engine,
|
||||
mode: graphMode,
|
||||
xaigen: isXaigenStudio(),
|
||||
snofsModel: params.snofsModel,
|
||||
snofsClip: params.snofsClip,
|
||||
consistencyModel: params.consistencyModel,
|
||||
consistencyClip: params.consistencyClip,
|
||||
kreaConcept: krea?.conceptLora
|
||||
})
|
||||
const appliedSpecials = imageV2StackSpecials(appliedStack, engine)
|
||||
const snofsModel = appliedSpecials.snofs?.strengthModel ?? params.snofsModel
|
||||
const snofsClip = appliedSpecials.snofs?.strengthClip ?? params.snofsClip
|
||||
const consistencyModel = appliedSpecials.consistency?.strengthModel ?? params.consistencyModel
|
||||
const consistencyClip = appliedSpecials.consistency?.strengthClip ?? params.consistencyClip
|
||||
|
||||
if (!generate && graphMode === 'compose' && params.reference) {
|
||||
uploadedRef = await uploadImage({
|
||||
...params.reference,
|
||||
@@ -144,7 +131,28 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
if (job.status === 'cancelled') throw new Error('Job interrupted.')
|
||||
if (index > 0 && library.stopAfterCurrent === true) break
|
||||
const last = index === prompts.length - 1
|
||||
const seed = index === 0 ? params.seed : Math.floor(Math.random() * 2_147_483_647)
|
||||
const pass = index > 0 ? extraPasses[index - 1] : undefined
|
||||
const { steps, cfg, seed, strength } = resolveImageIteration(params, pass)
|
||||
const turbo = pass?.steps !== undefined || pass?.cfg !== undefined ? false : params.turbo === true
|
||||
const appliedStack = mergeLegacyImageV2Loras(pass?.loraStack ?? params.loraStack ?? library.loraStack, {
|
||||
engine,
|
||||
mode: graphMode,
|
||||
xaigen: isXaigenStudio(),
|
||||
snofsModel: pass?.loraStack !== undefined ? 0 : params.snofsModel,
|
||||
snofsClip: pass?.loraStack !== undefined ? 0 : params.snofsClip,
|
||||
consistencyModel: pass?.loraStack !== undefined ? 0 : params.consistencyModel,
|
||||
consistencyClip: pass?.loraStack !== undefined ? 0 : params.consistencyClip,
|
||||
kreaConcept: krea?.conceptLora
|
||||
})
|
||||
const appliedSpecials = imageV2StackSpecials(appliedStack, engine)
|
||||
const snofsModel = appliedSpecials.snofs?.strengthModel ?? (pass?.loraStack !== undefined ? 0 : params.snofsModel)
|
||||
const snofsClip = appliedSpecials.snofs?.strengthClip ?? (pass?.loraStack !== undefined ? 0 : params.snofsClip)
|
||||
const consistencyModel = appliedSpecials.consistency?.strengthModel ?? (pass?.loraStack !== undefined ? 0 : params.consistencyModel)
|
||||
const consistencyClip = appliedSpecials.consistency?.strengthClip ?? (pass?.loraStack !== undefined ? 0 : params.consistencyClip)
|
||||
|
||||
library.steps = steps
|
||||
library.cfg = cfg
|
||||
job.maxStep = steps
|
||||
library.prompt = prompts[index]
|
||||
library.seed = seed
|
||||
library.chainIndex = index
|
||||
@@ -238,19 +246,19 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
imageAName: uploaded?.name,
|
||||
imageBName: uploadedRef?.name,
|
||||
maskName: uploadedMask?.name,
|
||||
strength: params.strength,
|
||||
strength,
|
||||
snofsModel,
|
||||
snofsClip,
|
||||
consistencyModel,
|
||||
consistencyClip,
|
||||
steps: params.steps,
|
||||
cfg: params.cfg,
|
||||
steps,
|
||||
cfg,
|
||||
seed,
|
||||
megapixels: params.megapixels,
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
aspect: params.aspect,
|
||||
turbo: params.turbo === true,
|
||||
turbo,
|
||||
filenamePrefix: chainTotal > 1 ? `${comfyImagePrefix()}_${comfyJobPrefix(job.id)}_p${index + 1}` : `${comfyImagePrefix()}_${comfyJobPrefix(job.id)}`,
|
||||
kreaUnetName: krea?.unet,
|
||||
kreaClipName: krea?.clip,
|
||||
@@ -265,7 +273,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
type: 'status',
|
||||
message: `Running ${built.workflowFile}...`,
|
||||
progress: 18,
|
||||
maxStep: params.steps
|
||||
maxStep: steps
|
||||
})
|
||||
|
||||
const output = await waitForImageEdit({
|
||||
@@ -283,7 +291,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
message: event.message,
|
||||
progress: event.progress,
|
||||
step: event.step,
|
||||
maxStep: event.maxStep || params.steps,
|
||||
maxStep: event.maxStep || steps,
|
||||
node: event.node
|
||||
})
|
||||
},
|
||||
@@ -291,6 +299,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
})
|
||||
|
||||
emitChainJob(job, { type: 'status', message: `Saving ${engineLabel} v2 still...`, progress: 94 })
|
||||
job.saving = true
|
||||
const buffer = await downloadEditedImage(output)
|
||||
const size = imageDimensions(buffer)
|
||||
const still = await saveStill({
|
||||
@@ -319,12 +328,12 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
snofsClip,
|
||||
consistencyModel,
|
||||
consistencyClip,
|
||||
steps: params.steps,
|
||||
cfg: params.cfg,
|
||||
steps,
|
||||
cfg,
|
||||
megapixels: generate || !(params.megapixels > 0) ? undefined : params.megapixels,
|
||||
scaleToTotalPixels: generate ? undefined : params.megapixels > 0,
|
||||
turbo: params.turbo === true,
|
||||
strength: generate ? undefined : params.strength,
|
||||
turbo,
|
||||
strength: generate ? undefined : strength,
|
||||
width: generate ? params.width : undefined,
|
||||
height: generate ? params.height : undefined,
|
||||
aspect: params.aspect || 'auto',
|
||||
@@ -346,6 +355,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
promptId: job.promptId
|
||||
})
|
||||
|
||||
job.saving = false
|
||||
if (!last) {
|
||||
emitChainJob(job, {
|
||||
type: 'checkpoint',
|
||||
@@ -390,6 +400,8 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
emitJob(job, { type: 'error', error: message, message })
|
||||
}
|
||||
} finally {
|
||||
job.saving = false
|
||||
library.chainContinuing = false
|
||||
const { onLiveVideoSettled } = await import('~/server/utils/studioQueue')
|
||||
await onLiveVideoSettled(job)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export interface Job {
|
||||
parentStillId?: string
|
||||
workflow?: import('~/utils/videoModels').VideoWorkflowId
|
||||
chainContinuing?: boolean
|
||||
passes?: { prompt: string }[]
|
||||
passes?: import('~/utils/imageIterations').ImageIteration[]
|
||||
queueId?: string
|
||||
queueAutoRun?: boolean
|
||||
queueBudget?: number
|
||||
|
||||
@@ -52,7 +52,7 @@ export interface StudioJobPayload {
|
||||
shotLoras?: string[]
|
||||
shotLoraStacks?: import('~/utils/loras').LoraStackItem[][]
|
||||
negative?: string
|
||||
passes?: { prompt: string }[]
|
||||
passes?: import('~/utils/imageIterations').ImageIteration[]
|
||||
passMode?: 'batch' | 'chain'
|
||||
referenceStillId?: string
|
||||
referenceStillFilename?: string
|
||||
@@ -1207,7 +1207,7 @@ async function startStudioEditJob(item: StudioJob) {
|
||||
height: payload.height,
|
||||
steps: payload.steps,
|
||||
turbo: payload.turbo === true,
|
||||
seed: payload.seed || Math.floor(Math.random() * 2_147_483_647),
|
||||
seed: payload.seed ?? Math.floor(Math.random() * 2_147_483_647),
|
||||
cfg: payload.cfg,
|
||||
stillId: payload.stillId,
|
||||
stillFilename: payload.stillFilename,
|
||||
@@ -1235,7 +1235,7 @@ async function startStudioEditJob(item: StudioJob) {
|
||||
prompt: payload.prompt,
|
||||
negative: payload.negative || '',
|
||||
steps: payload.steps,
|
||||
seed: payload.seed || Math.floor(Math.random() * 2_147_483_647),
|
||||
seed: payload.seed ?? Math.floor(Math.random() * 2_147_483_647),
|
||||
cfg: payload.cfg,
|
||||
snofsModel: v2Specials.snofs?.strengthModel ?? payload.snofsModel ?? 0,
|
||||
snofsClip: v2Specials.snofs?.strengthClip ?? payload.snofsClip ?? 0,
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import ts from 'typescript'
|
||||
function load(path, mocks = {}) {
|
||||
const source = readFileSync(new URL('../' + path, import.meta.url), 'utf8')
|
||||
const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText
|
||||
const exports = {}
|
||||
new Function('require', 'exports', code)(id => {
|
||||
if (!(id in mocks)) throw Error('Unexpected dependency ' + id)
|
||||
return mocks[id]
|
||||
}, exports)
|
||||
return exports
|
||||
}
|
||||
const iterations = load('utils/imageIterations.ts')
|
||||
const { normalizeImageIterations: normalize, resolveImageIteration: resolve, imageIterationSweep: sweep } = iterations
|
||||
|
||||
test('requested sweep produces four ordered variations with one comparison seed', () => {
|
||||
const rows = sweep('24, 20, 18, 19', '1.5')
|
||||
assert.deepEqual(rows.map(row => row.steps), [24,20,18,19])
|
||||
assert.ok(rows.every(row => row.cfg === 1.5 && row.seed === 'same'))
|
||||
assert.equal(sweep('20 24', '1, 1.5').length, 4)
|
||||
})
|
||||
test('blank overrides inherit, explicit zero and no-LoRA remain explicit', () => {
|
||||
const pass = normalize([{ prompt: '', steps: '', cfg: 0, seed: 0, loraStack: [] }])[0]
|
||||
assert.equal(pass.steps, undefined)
|
||||
assert.deepEqual(pass.loraStack, [])
|
||||
assert.deepEqual(resolve({steps:24,cfg:1.5,seed:42},pass), {steps:24,cfg:0,seed:0,strength:undefined})
|
||||
assert.equal(resolve({steps:24,cfg:1.5,seed:42},{prompt:'',seed:'same'}).seed,42)
|
||||
})
|
||||
test('malformed settings reject visibly instead of being silently discarded', () => {
|
||||
for (const raw of ['bad JSON', {}, [null], [{steps:51}], [{steps:1.5}], [{cfg:-1}], [{seed:-1}], [{strength:2}], [{loraStack:[{name:'x',strengthModel:3}]}], Array(50).fill({})]) {
|
||||
assert.throws(() => normalize(raw))
|
||||
}
|
||||
assert.throws(() => sweep('24, potato', '1.5'))
|
||||
assert.throws(() => sweep('', '1.5'))
|
||||
})
|
||||
|
||||
test('iteration runner sends per-image settings to Comfy and saves matching metadata', async () => {
|
||||
const built = [], saved = []
|
||||
const job = {id:'test', clientId:'client', status:'queued', progress:0, library:{ownerKey:'owner',prompt:'shared prompt',promptMid:'shared prompt',name:'test',folderId:'inbox'}}
|
||||
const baseStack = [{ name:'base.safetensors', strengthModel:1, strengthClip:1 }]
|
||||
const customStack = [{ name:'custom.safetensors', strengthModel:0.5, strengthClip:0.5 }]
|
||||
const mocks = {
|
||||
'~/utils/imageIterations': iterations,
|
||||
'~/server/utils/jobs': {createJob:()=>job,emitJob:()=>{}},
|
||||
'~/server/utils/comfyLifecycle': {ensureComfyReady:async()=>{}},
|
||||
'~/server/utils/comfy': {assertImageScaleToTotalPixelsNode:async()=>{},comfyImagePrefix:()=> 'test',getComfyHost:()=> 'fake',uploadImage:async()=>({name:'input'}),queuePrompt:async()=>({prompt_id:'p'+built.length})},
|
||||
'~/server/utils/imageComfy': {withImageComfyHost:async(_,fn)=>fn(),waitForImageEdit:async()=>({filename:'out.png'}),downloadEditedImage:async()=>Buffer.from('test'),purgeImageComfyArtifacts:async()=>{}},
|
||||
'~/server/utils/imageWorkflowV2': {buildImageV2Workflow:params=>{built.push(params);return {graph:{},workflowFile:'fake'}},IMAGE_V2_NODE_LABELS:{},resolveKreaGenerateAssets:async()=>({})},
|
||||
'~/server/utils/loras': {ensureComfyLoraNames:async()=>{},persistLoraFields:stack=>({loraStack:stack})},
|
||||
'~/server/utils/resolution': {imageDimensions:()=>({width:1024,height:1024})},
|
||||
'~/server/utils/watch': {emitChainJob:()=>{}},
|
||||
'~/server/utils/library': {saveStill:async params=>{assert.equal(job.saving,true);assert.equal(job.library.chainContinuing,true);saved.push(params);return {id:'s'+saved.length,filename:'out.png'}},stillChainName:(_,i)=>'image'+i},
|
||||
'~/utils/outputNames': {comfyJobPrefix:()=> 'test'},
|
||||
'~/utils/promptParts': {restorePromptParts:prompt=>({prompt}),wrappedPromptForComfy:(_,prompt)=>prompt},
|
||||
'~/utils/videoModels': {isXaigenStudio:()=>false},
|
||||
'~/utils/imageV2': {imageV2GraphMode:()=> 'generate',imageV2StackSpecials:()=>({}),mergeLegacyImageV2Loras:stack=>stack || [],parseImageV2Engine:()=> 'flux'},
|
||||
'~/server/utils/imagePad': {applyImageFill:async({image})=>({image,padded:false})},
|
||||
'~/utils/imageAspect': {composeFillPrompt:prompt=>prompt},
|
||||
'~/server/utils/studioQueue': {onLiveVideoSettled:async()=>{assert.equal(job.saving,false);assert.equal(job.library.chainContinuing,false)}}
|
||||
}
|
||||
const {runEditV2} = load('server/utils/imageChainV2.ts', mocks)
|
||||
await runEditV2(job,{ mode:'iterate',task:'t2i',prompt:'shared prompt',negative:'',steps:24,cfg:1.5,seed:42,loraStack:baseStack,
|
||||
snofsModel:0,snofsClip:0,consistencyModel:0,consistencyClip:0,megapixels:0,reference:null,
|
||||
passes:[{prompt:'',steps:20,seed:'same'},{prompt:'alternate',steps:18,cfg:2,seed:0,loraStack:customStack},{prompt:'',steps:19,seed:'same',loraStack:[]}] })
|
||||
assert.equal(job.status,'complete',job.error)
|
||||
assert.deepEqual(built.map(p=>p.steps),[24,20,18,19])
|
||||
assert.deepEqual(built.map(p=>p.cfg),[1.5,1.5,2,1.5])
|
||||
assert.deepEqual(built.map(p=>p.seed),[42,42,0,42])
|
||||
assert.deepEqual(built.map(p=>p.prompt),['shared prompt','shared prompt','alternate','shared prompt'])
|
||||
assert.deepEqual(built.map(p=>p.loraStack),[baseStack,baseStack,customStack,[]])
|
||||
assert.deepEqual(saved.map(p=>p.settings.steps),[24,20,18,19])
|
||||
assert.deepEqual(saved.map(p=>p.settings.loraStack),[baseStack,baseStack,customStack,[]])
|
||||
})
|
||||
|
||||
test('editing a queued batch preserves its per-variation settings', () => {
|
||||
const queued = load('utils/queuedJob.ts', {
|
||||
'~/utils/loras': {persistLoraFields:()=>({}),readLoraStack:()=>[]},
|
||||
'~/utils/music': {}
|
||||
})
|
||||
const payload = {prompt:'shared',passes:[{prompt:'',steps:18,cfg:1.5,seed:'same',loraStack:[]}]}
|
||||
const shots = queued.queuedJobShots(payload,'edit')
|
||||
shots[1].steps = 19
|
||||
const restored = queued.applyQueuedShots('edit',shots,payload)
|
||||
assert.deepEqual(restored.passes,[{prompt:'',steps:19,cfg:1.5,seed:'same',loraStack:[]}])
|
||||
assert.deepEqual(normalize(JSON.stringify(restored.passes)),restored.passes)
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { LoraStackItem } from './loras'
|
||||
|
||||
/** An omitted field inherits the shared settings; an empty LoRA stack means none. */
|
||||
export interface ImageIteration {
|
||||
prompt: string
|
||||
steps?: number
|
||||
cfg?: number
|
||||
seed?: number | 'same' | 'random'
|
||||
strength?: number
|
||||
loraStack?: LoraStackItem[]
|
||||
}
|
||||
|
||||
export const MAX_IMAGE_ITERATIONS = 50
|
||||
|
||||
function optionalNumber(raw: unknown, label: string, min: number, max: number, integer = false) {
|
||||
if (raw == null || raw === '') return undefined
|
||||
const value = Number(raw)
|
||||
if (!Number.isFinite(value) || value < min || value > max || (integer && !Number.isInteger(value))) {
|
||||
throw new Error(`${label} must be ${integer ? 'a whole number ' : ''}between ${min} and ${max}.`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function normalizeImageIterations(raw: unknown): ImageIteration[] {
|
||||
if (raw == null || raw === '') return []
|
||||
if (typeof raw === 'string') {
|
||||
try { raw = JSON.parse(raw) } catch { throw new Error('Iterations must be a valid JSON list.') }
|
||||
}
|
||||
if (!Array.isArray(raw)) throw new Error('Iterations must be a list.')
|
||||
if (raw.length >= MAX_IMAGE_ITERATIONS) throw new Error(`Use at most ${MAX_IMAGE_ITERATIONS} images including the first.`)
|
||||
return raw.map((item, index) => {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error(`Iteration ${index + 2} is invalid.`)
|
||||
const result: ImageIteration = { prompt: String(item.prompt ?? '').trim().slice(0, 8000) }
|
||||
const steps = optionalNumber(item.steps, 'Steps', 1, 50, true)
|
||||
const cfg = optionalNumber(item.cfg, 'CFG', 0, 20)
|
||||
const strength = optionalNumber(item.strength, 'Denoise', 0.15, 0.75)
|
||||
if (steps !== undefined) result.steps = steps
|
||||
if (cfg !== undefined) result.cfg = Math.round(cfg * 10) / 10
|
||||
if (strength !== undefined) result.strength = Math.round(strength * 100) / 100
|
||||
if (item.seed === 'same' || item.seed === 'random') result.seed = item.seed
|
||||
else {
|
||||
const seed = optionalNumber(item.seed, 'Seed', 0, 2_147_483_647, true)
|
||||
if (seed !== undefined) result.seed = seed
|
||||
}
|
||||
if (item.loraStack != null) {
|
||||
if (!Array.isArray(item.loraStack) || item.loraStack.length > 8) throw new Error('Use at most 8 LoRAs per iteration.')
|
||||
result.loraStack = item.loraStack.map((lora: Record<string, unknown>) => {
|
||||
const name = String(lora?.name ?? '').trim()
|
||||
if (!name) throw new Error('Each iteration LoRA needs a name.')
|
||||
return { name,
|
||||
strengthModel: optionalNumber(lora.strengthModel, 'LoRA model strength', -2, 2) ?? 1,
|
||||
strengthClip: optionalNumber(lora.strengthClip, 'LoRA CLIP strength', -2, 2) ?? 1 }
|
||||
})
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export function resolveImageIteration(base: { steps: number; cfg: number; seed: number; strength?: number }, pass?: ImageIteration, random = Math.random) {
|
||||
return {
|
||||
steps: pass?.steps ?? base.steps,
|
||||
cfg: pass?.cfg ?? base.cfg,
|
||||
strength: pass?.strength ?? base.strength,
|
||||
seed: !pass || pass.seed === 'same' ? base.seed
|
||||
: typeof pass.seed === 'number' ? pass.seed : Math.floor(random() * 2_147_483_647)
|
||||
}
|
||||
}
|
||||
|
||||
/** Ordered Cartesian product: each CFG value is rendered at each requested step count. */
|
||||
export function imageIterationSweep(stepText: string, cfgText: string) {
|
||||
const parse = (text: string) => text.split(/[,\s]+/).filter(Boolean).map(Number)
|
||||
const steps = parse(stepText)
|
||||
const cfgs = parse(cfgText)
|
||||
if (!steps.length || !cfgs.length) throw new Error('Enter at least one steps value and one CFG value.')
|
||||
if (steps.length * cfgs.length > MAX_IMAGE_ITERATIONS) throw new Error(`A sweep can contain at most ${MAX_IMAGE_ITERATIONS} images.`)
|
||||
const rows = cfgs.flatMap(cfg => steps.map(steps => ({ prompt: '', steps, cfg, seed: 'same' as const })))
|
||||
// Validate the first row too; the normal parser reserves one slot for the shared first image.
|
||||
return rows.map(row => normalizeImageIterations([row])[0])
|
||||
}
|
||||
+5
-4
@@ -14,7 +14,7 @@ export const LOAD_GENERATION_PAYLOAD_KEY = 'aigen-load-generation-payload'
|
||||
export type QueuedInspectKind = 'video' | 'edit' | 'music'
|
||||
export type QueuedInspectSource = 'studio' | 'live'
|
||||
|
||||
export type QueuedShotDraft = {
|
||||
export type QueuedShotDraft = import('~/utils/imageIterations').ImageIteration & {
|
||||
prompt: string
|
||||
duration: number
|
||||
loraName?: string
|
||||
@@ -48,7 +48,8 @@ export type QueuedInspectPayload = {
|
||||
loraName?: string
|
||||
loraStack?: LoraStackItem[]
|
||||
negative?: string
|
||||
passes?: { prompt: string }[]
|
||||
passes?: import('~/utils/imageIterations').ImageIteration[]
|
||||
passMode?: 'batch' | 'chain'
|
||||
imagePipeline?: 'v1' | 'v2'
|
||||
v2Mode?: string
|
||||
v2Task?: string
|
||||
@@ -99,7 +100,7 @@ export function queuedJobEditable(job: { source?: string; status?: string }) {
|
||||
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 }))
|
||||
const rest = (payload.passes || []).map(item => ({ ...item, prompt: item.prompt || '', duration: 0 }))
|
||||
return rest.length ? [first, ...rest] : [first]
|
||||
}
|
||||
if (kind === 'music') return []
|
||||
@@ -126,7 +127,7 @@ export function applyQueuedShots(kind: string, shots: QueuedShotDraft[], payload
|
||||
}
|
||||
if (kind === 'edit') {
|
||||
next.prompt = shots[0]?.prompt || ''
|
||||
next.passes = shots.slice(1).map(item => ({ prompt: item.prompt || '' }))
|
||||
next.passes = shots.slice(1).map(({ duration, ...item }) => ({ ...item, prompt: item.prompt || '' }))
|
||||
return next
|
||||
}
|
||||
next.prompt = shots[0]?.prompt || ''
|
||||
|
||||
Reference in New Issue
Block a user