Add per-image iteration settings and steps/CFG sweeps
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import type { LoraStackItem } from './loras'
|
||||
|
||||
/** An omitted field inherits the shared settings; an empty LoRA stack means none. */
|
||||
export interface ImageIteration {
|
||||
prompt: string
|
||||
steps?: number
|
||||
cfg?: number
|
||||
seed?: number | 'same' | 'random'
|
||||
strength?: number
|
||||
loraStack?: LoraStackItem[]
|
||||
}
|
||||
|
||||
export const MAX_IMAGE_ITERATIONS = 50
|
||||
|
||||
function optionalNumber(raw: unknown, label: string, min: number, max: number, integer = false) {
|
||||
if (raw == null || raw === '') return undefined
|
||||
const value = Number(raw)
|
||||
if (!Number.isFinite(value) || value < min || value > max || (integer && !Number.isInteger(value))) {
|
||||
throw new Error(`${label} must be ${integer ? 'a whole number ' : ''}between ${min} and ${max}.`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function normalizeImageIterations(raw: unknown): ImageIteration[] {
|
||||
if (raw == null || raw === '') return []
|
||||
if (typeof raw === 'string') {
|
||||
try { raw = JSON.parse(raw) } catch { throw new Error('Iterations must be a valid JSON list.') }
|
||||
}
|
||||
if (!Array.isArray(raw)) throw new Error('Iterations must be a list.')
|
||||
if (raw.length >= MAX_IMAGE_ITERATIONS) throw new Error(`Use at most ${MAX_IMAGE_ITERATIONS} images including the first.`)
|
||||
return raw.map((item, index) => {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error(`Iteration ${index + 2} is invalid.`)
|
||||
const result: ImageIteration = { prompt: String(item.prompt ?? '').trim().slice(0, 8000) }
|
||||
const steps = optionalNumber(item.steps, 'Steps', 1, 50, true)
|
||||
const cfg = optionalNumber(item.cfg, 'CFG', 0, 20)
|
||||
const strength = optionalNumber(item.strength, 'Denoise', 0.15, 0.75)
|
||||
if (steps !== undefined) result.steps = steps
|
||||
if (cfg !== undefined) result.cfg = Math.round(cfg * 10) / 10
|
||||
if (strength !== undefined) result.strength = Math.round(strength * 100) / 100
|
||||
if (item.seed === 'same' || item.seed === 'random') result.seed = item.seed
|
||||
else {
|
||||
const seed = optionalNumber(item.seed, 'Seed', 0, 2_147_483_647, true)
|
||||
if (seed !== undefined) result.seed = seed
|
||||
}
|
||||
if (item.loraStack != null) {
|
||||
if (!Array.isArray(item.loraStack) || item.loraStack.length > 8) throw new Error('Use at most 8 LoRAs per iteration.')
|
||||
result.loraStack = item.loraStack.map((lora: Record<string, unknown>) => {
|
||||
const name = String(lora?.name ?? '').trim()
|
||||
if (!name) throw new Error('Each iteration LoRA needs a name.')
|
||||
return { name,
|
||||
strengthModel: optionalNumber(lora.strengthModel, 'LoRA model strength', -2, 2) ?? 1,
|
||||
strengthClip: optionalNumber(lora.strengthClip, 'LoRA CLIP strength', -2, 2) ?? 1 }
|
||||
})
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export function resolveImageIteration(base: { steps: number; cfg: number; seed: number; strength?: number }, pass?: ImageIteration, random = Math.random) {
|
||||
return {
|
||||
steps: pass?.steps ?? base.steps,
|
||||
cfg: pass?.cfg ?? base.cfg,
|
||||
strength: pass?.strength ?? base.strength,
|
||||
seed: !pass || pass.seed === 'same' ? base.seed
|
||||
: typeof pass.seed === 'number' ? pass.seed : Math.floor(random() * 2_147_483_647)
|
||||
}
|
||||
}
|
||||
|
||||
/** Ordered Cartesian product: each CFG value is rendered at each requested step count. */
|
||||
export function imageIterationSweep(stepText: string, cfgText: string) {
|
||||
const parse = (text: string) => text.split(/[,\s]+/).filter(Boolean).map(Number)
|
||||
const steps = parse(stepText)
|
||||
const cfgs = parse(cfgText)
|
||||
if (!steps.length || !cfgs.length) throw new Error('Enter at least one steps value and one CFG value.')
|
||||
if (steps.length * cfgs.length > MAX_IMAGE_ITERATIONS) throw new Error(`A sweep can contain at most ${MAX_IMAGE_ITERATIONS} images.`)
|
||||
const rows = cfgs.flatMap(cfg => steps.map(steps => ({ prompt: '', steps, cfg, seed: 'same' as const })))
|
||||
// Validate the first row too; the normal parser reserves one slot for the shared first image.
|
||||
return rows.map(row => normalizeImageIterations([row])[0])
|
||||
}
|
||||
+5
-4
@@ -14,7 +14,7 @@ export const LOAD_GENERATION_PAYLOAD_KEY = 'aigen-load-generation-payload'
|
||||
export type QueuedInspectKind = 'video' | 'edit' | 'music'
|
||||
export type QueuedInspectSource = 'studio' | 'live'
|
||||
|
||||
export type QueuedShotDraft = {
|
||||
export type QueuedShotDraft = import('~/utils/imageIterations').ImageIteration & {
|
||||
prompt: string
|
||||
duration: number
|
||||
loraName?: string
|
||||
@@ -48,7 +48,8 @@ export type QueuedInspectPayload = {
|
||||
loraName?: string
|
||||
loraStack?: LoraStackItem[]
|
||||
negative?: string
|
||||
passes?: { prompt: string }[]
|
||||
passes?: import('~/utils/imageIterations').ImageIteration[]
|
||||
passMode?: 'batch' | 'chain'
|
||||
imagePipeline?: 'v1' | 'v2'
|
||||
v2Mode?: string
|
||||
v2Task?: string
|
||||
@@ -99,7 +100,7 @@ export function queuedJobEditable(job: { source?: string; status?: string }) {
|
||||
export function queuedJobShots(payload: QueuedInspectPayload, kind: string): QueuedShotDraft[] {
|
||||
if (kind === 'edit') {
|
||||
const first = { prompt: payload.prompt || '', duration: 0 }
|
||||
const rest = (payload.passes || []).map(item => ({ prompt: item.prompt || '', duration: 0 }))
|
||||
const rest = (payload.passes || []).map(item => ({ ...item, prompt: item.prompt || '', duration: 0 }))
|
||||
return rest.length ? [first, ...rest] : [first]
|
||||
}
|
||||
if (kind === 'music') return []
|
||||
@@ -126,7 +127,7 @@ export function applyQueuedShots(kind: string, shots: QueuedShotDraft[], payload
|
||||
}
|
||||
if (kind === 'edit') {
|
||||
next.prompt = shots[0]?.prompt || ''
|
||||
next.passes = shots.slice(1).map(item => ({ prompt: item.prompt || '' }))
|
||||
next.passes = shots.slice(1).map(({ duration, ...item }) => ({ ...item, prompt: item.prompt || '' }))
|
||||
return next
|
||||
}
|
||||
next.prompt = shots[0]?.prompt || ''
|
||||
|
||||
Reference in New Issue
Block a user