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
+13 -3
View File
@@ -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') {
+8 -10
View File
@@ -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))