Add Image Iterate: nested multi-prompt still families.

Iterate runs a series of prompts as generate (no still), edit (still A), or compose (A+B), saves each output under one library collection like video extensions, and defaults to iterating from the original still.
This commit is contained in:
2026-09-04 22:46:19 -05:00
parent 279ac72049
commit 2435167a41
6 changed files with 261 additions and 130 deletions
+118 -36
View File
@@ -24,6 +24,12 @@
<p class="text-sm text-zinc-400">{{ studioMode === 'editv2'
? (v2Mode === 'generate'
? 'Generate: no reference image. Prompt only.'
: v2Mode === 'iterate'
? (file
? (editRefFile
? 'Iterate: each prompt runs as Compose from stills A and B (or chain from the last output). All results nest in one library collection.'
: 'Iterate: each prompt runs from still A (batch) or from the last output (chain). All results nest in one library collection.')
: 'Iterate: each prompt is a separate generate. All results nest in one library collection like video extensions.')
: v2Mode === 'refine'
? 'Refine: paint a mask on Still A. Prompt only what should change in the painted area. Strength is denoise in that region.'
: v2Mode === 'compose'
@@ -35,7 +41,7 @@
</div>
<div
v-if="!(studioMode === 'editv2' && v2Mode === 'generate')"
v-if="!(studioMode === 'editv2' && (v2Mode === 'generate' || (v2Mode === 'iterate' && !file && !inputStillId)))"
class="relative cursor-pointer rounded-2xl border border-dashed border-white/15 bg-zinc-950/50 p-4 transition hover:border-amber-300/50"
:class="{ 'border-amber-300/70 bg-amber-400/5': dragging }"
@dragover.prevent="dragging = true"
@@ -71,7 +77,14 @@
v-else-if="studioMode === 'editv2' && v2Mode === 'generate' && (file || inputStillId)"
class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3 text-sm text-zinc-400"
>
Still A is loaded for Edit / Compose / Refine. Generate ignores it and uses the prompt only.
Still A is loaded for Edit / Compose / Refine / Iterate. Generate ignores it and uses the prompt only.
</div>
<div
v-else-if="studioMode === 'editv2' && v2Mode === 'iterate'"
class="rounded-2xl border border-dashed border-white/15 bg-zinc-950/40 px-4 py-3 text-sm text-zinc-400"
>
<button type="button" class="font-medium text-amber-100 hover:underline" @click="openPicker('main')">Optional · add still A</button>
<span class="mt-1 block text-zinc-500">No still = text generate iterations. With a still = edit (or compose if you add still B).</span>
</div>
<div v-if="file || inputStillId" class="mt-2 flex min-w-0 items-center gap-2">
@@ -144,7 +157,17 @@
>
Generate
</button>
<label v-if="v2Mode === 'compose'" class="ml-auto text-xs">
<button
type="button"
class="rounded-full border px-3 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-40"
:class="v2Mode === 'iterate' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
:disabled="v2ModeLocked && v2Mode !== 'iterate'"
:title="v2ModeLocked && v2Mode !== 'iterate' ? 'Remove extra passes to change graphs' : ''"
@click="setV2Mode('iterate')"
>
Iterate
</button>
<label v-if="v2Mode === 'compose' || (v2Mode === 'iterate' && editRefFile)" class="ml-auto text-xs">
<span class="mr-2 text-zinc-500">Task</span>
<select
v-model="v2Task"
@@ -158,9 +181,9 @@
</label>
</div>
<div v-if="studioMode === 'edit' || (studioMode === 'editv2' && v2Mode === 'compose')" class="rounded-2xl border border-dashed border-white/15 bg-zinc-950/50 p-4">
<div v-if="studioMode === 'edit' || (studioMode === 'editv2' && (v2Mode === 'compose' || (v2Mode === 'iterate' && file)))" class="rounded-2xl border border-dashed border-white/15 bg-zinc-950/50 p-4">
<div class="mb-2 flex items-center justify-between gap-2">
<p class="text-sm font-medium text-zinc-300">{{ studioMode === 'editv2' ? 'Still B · required' : 'Image 2 · optional' }}</p>
<p class="text-sm font-medium text-zinc-300">{{ studioMode === 'editv2' && v2Mode === 'compose' ? 'Still B · required' : studioMode === 'editv2' ? 'Still B · optional' : 'Image 2 · optional' }}</p>
<button
v-if="editRefFile"
type="button"
@@ -203,7 +226,7 @@
<div>
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
<label class="text-sm font-medium text-zinc-300">{{ studioMode === 'editv2' && v2Mode === 'generate' ? 'Generate prompt' : studioMode === 'editv2' && v2Mode === 'refine' ? 'Refine prompt' : studioMode === 'edit' || studioMode === 'editv2' ? 'Edit prompt' : 'Motion & scene prompt' }}</label>
<label class="text-sm font-medium text-zinc-300">{{ studioMode === 'editv2' && v2Mode === 'generate' ? 'Generate prompt' : studioMode === 'editv2' && v2Mode === 'iterate' ? 'Iteration 1' : studioMode === 'editv2' && v2Mode === 'refine' ? 'Refine prompt' : studioMode === 'edit' || studioMode === 'editv2' ? 'Edit prompt' : 'Motion & scene prompt' }}</label>
<div class="flex flex-wrap items-center gap-3">
<button
v-if="identityPrompting"
@@ -292,6 +315,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>
<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">
@@ -345,14 +369,14 @@
</div>
</div>
<div v-if="studioMode === 'edit' || v2PassChaining" class="mt-3 space-y-3">
<div class="grid grid-cols-2 gap-2">
<div v-if="!(v2Mode === 'iterate' && !file)" class="grid grid-cols-2 gap-2">
<button
type="button"
class="rounded-xl border px-3 py-2 text-left text-sm transition"
:class="imagePassMode === 'batch' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
@click="imagePassMode = 'batch'"
>
<span class="block font-semibold">Batch</span>
<span class="block font-semibold">{{ v2Mode === 'iterate' ? 'From original' : 'Batch' }}</span>
<span class="text-[11px] leading-snug text-zinc-400">Each prompt from the start still</span>
</button>
<button
@@ -361,7 +385,7 @@
:class="imagePassMode === 'chain' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
@click="imagePassMode = 'chain'"
>
<span class="block font-semibold">Chain</span>
<span class="block font-semibold">{{ v2Mode === 'iterate' ? 'From last' : 'Chain' }}</span>
<span class="text-[11px] leading-snug text-zinc-400">Each pass from the last output</span>
</button>
</div>
@@ -383,7 +407,7 @@
class="rounded-2xl border border-white/10 bg-zinc-950/50 p-4"
>
<div class="mb-3 flex items-center justify-between gap-2">
<p class="text-sm font-medium text-zinc-200">{{ imagePassMode === 'batch' ? 'Prompt' : 'Pass' }} {{ index + 2 }}</p>
<p class="text-sm font-medium text-zinc-200">{{ v2Mode === 'iterate' ? 'Iteration' : imagePassMode === 'batch' ? 'Prompt' : 'Pass' }} {{ index + 2 }}</p>
<button
type="button"
class="text-xs text-red-300 hover:text-red-200"
@@ -844,7 +868,7 @@
compact
:disabled="v2Turbo"
/>
<template v-if="v2Mode !== 'generate'">
<template v-if="v2Mode !== 'generate' && !(v2Mode === 'iterate' && !file)">
<LampToggle :model-value="v2ScaleOn" label="Scale to MP" @update:model-value="setV2ScaleOn" />
<Thumbwheel
v-model="v2Megapixels"
@@ -1982,7 +2006,7 @@
<p class="truncate text-xs text-zinc-500">{{ stillMetaLine(entry.stillGroup.head) }}</p>
<div class="mt-2 flex flex-wrap items-center gap-3">
<button v-if="entry.stillGroup.parts.length > 1" type="button" class="text-xs text-amber-200" @click="toggleFamily(entry.stillGroup.key)">
{{ expandedFamilies[entry.stillGroup.key] ? 'Collapse' : `Expand ${entry.stillGroup.parts.length} passes` }}
{{ expandedFamilies[entry.stillGroup.key] ? 'Collapse' : `Expand ${entry.stillGroup.parts.length} ${entry.stillGroup.head.settings?.mode === 'iterate' ? 'iterations' : 'passes'}` }}
</button>
<button type="button" class="text-xs text-amber-300" @click="openStill(entry.stillGroup.head)">Open</button>
<button type="button" class="text-xs text-amber-300" @click="useStillAsInput(entry.stillGroup.head)">Use as input</button>
@@ -3198,38 +3222,52 @@ const stillBHint = computed(() => {
const editV2Blocked = computed(() => {
if (!folderId.value) return true
if (!prompt.value.trim() && !imageFillWithoutPrompt.value) return true
if (v2Mode.value !== 'generate' && !file.value) return true
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' && !editPassQueue.value.length) return true
return false
})
const editV2BlockReason = computed(() => {
if (v2Mode.value !== 'generate' && !file.value) return 'Load still A first.'
if (v2Mode.value !== 'generate' && v2Mode.value !== 'iterate' && !file.value) return 'Load still A first.'
if (v2Mode.value === 'compose' && !editRefFile.value) return 'Compose requires still B. This will not fall back to one-image generation.'
if (v2Mode.value === 'refine' && !refineMaskDirty.value) return 'Paint a mask on Still A first. Refine will not fall back to Edit.'
if (!prompt.value.trim() && !imageFillWithoutPrompt.value) {
return v2Mode.value === 'generate'
return v2Mode.value === 'generate' || (v2Mode.value === 'iterate' && !file.value)
? '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 (!folderId.value) return 'Choose a library folder before generating.'
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image v2 is not configured. Set COMFY_HOST.'
return ''
})
const v2PassChaining = computed(() => studioMode.value === 'editv2' && (v2Mode.value === 'edit' || v2Mode.value === 'compose'))
const v2PassChaining = computed(() => studioMode.value === 'editv2' && (v2Mode.value === 'edit' || v2Mode.value === 'compose' || v2Mode.value === 'iterate'))
const v2ModeLocked = computed(() => v2PassChaining.value && editPassQueue.value.length > 0)
const extraPassButtonLabel = computed(() => {
if (v2Mode.value === 'iterate') return '+ Iteration'
if (imagePassMode.value === 'batch') return '+ Prompt'
return studioMode.value === 'editv2' && v2Mode.value === 'compose' ? '+ Compose pass' : '+ Edit pass'
})
const imageBatchButtonHint = computed(() => {
const n = 1 + editPassQueue.value.length
if (imageBatchScript.value.trim() && n > 1) return `${n} prompts on the start still. Same format as a video shot list.`
return 'Paste shot 1, shot 2, shot 3. Each image is a new prompt on the original still.'
if (imageBatchScript.value.trim() && n > 1) {
return v2Mode.value === 'iterate'
? `${n} iterations. Same format as a video shot list.`
: `${n} prompts on the start still. Same format as a video shot list.`
}
return v2Mode.value === 'iterate'
? 'Paste shot 1, shot 2, shot 3. Each line becomes one nested still.'
: 'Paste shot 1, shot 2, shot 3. Each image is a new prompt on the original still.'
})
const extraPassHint = computed(() => {
if (v2Mode.value === 'iterate') {
if (!file.value) return 'Each prompt is a separate generate. Outputs nest under one library collection.'
if (imagePassMode.value === 'batch') return 'Every prompt runs from the start still. Outputs nest under one library collection.'
return 'Each prompt runs from the last output. Outputs nest under one library collection.'
}
if (imagePassMode.value === 'batch') {
return 'Every prompt runs from the start still. Paste a shot list, or add prompts one at a time. Outputs do not feed the next prompt.'
}
@@ -3239,6 +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 (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…'
@@ -3247,14 +3286,25 @@ const extraPassPlaceholder = computed(() => {
const editV2SubmitLabel = computed(() => {
const occupied = videoBusy.value || editBusy.value || studioJobs.value.some(job => job.status === 'running')
const n = v2PassChaining.value ? editPassQueue.value.length : 0
const label = v2Mode.value === 'generate' ? 'Generate image' : v2Mode.value === 'refine' ? 'Refine image' : v2Mode.value === 'compose' ? 'Compose image' : 'Edit image'
const label = v2Mode.value === 'generate'
? 'Generate image'
: v2Mode.value === 'iterate'
? 'Iterate'
: v2Mode.value === 'refine'
? 'Refine image'
: v2Mode.value === 'compose'
? 'Compose image'
: 'Edit image'
if (!occupied) {
if (v2Mode.value === 'iterate') return n ? `Iterate × ${n + 1}` : 'Iterate'
if (!n) return label
if (imagePassMode.value === 'batch') return n === 1 ? `${label} × 2` : `${label} × ${n + 1}`
return n === 1 ? `${label} + 1 pass` : `${label} + ${n} passes`
}
if (n) {
return imagePassMode.value === 'batch' ? `Queue ${n + 1} from start still` : `Queue ${n + 1} passes`
return v2Mode.value === 'iterate'
? `Queue ${n + 1} iterations`
: imagePassMode.value === 'batch' ? `Queue ${n + 1} from start still` : `Queue ${n + 1} passes`
}
return `Queue ${label.toLowerCase()}`
})
@@ -3646,7 +3696,8 @@ function stillFamilyScript(parts: LibraryStill[]) {
function stillMetaLine(still: LibraryStill) {
const res = still.width && still.height ? `${still.width}×${still.height}` : 'image'
const mode = stillModeLabel(still)
const pass = typeof still.chainIndex === 'number' ? `pass ${still.chainIndex + 1}` : ''
const unit = still.settings?.mode === 'iterate' ? 'iteration' : 'pass'
const pass = typeof still.chainIndex === 'number' ? `${unit} ${still.chainIndex + 1}` : ''
return [res, mode, pass].filter(Boolean).join(' · ')
}
@@ -4618,7 +4669,15 @@ function applyGenerationPreset(preset: GenerationPreset) {
if (preset.kind === 'imagev2') {
const settings = preset.settings as ImageV2PresetSettings
persistImageV2LoraStack(imageV2LoraStack.value, v2Engine.value)
v2Mode.value = settings.mode === 'compose' ? 'compose' : settings.mode === 'refine' ? 'refine' : settings.mode === 'generate' ? 'generate' : 'edit'
v2Mode.value = settings.mode === 'compose'
? 'compose'
: settings.mode === 'refine'
? 'refine'
: settings.mode === 'generate'
? 'generate'
: settings.mode === 'iterate'
? 'iterate'
: 'edit'
if (v2Mode.value !== 'edit' && v2Mode.value !== 'compose') clearEditPasses()
v2Engine.value = parseImageV2Engine(settings.engine, 'flux')
v2Task.value = v2Mode.value === 'compose' ? (settings.task || 'scene') : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'generate' ? 't2i' : 'scene'
@@ -5419,7 +5478,7 @@ async function restoreStudioEditJob(payload: {
loraStack?: LoraStackItem[]
negative?: string
imagePipeline?: 'v1' | 'v2'
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate'
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate' | 'iterate'
v2Task?: 'scene' | 'identity' | 'outfit' | 'face_lock' | 'refine' | 't2i'
engine?: 'flux' | 'krea'
snofsModel?: number
@@ -5445,7 +5504,7 @@ async function restoreStudioEditJob(payload: {
if (v2) {
studioMode.value = 'editv2'
outputFocus.value = 'edit'
const mode = payload.v2Mode === 'compose' || payload.v2Mode === 'refine' || payload.v2Mode === 'generate'
const mode = payload.v2Mode === 'compose' || payload.v2Mode === 'refine' || payload.v2Mode === 'generate' || payload.v2Mode === 'iterate'
? payload.v2Mode
: 'edit'
setV2Mode(mode)
@@ -5552,7 +5611,7 @@ async function restoreStudioPayload(kind: string, payload: {
shotLoraStacks?: LoraStackItem[][]
negative?: string
imagePipeline?: 'v1' | 'v2'
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate'
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate' | 'iterate'
v2Task?: 'scene' | 'identity' | 'outfit' | 'face_lock' | 'refine' | 't2i'
engine?: 'flux' | 'krea'
snofsModel?: number
@@ -6332,7 +6391,7 @@ function applyStillSettings(still: LibraryStill) {
if (dest === 'imagev2') {
studioMode.value = 'editv2'
outputFocus.value = 'edit'
const mode = settings?.mode === 'compose' || settings?.mode === 'refine' || settings?.mode === 'generate'
const mode = settings?.mode === 'compose' || settings?.mode === 'refine' || settings?.mode === 'generate' || settings?.mode === 'iterate'
? settings.mode
: 'edit'
setV2Mode(mode)
@@ -6392,6 +6451,7 @@ function stillUseAsInputMessage(still: LibraryStill) {
const dest = inferStillPipeline(still)
const mode = still.settings?.mode
if (dest === 'imagev2' && mode === 'generate') return 'Generate settings and still loaded. Switch to Edit anytime — the image is already in Still A.'
if (dest === 'imagev2' && mode === 'iterate') return 'Iterate settings and still loaded.'
if (dest === 'imagev2' && mode === 'compose') {
return still.settings?.referenceStillId
? 'Compose settings loaded with stills A and B.'
@@ -6976,6 +7036,7 @@ function absorbPastedImageBatch() {
}
function imagePassUnit() {
if (v2Mode.value === 'iterate') return 'Iteration'
return imagePassMode.value === 'batch' ? 'Batch' : 'Pass'
}
@@ -7102,8 +7163,12 @@ function setV2Mode(mode: ImageV2Mode) {
if (mode === 'generate') {
v2Task.value = 't2i'
}
if (mode === 'iterate') {
v2Task.value = 'scene'
imagePassMode.value = 'batch'
}
if (v2Engine.value === 'krea' && mode === 'edit') v2Denoise.value = IMAGE_V2_KREA_EDIT_DENOISE
if (v2Engine.value === 'krea' && mode === 'compose') v2Denoise.value = IMAGE_V2_KREA_COMPOSE_DENOISE
if (v2Engine.value === 'krea' && (mode === 'compose' || mode === 'iterate')) v2Denoise.value = IMAGE_V2_KREA_COMPOSE_DENOISE
}
function setV2Engine(engine: ImageV2Engine) {
@@ -7228,10 +7293,17 @@ async function editImageV2() {
await ensureGenerationNotifyPermission()
const body = new FormData()
body.append('mode', v2Mode.value)
body.append('task', v2Mode.value === 'generate' ? 't2i' : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'compose' ? v2Task.value : 'scene')
const iterateGenerate = v2Mode.value === 'iterate' && !file.value
body.append('task', v2Mode.value === 'generate' || iterateGenerate
? 't2i'
: v2Mode.value === 'refine'
? 'refine'
: (v2Mode.value === 'compose' || (v2Mode.value === 'iterate' && editRefFile.value))
? v2Task.value
: 'scene')
if (v2Mode.value !== 'generate' && file.value) body.append('image_a', file.value)
if (v2Mode.value === 'compose' && editRefFile.value) body.append('image_b', editRefFile.value)
if (v2Mode.value === 'refine' || (v2Engine.value === 'krea' && v2Mode.value !== 'generate')) {
if ((v2Mode.value === 'compose' || v2Mode.value === 'iterate') && editRefFile.value) body.append('image_b', editRefFile.value)
if (v2Mode.value === 'refine' || (v2Engine.value === 'krea' && v2Mode.value !== 'generate' && !iterateGenerate)) {
if (v2Mode.value === 'refine') {
const maskBlob = await refinePainter.value?.exportPng()
if (!maskBlob) {
@@ -7242,7 +7314,7 @@ async function editImageV2() {
}
body.append('strength', String(clampImageV2Denoise(v2Denoise.value)))
}
if (v2Mode.value === 'generate') {
if (v2Mode.value === 'generate' || iterateGenerate) {
const size = generateSizeForAspect(aspect.value)
body.append('width', String(size.width))
body.append('height', String(size.height))
@@ -7260,7 +7332,7 @@ async function editImageV2() {
body.append('consistency_clip', String(imageV2EngineLoras().find(item => isImageV2ConsistencyLora(item.name))?.strengthClip ?? 0))
body.append('steps', String(clampImageSteps(v2Steps.value, v2Engine.value === 'krea' ? IMAGE_V2_KREA_STEPS : IMAGE_V2_STEPS_DEFAULT)))
body.append('cfg', String(clampImageCfg(v2Cfg.value, v2Engine.value === 'krea' ? IMAGE_V2_KREA_CFG : IMAGE_V2_CFG_DEFAULT)))
if (v2Mode.value !== 'generate') {
if (v2Mode.value !== 'generate' && !iterateGenerate) {
body.append('scaleToTotalPixels', String(v2ScaleOn.value))
if (v2ScaleOn.value) body.append('megapixels', String(clampImageScaleMegapixels(v2Megapixels.value, 1)))
}
@@ -7270,9 +7342,9 @@ async function editImageV2() {
body.append('hideInput', String(showPrivacyToggles.value && hideInputPreview.value))
body.append('hideThumbnail', String(showPrivacyToggles.value && hideThumbnail.value))
body.append('name', clipName.value.trim())
if (inputStillId.value) body.append('stillId', inputStillId.value)
if (inputStillId.value && file.value) body.append('stillId', inputStillId.value)
if (extraPasses.length) body.append('passes', JSON.stringify(extraPasses))
body.append('passMode', imagePassMode.value)
body.append('passMode', iterateGenerate ? 'batch' : imagePassMode.value)
const v2Stack = filterLoraStackForImageEngine(
filterLoraStackForStudio(imageV2LoraStack.value, ltxEnabled.value),
v2Engine.value
@@ -7301,7 +7373,9 @@ async function editImageV2() {
if (started.queued) {
const n = 1 + extraPasses.length
toast(n > 1
? (imagePassMode.value === 'batch'
? (v2Mode.value === 'iterate'
? `Queued ${n} iterations behind the current job.`
: imagePassMode.value === 'batch'
? `Queued ${n} Image v2 stills from the start image behind the current job.`
: `Queued ${n} Image v2 passes behind the current job.`)
: (v2Engine.value === 'krea' ? 'Queued Krea generate behind the current job.' : 'Queued Image v2 behind the current job.'))
@@ -7324,7 +7398,15 @@ async function editImageV2() {
editChainLabel.value = extraPasses.length ? `${imagePassUnit()} 1` : ''
editOverallProgress.value = 0
editCompletedChainStep.value = 0
const passNoun = v2Mode.value === 'compose' ? 'Compose' : v2Mode.value === 'refine' ? 'Refine' : v2Mode.value === 'generate' ? 'Generate' : 'Edit'
const passNoun = v2Mode.value === 'iterate'
? 'Iteration'
: v2Mode.value === 'compose'
? 'Compose'
: v2Mode.value === 'refine'
? 'Refine'
: v2Mode.value === 'generate'
? 'Generate'
: 'Edit'
editActiveChainPlan.value = imagePassPlan(prompt.value.trim(), extraPasses, passNoun)
startTimer('edit')
editDownloadName.value = downloadName
+38 -21
View File
@@ -131,7 +131,7 @@ export default defineEventHandler(async (event) => {
const mode = parseImageV2Mode(fields.mode)
if (!mode) {
throw createError({ statusCode: 400, statusMessage: 'mode must be edit, compose, refine, or generate' })
throw createError({ statusCode: 400, statusMessage: 'mode must be edit, compose, refine, generate, or iterate' })
}
const task = parseImageV2Task(
fields.task,
@@ -145,13 +145,19 @@ export default defineEventHandler(async (event) => {
const extraPasses = parsePasses(fields.passes)
const passMode = parseImagePassMode(fields.passMode)
const aspect = parseImageAspect(fields.aspect)
if (!promptMid && (mode === 'generate' || aspect === 'auto')) {
if (!promptMid && (mode === 'generate' || mode === 'iterate' || aspect === 'auto')) {
throw createError({ statusCode: 400, statusMessage: 'A prompt is required' })
}
if (extraPasses.length && (mode === 'refine' || mode === 'generate')) {
throw createError({
statusCode: 400,
statusMessage: 'Extra passes are only for Edit and Compose. Refine and Generate stay a single run.'
statusMessage: 'Extra passes are only for Edit, Compose, and Iterate. Refine and Generate stay a single run.'
})
}
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.'
})
}
if (extraPasses.some(item => !item.prompt)) {
@@ -172,13 +178,24 @@ export default defineEventHandler(async (event) => {
}
const ownerKey = libraryOwnerKey(event)
const imageA = mode === 'generate' ? null : await resolveImageRef(ownerKey, fields.image_a, uploadedA)
const imageB = mode === 'refine' || mode === 'generate' ? null : await resolveImageRef(ownerKey, fields.image_b, uploadedB)
const mask = mode === 'refine' ? await resolveImageRef(ownerKey, fields.mask, uploadedMask) : null
if (mode !== 'generate' && !imageA) {
const imageA = mode === 'generate'
? null
: await resolveImageRef(ownerKey, fields.image_a || fields.stillId, uploadedA).catch(() => null)
// Iterate with no still A is text-only generate iterations.
if (mode !== 'generate' && mode !== 'iterate' && !imageA) {
throw createError({ statusCode: 400, statusMessage: 'image_a is required' })
}
if (mode === 'iterate' && !imageA && (uploadedA || String(fields.image_a || fields.stillId || '').trim())) {
throw createError({ statusCode: 400, statusMessage: 'Could not load still A for iterate' })
}
const imageB = mode === 'refine' || mode === 'generate' || (mode === 'iterate' && !imageA)
? null
: await resolveImageRef(ownerKey, fields.image_b, uploadedB).catch(() => null)
const mask = mode === 'refine' ? await resolveImageRef(ownerKey, fields.mask, uploadedMask) : null
const graphMode = mode === 'iterate'
? (imageA ? (imageB ? 'compose' as const : 'edit' as const) : 'generate' as const)
: mode
if (mode === 'refine' && !mask) {
throw createError({
statusCode: 400,
@@ -220,7 +237,7 @@ export default defineEventHandler(async (event) => {
const cfg = engine === 'krea'
? clampImageCfg(fields.cfg, IMAGE_V2_KREA_CFG)
: turbo ? IMAGE_V2_TURBO_CFG : clampImageCfg(fields.cfg, IMAGE_V2_CFG_DEFAULT)
const megapixels = mode === 'generate'
const megapixels = graphMode === 'generate'
? 0
: fields.scaleToTotalPixels == null
? clampImageScaleMegapixels(fields.megapixels ?? fields.scaleMegapixels, 1)
@@ -230,31 +247,31 @@ export default defineEventHandler(async (event) => {
const seed = fields.seed && String(fields.seed) !== 'random'
? Number(fields.seed)
: Math.floor(Math.random() * 2_147_483_647)
const generateSize = mode === 'generate'
const generateSize = graphMode === 'generate'
? generateSizeForAspect(aspect)
: clampImageV2Size(fields.width, fields.height, fields.aspect)
const size = mode === 'generate'
const size = graphMode === 'generate'
? { width: generateSize.width, height: generateSize.height }
: imageDimensions(imageA!.data)
const v2Mode = mode as ImageV2Mode
const v2Task = (
mode === 'generate' ? 't2i' : mode === 'refine' ? 'refine' : mode === 'edit' ? 'scene' : task
graphMode === 'generate' ? 't2i' : graphMode === 'refine' ? 'refine' : graphMode === 'edit' ? 'scene' : task
) as ImageV2Task
const refineStrength = mode === 'generate'
const refineStrength = graphMode === 'generate'
? undefined
: engine === 'krea' || mode === 'refine'
: engine === 'krea' || graphMode === 'refine'
? clampImageV2Denoise(
fields.strength,
mode === 'compose' ? IMAGE_V2_KREA_COMPOSE_DENOISE : mode === 'edit' ? IMAGE_V2_KREA_EDIT_DENOISE : IMAGE_V2_DENOISE_DEFAULT
graphMode === 'compose' ? IMAGE_V2_KREA_COMPOSE_DENOISE : graphMode === 'edit' ? IMAGE_V2_KREA_EDIT_DENOISE : IMAGE_V2_DENOISE_DEFAULT
)
: undefined
const generateConsistencyModel = clampImageV2Strength(
fields.consistency_model,
mode === 'generate' || engine === 'krea' ? 0 : IMAGE_V2_CONSISTENCY_MODEL
graphMode === 'generate' || engine === 'krea' ? 0 : IMAGE_V2_CONSISTENCY_MODEL
)
const generateConsistencyClip = clampImageV2Strength(
fields.consistency_clip,
mode === 'generate' || engine === 'krea' ? 0 : IMAGE_V2_CONSISTENCY_CLIP
graphMode === 'generate' || engine === 'krea' ? 0 : IMAGE_V2_CONSISTENCY_CLIP
)
const conceptAllowed = isXaigenStudio()
const generateSnofsFallback = 0
@@ -325,7 +342,7 @@ export default defineEventHandler(async (event) => {
promptPost: promptPost || undefined,
name: clipName,
folderId,
aspect: mode === 'generate' ? generateSize.aspect : aspect,
aspect: graphMode === 'generate' ? generateSize.aspect : aspect,
width: size?.width || 0,
height: size?.height || 0,
steps,
@@ -349,10 +366,10 @@ export default defineEventHandler(async (event) => {
queueAutoRun: false,
negative: String(fields.negative || '').trim(),
passes: extraPasses,
passMode,
passMode: mode === 'iterate' && graphMode === 'generate' ? 'batch' : passMode,
referenceStillId: savedRef?.id,
referenceStillFilename: savedRef?.filename,
scaleToTotalPixels: mode !== 'generate' && megapixels > 0,
scaleToTotalPixels: graphMode !== 'generate' && megapixels > 0,
scaleMegapixels: megapixels,
imagePipeline: 'v2',
v2Mode,
@@ -386,7 +403,7 @@ export default defineEventHandler(async (event) => {
mode: v2Mode,
task: v2Task,
engine,
workflow: imageV2WorkflowFile(v2Mode, engine),
workflow: imageV2WorkflowFile(v2Mode, engine, { hasImage: Boolean(imageA), hasReference: Boolean(imageB) }),
width: size?.width || 0,
height: size?.height || 0,
strength: refineStrength,
+34 -26
View File
@@ -11,6 +11,7 @@ import { comfyJobPrefix } from '~/utils/outputNames'
import { restorePromptParts, wrappedPromptForComfy } from '~/utils/promptParts'
import { isXaigenStudio } from '~/utils/videoModels'
import {
imageV2GraphMode,
imageV2StackSpecials,
mergeLegacyImageV2Loras,
parseImageV2Engine,
@@ -52,20 +53,24 @@ export type EditV2RunParams = {
export async function runEditV2(job: Job, params: EditV2RunParams) {
const library = job.library
if (!library) throw new Error('Edit job is missing library metadata')
if (params.mode === 'refine' && !params.mask) {
const graphMode = imageV2GraphMode(params.mode, {
hasImage: Boolean(params.image),
hasReference: Boolean(params.reference)
})
if (graphMode === 'refine' && !params.mask) {
throw new Error('Refine requires a mask. Refusing to fall back to Edit.')
}
if (params.mode === 'compose' && !params.reference) {
if (graphMode === 'compose' && !params.reference) {
throw new Error('Compose requires image B. Refusing to fall back to one-image generation.')
}
if (params.mode === 'edit' && params.reference) {
if (graphMode === 'edit' && params.reference && params.mode !== 'iterate') {
throw new Error('Edit mode takes one image. Use Compose for two stills.')
}
if (params.mode !== 'generate' && !params.image) {
if (graphMode !== 'generate' && !params.image) {
throw new Error('This v2 mode requires still A.')
}
const canChain = params.mode === 'edit' || params.mode === 'compose'
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)
: []
@@ -78,8 +83,9 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
...extraPasses.map(item => item.prompt)
]
const chainTotal = prompts.length
const batch = params.passMode !== 'chain'
const unit = batch ? 'Batch' : 'Pass'
// Text-only iterate is always independent generations. With a still, honor batch vs chain.
const batch = graphMode === 'generate' || params.passMode !== 'chain'
const unit = params.mode === 'iterate' ? 'Iteration' : (batch ? 'Batch' : 'Pass')
library.chainTotal = chainTotal
library.familyId = chainTotal > 1 ? (library.familyId || crypto.randomUUID()) : library.familyId
@@ -96,7 +102,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
job.imageComfyHost = getComfyHost()
await withImageComfyHost(job.imageComfyHost, async () => {
const generate = params.mode === 'generate'
const generate = graphMode === 'generate'
const engine = parseImageV2Engine(params.engine, 'flux')
const engineLabel = engine === 'krea' ? 'Krea' : 'Flux'
const { applyImageFill } = await import('~/server/utils/imagePad')
@@ -113,7 +119,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
const krea = engine === 'krea' ? await resolveKreaGenerateAssets() : null
const appliedStack = mergeLegacyImageV2Loras(params.loraStack?.length ? params.loraStack : library.loraStack, {
engine,
mode: params.mode,
mode: graphMode,
xaigen: isXaigenStudio(),
snofsModel: params.snofsModel,
snofsClip: params.snofsClip,
@@ -127,7 +133,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
const consistencyModel = appliedSpecials.consistency?.strengthModel ?? params.consistencyModel
const consistencyClip = appliedSpecials.consistency?.strengthClip ?? params.consistencyClip
if (!generate && params.mode === 'compose' && params.reference) {
if (!generate && graphMode === 'compose' && params.reference) {
uploadedRef = await uploadImage({
...params.reference,
filename: `ref_${params.reference.filename || 'image_b.png'}`
@@ -163,17 +169,17 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
emitChainJob(job, {
type: 'status',
message: generate
? `Queueing ${engineLabel} generate on Beast...`
: params.mode === 'refine'
? `Queueing ${engineLabel} iterate generate on Beast...`
: graphMode === 'refine'
? 'Uploading canvas and mask to Beast...'
: params.mode === 'compose' ? 'Uploading stills A and B to Beast...' : 'Uploading still A to Beast...',
: graphMode === 'compose' ? 'Uploading stills A and B to Beast...' : 'Uploading still A to Beast...',
progress: generate ? 12 : 8
})
if (index === 0 && !generate && image) {
const filled = await applyImageFill({
image,
mask: params.mode === 'refine' ? mask : null,
mask: graphMode === 'refine' ? mask : null,
aspect: params.aspect || 'auto'
})
image = filled.image
@@ -199,7 +205,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
prompt = composeFillPrompt(prompt, true)
}
if (index === 0 && params.mode === 'refine' && mask && !uploadedMask) {
if (index === 0 && graphMode === 'refine' && mask && !uploadedMask) {
uploadedMask = await uploadImage({
...mask,
filename: `mask_${mask.filename || 'refine-mask.png'}`
@@ -212,9 +218,11 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
if (!generate) {
emitChainJob(job, {
type: 'status',
message: params.mode === 'refine'
message: params.mode === 'iterate'
? `Queueing ${engineLabel} iterate (${graphMode}) on Beast...`
: graphMode === 'refine'
? `Queueing ${engineLabel} v2 refine on Beast...`
: params.mode === 'compose'
: graphMode === 'compose'
? `Queueing ${engineLabel} v2 compose (${params.task}) on Beast...`
: `Queueing ${engineLabel} v2 edit on Beast...`,
progress: 12
@@ -222,9 +230,9 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
}
const built = buildImageV2Workflow({
mode: params.mode,
mode: graphMode,
engine,
task: params.task,
task: generate ? 't2i' : params.task,
prompt: wrappedPromptForComfy(library, prompt, index === 0 ? params.prompt : prompt),
negative: params.negative,
imageAName: uploaded?.name,
@@ -305,7 +313,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
kind: 'imagev2',
mode: params.mode,
engine,
task: params.task,
task: generate ? 't2i' : params.task,
negative: params.negative,
snofsModel,
snofsClip,
@@ -313,12 +321,12 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
consistencyClip,
steps: params.steps,
cfg: params.cfg,
megapixels: params.mode === 'generate' || !(params.megapixels > 0) ? undefined : params.megapixels,
scaleToTotalPixels: params.mode === 'generate' ? undefined : params.megapixels > 0,
megapixels: generate || !(params.megapixels > 0) ? undefined : params.megapixels,
scaleToTotalPixels: generate ? undefined : params.megapixels > 0,
turbo: params.turbo === true,
strength: params.mode === 'generate' ? undefined : params.strength,
width: params.mode === 'generate' ? params.width : undefined,
height: params.mode === 'generate' ? params.height : undefined,
strength: generate ? undefined : params.strength,
width: generate ? params.width : undefined,
height: generate ? params.height : undefined,
aspect: params.aspect || 'auto',
seed,
sourceStillId: params.sourceStillId || library.stillId,
@@ -362,7 +370,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
emitChainJob(job, {
type: 'complete',
message: chainTotal > 1
? `${engineLabel} v2 ${batch ? 'batch' : 'chain'} finished on Beast`
? `${engineLabel} ${params.mode === 'iterate' ? 'iterate' : `v2 ${batch ? 'batch' : 'chain'}`} finished on Beast`
: `${engineLabel} v2 finished on Beast`,
progress: 100,
stillId: still?.id,
+27 -20
View File
@@ -58,7 +58,7 @@ export interface StudioJobPayload {
scaleToTotalPixels?: boolean
scaleMegapixels?: number
imagePipeline?: 'v1' | 'v2'
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate'
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate' | 'iterate'
v2Task?: 'scene' | 'identity' | 'outfit' | 'face_lock' | 'refine' | 't2i'
engine?: 'flux' | 'krea'
snofsModel?: number
@@ -1017,7 +1017,8 @@ async function startStudioEditJob(item: StudioJob) {
const { runEditV2 } = await import('~/server/utils/imageChainV2')
const payload = item.payload
const generate = payload.imagePipeline === 'v2' && payload.v2Mode === 'generate'
const iterate = payload.imagePipeline === 'v2' && payload.v2Mode === 'iterate'
const generate = payload.imagePipeline === 'v2' && (payload.v2Mode === 'generate' || (iterate && !payload.stillId))
if (!generate && (!payload.stillId || !existsSync(stillPath(item.ownerKey, payload.stillId)))) {
throw new Error('The input still is missing from the library')
}
@@ -1038,30 +1039,36 @@ async function startStudioEditJob(item: StudioJob) {
: null
if (payload.imagePipeline === 'v2') {
const mode = payload.v2Mode === 'compose'
const mode = payload.v2Mode === 'iterate'
? 'iterate'
: payload.v2Mode === 'compose'
? 'compose'
: payload.v2Mode === 'refine'
? 'refine'
: payload.v2Mode === 'generate'
? 'generate'
: 'edit'
const graphMode = mode === 'iterate'
? (image ? (reference ? 'compose' : 'edit') : 'generate')
: mode
const maskId = payload.maskStillId
const mask = mode === 'refine' && maskId && existsSync(stillPath(item.ownerKey, maskId))
const mask = graphMode === 'refine' && maskId && existsSync(stillPath(item.ownerKey, maskId))
? {
filename: payload.maskStillFilename || 'refine-mask.png',
data: readFileSync(stillPath(item.ownerKey, maskId)),
type: 'image/png'
}
: null
if (mode === 'refine' && !mask) {
if (graphMode === 'refine' && !mask) {
throw new Error('Refine requires a mask. Refusing to fall back to Edit.')
}
if (mode === 'compose' && !reference) {
if (graphMode === 'compose' && !reference) {
throw new Error('Compose requires image B. Refusing to fall back to one-image generation.')
}
if (mode === 'edit' && reference) {
throw new Error('Edit mode takes one image. Use Compose for two stills.')
}
const passCount = (mode === 'edit' || mode === 'compose' || mode === 'iterate') ? (payload.passes?.length || 0) : 0
live = createEditLiveJob({
steps: payload.steps,
hideThumbnail: payload.hideThumbnail,
@@ -1088,9 +1095,9 @@ async function startStudioEditJob(item: StudioJob) {
familyId: item.familyId,
chainIndex: 0,
chainStep: 1,
chainTotal: 1 + ((mode === 'edit' || mode === 'compose') ? (payload.passes?.length || 0) : 0),
chainLabel: (mode === 'edit' || mode === 'compose') && payload.passes?.length
? (payload.passMode === 'chain' ? 'Pass 1' : 'Batch 1')
chainTotal: 1 + passCount,
chainLabel: passCount
? (mode === 'iterate' ? 'Iteration 1' : (payload.passMode === 'chain' ? 'Pass 1' : 'Batch 1'))
: undefined,
passes: payload.passes,
...persistLoraFields(payload.loraStack || payload.loraName)
@@ -1102,30 +1109,30 @@ async function startStudioEditJob(item: StudioJob) {
void runEditV2(live, {
mode,
engine: v2Engine,
task: payload.v2Task || 'scene',
image: mode === 'generate' ? null : image,
reference: mode === 'refine' || mode === 'generate' ? null : reference,
task: payload.v2Task || (graphMode === 'generate' ? 't2i' : 'scene'),
image: graphMode === 'generate' ? null : image,
reference: graphMode === 'compose' ? reference : null,
mask,
prompt: payload.prompt,
passes: payload.passes || [],
passMode: payload.passMode === 'chain' ? 'chain' : 'batch',
negative: payload.negative || '',
steps: payload.steps,
seed: live.library?.seed || payload.seed,
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,
consistencyModel: v2Specials.consistency?.strengthModel ?? payload.consistencyModel ?? (mode === 'generate' || payload.engine === 'krea' ? 0 : 0.7),
consistencyClip: v2Specials.consistency?.strengthClip ?? payload.consistencyClip ?? (mode === 'generate' || payload.engine === 'krea' ? 0 : 0.7),
megapixels: payload.scaleToTotalPixels === false ? 0 : (payload.scaleMegapixels ?? 1),
consistencyModel: v2Specials.consistency?.strengthModel ?? payload.consistencyModel ?? 0,
consistencyClip: v2Specials.consistency?.strengthClip ?? payload.consistencyClip ?? 0,
megapixels: payload.scaleMegapixels || 0,
strength: payload.refineStrength,
aspect: payload.aspect || 'auto',
width: payload.width,
height: payload.height,
turbo: payload.turbo === true,
aspect: payload.aspect,
sourceStillId: payload.stillId,
referenceStillId: payload.referenceStillId,
loraStack: payload.loraStack
loraStack: payload.loraStack,
passes: payload.passes,
passMode: payload.passMode === 'chain' ? 'chain' : 'batch',
}).catch((error) => {
const message = error instanceof Error ? error.message : String(error)
if (live && live.status !== 'error' && live.status !== 'cancelled' && live.status !== 'deferred') {
+25 -10
View File
@@ -6,13 +6,26 @@ import {
type LoraStackItem
} from '~/utils/loras'
export const IMAGE_V2_MODES = ['edit', 'compose', 'refine', 'generate'] as const
export const IMAGE_V2_MODES = ['edit', 'compose', 'refine', 'generate', 'iterate'] as const
export const IMAGE_V2_TASKS = ['scene', 'identity', 'outfit', 'face_lock', 'refine', 't2i'] as const
export const IMAGE_V2_ENGINES = ['flux', 'krea'] as const
export type ImageV2Mode = (typeof IMAGE_V2_MODES)[number]
export type ImageV2Task = (typeof IMAGE_V2_TASKS)[number]
export type ImageV2Engine = (typeof IMAGE_V2_ENGINES)[number]
export type ImageV2GraphMode = Exclude<ImageV2Mode, 'iterate'>
/** Iterate picks a real graph from the attached stills: none → generate, A → edit, A+B → compose. */
export function iterateGraphMode(opts: { hasImage?: boolean; hasReference?: boolean }): Exclude<ImageV2GraphMode, 'refine'> {
if (!opts.hasImage) return 'generate'
if (opts.hasReference) return 'compose'
return 'edit'
}
export function imageV2GraphMode(mode: ImageV2Mode, opts: { hasImage?: boolean; hasReference?: boolean } = {}): ImageV2GraphMode {
if (mode === 'iterate') return iterateGraphMode(opts)
return mode
}
export const IMAGE_V2_STEPS_DEFAULT = 24
export const IMAGE_V2_CFG_DEFAULT = 4
@@ -97,16 +110,17 @@ export function parseImageV2Engine(raw: unknown, fallback: ImageV2Engine = 'flux
return IMAGE_V2_ENGINES.includes(value as ImageV2Engine) ? value as ImageV2Engine : fallback
}
export function imageV2WorkflowFile(mode: ImageV2Mode, engine: ImageV2Engine = 'flux') {
export function imageV2WorkflowFile(mode: ImageV2Mode, engine: ImageV2Engine = 'flux', opts: { hasImage?: boolean; hasReference?: boolean } = {}) {
const graph = imageV2GraphMode(mode, opts)
if (engine === 'krea') {
if (mode === 'generate') return 'krea_v2_generate.json'
if (mode === 'refine') return 'krea_v2_refine.json'
if (mode === 'compose') return 'krea_v2_compose.json'
if (graph === 'generate') return 'krea_v2_generate.json'
if (graph === 'refine') return 'krea_v2_refine.json'
if (graph === 'compose') return 'krea_v2_compose.json'
return 'krea_v2_edit.json'
}
if (mode === 'generate') return 'klein_v2_generate.json'
if (mode === 'refine') return 'klein_v2_refine.json'
if (mode === 'compose') return 'klein_v2_compose.json'
if (graph === 'generate') return 'klein_v2_generate.json'
if (graph === 'refine') return 'klein_v2_refine.json'
if (graph === 'compose') return 'klein_v2_compose.json'
return 'klein_v2_edit.json'
}
@@ -150,9 +164,10 @@ export function clampImageV2Size(width: unknown, height: unknown, aspect?: unkno
}
}
export function composeImageV2Prompt(mode: ImageV2Mode, task: ImageV2Task, prompt: string) {
export function composeImageV2Prompt(mode: ImageV2Mode, task: ImageV2Task, prompt: string, opts: { hasImage?: boolean; hasReference?: boolean } = {}) {
const body = String(prompt || '').trim()
if (mode === 'generate' || mode === 'refine' || mode !== 'compose' || task === 'scene' || task === 't2i') return body
const graph = imageV2GraphMode(mode, opts)
if (graph === 'generate' || graph === 'refine' || graph !== 'compose' || task === 'scene' || task === 't2i') return body
const header = IMAGE_V2_ROLE_HEADERS[task as Exclude<ImageV2Task, 'scene' | 'refine' | 't2i'>]
return header ? `${header}\n\n${body}` : body
}
+3 -1
View File
@@ -92,7 +92,7 @@ function stillDisplayLoraStack(settings: StillGenerationSettings) {
export function normalizeStillSettings(raw: unknown): StillGenerationSettings | undefined {
if (!raw || typeof raw !== 'object') return undefined
const value = raw as Record<string, unknown>
const mode = value.mode === 'compose' || value.mode === 'refine' || value.mode === 'generate' || value.mode === 'edit'
const mode = value.mode === 'compose' || value.mode === 'refine' || value.mode === 'generate' || value.mode === 'edit' || value.mode === 'iterate'
? value.mode
: undefined
const engine = value.engine === 'krea' || value.engine === 'flux' ? value.engine : undefined
@@ -155,6 +155,8 @@ export function stillModeLabel(still: { filename?: string; settings?: StillGener
if (settings?.kind === 'imagev2' || settings?.mode) {
const mode = settings.mode === 'generate'
? 'Generate'
: settings.mode === 'iterate'
? 'Iterate'
: settings.mode === 'compose'
? 'Compose'
: settings.mode === 'refine'