Add per-image iteration settings and steps/CFG sweeps

This commit is contained in:
Towsty
2026-09-06 17:02:42 -05:00
parent 0c6a035d04
commit 666c922f40
12 changed files with 370 additions and 59 deletions
+48
View File
@@ -0,0 +1,48 @@
<template>
<div class="mt-3 space-y-3 border-t border-white/10 pt-3">
<p class="text-xs text-zinc-400">Blank values use the shared settings. A blank prompt uses the first prompt.</p>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
<label class="text-sm text-zinc-300">Steps
<input :value="modelValue.steps" type="number" min="1" max="50" step="1" :placeholder="String(baseSteps)" class="iteration-input" :disabled="disabled" @input="numberField('steps', $event)">
</label>
<label class="text-sm text-zinc-300">CFG
<input :value="modelValue.cfg" type="number" min="0" max="20" step="0.1" :placeholder="String(baseCfg)" class="iteration-input" :disabled="disabled" @input="numberField('cfg', $event)">
</label>
<label class="text-sm text-zinc-300">Seed
<select :value="seedMode" class="iteration-input" :disabled="disabled" @change="setSeedMode($event)">
<option value="random">Random</option><option value="same">Same as first</option><option value="custom">Custom</option>
</select>
</label>
<label v-if="seedMode === 'custom'" class="text-sm text-zinc-300">Seed number
<input :value="modelValue.seed" type="number" min="0" max="2147483647" step="1" class="iteration-input" :disabled="disabled" @input="numberField('seed', $event)">
</label>
<label v-if="allowStrength" class="text-sm text-zinc-300">Denoise
<input :value="modelValue.strength" type="number" min="0.15" max="0.75" step="0.01" placeholder="Shared" class="iteration-input" :disabled="disabled" @input="numberField('strength', $event)">
</label>
</div>
<label class="flex items-center gap-2 text-sm text-zinc-300">
<input type="checkbox" :checked="modelValue.loraStack === undefined" :disabled="disabled" @change="inheritLoras($event)"> Use shared LoRAs
</label>
<LoraSelect v-if="modelValue.loraStack !== undefined" :model-value="modelValue.loraStack" :names="loraNames" :engine="engine" :disabled="disabled" label="This variation's LoRAs" empty-label="No LoRAs" @update:model-value="update({ loraStack: $event })" />
<p v-if="modelValue.steps !== undefined || modelValue.cfg !== undefined" class="text-xs text-zinc-400">Custom steps or CFG override the shared Turbo preset for this variation.</p>
</div>
</template>
<script setup lang="ts">
import type { ImageIteration } from '~/utils/imageIterations'
const props = withDefaults(defineProps<{ modelValue: ImageIteration; baseSteps?: number; baseCfg?: number; loraNames?: string[]; engine?: 'flux' | 'krea'; allowStrength?: boolean; disabled?: boolean }>(), { baseSteps: 20, baseCfg: 1, loraNames: () => [], engine: 'flux', allowStrength: false, disabled: false })
const emit = defineEmits<{ 'update:modelValue': [value: ImageIteration] }>()
const seedMode = computed(() => typeof props.modelValue.seed === 'number' ? 'custom' : props.modelValue.seed || 'random')
function update(patch: Partial<ImageIteration>) { emit('update:modelValue', { ...props.modelValue, ...patch }) }
function numberField(key: 'steps' | 'cfg' | 'seed' | 'strength', event: Event) {
const value = (event.target as HTMLInputElement).value
update({ [key]: value === '' ? undefined : Number(value) })
}
function setSeedMode(event: Event) {
const mode = (event.target as HTMLSelectElement).value
update({ seed: mode === 'custom' ? 0 : mode === 'same' ? 'same' : 'random' })
}
function inheritLoras(event: Event) { update({ loraStack: (event.target as HTMLInputElement).checked ? undefined : [] }) }
</script>
<style scoped>
.iteration-input { @apply mt-1 w-full rounded-lg border border-white/15 bg-zinc-950 px-3 py-2 text-sm text-zinc-100 outline-none focus:ring-2 focus:ring-amber-300/40 disabled:opacity-50; }
</style>
+29
View File
@@ -0,0 +1,29 @@
<template>
<details class="rounded-2xl border border-amber-300/25 bg-amber-400/5 p-4">
<summary class="cursor-pointer text-sm font-semibold text-amber-100">Compare settings · build a sweep</summary>
<p class="mt-3 text-sm text-zinc-300">Use one prompt with several step counts or CFG values. Each combination becomes an editable variation.</p>
<div class="mt-3 grid gap-3 sm:grid-cols-2">
<label class="text-sm text-zinc-300">Steps, separated by commas
<input v-model="steps" class="mt-1 w-full rounded-lg border border-white/15 bg-zinc-950 px-3 py-2" placeholder="24, 20, 18, 19">
</label>
<label class="text-sm text-zinc-300">CFG values
<input v-model="cfg" class="mt-1 w-full rounded-lg border border-white/15 bg-zinc-950 px-3 py-2" placeholder="1.5">
</label>
</div>
<p class="mt-3 text-sm text-zinc-300">{{ preview }}</p>
<p class="mt-1 text-xs text-zinc-400">Uses the same seed for comparison and the original input for every image. Replaces the current variations; nothing renders until you queue.</p>
<button type="button" class="mt-3 rounded-lg bg-amber-400 px-4 py-2 text-sm font-semibold text-zinc-950 disabled:opacity-50" :disabled="!rows.length" @click="emit('apply', rows)">Build {{ rows.length || '' }} variations</button>
</details>
</template>
<script setup lang="ts">
import { imageIterationSweep, type ImageIteration } from '~/utils/imageIterations'
const emit = defineEmits<{ apply: [rows: ImageIteration[]] }>()
const steps = ref('24, 20, 18, 19')
const cfg = ref('1.5')
const result = computed(() => {
try { return { rows: imageIterationSweep(steps.value, cfg.value), error: '' } }
catch (error) { return { rows: [] as ImageIteration[], error: error instanceof Error ? error.message : String(error) } }
})
const rows = computed(() => result.value.rows)
const preview = computed(() => result.value.error || `${rows.value.length} images · ${rows.value.map(row => `${row.steps} steps / CFG ${row.cfg}`).join(' · ')}`)
</script>
+11
View File
@@ -76,6 +76,17 @@
<div v-for="(shot, index) in extraShots" :key="index" class="rounded-2xl border border-white/10 bg-zinc-950/50 px-3 py-3">
<p class="text-sm font-medium text-zinc-200">{{ job.kind === 'edit' ? `Pass ${index + 2}` : `Shot ${index + 2}` }}</p>
<textarea v-model="shot.prompt" rows="3" class="mt-2 max-h-36 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
<ImageIterationSettings
v-if="job.kind === 'edit' && job.payload.v2Mode === 'iterate'"
:model-value="shot"
:base-steps="draft.steps"
:base-cfg="draft.cfg"
:lora-names="loraNames"
:engine="job.payload.engine === 'krea' ? 'krea' : 'flux'"
:allow-strength="job.payload.engine === 'krea' && Boolean(job.payload.stillId)"
:disabled="!editable"
@update:model-value="extraShots[index] = { ...$event, duration: shot.duration }"
/>
<label v-if="job.kind !== 'edit'" class="mt-2 block text-xs text-zinc-400">
Duration
<input v-model.number="shot.duration" type="number" min="0.5" max="120" step="0.1" class="mt-1 w-28 rounded-lg border border-white/10 bg-zinc-950 px-2 py-1 text-sm disabled:opacity-70" :disabled="!editable">