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:
+118
-36
@@ -24,6 +24,12 @@
|
|||||||
<p class="text-sm text-zinc-400">{{ studioMode === 'editv2'
|
<p class="text-sm text-zinc-400">{{ studioMode === 'editv2'
|
||||||
? (v2Mode === 'generate'
|
? (v2Mode === 'generate'
|
||||||
? 'Generate: no reference image. Prompt only.'
|
? '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'
|
: v2Mode === 'refine'
|
||||||
? 'Refine: paint a mask on Still A. Prompt only what should change in the painted area. Strength is denoise in that region.'
|
? 'Refine: paint a mask on Still A. Prompt only what should change in the painted area. Strength is denoise in that region.'
|
||||||
: v2Mode === 'compose'
|
: v2Mode === 'compose'
|
||||||
@@ -35,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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="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 }"
|
:class="{ 'border-amber-300/70 bg-amber-400/5': dragging }"
|
||||||
@dragover.prevent="dragging = true"
|
@dragover.prevent="dragging = true"
|
||||||
@@ -71,7 +77,14 @@
|
|||||||
v-else-if="studioMode === 'editv2' && v2Mode === 'generate' && (file || inputStillId)"
|
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"
|
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>
|
||||||
|
|
||||||
<div v-if="file || inputStillId" class="mt-2 flex min-w-0 items-center gap-2">
|
<div v-if="file || inputStillId" class="mt-2 flex min-w-0 items-center gap-2">
|
||||||
@@ -144,7 +157,17 @@
|
|||||||
>
|
>
|
||||||
Generate
|
Generate
|
||||||
</button>
|
</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>
|
<span class="mr-2 text-zinc-500">Task</span>
|
||||||
<select
|
<select
|
||||||
v-model="v2Task"
|
v-model="v2Task"
|
||||||
@@ -158,9 +181,9 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</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">
|
<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
|
<button
|
||||||
v-if="editRefFile"
|
v-if="editRefFile"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -203,7 +226,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
|
<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">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<button
|
<button
|
||||||
v-if="identityPrompting"
|
v-if="identityPrompting"
|
||||||
@@ -292,6 +315,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="studioMode === 'editv2' && v2Mode === 'generate'" class="mt-3 text-xs text-zinc-500">No reference image. Prompt only.</p>
|
<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">
|
<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>
|
<p class="text-xs text-zinc-500">Prompt only what should change in the painted area.</p>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
@@ -345,14 +369,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="studioMode === 'edit' || v2PassChaining" class="mt-3 space-y-3">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-xl border px-3 py-2 text-left text-sm transition"
|
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'"
|
:class="imagePassMode === 'batch' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
|
||||||
@click="imagePassMode = 'batch'"
|
@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>
|
<span class="text-[11px] leading-snug text-zinc-400">Each prompt from the start still</span>
|
||||||
</button>
|
</button>
|
||||||
<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'"
|
:class="imagePassMode === 'chain' ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
|
||||||
@click="imagePassMode = 'chain'"
|
@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>
|
<span class="text-[11px] leading-snug text-zinc-400">Each pass from the last output</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -383,7 +407,7 @@
|
|||||||
class="rounded-2xl border border-white/10 bg-zinc-950/50 p-4"
|
class="rounded-2xl border border-white/10 bg-zinc-950/50 p-4"
|
||||||
>
|
>
|
||||||
<div class="mb-3 flex items-center justify-between gap-2">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-xs text-red-300 hover:text-red-200"
|
class="text-xs text-red-300 hover:text-red-200"
|
||||||
@@ -844,7 +868,7 @@
|
|||||||
compact
|
compact
|
||||||
:disabled="v2Turbo"
|
: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" />
|
<LampToggle :model-value="v2ScaleOn" label="Scale to MP" @update:model-value="setV2ScaleOn" />
|
||||||
<Thumbwheel
|
<Thumbwheel
|
||||||
v-model="v2Megapixels"
|
v-model="v2Megapixels"
|
||||||
@@ -1982,7 +2006,7 @@
|
|||||||
<p class="truncate text-xs text-zinc-500">{{ stillMetaLine(entry.stillGroup.head) }}</p>
|
<p class="truncate text-xs text-zinc-500">{{ stillMetaLine(entry.stillGroup.head) }}</p>
|
||||||
<div class="mt-2 flex flex-wrap items-center gap-3">
|
<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)">
|
<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>
|
||||||
<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="openStill(entry.stillGroup.head)">Open</button>
|
||||||
<button type="button" class="text-xs text-amber-300" @click="useStillAsInput(entry.stillGroup.head)">Use as input</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(() => {
|
const editV2Blocked = computed(() => {
|
||||||
if (!folderId.value) return true
|
if (!folderId.value) return true
|
||||||
if (!prompt.value.trim() && !imageFillWithoutPrompt.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 === 'compose' && !editRefFile.value) return true
|
||||||
if (v2Mode.value === 'refine' && !refineMaskDirty.value) return true
|
if (v2Mode.value === 'refine' && !refineMaskDirty.value) return true
|
||||||
if (v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) 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
|
return false
|
||||||
})
|
})
|
||||||
const editV2BlockReason = computed(() => {
|
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 === '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 (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) {
|
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 first.'
|
||||||
: 'Write a prompt, or pick Landscape / Portrait / Square to fill the frame.'
|
: '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 (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 (!folderId.value) return 'Choose a library folder before generating.'
|
||||||
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image v2 is not configured. Set COMFY_HOST.'
|
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image v2 is not configured. Set COMFY_HOST.'
|
||||||
return ''
|
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 v2ModeLocked = computed(() => v2PassChaining.value && editPassQueue.value.length > 0)
|
||||||
const extraPassButtonLabel = computed(() => {
|
const extraPassButtonLabel = computed(() => {
|
||||||
|
if (v2Mode.value === 'iterate') return '+ Iteration'
|
||||||
if (imagePassMode.value === 'batch') return '+ Prompt'
|
if (imagePassMode.value === 'batch') return '+ Prompt'
|
||||||
return studioMode.value === 'editv2' && v2Mode.value === 'compose' ? '+ Compose pass' : '+ Edit pass'
|
return studioMode.value === 'editv2' && v2Mode.value === 'compose' ? '+ Compose pass' : '+ Edit pass'
|
||||||
})
|
})
|
||||||
const imageBatchButtonHint = computed(() => {
|
const imageBatchButtonHint = computed(() => {
|
||||||
const n = 1 + editPassQueue.value.length
|
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.`
|
if (imageBatchScript.value.trim() && n > 1) {
|
||||||
return 'Paste shot 1, shot 2, shot 3. Each image is a new prompt on the original still.'
|
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(() => {
|
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') {
|
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.'
|
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.'
|
return 'Each extra pass is another Edit on the last output. You cannot switch graphs between passes.'
|
||||||
})
|
})
|
||||||
const extraPassPlaceholder = computed(() => {
|
const extraPassPlaceholder = computed(() => {
|
||||||
|
if (v2Mode.value === 'iterate') return 'Next iteration prompt…'
|
||||||
if (imagePassMode.value === 'batch') return 'Another prompt for the same start still…'
|
if (imagePassMode.value === 'batch') return 'Another prompt for the same start still…'
|
||||||
return studioMode.value === 'editv2' && v2Mode.value === 'compose'
|
return studioMode.value === 'editv2' && v2Mode.value === 'compose'
|
||||||
? 'Next compose prompt, applied to the previous pass output…'
|
? 'Next compose prompt, applied to the previous pass output…'
|
||||||
@@ -3247,14 +3286,25 @@ const extraPassPlaceholder = computed(() => {
|
|||||||
const editV2SubmitLabel = computed(() => {
|
const editV2SubmitLabel = computed(() => {
|
||||||
const occupied = videoBusy.value || editBusy.value || studioJobs.value.some(job => job.status === 'running')
|
const occupied = videoBusy.value || editBusy.value || studioJobs.value.some(job => job.status === 'running')
|
||||||
const n = v2PassChaining.value ? editPassQueue.value.length : 0
|
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 (!occupied) {
|
||||||
|
if (v2Mode.value === 'iterate') return n ? `Iterate × ${n + 1}` : 'Iterate'
|
||||||
if (!n) return label
|
if (!n) return label
|
||||||
if (imagePassMode.value === 'batch') return n === 1 ? `${label} × 2` : `${label} × ${n + 1}`
|
if (imagePassMode.value === 'batch') return n === 1 ? `${label} × 2` : `${label} × ${n + 1}`
|
||||||
return n === 1 ? `${label} + 1 pass` : `${label} + ${n} passes`
|
return n === 1 ? `${label} + 1 pass` : `${label} + ${n} passes`
|
||||||
}
|
}
|
||||||
if (n) {
|
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()}`
|
return `Queue ${label.toLowerCase()}`
|
||||||
})
|
})
|
||||||
@@ -3646,7 +3696,8 @@ function stillFamilyScript(parts: LibraryStill[]) {
|
|||||||
function stillMetaLine(still: LibraryStill) {
|
function stillMetaLine(still: LibraryStill) {
|
||||||
const res = still.width && still.height ? `${still.width}×${still.height}` : 'image'
|
const res = still.width && still.height ? `${still.width}×${still.height}` : 'image'
|
||||||
const mode = stillModeLabel(still)
|
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(' · ')
|
return [res, mode, pass].filter(Boolean).join(' · ')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4618,7 +4669,15 @@ function applyGenerationPreset(preset: GenerationPreset) {
|
|||||||
if (preset.kind === 'imagev2') {
|
if (preset.kind === 'imagev2') {
|
||||||
const settings = preset.settings as ImageV2PresetSettings
|
const settings = preset.settings as ImageV2PresetSettings
|
||||||
persistImageV2LoraStack(imageV2LoraStack.value, v2Engine.value)
|
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()
|
if (v2Mode.value !== 'edit' && v2Mode.value !== 'compose') clearEditPasses()
|
||||||
v2Engine.value = parseImageV2Engine(settings.engine, 'flux')
|
v2Engine.value = parseImageV2Engine(settings.engine, 'flux')
|
||||||
v2Task.value = v2Mode.value === 'compose' ? (settings.task || 'scene') : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'generate' ? 't2i' : 'scene'
|
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[]
|
loraStack?: LoraStackItem[]
|
||||||
negative?: string
|
negative?: string
|
||||||
imagePipeline?: 'v1' | 'v2'
|
imagePipeline?: 'v1' | 'v2'
|
||||||
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate'
|
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate' | 'iterate'
|
||||||
v2Task?: 'scene' | 'identity' | 'outfit' | 'face_lock' | 'refine' | 't2i'
|
v2Task?: 'scene' | 'identity' | 'outfit' | 'face_lock' | 'refine' | 't2i'
|
||||||
engine?: 'flux' | 'krea'
|
engine?: 'flux' | 'krea'
|
||||||
snofsModel?: number
|
snofsModel?: number
|
||||||
@@ -5445,7 +5504,7 @@ async function restoreStudioEditJob(payload: {
|
|||||||
if (v2) {
|
if (v2) {
|
||||||
studioMode.value = 'editv2'
|
studioMode.value = 'editv2'
|
||||||
outputFocus.value = 'edit'
|
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
|
? payload.v2Mode
|
||||||
: 'edit'
|
: 'edit'
|
||||||
setV2Mode(mode)
|
setV2Mode(mode)
|
||||||
@@ -5552,7 +5611,7 @@ async function restoreStudioPayload(kind: string, payload: {
|
|||||||
shotLoraStacks?: LoraStackItem[][]
|
shotLoraStacks?: LoraStackItem[][]
|
||||||
negative?: string
|
negative?: string
|
||||||
imagePipeline?: 'v1' | 'v2'
|
imagePipeline?: 'v1' | 'v2'
|
||||||
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate'
|
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate' | 'iterate'
|
||||||
v2Task?: 'scene' | 'identity' | 'outfit' | 'face_lock' | 'refine' | 't2i'
|
v2Task?: 'scene' | 'identity' | 'outfit' | 'face_lock' | 'refine' | 't2i'
|
||||||
engine?: 'flux' | 'krea'
|
engine?: 'flux' | 'krea'
|
||||||
snofsModel?: number
|
snofsModel?: number
|
||||||
@@ -6332,7 +6391,7 @@ function applyStillSettings(still: LibraryStill) {
|
|||||||
if (dest === 'imagev2') {
|
if (dest === 'imagev2') {
|
||||||
studioMode.value = 'editv2'
|
studioMode.value = 'editv2'
|
||||||
outputFocus.value = 'edit'
|
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
|
? settings.mode
|
||||||
: 'edit'
|
: 'edit'
|
||||||
setV2Mode(mode)
|
setV2Mode(mode)
|
||||||
@@ -6392,6 +6451,7 @@ function stillUseAsInputMessage(still: LibraryStill) {
|
|||||||
const dest = inferStillPipeline(still)
|
const dest = inferStillPipeline(still)
|
||||||
const mode = still.settings?.mode
|
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 === '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') {
|
if (dest === 'imagev2' && mode === 'compose') {
|
||||||
return still.settings?.referenceStillId
|
return still.settings?.referenceStillId
|
||||||
? 'Compose settings loaded with stills A and B.'
|
? 'Compose settings loaded with stills A and B.'
|
||||||
@@ -6976,6 +7036,7 @@ function absorbPastedImageBatch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function imagePassUnit() {
|
function imagePassUnit() {
|
||||||
|
if (v2Mode.value === 'iterate') return 'Iteration'
|
||||||
return imagePassMode.value === 'batch' ? 'Batch' : 'Pass'
|
return imagePassMode.value === 'batch' ? 'Batch' : 'Pass'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7102,8 +7163,12 @@ function setV2Mode(mode: ImageV2Mode) {
|
|||||||
if (mode === 'generate') {
|
if (mode === 'generate') {
|
||||||
v2Task.value = 't2i'
|
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 === '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) {
|
function setV2Engine(engine: ImageV2Engine) {
|
||||||
@@ -7228,10 +7293,17 @@ async function editImageV2() {
|
|||||||
await ensureGenerationNotifyPermission()
|
await ensureGenerationNotifyPermission()
|
||||||
const body = new FormData()
|
const body = new FormData()
|
||||||
body.append('mode', v2Mode.value)
|
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 !== 'generate' && file.value) body.append('image_a', file.value)
|
||||||
if (v2Mode.value === 'compose' && editRefFile.value) body.append('image_b', editRefFile.value)
|
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')) {
|
if (v2Mode.value === 'refine' || (v2Engine.value === 'krea' && v2Mode.value !== 'generate' && !iterateGenerate)) {
|
||||||
if (v2Mode.value === 'refine') {
|
if (v2Mode.value === 'refine') {
|
||||||
const maskBlob = await refinePainter.value?.exportPng()
|
const maskBlob = await refinePainter.value?.exportPng()
|
||||||
if (!maskBlob) {
|
if (!maskBlob) {
|
||||||
@@ -7242,7 +7314,7 @@ async function editImageV2() {
|
|||||||
}
|
}
|
||||||
body.append('strength', String(clampImageV2Denoise(v2Denoise.value)))
|
body.append('strength', String(clampImageV2Denoise(v2Denoise.value)))
|
||||||
}
|
}
|
||||||
if (v2Mode.value === 'generate') {
|
if (v2Mode.value === 'generate' || iterateGenerate) {
|
||||||
const size = generateSizeForAspect(aspect.value)
|
const size = generateSizeForAspect(aspect.value)
|
||||||
body.append('width', String(size.width))
|
body.append('width', String(size.width))
|
||||||
body.append('height', String(size.height))
|
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('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('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)))
|
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))
|
body.append('scaleToTotalPixels', String(v2ScaleOn.value))
|
||||||
if (v2ScaleOn.value) body.append('megapixels', String(clampImageScaleMegapixels(v2Megapixels.value, 1)))
|
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('hideInput', String(showPrivacyToggles.value && hideInputPreview.value))
|
||||||
body.append('hideThumbnail', String(showPrivacyToggles.value && hideThumbnail.value))
|
body.append('hideThumbnail', String(showPrivacyToggles.value && hideThumbnail.value))
|
||||||
body.append('name', clipName.value.trim())
|
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))
|
if (extraPasses.length) body.append('passes', JSON.stringify(extraPasses))
|
||||||
body.append('passMode', imagePassMode.value)
|
body.append('passMode', iterateGenerate ? 'batch' : imagePassMode.value)
|
||||||
const v2Stack = filterLoraStackForImageEngine(
|
const v2Stack = filterLoraStackForImageEngine(
|
||||||
filterLoraStackForStudio(imageV2LoraStack.value, ltxEnabled.value),
|
filterLoraStackForStudio(imageV2LoraStack.value, ltxEnabled.value),
|
||||||
v2Engine.value
|
v2Engine.value
|
||||||
@@ -7301,7 +7373,9 @@ async function editImageV2() {
|
|||||||
if (started.queued) {
|
if (started.queued) {
|
||||||
const n = 1 + extraPasses.length
|
const n = 1 + extraPasses.length
|
||||||
toast(n > 1
|
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 stills from the start image behind the current job.`
|
||||||
: `Queued ${n} Image v2 passes 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.'))
|
: (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` : ''
|
editChainLabel.value = extraPasses.length ? `${imagePassUnit()} 1` : ''
|
||||||
editOverallProgress.value = 0
|
editOverallProgress.value = 0
|
||||||
editCompletedChainStep.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)
|
editActiveChainPlan.value = imagePassPlan(prompt.value.trim(), extraPasses, passNoun)
|
||||||
startTimer('edit')
|
startTimer('edit')
|
||||||
editDownloadName.value = downloadName
|
editDownloadName.value = downloadName
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const mode = parseImageV2Mode(fields.mode)
|
const mode = parseImageV2Mode(fields.mode)
|
||||||
if (!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(
|
const task = parseImageV2Task(
|
||||||
fields.task,
|
fields.task,
|
||||||
@@ -145,13 +145,19 @@ export default defineEventHandler(async (event) => {
|
|||||||
const extraPasses = parsePasses(fields.passes)
|
const extraPasses = parsePasses(fields.passes)
|
||||||
const passMode = parseImagePassMode(fields.passMode)
|
const passMode = parseImagePassMode(fields.passMode)
|
||||||
const aspect = parseImageAspect(fields.aspect)
|
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' })
|
throw createError({ statusCode: 400, statusMessage: 'A prompt is required' })
|
||||||
}
|
}
|
||||||
if (extraPasses.length && (mode === 'refine' || mode === 'generate')) {
|
if (extraPasses.length && (mode === 'refine' || mode === 'generate')) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
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)) {
|
if (extraPasses.some(item => !item.prompt)) {
|
||||||
@@ -172,13 +178,24 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ownerKey = libraryOwnerKey(event)
|
const ownerKey = libraryOwnerKey(event)
|
||||||
const imageA = mode === 'generate' ? null : await resolveImageRef(ownerKey, fields.image_a, uploadedA)
|
const imageA = mode === 'generate'
|
||||||
const imageB = mode === 'refine' || mode === 'generate' ? null : await resolveImageRef(ownerKey, fields.image_b, uploadedB)
|
? null
|
||||||
const mask = mode === 'refine' ? await resolveImageRef(ownerKey, fields.mask, uploadedMask) : 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' && !imageA) {
|
if (mode !== 'generate' && mode !== 'iterate' && !imageA) {
|
||||||
throw createError({ statusCode: 400, statusMessage: 'image_a is required' })
|
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) {
|
if (mode === 'refine' && !mask) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
@@ -220,7 +237,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const cfg = engine === 'krea'
|
const cfg = engine === 'krea'
|
||||||
? clampImageCfg(fields.cfg, IMAGE_V2_KREA_CFG)
|
? clampImageCfg(fields.cfg, IMAGE_V2_KREA_CFG)
|
||||||
: turbo ? IMAGE_V2_TURBO_CFG : clampImageCfg(fields.cfg, IMAGE_V2_CFG_DEFAULT)
|
: turbo ? IMAGE_V2_TURBO_CFG : clampImageCfg(fields.cfg, IMAGE_V2_CFG_DEFAULT)
|
||||||
const megapixels = mode === 'generate'
|
const megapixels = graphMode === 'generate'
|
||||||
? 0
|
? 0
|
||||||
: fields.scaleToTotalPixels == null
|
: fields.scaleToTotalPixels == null
|
||||||
? clampImageScaleMegapixels(fields.megapixels ?? fields.scaleMegapixels, 1)
|
? clampImageScaleMegapixels(fields.megapixels ?? fields.scaleMegapixels, 1)
|
||||||
@@ -230,31 +247,31 @@ export default defineEventHandler(async (event) => {
|
|||||||
const seed = fields.seed && String(fields.seed) !== 'random'
|
const seed = fields.seed && String(fields.seed) !== 'random'
|
||||||
? Number(fields.seed)
|
? Number(fields.seed)
|
||||||
: Math.floor(Math.random() * 2_147_483_647)
|
: Math.floor(Math.random() * 2_147_483_647)
|
||||||
const generateSize = mode === 'generate'
|
const generateSize = graphMode === 'generate'
|
||||||
? generateSizeForAspect(aspect)
|
? generateSizeForAspect(aspect)
|
||||||
: clampImageV2Size(fields.width, fields.height, fields.aspect)
|
: clampImageV2Size(fields.width, fields.height, fields.aspect)
|
||||||
const size = mode === 'generate'
|
const size = graphMode === 'generate'
|
||||||
? { width: generateSize.width, height: generateSize.height }
|
? { width: generateSize.width, height: generateSize.height }
|
||||||
: imageDimensions(imageA!.data)
|
: imageDimensions(imageA!.data)
|
||||||
const v2Mode = mode as ImageV2Mode
|
const v2Mode = mode as ImageV2Mode
|
||||||
const v2Task = (
|
const v2Task = (
|
||||||
mode === 'generate' ? 't2i' : mode === 'refine' ? 'refine' : mode === 'edit' ? 'scene' : task
|
graphMode === 'generate' ? 't2i' : graphMode === 'refine' ? 'refine' : graphMode === 'edit' ? 'scene' : task
|
||||||
) as ImageV2Task
|
) as ImageV2Task
|
||||||
const refineStrength = mode === 'generate'
|
const refineStrength = graphMode === 'generate'
|
||||||
? undefined
|
? undefined
|
||||||
: engine === 'krea' || mode === 'refine'
|
: engine === 'krea' || graphMode === 'refine'
|
||||||
? clampImageV2Denoise(
|
? clampImageV2Denoise(
|
||||||
fields.strength,
|
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
|
: undefined
|
||||||
const generateConsistencyModel = clampImageV2Strength(
|
const generateConsistencyModel = clampImageV2Strength(
|
||||||
fields.consistency_model,
|
fields.consistency_model,
|
||||||
mode === 'generate' || engine === 'krea' ? 0 : IMAGE_V2_CONSISTENCY_MODEL
|
graphMode === 'generate' || engine === 'krea' ? 0 : IMAGE_V2_CONSISTENCY_MODEL
|
||||||
)
|
)
|
||||||
const generateConsistencyClip = clampImageV2Strength(
|
const generateConsistencyClip = clampImageV2Strength(
|
||||||
fields.consistency_clip,
|
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 conceptAllowed = isXaigenStudio()
|
||||||
const generateSnofsFallback = 0
|
const generateSnofsFallback = 0
|
||||||
@@ -325,7 +342,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
promptPost: promptPost || undefined,
|
promptPost: promptPost || undefined,
|
||||||
name: clipName,
|
name: clipName,
|
||||||
folderId,
|
folderId,
|
||||||
aspect: mode === 'generate' ? generateSize.aspect : aspect,
|
aspect: graphMode === 'generate' ? generateSize.aspect : aspect,
|
||||||
width: size?.width || 0,
|
width: size?.width || 0,
|
||||||
height: size?.height || 0,
|
height: size?.height || 0,
|
||||||
steps,
|
steps,
|
||||||
@@ -349,10 +366,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
queueAutoRun: false,
|
queueAutoRun: false,
|
||||||
negative: String(fields.negative || '').trim(),
|
negative: String(fields.negative || '').trim(),
|
||||||
passes: extraPasses,
|
passes: extraPasses,
|
||||||
passMode,
|
passMode: mode === 'iterate' && graphMode === 'generate' ? 'batch' : passMode,
|
||||||
referenceStillId: savedRef?.id,
|
referenceStillId: savedRef?.id,
|
||||||
referenceStillFilename: savedRef?.filename,
|
referenceStillFilename: savedRef?.filename,
|
||||||
scaleToTotalPixels: mode !== 'generate' && megapixels > 0,
|
scaleToTotalPixels: graphMode !== 'generate' && megapixels > 0,
|
||||||
scaleMegapixels: megapixels,
|
scaleMegapixels: megapixels,
|
||||||
imagePipeline: 'v2',
|
imagePipeline: 'v2',
|
||||||
v2Mode,
|
v2Mode,
|
||||||
@@ -386,7 +403,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
mode: v2Mode,
|
mode: v2Mode,
|
||||||
task: v2Task,
|
task: v2Task,
|
||||||
engine,
|
engine,
|
||||||
workflow: imageV2WorkflowFile(v2Mode, engine),
|
workflow: imageV2WorkflowFile(v2Mode, engine, { hasImage: Boolean(imageA), hasReference: Boolean(imageB) }),
|
||||||
width: size?.width || 0,
|
width: size?.width || 0,
|
||||||
height: size?.height || 0,
|
height: size?.height || 0,
|
||||||
strength: refineStrength,
|
strength: refineStrength,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { comfyJobPrefix } from '~/utils/outputNames'
|
|||||||
import { restorePromptParts, wrappedPromptForComfy } from '~/utils/promptParts'
|
import { restorePromptParts, wrappedPromptForComfy } from '~/utils/promptParts'
|
||||||
import { isXaigenStudio } from '~/utils/videoModels'
|
import { isXaigenStudio } from '~/utils/videoModels'
|
||||||
import {
|
import {
|
||||||
|
imageV2GraphMode,
|
||||||
imageV2StackSpecials,
|
imageV2StackSpecials,
|
||||||
mergeLegacyImageV2Loras,
|
mergeLegacyImageV2Loras,
|
||||||
parseImageV2Engine,
|
parseImageV2Engine,
|
||||||
@@ -52,20 +53,24 @@ export type EditV2RunParams = {
|
|||||||
export async function runEditV2(job: Job, params: EditV2RunParams) {
|
export async function runEditV2(job: Job, params: EditV2RunParams) {
|
||||||
const library = job.library
|
const library = job.library
|
||||||
if (!library) throw new Error('Edit job is missing library metadata')
|
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.')
|
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.')
|
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.')
|
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.')
|
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
|
const extraPasses = canChain
|
||||||
? (params.passes || []).map(item => ({ prompt: String(item.prompt || '').trim() })).filter(item => item.prompt)
|
? (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)
|
...extraPasses.map(item => item.prompt)
|
||||||
]
|
]
|
||||||
const chainTotal = prompts.length
|
const chainTotal = prompts.length
|
||||||
const batch = params.passMode !== 'chain'
|
// Text-only iterate is always independent generations. With a still, honor batch vs chain.
|
||||||
const unit = batch ? 'Batch' : 'Pass'
|
const batch = graphMode === 'generate' || params.passMode !== 'chain'
|
||||||
|
const unit = params.mode === 'iterate' ? 'Iteration' : (batch ? 'Batch' : 'Pass')
|
||||||
library.chainTotal = chainTotal
|
library.chainTotal = chainTotal
|
||||||
library.familyId = chainTotal > 1 ? (library.familyId || crypto.randomUUID()) : library.familyId
|
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()
|
job.imageComfyHost = getComfyHost()
|
||||||
|
|
||||||
await withImageComfyHost(job.imageComfyHost, async () => {
|
await withImageComfyHost(job.imageComfyHost, async () => {
|
||||||
const generate = params.mode === 'generate'
|
const generate = graphMode === 'generate'
|
||||||
const engine = parseImageV2Engine(params.engine, 'flux')
|
const engine = parseImageV2Engine(params.engine, 'flux')
|
||||||
const engineLabel = engine === 'krea' ? 'Krea' : 'Flux'
|
const engineLabel = engine === 'krea' ? 'Krea' : 'Flux'
|
||||||
const { applyImageFill } = await import('~/server/utils/imagePad')
|
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 krea = engine === 'krea' ? await resolveKreaGenerateAssets() : null
|
||||||
const appliedStack = mergeLegacyImageV2Loras(params.loraStack?.length ? params.loraStack : library.loraStack, {
|
const appliedStack = mergeLegacyImageV2Loras(params.loraStack?.length ? params.loraStack : library.loraStack, {
|
||||||
engine,
|
engine,
|
||||||
mode: params.mode,
|
mode: graphMode,
|
||||||
xaigen: isXaigenStudio(),
|
xaigen: isXaigenStudio(),
|
||||||
snofsModel: params.snofsModel,
|
snofsModel: params.snofsModel,
|
||||||
snofsClip: params.snofsClip,
|
snofsClip: params.snofsClip,
|
||||||
@@ -127,7 +133,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
|||||||
const consistencyModel = appliedSpecials.consistency?.strengthModel ?? params.consistencyModel
|
const consistencyModel = appliedSpecials.consistency?.strengthModel ?? params.consistencyModel
|
||||||
const consistencyClip = appliedSpecials.consistency?.strengthClip ?? params.consistencyClip
|
const consistencyClip = appliedSpecials.consistency?.strengthClip ?? params.consistencyClip
|
||||||
|
|
||||||
if (!generate && params.mode === 'compose' && params.reference) {
|
if (!generate && graphMode === 'compose' && params.reference) {
|
||||||
uploadedRef = await uploadImage({
|
uploadedRef = await uploadImage({
|
||||||
...params.reference,
|
...params.reference,
|
||||||
filename: `ref_${params.reference.filename || 'image_b.png'}`
|
filename: `ref_${params.reference.filename || 'image_b.png'}`
|
||||||
@@ -163,17 +169,17 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
|||||||
emitChainJob(job, {
|
emitChainJob(job, {
|
||||||
type: 'status',
|
type: 'status',
|
||||||
message: generate
|
message: generate
|
||||||
? `Queueing ${engineLabel} generate on Beast...`
|
? `Queueing ${engineLabel} iterate generate on Beast...`
|
||||||
: params.mode === 'refine'
|
: graphMode === 'refine'
|
||||||
? 'Uploading canvas and mask to Beast...'
|
? '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
|
progress: generate ? 12 : 8
|
||||||
})
|
})
|
||||||
|
|
||||||
if (index === 0 && !generate && image) {
|
if (index === 0 && !generate && image) {
|
||||||
const filled = await applyImageFill({
|
const filled = await applyImageFill({
|
||||||
image,
|
image,
|
||||||
mask: params.mode === 'refine' ? mask : null,
|
mask: graphMode === 'refine' ? mask : null,
|
||||||
aspect: params.aspect || 'auto'
|
aspect: params.aspect || 'auto'
|
||||||
})
|
})
|
||||||
image = filled.image
|
image = filled.image
|
||||||
@@ -199,7 +205,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
|||||||
prompt = composeFillPrompt(prompt, true)
|
prompt = composeFillPrompt(prompt, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index === 0 && params.mode === 'refine' && mask && !uploadedMask) {
|
if (index === 0 && graphMode === 'refine' && mask && !uploadedMask) {
|
||||||
uploadedMask = await uploadImage({
|
uploadedMask = await uploadImage({
|
||||||
...mask,
|
...mask,
|
||||||
filename: `mask_${mask.filename || 'refine-mask.png'}`
|
filename: `mask_${mask.filename || 'refine-mask.png'}`
|
||||||
@@ -212,9 +218,11 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
|||||||
if (!generate) {
|
if (!generate) {
|
||||||
emitChainJob(job, {
|
emitChainJob(job, {
|
||||||
type: 'status',
|
type: 'status',
|
||||||
message: params.mode === 'refine'
|
message: params.mode === 'iterate'
|
||||||
|
? `Queueing ${engineLabel} iterate (${graphMode}) on Beast...`
|
||||||
|
: graphMode === 'refine'
|
||||||
? `Queueing ${engineLabel} v2 refine on Beast...`
|
? `Queueing ${engineLabel} v2 refine on Beast...`
|
||||||
: params.mode === 'compose'
|
: graphMode === 'compose'
|
||||||
? `Queueing ${engineLabel} v2 compose (${params.task}) on Beast...`
|
? `Queueing ${engineLabel} v2 compose (${params.task}) on Beast...`
|
||||||
: `Queueing ${engineLabel} v2 edit on Beast...`,
|
: `Queueing ${engineLabel} v2 edit on Beast...`,
|
||||||
progress: 12
|
progress: 12
|
||||||
@@ -222,9 +230,9 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const built = buildImageV2Workflow({
|
const built = buildImageV2Workflow({
|
||||||
mode: params.mode,
|
mode: graphMode,
|
||||||
engine,
|
engine,
|
||||||
task: params.task,
|
task: generate ? 't2i' : params.task,
|
||||||
prompt: wrappedPromptForComfy(library, prompt, index === 0 ? params.prompt : prompt),
|
prompt: wrappedPromptForComfy(library, prompt, index === 0 ? params.prompt : prompt),
|
||||||
negative: params.negative,
|
negative: params.negative,
|
||||||
imageAName: uploaded?.name,
|
imageAName: uploaded?.name,
|
||||||
@@ -305,7 +313,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
|||||||
kind: 'imagev2',
|
kind: 'imagev2',
|
||||||
mode: params.mode,
|
mode: params.mode,
|
||||||
engine,
|
engine,
|
||||||
task: params.task,
|
task: generate ? 't2i' : params.task,
|
||||||
negative: params.negative,
|
negative: params.negative,
|
||||||
snofsModel,
|
snofsModel,
|
||||||
snofsClip,
|
snofsClip,
|
||||||
@@ -313,12 +321,12 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
|||||||
consistencyClip,
|
consistencyClip,
|
||||||
steps: params.steps,
|
steps: params.steps,
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
megapixels: params.mode === 'generate' || !(params.megapixels > 0) ? undefined : params.megapixels,
|
megapixels: generate || !(params.megapixels > 0) ? undefined : params.megapixels,
|
||||||
scaleToTotalPixels: params.mode === 'generate' ? undefined : params.megapixels > 0,
|
scaleToTotalPixels: generate ? undefined : params.megapixels > 0,
|
||||||
turbo: params.turbo === true,
|
turbo: params.turbo === true,
|
||||||
strength: params.mode === 'generate' ? undefined : params.strength,
|
strength: generate ? undefined : params.strength,
|
||||||
width: params.mode === 'generate' ? params.width : undefined,
|
width: generate ? params.width : undefined,
|
||||||
height: params.mode === 'generate' ? params.height : undefined,
|
height: generate ? params.height : undefined,
|
||||||
aspect: params.aspect || 'auto',
|
aspect: params.aspect || 'auto',
|
||||||
seed,
|
seed,
|
||||||
sourceStillId: params.sourceStillId || library.stillId,
|
sourceStillId: params.sourceStillId || library.stillId,
|
||||||
@@ -362,7 +370,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) {
|
|||||||
emitChainJob(job, {
|
emitChainJob(job, {
|
||||||
type: 'complete',
|
type: 'complete',
|
||||||
message: chainTotal > 1
|
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`,
|
: `${engineLabel} v2 finished on Beast`,
|
||||||
progress: 100,
|
progress: 100,
|
||||||
stillId: still?.id,
|
stillId: still?.id,
|
||||||
|
|||||||
+27
-20
@@ -58,7 +58,7 @@ export interface StudioJobPayload {
|
|||||||
scaleToTotalPixels?: boolean
|
scaleToTotalPixels?: boolean
|
||||||
scaleMegapixels?: number
|
scaleMegapixels?: number
|
||||||
imagePipeline?: 'v1' | 'v2'
|
imagePipeline?: 'v1' | 'v2'
|
||||||
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate'
|
v2Mode?: 'edit' | 'compose' | 'refine' | 'generate' | 'iterate'
|
||||||
v2Task?: 'scene' | 'identity' | 'outfit' | 'face_lock' | 'refine' | 't2i'
|
v2Task?: 'scene' | 'identity' | 'outfit' | 'face_lock' | 'refine' | 't2i'
|
||||||
engine?: 'flux' | 'krea'
|
engine?: 'flux' | 'krea'
|
||||||
snofsModel?: number
|
snofsModel?: number
|
||||||
@@ -1017,7 +1017,8 @@ async function startStudioEditJob(item: StudioJob) {
|
|||||||
const { runEditV2 } = await import('~/server/utils/imageChainV2')
|
const { runEditV2 } = await import('~/server/utils/imageChainV2')
|
||||||
|
|
||||||
const payload = item.payload
|
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)))) {
|
if (!generate && (!payload.stillId || !existsSync(stillPath(item.ownerKey, payload.stillId)))) {
|
||||||
throw new Error('The input still is missing from the library')
|
throw new Error('The input still is missing from the library')
|
||||||
}
|
}
|
||||||
@@ -1038,30 +1039,36 @@ async function startStudioEditJob(item: StudioJob) {
|
|||||||
: null
|
: null
|
||||||
|
|
||||||
if (payload.imagePipeline === 'v2') {
|
if (payload.imagePipeline === 'v2') {
|
||||||
const mode = payload.v2Mode === 'compose'
|
const mode = payload.v2Mode === 'iterate'
|
||||||
|
? 'iterate'
|
||||||
|
: payload.v2Mode === 'compose'
|
||||||
? 'compose'
|
? 'compose'
|
||||||
: payload.v2Mode === 'refine'
|
: payload.v2Mode === 'refine'
|
||||||
? 'refine'
|
? 'refine'
|
||||||
: payload.v2Mode === 'generate'
|
: payload.v2Mode === 'generate'
|
||||||
? 'generate'
|
? 'generate'
|
||||||
: 'edit'
|
: 'edit'
|
||||||
|
const graphMode = mode === 'iterate'
|
||||||
|
? (image ? (reference ? 'compose' : 'edit') : 'generate')
|
||||||
|
: mode
|
||||||
const maskId = payload.maskStillId
|
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',
|
filename: payload.maskStillFilename || 'refine-mask.png',
|
||||||
data: readFileSync(stillPath(item.ownerKey, maskId)),
|
data: readFileSync(stillPath(item.ownerKey, maskId)),
|
||||||
type: 'image/png'
|
type: 'image/png'
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
if (mode === 'refine' && !mask) {
|
if (graphMode === 'refine' && !mask) {
|
||||||
throw new Error('Refine requires a mask. Refusing to fall back to Edit.')
|
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.')
|
throw new Error('Compose requires image B. Refusing to fall back to one-image generation.')
|
||||||
}
|
}
|
||||||
if (mode === 'edit' && reference) {
|
if (mode === 'edit' && reference) {
|
||||||
throw new Error('Edit mode takes one image. Use Compose for two stills.')
|
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({
|
live = createEditLiveJob({
|
||||||
steps: payload.steps,
|
steps: payload.steps,
|
||||||
hideThumbnail: payload.hideThumbnail,
|
hideThumbnail: payload.hideThumbnail,
|
||||||
@@ -1088,9 +1095,9 @@ async function startStudioEditJob(item: StudioJob) {
|
|||||||
familyId: item.familyId,
|
familyId: item.familyId,
|
||||||
chainIndex: 0,
|
chainIndex: 0,
|
||||||
chainStep: 1,
|
chainStep: 1,
|
||||||
chainTotal: 1 + ((mode === 'edit' || mode === 'compose') ? (payload.passes?.length || 0) : 0),
|
chainTotal: 1 + passCount,
|
||||||
chainLabel: (mode === 'edit' || mode === 'compose') && payload.passes?.length
|
chainLabel: passCount
|
||||||
? (payload.passMode === 'chain' ? 'Pass 1' : 'Batch 1')
|
? (mode === 'iterate' ? 'Iteration 1' : (payload.passMode === 'chain' ? 'Pass 1' : 'Batch 1'))
|
||||||
: undefined,
|
: undefined,
|
||||||
passes: payload.passes,
|
passes: payload.passes,
|
||||||
...persistLoraFields(payload.loraStack || payload.loraName)
|
...persistLoraFields(payload.loraStack || payload.loraName)
|
||||||
@@ -1102,30 +1109,30 @@ async function startStudioEditJob(item: StudioJob) {
|
|||||||
void runEditV2(live, {
|
void runEditV2(live, {
|
||||||
mode,
|
mode,
|
||||||
engine: v2Engine,
|
engine: v2Engine,
|
||||||
task: payload.v2Task || 'scene',
|
task: payload.v2Task || (graphMode === 'generate' ? 't2i' : 'scene'),
|
||||||
image: mode === 'generate' ? null : image,
|
image: graphMode === 'generate' ? null : image,
|
||||||
reference: mode === 'refine' || mode === 'generate' ? null : reference,
|
reference: graphMode === 'compose' ? reference : null,
|
||||||
mask,
|
mask,
|
||||||
prompt: payload.prompt,
|
prompt: payload.prompt,
|
||||||
passes: payload.passes || [],
|
|
||||||
passMode: payload.passMode === 'chain' ? 'chain' : 'batch',
|
|
||||||
negative: payload.negative || '',
|
negative: payload.negative || '',
|
||||||
steps: payload.steps,
|
steps: payload.steps,
|
||||||
seed: live.library?.seed || payload.seed,
|
seed: payload.seed || Math.floor(Math.random() * 2_147_483_647),
|
||||||
cfg: payload.cfg,
|
cfg: payload.cfg,
|
||||||
snofsModel: v2Specials.snofs?.strengthModel ?? payload.snofsModel ?? 0,
|
snofsModel: v2Specials.snofs?.strengthModel ?? payload.snofsModel ?? 0,
|
||||||
snofsClip: v2Specials.snofs?.strengthClip ?? payload.snofsClip ?? 0,
|
snofsClip: v2Specials.snofs?.strengthClip ?? payload.snofsClip ?? 0,
|
||||||
consistencyModel: v2Specials.consistency?.strengthModel ?? payload.consistencyModel ?? (mode === 'generate' || payload.engine === 'krea' ? 0 : 0.7),
|
consistencyModel: v2Specials.consistency?.strengthModel ?? payload.consistencyModel ?? 0,
|
||||||
consistencyClip: v2Specials.consistency?.strengthClip ?? payload.consistencyClip ?? (mode === 'generate' || payload.engine === 'krea' ? 0 : 0.7),
|
consistencyClip: v2Specials.consistency?.strengthClip ?? payload.consistencyClip ?? 0,
|
||||||
megapixels: payload.scaleToTotalPixels === false ? 0 : (payload.scaleMegapixels ?? 1),
|
megapixels: payload.scaleMegapixels || 0,
|
||||||
strength: payload.refineStrength,
|
strength: payload.refineStrength,
|
||||||
aspect: payload.aspect || 'auto',
|
|
||||||
width: payload.width,
|
width: payload.width,
|
||||||
height: payload.height,
|
height: payload.height,
|
||||||
turbo: payload.turbo === true,
|
turbo: payload.turbo === true,
|
||||||
|
aspect: payload.aspect,
|
||||||
sourceStillId: payload.stillId,
|
sourceStillId: payload.stillId,
|
||||||
referenceStillId: payload.referenceStillId,
|
referenceStillId: payload.referenceStillId,
|
||||||
loraStack: payload.loraStack
|
loraStack: payload.loraStack,
|
||||||
|
passes: payload.passes,
|
||||||
|
passMode: payload.passMode === 'chain' ? 'chain' : 'batch',
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
if (live && live.status !== 'error' && live.status !== 'cancelled' && live.status !== 'deferred') {
|
if (live && live.status !== 'error' && live.status !== 'cancelled' && live.status !== 'deferred') {
|
||||||
|
|||||||
+25
-10
@@ -6,13 +6,26 @@ import {
|
|||||||
type LoraStackItem
|
type LoraStackItem
|
||||||
} from '~/utils/loras'
|
} 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_TASKS = ['scene', 'identity', 'outfit', 'face_lock', 'refine', 't2i'] as const
|
||||||
export const IMAGE_V2_ENGINES = ['flux', 'krea'] as const
|
export const IMAGE_V2_ENGINES = ['flux', 'krea'] as const
|
||||||
|
|
||||||
export type ImageV2Mode = (typeof IMAGE_V2_MODES)[number]
|
export type ImageV2Mode = (typeof IMAGE_V2_MODES)[number]
|
||||||
export type ImageV2Task = (typeof IMAGE_V2_TASKS)[number]
|
export type ImageV2Task = (typeof IMAGE_V2_TASKS)[number]
|
||||||
export type ImageV2Engine = (typeof IMAGE_V2_ENGINES)[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_STEPS_DEFAULT = 24
|
||||||
export const IMAGE_V2_CFG_DEFAULT = 4
|
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
|
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 (engine === 'krea') {
|
||||||
if (mode === 'generate') return 'krea_v2_generate.json'
|
if (graph === 'generate') return 'krea_v2_generate.json'
|
||||||
if (mode === 'refine') return 'krea_v2_refine.json'
|
if (graph === 'refine') return 'krea_v2_refine.json'
|
||||||
if (mode === 'compose') return 'krea_v2_compose.json'
|
if (graph === 'compose') return 'krea_v2_compose.json'
|
||||||
return 'krea_v2_edit.json'
|
return 'krea_v2_edit.json'
|
||||||
}
|
}
|
||||||
if (mode === 'generate') return 'klein_v2_generate.json'
|
if (graph === 'generate') return 'klein_v2_generate.json'
|
||||||
if (mode === 'refine') return 'klein_v2_refine.json'
|
if (graph === 'refine') return 'klein_v2_refine.json'
|
||||||
if (mode === 'compose') return 'klein_v2_compose.json'
|
if (graph === 'compose') return 'klein_v2_compose.json'
|
||||||
return 'klein_v2_edit.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()
|
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'>]
|
const header = IMAGE_V2_ROLE_HEADERS[task as Exclude<ImageV2Task, 'scene' | 'refine' | 't2i'>]
|
||||||
return header ? `${header}\n\n${body}` : body
|
return header ? `${header}\n\n${body}` : body
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ function stillDisplayLoraStack(settings: StillGenerationSettings) {
|
|||||||
export function normalizeStillSettings(raw: unknown): StillGenerationSettings | undefined {
|
export function normalizeStillSettings(raw: unknown): StillGenerationSettings | undefined {
|
||||||
if (!raw || typeof raw !== 'object') return undefined
|
if (!raw || typeof raw !== 'object') return undefined
|
||||||
const value = raw as Record<string, unknown>
|
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
|
? value.mode
|
||||||
: undefined
|
: undefined
|
||||||
const engine = value.engine === 'krea' || value.engine === 'flux' ? value.engine : 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) {
|
if (settings?.kind === 'imagev2' || settings?.mode) {
|
||||||
const mode = settings.mode === 'generate'
|
const mode = settings.mode === 'generate'
|
||||||
? 'Generate'
|
? 'Generate'
|
||||||
|
: settings.mode === 'iterate'
|
||||||
|
? 'Iterate'
|
||||||
: settings.mode === 'compose'
|
: settings.mode === 'compose'
|
||||||
? 'Compose'
|
? 'Compose'
|
||||||
: settings.mode === 'refine'
|
: settings.mode === 'refine'
|
||||||
|
|||||||
Reference in New Issue
Block a user