Add Krea as a second Image v2 Generate engine beside Flux.

Generate can queue krea_v2_generate.json without sharing Klein encoders or falling back if Krea assets are missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-29 07:05:01 -05:00
co-authored by Cursor
parent be5211f488
commit f6663444f5
11 changed files with 567 additions and 108 deletions
+110 -61
View File
@@ -66,7 +66,7 @@
<h2 class="font-display text-2xl font-bold">Input</h2>
<p class="text-sm text-zinc-400">{{ studioMode === 'editv2'
? (v2Mode === 'generate'
? 'Generate: no reference image. Prompt only. Klein 9B Base text-to-image.'
? 'Generate: no reference image. Prompt only.'
: 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'
@@ -710,38 +710,44 @@
/>
</div>
<div v-else-if="studioMode === 'editv2'" class="space-y-3">
<p class="text-xs text-zinc-500">Klein v2 · 9B Base · euler. Edit, Compose, Refine, and Generate are separate graphs. Shares the desktop GPU with video.</p>
<div v-if="v2Mode === 'generate'" class="space-y-2 rounded-2xl border border-white/10 bg-zinc-950/40 p-3">
<p class="text-xs text-zinc-400">Size · near 1 MP</p>
<div class="flex flex-wrap gap-2">
<button
v-for="(pair, aspect) in IMAGE_V2_GENERATE_ASPECTS"
:key="aspect"
type="button"
class="rounded-full border px-2.5 py-1 text-xs"
:class="v2Width === pair[0] && v2Height === pair[1] ? 'border-amber-300/70 text-amber-100' : 'border-white/10 text-zinc-400'"
@click="setGenerateSize(aspect)"
>
{{ aspect }} · {{ pair[0] }}×{{ pair[1] }}
</button>
<p class="text-xs text-zinc-500">Edit, Compose, Refine, and Generate are separate graphs. Shares the desktop GPU with video.</p>
<div v-if="v2Mode === 'generate'" class="space-y-3 rounded-2xl border border-white/10 bg-zinc-950/40 p-3">
<div>
<p class="text-xs text-zinc-400">Engine</p>
<div class="mt-2 flex flex-wrap gap-2">
<button
type="button"
class="rounded-full border px-2.5 py-1 text-xs"
:class="v2Engine === 'flux' ? 'border-amber-300/70 text-amber-100' : 'border-white/10 text-zinc-400'"
@click="setV2Engine('flux')"
>
Flux
</button>
<button
type="button"
class="rounded-full border px-2.5 py-1 text-xs"
:class="v2Engine === 'krea' ? 'border-amber-300/70 text-amber-100' : 'border-white/10 text-zinc-400'"
@click="setV2Engine('krea')"
>
Krea
</button>
</div>
<p class="mt-2 text-xs text-zinc-500">Flux is the current edit and compose engine. Krea is a separate photograph engine.</p>
</div>
<div class="flex flex-wrap gap-2">
<button
type="button"
class="rounded-full border px-2.5 py-1 text-xs"
:class="v2SnofsModel === 0 && v2SnofsClip === 0 ? 'border-amber-300/70 text-amber-100' : 'border-white/10 text-zinc-400'"
@click="applyGenerateLook('sfw')"
>
SFW scene
</button>
<button
type="button"
class="rounded-full border px-2.5 py-1 text-xs"
:class="v2SnofsModel === IMAGE_V2_GENERATE_SNOFS.snofsModel && v2SnofsClip === IMAGE_V2_GENERATE_SNOFS.snofsClip ? 'border-amber-300/70 text-amber-100' : 'border-white/10 text-zinc-400'"
@click="applyGenerateLook('snofs')"
>
SNOF T2I
</button>
<div>
<p class="text-xs text-zinc-400">Size · near 1 MP</p>
<div class="mt-2 flex flex-wrap gap-2">
<button
v-for="(pair, aspect) in IMAGE_V2_GENERATE_ASPECTS"
:key="aspect"
type="button"
class="rounded-full border px-2.5 py-1 text-xs"
:class="v2Width === pair[0] && v2Height === pair[1] ? 'border-amber-300/70 text-amber-100' : 'border-white/10 text-zinc-400'"
@click="setGenerateSize(aspect)"
>
{{ aspect }} · {{ pair[0] }}×{{ pair[1] }}
</button>
</div>
</div>
</div>
<div v-if="v2Mode === 'refine'" class="space-y-2 rounded-2xl border border-white/10 bg-zinc-950/40 p-3">
@@ -788,19 +794,36 @@
</div>
<div class="grid grid-cols-2 gap-3">
<label class="block text-sm">
<span class="mb-1 block text-zinc-400">SNOFS Model</span>
<input v-model.number="v2SnofsModel" type="number" min="0" max="2" step="0.05" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2">
<span class="mb-1 block text-zinc-400">Concept LoRA — Model</span>
<input
v-model.number="v2SnofsModel"
type="number"
min="0"
max="2"
step="0.05"
:disabled="v2Engine === 'krea' && !kreaConceptConfigured"
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2 disabled:opacity-50"
>
</label>
<label class="block text-sm">
<span class="mb-1 block text-zinc-400">SNOFS CLIP</span>
<input v-model.number="v2SnofsClip" type="number" min="0" max="2" step="0.05" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2">
<span class="mb-1 block text-zinc-400">Concept LoRA — CLIP</span>
<input
v-model.number="v2SnofsClip"
type="number"
min="0"
max="2"
step="0.05"
:disabled="v2Engine === 'krea' && !kreaConceptConfigured"
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2 disabled:opacity-50"
>
</label>
<label v-if="v2Mode !== 'generate'" class="block text-sm">
<span class="mb-1 block text-zinc-400">Consistency Model</span>
<p v-if="v2Engine === 'krea' && !kreaConceptConfigured" class="col-span-2 text-xs text-zinc-500">No Krea concept LoRA is configured.</p>
<label v-if="v2Mode !== 'generate' && v2Engine !== 'krea'" class="block text-sm">
<span class="mb-1 block text-zinc-400">Consistency — Model</span>
<input v-model.number="v2ConsistencyModel" type="number" min="0" max="2" step="0.05" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2">
</label>
<label v-if="v2Mode !== 'generate'" class="block text-sm">
<span class="mb-1 block text-zinc-400">Consistency CLIP</span>
<label v-if="v2Mode !== 'generate' && v2Engine !== 'krea'" class="block text-sm">
<span class="mb-1 block text-zinc-400">Consistency — CLIP</span>
<input v-model.number="v2ConsistencyClip" type="number" min="0" max="2" step="0.05" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2">
</label>
<label class="block text-sm">
@@ -820,7 +843,7 @@
<input v-model.number="v2Megapixels" type="number" :min="IMAGE_SCALE_MP_MIN" :max="IMAGE_SCALE_MP_MAX" :step="IMAGE_SCALE_MP_STEP" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2">
</label>
</div>
<label class="flex cursor-pointer items-center gap-2 text-xs text-zinc-400">
<label v-if="v2Engine !== 'krea'" class="flex cursor-pointer items-center gap-2 text-xs text-zinc-400">
<span class="relative inline-flex h-5 w-9 shrink-0 items-center">
<input v-model="v2Turbo" type="checkbox" class="peer sr-only" role="switch">
<span class="absolute inset-0 rounded-full bg-zinc-700 transition peer-checked:bg-amber-400" />
@@ -1959,9 +1982,10 @@ import {
IMAGE_V2_DENOISE_STEP,
IMAGE_V2_GENERATE_ASPECTS,
IMAGE_V2_GENERATE_HEIGHT,
IMAGE_V2_GENERATE_SNOFS,
IMAGE_V2_GENERATE_SFW,
IMAGE_V2_GENERATE_WIDTH,
IMAGE_V2_KREA_CFG,
IMAGE_V2_KREA_CONCEPT_LORA,
IMAGE_V2_KREA_STEPS,
IMAGE_V2_REFINE_FACE,
IMAGE_V2_REFINE_FACE_PROMPT,
IMAGE_V2_REFINE_HAND,
@@ -1972,6 +1996,8 @@ import {
clampImageV2Denoise,
clampImageV2Size,
clampImageV2Strength,
parseImageV2Engine,
type ImageV2Engine,
type ImageV2GenerateAspect,
type ImageV2Mode,
type ImageV2PresetSettings,
@@ -2245,7 +2271,9 @@ const editScaleMegapixels = ref(1)
const editRefFile = ref<File | null>(null)
const editRefPreview = ref('')
const v2Mode = ref<ImageV2Mode>('edit')
const v2Engine = ref<ImageV2Engine>('flux')
const v2Task = ref<ImageV2Task>('scene')
const kreaConceptConfigured = computed(() => Boolean(String(runtimeConfig.public.kreaConceptLora || IMAGE_V2_KREA_CONCEPT_LORA || '').trim()))
const v2Negative = ref('')
const v2SnofsModel = ref(IMAGE_V2_SNOFS_MODEL)
const v2SnofsClip = ref(IMAGE_V2_SNOFS_CLIP)
@@ -3469,6 +3497,7 @@ function currentPresetSnapshot() {
loraStack: [],
settings: {
mode: v2Mode.value,
engine: v2Mode.value === 'generate' ? v2Engine.value : 'flux',
task: v2Mode.value === 'compose' ? v2Task.value : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'generate' ? 't2i' : 'scene',
negative: v2Negative.value,
snofsModel: clampImageV2Strength(v2SnofsModel.value, IMAGE_V2_SNOFS_MODEL),
@@ -3521,6 +3550,7 @@ function applyGenerationPreset(preset: GenerationPreset) {
if (preset.kind === 'imagev2') {
const settings = preset.settings as ImageV2PresetSettings
v2Mode.value = settings.mode === 'compose' ? 'compose' : settings.mode === 'refine' ? 'refine' : settings.mode === 'generate' ? 'generate' : 'edit'
v2Engine.value = v2Mode.value === 'generate' ? parseImageV2Engine(settings.engine, 'flux') : 'flux'
v2Task.value = v2Mode.value === 'compose' ? (settings.task || 'scene') : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'generate' ? 't2i' : 'scene'
if (typeof settings.negative === 'string') v2Negative.value = settings.negative
v2SnofsModel.value = clampImageV2Strength(settings.snofsModel, IMAGE_V2_SNOFS_MODEL)
@@ -4803,6 +4833,7 @@ function applyStillSettings(still: LibraryStill) {
? settings.mode
: 'edit'
setV2Mode(mode)
v2Engine.value = mode === 'generate' ? parseImageV2Engine(settings.engine, 'flux') : 'flux'
if (!settings) return
if (typeof settings.negative === 'string') v2Negative.value = settings.negative
if (settings.snofsModel != null) v2SnofsModel.value = clampImageV2Strength(settings.snofsModel, IMAGE_V2_SNOFS_MODEL)
@@ -5424,9 +5455,34 @@ function setV2Mode(mode: ImageV2Mode) {
v2Task.value = 't2i'
v2ConsistencyModel.value = 0
v2ConsistencyClip.value = 0
} else if (previous === 'generate' && v2ConsistencyModel.value === 0 && v2ConsistencyClip.value === 0) {
v2ConsistencyModel.value = IMAGE_V2_CONSISTENCY_MODEL
v2ConsistencyClip.value = IMAGE_V2_CONSISTENCY_CLIP
} else {
v2Engine.value = 'flux'
if (previous === 'generate' && v2ConsistencyModel.value === 0 && v2ConsistencyClip.value === 0) {
v2ConsistencyModel.value = IMAGE_V2_CONSISTENCY_MODEL
v2ConsistencyClip.value = IMAGE_V2_CONSISTENCY_CLIP
}
}
}
function setV2Engine(engine: ImageV2Engine) {
if (engine === 'krea' && v2Mode.value !== 'generate') return
const previous = v2Engine.value
v2Engine.value = engine
if (engine === 'krea') {
v2Steps.value = IMAGE_V2_KREA_STEPS
v2Cfg.value = IMAGE_V2_KREA_CFG
v2SnofsModel.value = 0
v2SnofsClip.value = 0
v2ConsistencyModel.value = 0
v2ConsistencyClip.value = 0
v2Turbo.value = false
return
}
if (previous === 'krea') {
v2Steps.value = IMAGE_V2_STEPS_DEFAULT
v2Cfg.value = IMAGE_V2_CFG_DEFAULT
v2SnofsModel.value = IMAGE_V2_SNOFS_MODEL
v2SnofsClip.value = IMAGE_V2_SNOFS_CLIP
}
}
@@ -5436,14 +5492,6 @@ function setGenerateSize(aspect: ImageV2GenerateAspect) {
v2Height.value = height
}
function applyGenerateLook(kind: 'sfw' | 'snofs') {
const look = kind === 'sfw' ? IMAGE_V2_GENERATE_SFW : IMAGE_V2_GENERATE_SNOFS
v2SnofsModel.value = look.snofsModel
v2SnofsClip.value = look.snofsClip
v2ConsistencyModel.value = 0
v2ConsistencyClip.value = 0
}
function applyRefinePreset(kind: 'face' | 'hand' | 'heavy') {
if (kind === 'face') {
v2Denoise.value = IMAGE_V2_REFINE_FACE.strength
@@ -5537,15 +5585,16 @@ async function editImageV2() {
const size = clampImageV2Size(v2Width.value, v2Height.value)
body.append('width', String(size.width))
body.append('height', String(size.height))
body.append('engine', v2Engine.value)
}
body.append('prompt', prompt.value.trim())
body.append('negative', v2Negative.value)
body.append('snofs_model', String(clampImageV2Strength(v2SnofsModel.value, IMAGE_V2_SNOFS_MODEL)))
body.append('snofs_clip', String(clampImageV2Strength(v2SnofsClip.value, IMAGE_V2_SNOFS_CLIP)))
body.append('consistency_model', String(v2Mode.value === 'generate' ? 0 : clampImageV2Strength(v2ConsistencyModel.value, IMAGE_V2_CONSISTENCY_MODEL)))
body.append('consistency_clip', String(v2Mode.value === 'generate' ? 0 : clampImageV2Strength(v2ConsistencyClip.value, IMAGE_V2_CONSISTENCY_CLIP)))
body.append('steps', String(clampImageSteps(v2Steps.value, IMAGE_V2_STEPS_DEFAULT)))
body.append('cfg', String(clampImageCfg(v2Cfg.value, IMAGE_V2_CFG_DEFAULT)))
body.append('snofs_model', String(clampImageV2Strength(v2SnofsModel.value, v2Engine.value === 'krea' ? 0 : IMAGE_V2_SNOFS_MODEL)))
body.append('snofs_clip', String(clampImageV2Strength(v2SnofsClip.value, v2Engine.value === 'krea' ? 0 : IMAGE_V2_SNOFS_CLIP)))
body.append('consistency_model', String(v2Mode.value === 'generate' || v2Engine.value === 'krea' ? 0 : clampImageV2Strength(v2ConsistencyModel.value, IMAGE_V2_CONSISTENCY_MODEL)))
body.append('consistency_clip', String(v2Mode.value === 'generate' || v2Engine.value === 'krea' ? 0 : clampImageV2Strength(v2ConsistencyClip.value, IMAGE_V2_CONSISTENCY_CLIP)))
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') body.append('megapixels', String(clampImageScaleMegapixels(v2Megapixels.value, 1)))
body.append('turbo', String(v2Turbo.value === true))
body.append('seed', seedInput.value || 'random')
@@ -5566,7 +5615,7 @@ async function editImageV2() {
body
})
if (started.queued) {
toast('Queued Klein v2 behind the current job.')
toast(v2Engine.value === 'krea' ? 'Queued Krea generate behind the current job.' : 'Queued Image v2 behind the current job.')
await refreshStudioQueue()
return
}