Add per-image iteration settings and steps/CFG sweeps
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user