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
+43 -8
View File
@@ -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()