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:
+120
-38
@@ -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,9 +7373,11 @@ async function editImageV2() {
|
||||
if (started.queued) {
|
||||
const n = 1 + extraPasses.length
|
||||
toast(n > 1
|
||||
? (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.`)
|
||||
? (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.'))
|
||||
await refreshStudioQueue()
|
||||
return
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,19 +218,21 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||
if (!generate) {
|
||||
emitChainJob(job, {
|
||||
type: 'status',
|
||||
message: params.mode === 'refine'
|
||||
? `Queueing ${engineLabel} v2 refine on Beast...`
|
||||
: params.mode === 'compose'
|
||||
? `Queueing ${engineLabel} v2 compose (${params.task}) on Beast...`
|
||||
: `Queueing ${engineLabel} v2 edit on Beast...`,
|
||||
message: params.mode === 'iterate'
|
||||
? `Queueing ${engineLabel} iterate (${graphMode}) on Beast...`
|
||||
: graphMode === 'refine'
|
||||
? `Queueing ${engineLabel} v2 refine on Beast...`
|
||||
: graphMode === 'compose'
|
||||
? `Queueing ${engineLabel} v2 compose (${params.task}) on Beast...`
|
||||
: `Queueing ${engineLabel} v2 edit on Beast...`,
|
||||
progress: 12
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
+33
-26
@@ -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'
|
||||
? 'compose'
|
||||
: payload.v2Mode === 'refine'
|
||||
? 'refine'
|
||||
: payload.v2Mode === 'generate'
|
||||
? 'generate'
|
||||
: 'edit'
|
||||
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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,11 +155,13 @@ export function stillModeLabel(still: { filename?: string; settings?: StillGener
|
||||
if (settings?.kind === 'imagev2' || settings?.mode) {
|
||||
const mode = settings.mode === 'generate'
|
||||
? 'Generate'
|
||||
: settings.mode === 'compose'
|
||||
? 'Compose'
|
||||
: settings.mode === 'refine'
|
||||
? 'Refine'
|
||||
: 'Edit'
|
||||
: settings.mode === 'iterate'
|
||||
? 'Iterate'
|
||||
: settings.mode === 'compose'
|
||||
? 'Compose'
|
||||
: settings.mode === 'refine'
|
||||
? 'Refine'
|
||||
: 'Edit'
|
||||
const task = settings.mode === 'compose' ? composeTaskLabel(settings.task) : ''
|
||||
const engine = settings.engine === 'krea' ? 'Krea' : settings.engine === 'flux' ? 'Flux' : ''
|
||||
return [engine ? `Image v2 · ${mode} · ${engine}` : `Image v2 · ${mode}`, task].filter(Boolean).join(' · ')
|
||||
|
||||
Reference in New Issue
Block a user