Wire every Image v2 LoRA, including Concept, through one sampler chain.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+64
-46
@@ -386,17 +386,6 @@ function linkRef(value: unknown): [string, number] | null {
|
||||
: null
|
||||
}
|
||||
|
||||
function currentModelClipFeeds(graph: WorkflowGraph) {
|
||||
const prompt = Object.values(graph).find(node => node.class_type === 'CLIPTextEncode' && linkRef(node.inputs.clip))
|
||||
const modelNode = Object.values(graph).find(node => MODEL_FEED_CLASSES.has(node.class_type) && linkRef(node.inputs.model))
|
||||
const model = linkRef(modelNode?.inputs.model)
|
||||
const clip = linkRef(prompt?.inputs.clip)
|
||||
return {
|
||||
model: model && graph[model[0]] ? model : null,
|
||||
clip: clip && graph[clip[0]] ? clip : null
|
||||
}
|
||||
}
|
||||
|
||||
export function assertImageV2LoraStack(stack: unknown, engine: 'flux' | 'krea') {
|
||||
const items = normalizeLoraStack(stack)
|
||||
if (engine !== 'krea') return items
|
||||
@@ -407,16 +396,40 @@ export function assertImageV2LoraStack(stack: unknown, engine: 'flux' | 'krea')
|
||||
return items
|
||||
}
|
||||
|
||||
function stripExistingLoraLoaders(graph: WorkflowGraph) {
|
||||
const remaining = new Set(
|
||||
Object.entries(graph)
|
||||
.filter(([, node]) => node.class_type === LORA_LOADER)
|
||||
.map(([id]) => id)
|
||||
)
|
||||
while (remaining.size) {
|
||||
const id = [...remaining].find(candidate => (
|
||||
![...remaining].some(other => other !== candidate && linkSource(graph[other]?.inputs.model) === candidate)
|
||||
)) || [...remaining][0]
|
||||
bypassLoraNode(graph, id)
|
||||
remaining.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
function modelConsumers(graph: WorkflowGraph) {
|
||||
return Object.values(graph).filter(node => MODEL_FEED_CLASSES.has(node.class_type) && linkRef(node.inputs.model))
|
||||
}
|
||||
|
||||
function clipConsumers(graph: WorkflowGraph) {
|
||||
return Object.values(graph).filter(node => node.class_type === 'CLIPTextEncode' && linkRef(node.inputs.clip))
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a user LoRA chain after the current model/CLIP feed.
|
||||
* Flux v2 already has Concept/Consistency loaders; this stacks after those.
|
||||
* Empty stack is a no-op. Klein LoRAs on a Krea graph throw.
|
||||
* Replace any template LoRA loaders with the posted chain, in list order.
|
||||
* Strength 0 skips that card (off). Empty chain leaves UNET/CLIP wired straight to the sampler.
|
||||
*/
|
||||
export function applyImageV2UserLoras(
|
||||
graph: WorkflowGraph,
|
||||
stack?: unknown,
|
||||
engine: 'flux' | 'krea' = 'flux'
|
||||
) {
|
||||
stripExistingLoraLoaders(graph)
|
||||
|
||||
const xaigen = isXaigenStudio()
|
||||
const items = normalizeLoraStack(stack)
|
||||
.filter(item => xaigen || !isXaigenOnlyLora(item.name))
|
||||
@@ -424,8 +437,7 @@ export function applyImageV2UserLoras(
|
||||
...item,
|
||||
name: resolveUserLoraName(item.name, 'image')
|
||||
}))
|
||||
.filter(item => item.name && (xaigen || !isXaigenOnlyLora(item.name)) && !alreadyHasLora(graph, item.name))
|
||||
if (!items.length) return [] as LoraStackItem[]
|
||||
.filter(item => item.name && (xaigen || !isXaigenOnlyLora(item.name)) && (item.strengthModel !== 0 || item.strengthClip !== 0))
|
||||
|
||||
if (engine === 'krea') {
|
||||
const klein = items.find(item => isKleinImageLora(item.name))
|
||||
@@ -434,48 +446,54 @@ export function applyImageV2UserLoras(
|
||||
}
|
||||
}
|
||||
|
||||
const feeds = currentModelClipFeeds(graph)
|
||||
const modelFrom = feeds.model || (graph['4'] ? ['4', 0] as [string, number] : null)
|
||||
const clipFrom = feeds.clip || (graph['5'] ? ['5', 0] as [string, number] : null)
|
||||
const guiders = modelConsumers(graph)
|
||||
const encodes = clipConsumers(graph)
|
||||
const modelFrom = linkRef(guiders[0]?.inputs.model) || (graph['4'] ? ['4', 0] as [string, number] : null)
|
||||
const clipFrom = linkRef(encodes[0]?.inputs.clip) || (graph['5'] ? ['5', 0] as [string, number] : null)
|
||||
if (!modelFrom || !clipFrom) {
|
||||
throw createError({ statusCode: 500, statusMessage: 'v2 graph has no model/CLIP feed for user LoRAs.' })
|
||||
}
|
||||
if (!items.length) return [] as LoraStackItem[]
|
||||
if (!guiders.length || !encodes.length) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Image v2 LoRA chain is not connected to the sampler. Refusing to run without those adapters.'
|
||||
})
|
||||
}
|
||||
|
||||
const firstId = 'user:lora:0'
|
||||
graph[firstId] = {
|
||||
class_type: LORA_LOADER,
|
||||
inputs: {
|
||||
lora_name: items[0].name,
|
||||
strength_model: items[0].strengthModel,
|
||||
strength_clip: items[0].strengthClip,
|
||||
model: modelFrom,
|
||||
clip: clipFrom
|
||||
},
|
||||
_meta: { title: items.length === 1 ? 'User LoRA' : 'User LoRA 1' }
|
||||
}
|
||||
for (const [id, node] of Object.entries(graph)) {
|
||||
if (id === firstId) continue
|
||||
for (const [key, value] of Object.entries(node.inputs)) {
|
||||
if (!Array.isArray(value)) continue
|
||||
if (value[0] === modelFrom[0] && value[1] === modelFrom[1]) node.inputs[key] = [firstId, 0]
|
||||
else if (value[0] === clipFrom[0] && value[1] === clipFrom[1]) node.inputs[key] = [firstId, 1]
|
||||
let model: [string, number] = modelFrom
|
||||
let clip: [string, number] = clipFrom
|
||||
const ids: string[] = []
|
||||
for (const [index, item] of items.entries()) {
|
||||
const nodeId = String(70 + index)
|
||||
if (graph[nodeId]) {
|
||||
throw createError({ statusCode: 500, statusMessage: `v2 graph already has node ${nodeId}. Refusing to overwrite it with a LoRA.` })
|
||||
}
|
||||
}
|
||||
let sourceId = firstId
|
||||
for (const [index, item] of items.slice(1).entries()) {
|
||||
const nodeId = `user:lora:${index + 1}`
|
||||
injectAfter(graph, sourceId, nodeId, {
|
||||
graph[nodeId] = {
|
||||
class_type: LORA_LOADER,
|
||||
inputs: {
|
||||
lora_name: item.name,
|
||||
strength_model: item.strengthModel,
|
||||
strength_clip: item.strengthClip,
|
||||
model: [sourceId, 0],
|
||||
clip: [sourceId, 1]
|
||||
model,
|
||||
clip
|
||||
},
|
||||
_meta: { title: `User LoRA ${index + 2}` }
|
||||
_meta: { title: items.length === 1 ? 'LoRA' : `LoRA ${index + 1}` }
|
||||
}
|
||||
ids.push(nodeId)
|
||||
model = [nodeId, 0]
|
||||
clip = [nodeId, 1]
|
||||
}
|
||||
for (const node of guiders) node.inputs.model = model
|
||||
for (const node of encodes) node.inputs.clip = clip
|
||||
|
||||
const hooked = guiders.every(node => linkRef(node.inputs.model)?.[0] === ids[ids.length - 1])
|
||||
&& encodes.every(node => linkRef(node.inputs.clip)?.[0] === ids[ids.length - 1])
|
||||
if (!hooked) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Image v2 LoRA chain is not connected to the sampler. Refusing to run without those adapters.'
|
||||
})
|
||||
sourceId = nodeId
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user