From bfe94e4bb126702d88523cc26817003a5a7a31f4 Mon Sep 17 00:00:00 2001 From: Towsty Date: Sun, 30 Aug 2026 13:27:30 -0500 Subject: [PATCH] Add an xAIGen SNOFS switch for Flux and Krea. It is the same LoRA card as the rest of the chain; the toggle only adds or removes that card so it no longer comes back after you turn it off. Co-authored-by: Cursor --- pages/index.vue | 73 ++++++++++++++++++++++++++++++---- server/api/v2/generate.post.ts | 6 +-- server/utils/studioQueue.ts | 4 +- utils/imageV2.ts | 21 ++++++++-- utils/loras.ts | 2 +- 5 files changed, 88 insertions(+), 18 deletions(-) diff --git a/pages/index.vue b/pages/index.vue index f9b29f2..0aeeb0b 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -757,6 +757,23 @@

Flux is Klein 9B. Krea is Krea 2 Turbo. Video stays MiniMax or LTX.

+ mergeLoraOptions(videoLoras.value, [ ...extensionQueue.value.flatMap(item => (item.loraStack || []).map(entry => entry.name)) ])) const imageLoraOptions = computed(() => mergeLoraOptions(imageLoras.value, imageLoraStack.value.map(item => item.name))) +const imageV2ConceptFile = computed(() => imageV2ConceptName()) +const imageV2SnofsOn = computed(() => imageV2LoraStack.value.some(item => isImageV2ConceptLora(item.name, v2Engine.value))) const imageV2LoraOptions = computed(() => mergeLoraOptions( filterLorasForImageEngine(imageLoras.value, v2Engine.value), - filterLorasForImageEngine(imageV2LoraStack.value.map(item => item.name), v2Engine.value) + filterLorasForImageEngine([ + ...imageV2LoraStack.value.map(item => item.name), + ...(v2ConceptLoraEnabled.value && imageV2ConceptFile.value ? [imageV2ConceptFile.value] : []) + ], v2Engine.value) )) const identityRefs = ref<(File | null)[]>([null, null, null, null]) const identityPreviews = ref(['', '', '', '']) @@ -3578,14 +3614,37 @@ function persistImageV2LoraStack(stack: LoraStackItem[], engine: ImageV2Engine) } catch { /* ignore */ } } +function readStoredLoraStackIfPresent(key: string) { + try { + if (localStorage.getItem(key) == null) return null + return readLoraStack(localStorage.getItem(key) || '') + } catch { + return null + } +} + function readImageV2LoraStack(engine: ImageV2Engine) { - const keyed = readStoredLoraStack(imageV2LoraStoreKey(engine)) - if (keyed.length) return filterLoraStackForImageEngine(keyed, engine) - const legacy = readStoredLoraStack('aigen-image-v2-lora') - if (!legacy.length) return [] as LoraStackItem[] + const keyed = readStoredLoraStackIfPresent(imageV2LoraStoreKey(engine)) + if (keyed) return filterLoraStackForImageEngine(keyed, engine) + const legacy = readStoredLoraStackIfPresent('aigen-image-v2-lora') + if (!legacy) return [] as LoraStackItem[] return filterLoraStackForImageEngine(legacy, engine) } +function setImageV2Snofs(on: boolean) { + const name = imageV2ConceptName() + if (!name) return + if (on) { + imageV2LoraStack.value = upsertImageV2Lora(imageV2LoraStack.value, name, IMAGE_V2_SNOFS_MODEL, IMAGE_V2_SNOFS_CLIP) + return + } + imageV2LoraStack.value = removeImageV2ConceptLora(imageV2LoraStack.value, v2Engine.value) +} + +function onImageV2SnofsToggle(event: Event) { + setImageV2Snofs((event.target as HTMLInputElement).checked) +} + function mergeLoraOptions(listed: string[], extra: string[]) { const names = [...listed] for (const name of extra) { @@ -5724,7 +5783,7 @@ function applyRefinePreset(kind: 'face' | 'hand' | 'heavy') { if (kind === 'face') { v2Denoise.value = IMAGE_V2_REFINE_FACE.strength if (v2Engine.value === 'flux') { - if (v2ConceptLoraEnabled.value) { + if (v2ConceptLoraEnabled.value && imageV2SnofsOn.value) { imageV2LoraStack.value = upsertImageV2Lora(imageV2LoraStack.value, IMAGE_V2_SNOFS_LORA, IMAGE_V2_REFINE_FACE.snofsModel, IMAGE_V2_REFINE_FACE.snofsClip) } imageV2LoraStack.value = upsertImageV2Lora(imageV2LoraStack.value, IMAGE_V2_CONSISTENCY_LORA, IMAGE_V2_REFINE_FACE.consistencyModel, IMAGE_V2_REFINE_FACE.consistencyClip) @@ -5735,7 +5794,7 @@ function applyRefinePreset(kind: 'face' | 'hand' | 'heavy') { if (kind === 'hand') { v2Denoise.value = IMAGE_V2_REFINE_HAND.strength if (v2Engine.value === 'flux') { - if (v2ConceptLoraEnabled.value) { + if (v2ConceptLoraEnabled.value && imageV2SnofsOn.value) { imageV2LoraStack.value = upsertImageV2Lora(imageV2LoraStack.value, IMAGE_V2_SNOFS_LORA, IMAGE_V2_REFINE_HAND.snofsModel, IMAGE_V2_REFINE_HAND.snofsClip) } imageV2LoraStack.value = upsertImageV2Lora(imageV2LoraStack.value, IMAGE_V2_CONSISTENCY_LORA, IMAGE_V2_REFINE_HAND.consistencyModel, IMAGE_V2_REFINE_HAND.consistencyClip) diff --git a/server/api/v2/generate.post.ts b/server/api/v2/generate.post.ts index 19be2c9..6268c08 100644 --- a/server/api/v2/generate.post.ts +++ b/server/api/v2/generate.post.ts @@ -6,8 +6,6 @@ import { IMAGE_V2_CFG_DEFAULT, IMAGE_V2_CONSISTENCY_CLIP, IMAGE_V2_CONSISTENCY_MODEL, - IMAGE_V2_SNOFS_CLIP, - IMAGE_V2_SNOFS_MODEL, IMAGE_V2_STEPS_DEFAULT, IMAGE_V2_TURBO_CFG, IMAGE_V2_TURBO_STEPS, @@ -221,8 +219,8 @@ export default defineEventHandler(async (event) => { mode === 'generate' || engine === 'krea' ? 0 : IMAGE_V2_CONSISTENCY_CLIP ) const conceptAllowed = isXaigenStudio() - const generateSnofsFallback = !conceptAllowed || engine === 'krea' ? 0 : IMAGE_V2_SNOFS_MODEL - const generateSnofsClipFallback = !conceptAllowed || engine === 'krea' ? 0 : IMAGE_V2_SNOFS_CLIP + const generateSnofsFallback = 0 + const generateSnofsClipFallback = 0 await listStudioLoras() const loraStack = assertImageV2LoraStack(parsePostedLoraStack(fields.loraStack || fields.lora, 'image'), engine) diff --git a/server/utils/studioQueue.ts b/server/utils/studioQueue.ts index 3ea18ae..6b4799c 100644 --- a/server/utils/studioQueue.ts +++ b/server/utils/studioQueue.ts @@ -797,8 +797,8 @@ async function startStudioEditJob(item: StudioJob) { steps: payload.steps, seed: live.library?.seed || payload.seed, cfg: payload.cfg, - snofsModel: payload.snofsModel ?? (isXaigenStudio() ? 0.65 : 0), - snofsClip: payload.snofsClip ?? (isXaigenStudio() ? 0.35 : 0), + snofsModel: payload.snofsModel ?? 0, + snofsClip: payload.snofsClip ?? 0, consistencyModel: payload.consistencyModel ?? (mode === 'generate' || payload.engine === 'krea' ? 0 : 0.7), consistencyClip: payload.consistencyClip ?? (mode === 'generate' || payload.engine === 'krea' ? 0 : 0.7), megapixels: payload.scaleMegapixels ?? 1, diff --git a/utils/imageV2.ts b/utils/imageV2.ts index 7cd28e8..3c7e3d5 100644 --- a/utils/imageV2.ts +++ b/utils/imageV2.ts @@ -74,7 +74,7 @@ export const IMAGE_V2_KREA_UNET_CANDIDATES = [ 'krea2_turbo_nvfp4.safetensors', 'krea2_turbo_fp8_scaled.safetensors' ] as const -export const IMAGE_V2_KREA_CONCEPT_LORA = '' +export const IMAGE_V2_KREA_CONCEPT_LORA = 'xaigen-snofs_krea_v1_3D.safetensors' export const IMAGE_V2_ROLE_HEADERS: Record, string> = { outfit: 'Person, face, body, pose, and background from image 1. Clothing only from image 2. Fit the outfit from image 2 to the body in image 1. Do not copy image 2’s face, body shape, or pose.', @@ -175,9 +175,18 @@ export type ImageV2PresetSettings = { height?: number } -export function isImageV2ConceptLora(name: string) { +export function imageV2ConceptLoraName(engine: ImageV2Engine, kreaConcept?: string) { + if (engine === 'krea') return String(kreaConcept || IMAGE_V2_KREA_CONCEPT_LORA || '').trim() + return IMAGE_V2_SNOFS_LORA +} + +export function isImageV2ConceptLora(name: string, engine?: ImageV2Engine) { const id = loraIdentityKey(name) - return id === loraIdentityKey(IMAGE_V2_SNOFS_LORA) || /klein_snofs/i.test(id) + const flux = id === loraIdentityKey(IMAGE_V2_SNOFS_LORA) || /klein_snofs/i.test(id) + const krea = id === loraIdentityKey(IMAGE_V2_KREA_CONCEPT_LORA) || /snofs_krea/i.test(id) + if (engine === 'krea') return krea + if (engine === 'flux') return flux + return flux || krea } export function isImageV2ConsistencyLora(name: string) { @@ -203,6 +212,10 @@ export function upsertImageV2Lora(stack: unknown, name: string, model: number, c return [item, ...current] } +export function removeImageV2ConceptLora(stack: unknown, engine?: ImageV2Engine) { + return normalizeLoraStack(stack).filter(item => !isImageV2ConceptLora(item.name, engine)) +} + export function defaultImageV2LoraStack(opts: { engine: ImageV2Engine mode: ImageV2Mode @@ -242,7 +255,7 @@ export function mergeLegacyImageV2Loras(stack: unknown, opts: { const name = String(opts.kreaConcept || '').trim() const model = Number(opts.snofsModel) || 0 const clip = Number(opts.snofsClip) || 0 - if (name && (model > 0 || clip > 0) && !items.some(item => loraIdentityKey(item.name) === loraIdentityKey(name))) { + if (name && (model > 0 || clip > 0) && !items.some(item => isImageV2ConceptLora(item.name, 'krea'))) { return upsertImageV2Lora(items, name, model, clip) } return items diff --git a/utils/loras.ts b/utils/loras.ts index c33126b..2116ab8 100644 --- a/utils/loras.ts +++ b/utils/loras.ts @@ -31,7 +31,7 @@ export function loraIdentityKey(name: string) { export function isXaigenOnlyLora(name: string) { const base = loraBasename(name) - return /^xaigen/i.test(base) || /klein_snofs/i.test(base) + return /^xaigen/i.test(base) || /klein_snofs|snofs_krea/i.test(base) } /** Pick the real Comfy filename: exact path, then same basename ignoring `xaigen-`, then suffix/contains. */