Add per-image iteration settings and steps/CFG sweeps

This commit is contained in:
Towsty
2026-09-06 17:02:42 -05:00
parent 0c6a035d04
commit 666c922f40
12 changed files with 370 additions and 59 deletions
+42 -30
View File
@@ -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)
}