From 9fd96cb9908bde79da1a6e143ee8230649870c70 Mon Sep 17 00:00:00 2001 From: Towsty Date: Wed, 2 Sep 2026 21:33:30 -0500 Subject: [PATCH] Apply and print SNOFS from the LoRA dials, not a leftover Concept LoRA row. Co-authored-by: Cursor --- server/api/v2/generate.post.ts | 18 ++++++++++--- server/utils/imageChainV2.ts | 45 +++++++++++++++++++++++++-------- server/utils/imageWorkflowV2.ts | 2 +- server/utils/studioQueue.ts | 13 ++++++---- utils/imageV2.ts | 8 ++++++ utils/libraryGeneration.ts | 36 +++++++++++++++++++++----- 6 files changed, 94 insertions(+), 28 deletions(-) diff --git a/server/api/v2/generate.post.ts b/server/api/v2/generate.post.ts index fc9fdd6..dc0bfdb 100644 --- a/server/api/v2/generate.post.ts +++ b/server/api/v2/generate.post.ts @@ -18,6 +18,7 @@ import { clampImageV2Denoise, clampImageV2Size, clampImageV2Strength, + imageV2StackSpecials, imageV2WorkflowFile, parseImageV2Engine, parseImageV2Mode, @@ -259,6 +260,15 @@ export default defineEventHandler(async (event) => { const generateSnofsClipFallback = 0 await listStudioLoras() const loraStack = assertImageV2LoraStack(parsePostedLoraStack(fields.loraStack || fields.lora, 'image'), engine) + const stackSpecials = imageV2StackSpecials(loraStack, engine) + const snofsModel = conceptAllowed + ? (stackSpecials.snofs?.strengthModel ?? clampImageV2Strength(fields.snofs_model, generateSnofsFallback)) + : 0 + const snofsClip = conceptAllowed + ? (stackSpecials.snofs?.strengthClip ?? clampImageV2Strength(fields.snofs_clip, generateSnofsClipFallback)) + : 0 + const consistencyModel = stackSpecials.consistency?.strengthModel ?? generateConsistencyModel + const consistencyClip = stackSpecials.consistency?.strengthClip ?? generateConsistencyClip let inputFilename = imageA?.filename || '' const postedStillId = String(fields.stillId || '').trim() if (postedStillId) { @@ -346,10 +356,10 @@ export default defineEventHandler(async (event) => { v2Mode, v2Task, engine, - snofsModel: conceptAllowed ? clampImageV2Strength(fields.snofs_model, generateSnofsFallback) : 0, - snofsClip: conceptAllowed ? clampImageV2Strength(fields.snofs_clip, generateSnofsClipFallback) : 0, - consistencyModel: generateConsistencyModel, - consistencyClip: generateConsistencyClip, + snofsModel, + snofsClip, + consistencyModel, + consistencyClip, maskStillId: savedMask?.id, maskStillFilename: savedMask?.filename, refineStrength, diff --git a/server/utils/imageChainV2.ts b/server/utils/imageChainV2.ts index 7e035bd..29246c6 100644 --- a/server/utils/imageChainV2.ts +++ b/server/utils/imageChainV2.ts @@ -9,7 +9,15 @@ import { emitChainJob } from '~/server/utils/watch' import { saveStill, stillChainName } from '~/server/utils/library' import { comfyJobPrefix } from '~/utils/outputNames' import { joinPromptParts } from '~/utils/promptParts' -import { parseImageV2Engine, type ImageV2Engine, type ImageV2Mode, type ImageV2Task } from '~/utils/imageV2' +import { isXaigenStudio } from '~/utils/videoModels' +import { + imageV2StackSpecials, + mergeLegacyImageV2Loras, + parseImageV2Engine, + type ImageV2Engine, + type ImageV2Mode, + type ImageV2Task +} from '~/utils/imageV2' import type { EditImageFile } from '~/server/utils/imageChain' export type EditV2RunParams = { @@ -96,6 +104,21 @@ export async function runEditV2(job: Job, params: EditV2RunParams) { await ensureComfyLoraNames('image') if (!generate && params.megapixels > 0) await assertImageScaleToTotalPixelsNode() const krea = engine === 'krea' ? await resolveKreaGenerateAssets() : null + const appliedStack = mergeLegacyImageV2Loras(params.loraStack?.length ? params.loraStack : library.loraStack, { + engine, + mode: params.mode, + xaigen: isXaigenStudio(), + snofsModel: params.snofsModel, + snofsClip: params.snofsClip, + consistencyModel: params.consistencyModel, + consistencyClip: params.consistencyClip, + kreaConcept: krea?.conceptLora + }) + const appliedSpecials = imageV2StackSpecials(appliedStack, engine) + const snofsModel = appliedSpecials.snofs?.strengthModel ?? params.snofsModel + const snofsClip = appliedSpecials.snofs?.strengthClip ?? params.snofsClip + const consistencyModel = appliedSpecials.consistency?.strengthModel ?? params.consistencyModel + const consistencyClip = appliedSpecials.consistency?.strengthClip ?? params.consistencyClip if (!generate && params.mode === 'compose' && params.reference) { uploadedRef = await uploadImage({ @@ -201,10 +224,10 @@ export async function runEditV2(job: Job, params: EditV2RunParams) { imageBName: uploadedRef?.name, maskName: uploadedMask?.name, strength: params.strength, - snofsModel: params.snofsModel, - snofsClip: params.snofsClip, - consistencyModel: params.consistencyModel, - consistencyClip: params.consistencyClip, + snofsModel, + snofsClip, + consistencyModel, + consistencyClip, steps: params.steps, cfg: params.cfg, seed, @@ -218,7 +241,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) { kreaClipName: krea?.clip, kreaVaeName: krea?.vae, kreaConceptLora: krea?.conceptLora, - loraStack: params.loraStack?.length ? params.loraStack : (library.loraStack || undefined) + loraStack: appliedStack }) const queued = await queuePrompt(built.graph, job.clientId) job.promptId = queued.prompt_id @@ -277,10 +300,10 @@ export async function runEditV2(job: Job, params: EditV2RunParams) { engine, task: params.task, negative: params.negative, - snofsModel: params.snofsModel, - snofsClip: params.snofsClip, - consistencyModel: params.consistencyModel, - consistencyClip: params.consistencyClip, + snofsModel, + snofsClip, + consistencyModel, + consistencyClip, steps: params.steps, cfg: params.cfg, megapixels: params.mode === 'generate' || !(params.megapixels > 0) ? undefined : params.megapixels, @@ -293,7 +316,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) { seed, sourceStillId: params.sourceStillId || library.stillId, referenceStillId: params.referenceStillId, - ...persistLoraFields(params.loraStack) + ...persistLoraFields(appliedStack) } }) job.stillId = still?.id diff --git a/server/utils/imageWorkflowV2.ts b/server/utils/imageWorkflowV2.ts index 0cd96ad..227a8b8 100644 --- a/server/utils/imageWorkflowV2.ts +++ b/server/utils/imageWorkflowV2.ts @@ -584,7 +584,7 @@ export const IMAGE_V2_NODE_LABELS: Record = { '4': 'Loading diffusion model', '5': 'Loading CLIP', '6': 'Loading VAE', - '7': 'Applying Concept LoRA', + '7': 'Applying SNOFS', '8': 'Applying Consistency', '70': 'Applying LoRA', '71': 'Applying LoRA', diff --git a/server/utils/studioQueue.ts b/server/utils/studioQueue.ts index 47c7744..b63d251 100644 --- a/server/utils/studioQueue.ts +++ b/server/utils/studioQueue.ts @@ -7,6 +7,7 @@ import { fetchLiveQueue } from '~/server/utils/comfy' import { isLtxWorkflow, isTextToVideo, isXaigenStudio, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow, type VideoWorkflowId } from '~/utils/videoModels' import { persistLoraFields } from '~/server/utils/loras' import { resolveLoraStack } from '~/utils/loras' +import { imageV2StackSpecials } from '~/utils/imageV2' import { allowIdentityRefs, type PermanenceRef } from '~/utils/globalLocks' export type StudioJobStatus = 'waiting' | 'running' | 'held' | 'complete' | 'error' | 'cancelled' @@ -837,9 +838,11 @@ async function startStudioEditJob(item: StudioJob) { } }) await markStudioLive(item.ownerKey, item.id, live.id) + const v2Engine = payload.engine === 'krea' ? 'krea' : 'flux' + const v2Specials = imageV2StackSpecials(payload.loraStack, v2Engine) void runEditV2(live, { mode, - engine: payload.engine === 'krea' ? 'krea' : 'flux', + engine: v2Engine, task: payload.v2Task || 'scene', image: mode === 'generate' ? null : image, reference: mode === 'refine' || mode === 'generate' ? null : reference, @@ -851,10 +854,10 @@ async function startStudioEditJob(item: StudioJob) { steps: payload.steps, seed: live.library?.seed || payload.seed, cfg: payload.cfg, - 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), + snofsModel: v2Specials.snofs?.strengthModel ?? payload.snofsModel ?? 0, + snofsClip: v2Specials.snofs?.strengthClip ?? payload.snofsClip ?? 0, + consistencyModel: v2Specials.consistency?.strengthModel ?? payload.consistencyModel ?? (mode === 'generate' || payload.engine === 'krea' ? 0 : 0.7), + consistencyClip: v2Specials.consistency?.strengthClip ?? payload.consistencyClip ?? (mode === 'generate' || payload.engine === 'krea' ? 0 : 0.7), megapixels: payload.scaleToTotalPixels === false ? 0 : (payload.scaleMegapixels ?? 1), strength: payload.refineStrength, aspect: payload.aspect || 'auto', diff --git a/utils/imageV2.ts b/utils/imageV2.ts index 9bde324..d78a750 100644 --- a/utils/imageV2.ts +++ b/utils/imageV2.ts @@ -197,6 +197,14 @@ export function isImageV2ConsistencyLora(name: string) { return id === loraIdentityKey(IMAGE_V2_CONSISTENCY_LORA) || /flux2-klein-9b-consistency/i.test(id) } +/** Strengths that are actually on the stack cards. Separate snofs_* fields are legacy only. */ +export function imageV2StackSpecials(stack: unknown, engine?: ImageV2Engine) { + const items = normalizeLoraStack(stack) + const snofs = items.find(item => isImageV2ConceptLora(item.name, engine)) + const consistency = items.find(item => isImageV2ConsistencyLora(item.name)) + return { items, snofs, consistency } +} + export function upsertImageV2Lora(stack: unknown, name: string, model: number, clip: number): LoraStackItem[] { const item = makeLoraStackItem(name, model, clip) const current = normalizeLoraStack(stack) diff --git a/utils/libraryGeneration.ts b/utils/libraryGeneration.ts index e905c7b..80f7001 100644 --- a/utils/libraryGeneration.ts +++ b/utils/libraryGeneration.ts @@ -1,5 +1,12 @@ import { loraDisplayName, normalizeLoraStack, type LoraStackItem } from '~/utils/loras' -import type { ImageV2Engine, ImageV2Mode, ImageV2Task } from '~/utils/imageV2' +import { + imageV2ConceptLoraName, + imageV2StackSpecials, + upsertImageV2Lora, + type ImageV2Engine, + type ImageV2Mode, + type ImageV2Task +} from '~/utils/imageV2' import { isXaigenStudio, type VideoWorkflowId } from '~/utils/videoModels' import { restorePromptParts } from '~/utils/promptParts' @@ -72,6 +79,16 @@ function formatLoras(stack?: LoraStackItem[], name?: string) { }).join(', ') } +function stillDisplayLoraStack(settings: StillGenerationSettings) { + let stack = normalizeLoraStack(settings.loraStack || settings.loraName) + const { snofs } = imageV2StackSpecials(stack, settings.engine) + if (!snofs && isXaigenStudio() && (settings.snofsModel || settings.snofsClip)) { + const name = imageV2ConceptLoraName(settings.engine || 'flux') + if (name) stack = upsertImageV2Lora(stack, name, settings.snofsModel ?? 0, settings.snofsClip ?? 0) + } + return stack +} + export function normalizeStillSettings(raw: unknown): StillGenerationSettings | undefined { if (!raw || typeof raw !== 'object') return undefined const value = raw as Record @@ -171,13 +188,18 @@ export function stillSettingsRows(still: { prompt?: string; promptPre?: string; if (settings.engine) { rows.push({ label: 'Engine', value: settings.engine === 'krea' ? 'Krea' : 'Flux' }) } - if (isXaigenStudio() && (settings.snofsModel != null || settings.snofsClip != null)) { - rows.push({ label: 'Concept LoRA', value: formatPair(settings.snofsModel, settings.snofsClip) }) + const displayStack = stillDisplayLoraStack(settings) + const { consistency } = imageV2StackSpecials(displayStack, settings.engine) + if (settings.mode !== 'generate' && (consistency || settings.consistencyModel != null || settings.consistencyClip != null)) { + rows.push({ + label: 'Consistency', + value: formatPair( + consistency?.strengthModel ?? settings.consistencyModel, + consistency?.strengthClip ?? settings.consistencyClip + ) + }) } - if (settings.mode !== 'generate' && (settings.consistencyModel != null || settings.consistencyClip != null)) { - rows.push({ label: 'Consistency', value: formatPair(settings.consistencyModel, settings.consistencyClip) }) - } - const loras = formatLoras(settings.loraStack, settings.loraName) + const loras = formatLoras(displayStack) if (loras) rows.push({ label: 'LoRA', value: loras }) if (settings.mode === 'refine' && settings.strength != null) { rows.push({ label: 'Strength', value: formatNum(settings.strength) })