Load only the LoRAs the user picked on Klein; do not require snofs.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -87,6 +87,7 @@ import {
|
|||||||
clampLoraStrength,
|
clampLoraStrength,
|
||||||
formatLoraStrength,
|
formatLoraStrength,
|
||||||
loraDisplayName,
|
loraDisplayName,
|
||||||
|
loraIdentityKey,
|
||||||
makeLoraStackItem,
|
makeLoraStackItem,
|
||||||
type LoraStackItem
|
type LoraStackItem
|
||||||
} from '~/utils/loras'
|
} from '~/utils/loras'
|
||||||
@@ -111,8 +112,8 @@ const props = withDefaults(defineProps<{
|
|||||||
const model = defineModel<LoraStackItem[]>({ default: () => [] })
|
const model = defineModel<LoraStackItem[]>({ default: () => [] })
|
||||||
|
|
||||||
const availableNames = computed(() => {
|
const availableNames = computed(() => {
|
||||||
const taken = new Set(model.value.map(item => item.name.replace(/\\/g, '/').toLowerCase()))
|
const taken = new Set(model.value.map(item => loraIdentityKey(item.name)))
|
||||||
return props.names.filter(name => !taken.has(name.replace(/\\/g, '/').toLowerCase()))
|
return props.names.filter(name => !taken.has(loraIdentityKey(name)))
|
||||||
})
|
})
|
||||||
|
|
||||||
function onAdd(event: Event) {
|
function onAdd(event: Event) {
|
||||||
@@ -124,8 +125,8 @@ function onAdd(event: Event) {
|
|||||||
function addLora(name: string) {
|
function addLora(name: string) {
|
||||||
const item = makeLoraStackItem(name)
|
const item = makeLoraStackItem(name)
|
||||||
if (!item || model.value.length >= LORA_STACK_MAX) return
|
if (!item || model.value.length >= LORA_STACK_MAX) return
|
||||||
const taken = new Set(model.value.map(entry => entry.name.replace(/\\/g, '/').toLowerCase()))
|
const taken = new Set(model.value.map(entry => loraIdentityKey(entry.name)))
|
||||||
if (taken.has(item.name.replace(/\\/g, '/').toLowerCase())) return
|
if (taken.has(loraIdentityKey(item.name))) return
|
||||||
model.value = [...model.value, item]
|
model.value = [...model.value, item]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { copyFileSync, existsSync, readFileSync } from 'node:fs'
|
import { copyFileSync, existsSync, readFileSync } from 'node:fs'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { composeShotPrompt } from '~/utils/globalLocks'
|
import { composeShotPrompt } from '~/utils/globalLocks'
|
||||||
import { parsePostedLoraStack, listStudioLoras, persistLoraFields } from '~/server/utils/loras'
|
import { parsePostedLoraStack, listStudioLoras, persistLoraFields, ensureComfyLoraNames } from '~/server/utils/loras'
|
||||||
import { readLoraStack } from '~/utils/loras'
|
import { readLoraStack } from '~/utils/loras'
|
||||||
import { clampVideoCfg } from '~/utils/generationPresets'
|
import { clampVideoCfg } from '~/utils/generationPresets'
|
||||||
import { defaultVideoSteps, isLtxWorkflow, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow } from '~/utils/videoModels'
|
import { defaultVideoSteps, isLtxWorkflow, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow } from '~/utils/videoModels'
|
||||||
@@ -213,6 +213,7 @@ async function runExtend(
|
|||||||
}
|
}
|
||||||
emitJob(job, { type: 'status', message: 'Queueing extension on MiniMax H3...', progress: 6 })
|
emitJob(job, { type: 'status', message: 'Queueing extension on MiniMax H3...', progress: 6 })
|
||||||
await waitForComfySocket(job, 4000)
|
await waitForComfySocket(job, 4000)
|
||||||
|
await ensureComfyLoraNames('video')
|
||||||
|
|
||||||
const graph = buildWorkflow({
|
const graph = buildWorkflow({
|
||||||
prompt: params.prompt,
|
prompt: params.prompt,
|
||||||
|
|||||||
@@ -278,7 +278,7 @@
|
|||||||
},
|
},
|
||||||
"75:131": {
|
"75:131": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"lora_name": "klein_snofs_v1_4.safetensors",
|
"lora_name": "",
|
||||||
"strength_model": 1,
|
"strength_model": 1,
|
||||||
"strength_clip": 1,
|
"strength_clip": 1,
|
||||||
"model": [
|
"model": [
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
},
|
},
|
||||||
"class_type": "LoraLoader",
|
"class_type": "LoraLoader",
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"title": "Load LoRA (Model and CLIP)"
|
"title": "User LoRA (optional)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"92:102": {
|
"92:102": {
|
||||||
@@ -593,7 +593,7 @@
|
|||||||
},
|
},
|
||||||
"92:132": {
|
"92:132": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"lora_name": "klein_snofs_v1_4.safetensors",
|
"lora_name": "",
|
||||||
"strength_model": 1,
|
"strength_model": 1,
|
||||||
"strength_clip": 1,
|
"strength_clip": 1,
|
||||||
"model": [
|
"model": [
|
||||||
@@ -607,7 +607,7 @@
|
|||||||
},
|
},
|
||||||
"class_type": "LoraLoader",
|
"class_type": "LoraLoader",
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"title": "Load LoRA (Model and CLIP)"
|
"title": "User LoRA (optional)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import { ensureComfyReady } from '~/server/utils/comfyLifecycle'
|
|||||||
import { getComfyHost, uploadImage, queuePrompt, purgeComfyArtifacts } from '~/server/utils/comfy'
|
import { getComfyHost, uploadImage, queuePrompt, purgeComfyArtifacts } from '~/server/utils/comfy'
|
||||||
import { withImageComfyHost, waitForImageEdit, downloadEditedImage } from '~/server/utils/imageComfy'
|
import { withImageComfyHost, waitForImageEdit, downloadEditedImage } from '~/server/utils/imageComfy'
|
||||||
import { buildEditWorkflow } from '~/server/utils/imageWorkflow'
|
import { buildEditWorkflow } from '~/server/utils/imageWorkflow'
|
||||||
|
import { ensureComfyLoraNames } from '~/server/utils/loras'
|
||||||
import { imageDimensions } from '~/server/utils/resolution'
|
import { imageDimensions } from '~/server/utils/resolution'
|
||||||
import { emitChainJob } from '~/server/utils/watch'
|
import { emitChainJob } from '~/server/utils/watch'
|
||||||
import { saveStill, stillChainName } from '~/server/utils/library'
|
import { saveStill, stillChainName } from '~/server/utils/library'
|
||||||
@@ -91,6 +92,7 @@ export async function runEdit(job: Job, params: EditRunParams) {
|
|||||||
: 'Queueing Flux.2 Klein edit on Beast...',
|
: 'Queueing Flux.2 Klein edit on Beast...',
|
||||||
progress: 12
|
progress: 12
|
||||||
})
|
})
|
||||||
|
await ensureComfyLoraNames('image')
|
||||||
const graph = buildEditWorkflow({
|
const graph = buildEditWorkflow({
|
||||||
prompt,
|
prompt,
|
||||||
negative: params.negative,
|
negative: params.negative,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import template from '../assets/workflow_flux2_klein_edit.json'
|
import template from '../assets/workflow_flux2_klein_edit.json'
|
||||||
import { applyUserLoraToGraph } from '~/server/utils/loras'
|
import { applyOptionalLoraLoaders } from '~/server/utils/loras'
|
||||||
import type { LoraStackItem } from '~/utils/loras'
|
import type { LoraStackItem } from '~/utils/loras'
|
||||||
|
|
||||||
const IMAGE1 = '76'
|
const IMAGE1 = '76'
|
||||||
@@ -65,7 +65,8 @@ export function buildEditWorkflow(params: EditParams) {
|
|||||||
if (dual ? singleNode : dualNode) delete graph[id]
|
if (dual ? singleNode : dualNode) delete graph[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
applyUserLoraToGraph(graph, params.loraStack || params.loraName)
|
// User LoRAs only. Empty stack bypasses the optional LoraLoader (no snofs default).
|
||||||
|
applyOptionalLoraLoaders(graph, params.loraStack || params.loraName, 'image')
|
||||||
return graph
|
return graph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+132
-10
@@ -2,15 +2,18 @@ import { comfyConfigured, comfyFetch, getComfyHost } from '~/server/utils/comfy'
|
|||||||
import { getBeastImageHost, imageComfyFetch, sameImageHost } from '~/server/utils/imageComfy'
|
import { getBeastImageHost, imageComfyFetch, sameImageHost } from '~/server/utils/imageComfy'
|
||||||
import {
|
import {
|
||||||
XAIGEN_LORA_MESSAGE,
|
XAIGEN_LORA_MESSAGE,
|
||||||
|
MINIMAX_TURBO_LORA,
|
||||||
filterLorasForStudio,
|
filterLorasForStudio,
|
||||||
isXaigenOnlyLora,
|
isXaigenOnlyLora,
|
||||||
|
loraIdentityKey,
|
||||||
normalizeLoraName,
|
normalizeLoraName,
|
||||||
normalizeLoraStack,
|
normalizeLoraStack,
|
||||||
persistLoraFields,
|
persistLoraFields,
|
||||||
|
resolveComfyLoraName,
|
||||||
type LoraKind,
|
type LoraKind,
|
||||||
type LoraStackItem
|
type LoraStackItem
|
||||||
} from '~/utils/loras'
|
} from '~/utils/loras'
|
||||||
import { isXaigenStudio } from '~/utils/videoModels'
|
import { isXaigenStudio, LTX_DISTILLED_LORA } from '~/utils/videoModels'
|
||||||
|
|
||||||
type WorkflowNode = { class_type: string; inputs: Record<string, unknown>; _meta?: { title?: string } }
|
type WorkflowNode = { class_type: string; inputs: Record<string, unknown>; _meta?: { title?: string } }
|
||||||
type WorkflowGraph = Record<string, WorkflowNode>
|
type WorkflowGraph = Record<string, WorkflowNode>
|
||||||
@@ -28,6 +31,7 @@ const OBJECT_INFO_NODES = [
|
|||||||
]
|
]
|
||||||
const MODEL_FEED_CLASSES = new Set(['BasicGuider', 'CFGGuider', 'KSampler', 'KSamplerAdvanced'])
|
const MODEL_FEED_CLASSES = new Set(['BasicGuider', 'CFGGuider', 'KSampler', 'KSamplerAdvanced'])
|
||||||
const CACHE_MS = 20_000
|
const CACHE_MS = 20_000
|
||||||
|
const SYSTEM_LORA_PREFERRED = [MINIMAX_TURBO_LORA, LTX_DISTILLED_LORA]
|
||||||
|
|
||||||
type LoraCache = {
|
type LoraCache = {
|
||||||
at: number
|
at: number
|
||||||
@@ -150,11 +154,10 @@ async function loadLoraCache(): Promise<LoraCache> {
|
|||||||
const image = imageHost && !same
|
const image = imageHost && !same
|
||||||
? await discoverFromHost('image').catch(() => ({ image: [] as string[], video: [] as string[] }))
|
? await discoverFromHost('image').catch(() => ({ image: [] as string[], video: [] as string[] }))
|
||||||
: video
|
: video
|
||||||
const xaigen = isXaigenStudio()
|
|
||||||
const next: LoraCache = {
|
const next: LoraCache = {
|
||||||
at: Date.now(),
|
at: Date.now(),
|
||||||
image: filterLorasForStudio(uniqueNames([...image.image, ...video.image]), xaigen),
|
image: uniqueNames([...image.image, ...video.image]),
|
||||||
video: filterLorasForStudio(uniqueNames([...video.video, ...image.video]), xaigen)
|
video: uniqueNames([...video.video, ...image.video])
|
||||||
}
|
}
|
||||||
cache = next
|
cache = next
|
||||||
return next
|
return next
|
||||||
@@ -166,22 +169,74 @@ async function loadLoraCache(): Promise<LoraCache> {
|
|||||||
|
|
||||||
export async function listStudioLoras() {
|
export async function listStudioLoras() {
|
||||||
try {
|
try {
|
||||||
return await loadLoraCache()
|
const listed = await loadLoraCache()
|
||||||
|
const xaigen = isXaigenStudio()
|
||||||
|
return {
|
||||||
|
at: listed.at,
|
||||||
|
image: filterLorasForStudio(listed.image, xaigen),
|
||||||
|
video: filterLorasForStudio(listed.video, xaigen)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return { at: 0, image: [] as string[], video: [] as string[] }
|
return { at: 0, image: [] as string[], video: [] as string[] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Unfiltered Comfy filenames for graph system LoRAs (MiniMax turbo, LTX distilled). Reuses the listing cache. */
|
||||||
|
export function cachedComfyLoraNames(kind?: LoraKind) {
|
||||||
|
if (!cache) return [] as string[]
|
||||||
|
if (kind === 'image') return uniqueNames([...cache.image, ...cache.video])
|
||||||
|
if (kind === 'video') return uniqueNames([...cache.video, ...cache.image])
|
||||||
|
return uniqueNames([...cache.image, ...cache.video])
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureComfyLoraNames(kind?: LoraKind) {
|
||||||
|
try {
|
||||||
|
await loadLoraCache()
|
||||||
|
} catch {
|
||||||
|
/* keep whatever was cached */
|
||||||
|
}
|
||||||
|
return cachedComfyLoraNames(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveGraphLoraNames(graph: WorkflowGraph, kind?: LoraKind) {
|
||||||
|
const names = cachedComfyLoraNames(kind)
|
||||||
|
for (const node of Object.values(graph)) {
|
||||||
|
if (node.class_type !== LORA_LOADER && node.class_type !== LORA_MODEL_ONLY) continue
|
||||||
|
const current = String(node.inputs.lora_name || '').trim()
|
||||||
|
if (!current) continue
|
||||||
|
const preferred = SYSTEM_LORA_PREFERRED.find(item => loraIdentityKey(item) === loraIdentityKey(current)) || current
|
||||||
|
node.inputs.lora_name = names.length ? resolveComfyLoraName(preferred, names) : preferred
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowedLoraNames(kind?: LoraKind) {
|
||||||
|
return filterLorasForStudio(cachedComfyLoraNames(kind), isXaigenStudio())
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveUserLoraName(name: string, kind?: LoraKind) {
|
||||||
|
const allowed = allowedLoraNames(kind)
|
||||||
|
if (!allowed.length) return name
|
||||||
|
return resolveComfyLoraName(name, allowed)
|
||||||
|
}
|
||||||
|
|
||||||
export function assertLoraAllowed(raw: unknown, kind: LoraKind) {
|
export function assertLoraAllowed(raw: unknown, kind: LoraKind) {
|
||||||
const name = normalizeLoraName(raw)
|
const name = normalizeLoraName(raw)
|
||||||
if (!name) return ''
|
if (!name) return ''
|
||||||
if (isXaigenOnlyLora(name) && !isXaigenStudio()) {
|
if (isXaigenOnlyLora(name) && !isXaigenStudio()) {
|
||||||
throw createError({ statusCode: 400, statusMessage: XAIGEN_LORA_MESSAGE })
|
throw createError({ statusCode: 400, statusMessage: XAIGEN_LORA_MESSAGE })
|
||||||
}
|
}
|
||||||
const known = cache?.[kind]
|
const allowed = allowedLoraNames(kind)
|
||||||
if (known?.length && !known.includes(name) && !known.some(item => item.replace(/\\/g, '/') === name)) {
|
if (allowed.length) {
|
||||||
|
const identity = loraIdentityKey(name)
|
||||||
|
const listed = allowed.some((item) => {
|
||||||
|
const path = item.replace(/\\/g, '/')
|
||||||
|
return path === name || path.toLowerCase() === name.toLowerCase() || loraIdentityKey(item) === identity
|
||||||
|
})
|
||||||
|
if (!listed) {
|
||||||
throw createError({ statusCode: 400, statusMessage: `Unknown ${kind} LoRA` })
|
throw createError({ statusCode: 400, statusMessage: `Unknown ${kind} LoRA` })
|
||||||
}
|
}
|
||||||
|
return resolveUserLoraName(name, kind)
|
||||||
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,10 +280,11 @@ function linkSource(value: unknown): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function alreadyHasLora(graph: WorkflowGraph, name: string) {
|
function alreadyHasLora(graph: WorkflowGraph, name: string) {
|
||||||
const wanted = name.replace(/\\/g, '/').toLowerCase()
|
const wanted = loraIdentityKey(name)
|
||||||
|
if (!wanted) return false
|
||||||
return Object.values(graph).some((node) => {
|
return Object.values(graph).some((node) => {
|
||||||
if (node.class_type !== LORA_LOADER && node.class_type !== LORA_MODEL_ONLY) return false
|
if (node.class_type !== LORA_LOADER && node.class_type !== LORA_MODEL_ONLY) return false
|
||||||
return String(node.inputs.lora_name || '').replace(/\\/g, '/').toLowerCase() === wanted
|
return loraIdentityKey(String(node.inputs.lora_name || '')) === wanted
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,8 +316,74 @@ function findModelFeed(graph: WorkflowGraph) {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bypassLoraNode(graph: WorkflowGraph, id: string) {
|
||||||
|
const node = graph[id]
|
||||||
|
if (!node) return
|
||||||
|
const model = Array.isArray(node.inputs.model) ? node.inputs.model : null
|
||||||
|
const clip = Array.isArray(node.inputs.clip) ? node.inputs.clip : null
|
||||||
|
for (const [otherId, other] of Object.entries(graph)) {
|
||||||
|
if (otherId === id) continue
|
||||||
|
for (const [key, value] of Object.entries(other.inputs)) {
|
||||||
|
const src = linkSource(value)
|
||||||
|
if (src !== id || !Array.isArray(value)) continue
|
||||||
|
const slot = value[1]
|
||||||
|
if (slot === 0 && model) other.inputs[key] = [model[0], model[1]]
|
||||||
|
else if (slot === 1 && clip) other.inputs[key] = [clip[0], clip[1]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete graph[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Optional Klein-style LoraLoader: user stack only. Empty stack bypasses model/CLIP around the loader. */
|
||||||
|
export function applyOptionalLoraLoaders(graph: WorkflowGraph, stack?: unknown, kind: LoraKind = 'image') {
|
||||||
|
const loaders = Object.entries(graph).filter(([, node]) => node.class_type === LORA_LOADER)
|
||||||
|
const xaigen = isXaigenStudio()
|
||||||
|
const items = normalizeLoraStack(stack)
|
||||||
|
.filter(item => xaigen || !isXaigenOnlyLora(item.name))
|
||||||
|
.map(item => ({
|
||||||
|
...item,
|
||||||
|
name: resolveUserLoraName(item.name, kind)
|
||||||
|
}))
|
||||||
|
.filter(item => item.name && (xaigen || !isXaigenOnlyLora(item.name)))
|
||||||
|
if (!loaders.length) {
|
||||||
|
applyUserLoraToGraph(graph, items)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!items.length) {
|
||||||
|
for (const [id] of loaders) bypassLoraNode(graph, id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const [id] of loaders) {
|
||||||
|
const first = items[0]
|
||||||
|
graph[id].inputs.lora_name = first.name
|
||||||
|
graph[id].inputs.strength_model = first.strengthModel
|
||||||
|
graph[id].inputs.strength_clip = first.strengthClip
|
||||||
|
graph[id]._meta = { title: items.length === 1 ? 'User LoRA' : 'User LoRA 1' }
|
||||||
|
let sourceId = id
|
||||||
|
for (const [index, item] of items.slice(1).entries()) {
|
||||||
|
const nodeId = `user:lora:${id}:${index + 1}`
|
||||||
|
injectAfter(graph, sourceId, nodeId, {
|
||||||
|
class_type: LORA_LOADER,
|
||||||
|
inputs: {
|
||||||
|
lora_name: item.name,
|
||||||
|
strength_model: item.strengthModel,
|
||||||
|
strength_clip: item.strengthClip,
|
||||||
|
model: [sourceId, 0],
|
||||||
|
clip: [sourceId, 1]
|
||||||
|
},
|
||||||
|
_meta: { title: `User LoRA ${index + 2}` }
|
||||||
|
})
|
||||||
|
sourceId = nodeId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function applyUserLoraToGraph(graph: WorkflowGraph, stack?: unknown) {
|
export function applyUserLoraToGraph(graph: WorkflowGraph, stack?: unknown) {
|
||||||
const items = normalizeLoraStack(stack).filter(item => !alreadyHasLora(graph, item.name))
|
const xaigen = isXaigenStudio()
|
||||||
|
const items = normalizeLoraStack(stack)
|
||||||
|
.filter(item => xaigen || !isXaigenOnlyLora(item.name))
|
||||||
|
.map(item => ({ ...item, name: resolveUserLoraName(item.name) }))
|
||||||
|
.filter(item => item.name && (xaigen || !isXaigenOnlyLora(item.name)) && !alreadyHasLora(graph, item.name))
|
||||||
if (!items.length) return
|
if (!items.length) return
|
||||||
|
|
||||||
const clipLoaders = Object.entries(graph).filter(([, node]) => node.class_type === LORA_LOADER)
|
const clipLoaders = Object.entries(graph).filter(([, node]) => node.class_type === LORA_LOADER)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
updateShotQueue
|
updateShotQueue
|
||||||
} from '~/server/utils/shotQueue'
|
} from '~/server/utils/shotQueue'
|
||||||
import { composeShotPrompt, allowIdentityRefs, type PermanenceRef } from '~/utils/globalLocks'
|
import { composeShotPrompt, allowIdentityRefs, type PermanenceRef } from '~/utils/globalLocks'
|
||||||
import { persistLoraFields } from '~/server/utils/loras'
|
import { persistLoraFields, ensureComfyLoraNames } from '~/server/utils/loras'
|
||||||
import { readLoraStack, resolveLoraStack } from '~/utils/loras'
|
import { readLoraStack, resolveLoraStack } from '~/utils/loras'
|
||||||
import type { LoraStackItem } from '~/utils/loras'
|
import type { LoraStackItem } from '~/utils/loras'
|
||||||
|
|
||||||
@@ -180,6 +180,7 @@ export async function queueMiniMax(
|
|||||||
}
|
}
|
||||||
emitChainJob(job, { type: 'status', message: queueing, progress: 6 })
|
emitChainJob(job, { type: 'status', message: queueing, progress: 6 })
|
||||||
await waitForComfySocket(job, 4000)
|
await waitForComfySocket(job, 4000)
|
||||||
|
await ensureComfyLoraNames('video')
|
||||||
|
|
||||||
const shotIndex = job.library?.chainIndex || 0
|
const shotIndex = job.library?.chainIndex || 0
|
||||||
const composedPrompt = composeShotPrompt({
|
const composedPrompt = composeShotPrompt({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import workflowV1 from '../assets/workflow_minimax_video.json'
|
|||||||
import workflowV2 from '../assets/workflow_minimax_video_v2.json'
|
import workflowV2 from '../assets/workflow_minimax_video_v2.json'
|
||||||
import workflowLtx from '../assets/workflow_ltx_video.json'
|
import workflowLtx from '../assets/workflow_ltx_video.json'
|
||||||
import { buildIdentityPrompt } from '~/utils/identityPrompt'
|
import { buildIdentityPrompt } from '~/utils/identityPrompt'
|
||||||
import { applyUserLoraToGraph } from '~/server/utils/loras'
|
import { applyUserLoraToGraph, resolveGraphLoraNames } from '~/server/utils/loras'
|
||||||
import type { LoraStackItem } from '~/utils/loras'
|
import type { LoraStackItem } from '~/utils/loras'
|
||||||
import {
|
import {
|
||||||
isLtxWorkflow,
|
isLtxWorkflow,
|
||||||
@@ -225,6 +225,7 @@ export function buildWorkflow(params: GenerateParams) {
|
|||||||
if (graph['105:104']?.inputs) delete graph['105:104'].inputs.audio_vae
|
if (graph['105:104']?.inputs) delete graph['105:104'].inputs.audio_vae
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolveGraphLoraNames(graph, 'video')
|
||||||
applyUserLoraToGraph(graph, params.loraStack || params.loraName)
|
applyUserLoraToGraph(graph, params.loraStack || params.loraName)
|
||||||
return graph
|
return graph
|
||||||
}
|
}
|
||||||
@@ -298,6 +299,7 @@ function buildLtxWorkflow(params: GenerateParams, version: VideoWorkflowId) {
|
|||||||
if (graph['12']?.inputs) graph['12'].inputs.latent = ['8', 0]
|
if (graph['12']?.inputs) graph['12'].inputs.latent = ['8', 0]
|
||||||
if (graph['14']?.inputs) graph['14'].inputs.latent_image = ['8', 0]
|
if (graph['14']?.inputs) graph['14'].inputs.latent_image = ['8', 0]
|
||||||
}
|
}
|
||||||
|
resolveGraphLoraNames(graph, 'video')
|
||||||
applyUserLoraToGraph(graph, params.loraStack || params.loraName)
|
applyUserLoraToGraph(graph, params.loraStack || params.loraName)
|
||||||
return graph
|
return graph
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-1
@@ -16,16 +16,52 @@ export const LORA_STRENGTH_DEFAULT = 1
|
|||||||
export const LORA_STRENGTH_STEP = 0.05
|
export const LORA_STRENGTH_STEP = 0.05
|
||||||
export const LORA_STACK_MAX = 8
|
export const LORA_STACK_MAX = 8
|
||||||
|
|
||||||
|
/** Graph system LoRA for MiniMax turbo. Not a user pick. */
|
||||||
|
export const MINIMAX_TURBO_LORA = 'minimax_h3_fl2v_turbo_8step_v1.0_comfyui_bf16.safetensors'
|
||||||
|
|
||||||
export function loraBasename(name: string) {
|
export function loraBasename(name: string) {
|
||||||
const trimmed = String(name || '').trim().replace(/\\/g, '/')
|
const trimmed = String(name || '').trim().replace(/\\/g, '/')
|
||||||
const parts = trimmed.split('/').filter(Boolean)
|
const parts = trimmed.split('/').filter(Boolean)
|
||||||
return parts[parts.length - 1] || trimmed
|
return parts[parts.length - 1] || trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loraIdentityKey(name: string) {
|
||||||
|
return loraBasename(name).replace(/^xaigen-?/i, '').toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
export function isXaigenOnlyLora(name: string) {
|
export function isXaigenOnlyLora(name: string) {
|
||||||
return /^xaigen/i.test(loraBasename(name))
|
return /^xaigen/i.test(loraBasename(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Pick the real Comfy filename: exact path, then same basename ignoring `xaigen-`, then suffix/contains. */
|
||||||
|
export function resolveComfyLoraName(wanted: string, available: string[]) {
|
||||||
|
const target = String(wanted || '').trim().replace(/\\/g, '/')
|
||||||
|
if (!target) return ''
|
||||||
|
if (!available.length) return target
|
||||||
|
const targetLower = target.toLowerCase()
|
||||||
|
const exact = available.find(item => item.replace(/\\/g, '/') === target)
|
||||||
|
if (exact) return exact
|
||||||
|
const exactCi = available.find(item => item.replace(/\\/g, '/').toLowerCase() === targetLower)
|
||||||
|
if (exactCi) return exactCi
|
||||||
|
|
||||||
|
const identity = loraIdentityKey(target)
|
||||||
|
const identityHit = available.find(item => loraIdentityKey(item) === identity)
|
||||||
|
if (identityHit) return identityHit
|
||||||
|
|
||||||
|
const stem = identity.replace(/\.(safetensors|sft|ckpt|pt|bin)$/i, '')
|
||||||
|
if (!stem) return target
|
||||||
|
const containsHits = available.filter((item) => {
|
||||||
|
const base = loraBasename(item).toLowerCase()
|
||||||
|
return base.includes(stem) || loraIdentityKey(item).includes(stem)
|
||||||
|
})
|
||||||
|
if (!containsHits.length) return target
|
||||||
|
const suffix = containsHits.find((item) => {
|
||||||
|
const base = loraBasename(item).toLowerCase()
|
||||||
|
return base.endsWith(identity) || base.endsWith(stem) || loraIdentityKey(item).endsWith(identity)
|
||||||
|
})
|
||||||
|
return suffix || containsHits[0]
|
||||||
|
}
|
||||||
|
|
||||||
export function loraDisplayName(name: string) {
|
export function loraDisplayName(name: string) {
|
||||||
return loraBasename(name).replace(/\.(safetensors|sft|ckpt|pt|bin)$/i, '')
|
return loraBasename(name).replace(/\.(safetensors|sft|ckpt|pt|bin)$/i, '')
|
||||||
}
|
}
|
||||||
@@ -106,7 +142,7 @@ export function normalizeLoraStack(raw: unknown): LoraStackItem[] {
|
|||||||
if (out.length >= LORA_STACK_MAX) break
|
if (out.length >= LORA_STACK_MAX) break
|
||||||
const item = parseStackEntry(entry)
|
const item = parseStackEntry(entry)
|
||||||
if (!item) continue
|
if (!item) continue
|
||||||
const key = loraKey(item.name)
|
const key = loraIdentityKey(item.name)
|
||||||
if (seen.has(key)) continue
|
if (seen.has(key)) continue
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
out.push(item)
|
out.push(item)
|
||||||
@@ -159,6 +195,7 @@ export function loraStacksEqual(a?: unknown, b?: unknown) {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** User LoRA picker only. `xaigen-*` names are visible only on xAIGen. */
|
||||||
export function filterLorasForStudio(names: string[], xaigenStudio: boolean) {
|
export function filterLorasForStudio(names: string[], xaigenStudio: boolean) {
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const out: string[] = []
|
const out: string[] = []
|
||||||
|
|||||||
Reference in New Issue
Block a user