Add Image Iterate: nested multi-prompt still families.

Iterate runs a series of prompts as generate (no still), edit (still A), or compose (A+B), saves each output under one library collection like video extensions, and defaults to iterating from the original still.
This commit is contained in:
2026-09-04 22:46:19 -05:00
parent 279ac72049
commit 2435167a41
6 changed files with 261 additions and 130 deletions
+25 -10
View File
@@ -6,13 +6,26 @@ import {
type LoraStackItem
} from '~/utils/loras'
export const IMAGE_V2_MODES = ['edit', 'compose', 'refine', 'generate'] as const
export const IMAGE_V2_MODES = ['edit', 'compose', 'refine', 'generate', 'iterate'] as const
export const IMAGE_V2_TASKS = ['scene', 'identity', 'outfit', 'face_lock', 'refine', 't2i'] as const
export const IMAGE_V2_ENGINES = ['flux', 'krea'] as const
export type ImageV2Mode = (typeof IMAGE_V2_MODES)[number]
export type ImageV2Task = (typeof IMAGE_V2_TASKS)[number]
export type ImageV2Engine = (typeof IMAGE_V2_ENGINES)[number]
export type ImageV2GraphMode = Exclude<ImageV2Mode, 'iterate'>
/** Iterate picks a real graph from the attached stills: none → generate, A → edit, A+B → compose. */
export function iterateGraphMode(opts: { hasImage?: boolean; hasReference?: boolean }): Exclude<ImageV2GraphMode, 'refine'> {
if (!opts.hasImage) return 'generate'
if (opts.hasReference) return 'compose'
return 'edit'
}
export function imageV2GraphMode(mode: ImageV2Mode, opts: { hasImage?: boolean; hasReference?: boolean } = {}): ImageV2GraphMode {
if (mode === 'iterate') return iterateGraphMode(opts)
return mode
}
export const IMAGE_V2_STEPS_DEFAULT = 24
export const IMAGE_V2_CFG_DEFAULT = 4
@@ -97,16 +110,17 @@ export function parseImageV2Engine(raw: unknown, fallback: ImageV2Engine = 'flux
return IMAGE_V2_ENGINES.includes(value as ImageV2Engine) ? value as ImageV2Engine : fallback
}
export function imageV2WorkflowFile(mode: ImageV2Mode, engine: ImageV2Engine = 'flux') {
export function imageV2WorkflowFile(mode: ImageV2Mode, engine: ImageV2Engine = 'flux', opts: { hasImage?: boolean; hasReference?: boolean } = {}) {
const graph = imageV2GraphMode(mode, opts)
if (engine === 'krea') {
if (mode === 'generate') return 'krea_v2_generate.json'
if (mode === 'refine') return 'krea_v2_refine.json'
if (mode === 'compose') return 'krea_v2_compose.json'
if (graph === 'generate') return 'krea_v2_generate.json'
if (graph === 'refine') return 'krea_v2_refine.json'
if (graph === 'compose') return 'krea_v2_compose.json'
return 'krea_v2_edit.json'
}
if (mode === 'generate') return 'klein_v2_generate.json'
if (mode === 'refine') return 'klein_v2_refine.json'
if (mode === 'compose') return 'klein_v2_compose.json'
if (graph === 'generate') return 'klein_v2_generate.json'
if (graph === 'refine') return 'klein_v2_refine.json'
if (graph === 'compose') return 'klein_v2_compose.json'
return 'klein_v2_edit.json'
}
@@ -150,9 +164,10 @@ export function clampImageV2Size(width: unknown, height: unknown, aspect?: unkno
}
}
export function composeImageV2Prompt(mode: ImageV2Mode, task: ImageV2Task, prompt: string) {
export function composeImageV2Prompt(mode: ImageV2Mode, task: ImageV2Task, prompt: string, opts: { hasImage?: boolean; hasReference?: boolean } = {}) {
const body = String(prompt || '').trim()
if (mode === 'generate' || mode === 'refine' || mode !== 'compose' || task === 'scene' || task === 't2i') return body
const graph = imageV2GraphMode(mode, opts)
if (graph === 'generate' || graph === 'refine' || graph !== 'compose' || task === 'scene' || task === 't2i') return body
const header = IMAGE_V2_ROLE_HEADERS[task as Exclude<ImageV2Task, 'scene' | 'refine' | 't2i'>]
return header ? `${header}\n\n${body}` : body
}
+8 -6
View File
@@ -92,7 +92,7 @@ function stillDisplayLoraStack(settings: StillGenerationSettings) {
export function normalizeStillSettings(raw: unknown): StillGenerationSettings | undefined {
if (!raw || typeof raw !== 'object') return undefined
const value = raw as Record<string, unknown>
const mode = value.mode === 'compose' || value.mode === 'refine' || value.mode === 'generate' || value.mode === 'edit'
const mode = value.mode === 'compose' || value.mode === 'refine' || value.mode === 'generate' || value.mode === 'edit' || value.mode === 'iterate'
? value.mode
: undefined
const engine = value.engine === 'krea' || value.engine === 'flux' ? value.engine : undefined
@@ -155,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(' · ')