Add LoRA stacks with per-item strengths and named video/image generation presets so setups survive reload without mixing Klein onto MiniMax. Keep LTX Pink Cherry and xAIGen LoRAs on xAIGen, and run Recommend on Beast.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+177
@@ -0,0 +1,177 @@
|
||||
export const XAIGEN_LORA_MESSAGE = 'This LoRA is only available on xAIGen'
|
||||
|
||||
export type LoraKind = 'image' | 'video'
|
||||
|
||||
export type LoraStackItem = {
|
||||
name: string
|
||||
strengthModel: number
|
||||
strengthClip: number
|
||||
}
|
||||
|
||||
export type LoraStack = LoraStackItem[]
|
||||
|
||||
export const LORA_STRENGTH_MIN = -2
|
||||
export const LORA_STRENGTH_MAX = 2
|
||||
export const LORA_STRENGTH_DEFAULT = 1
|
||||
export const LORA_STRENGTH_STEP = 0.05
|
||||
export const LORA_STACK_MAX = 8
|
||||
|
||||
export function loraBasename(name: string) {
|
||||
const trimmed = String(name || '').trim().replace(/\\/g, '/')
|
||||
const parts = trimmed.split('/').filter(Boolean)
|
||||
return parts[parts.length - 1] || trimmed
|
||||
}
|
||||
|
||||
export function isXaigenOnlyLora(name: string) {
|
||||
return /^xaigen/i.test(loraBasename(name))
|
||||
}
|
||||
|
||||
export function loraDisplayName(name: string) {
|
||||
return loraBasename(name).replace(/\.(safetensors|sft|ckpt|pt|bin)$/i, '')
|
||||
}
|
||||
|
||||
export function normalizeLoraName(raw: unknown) {
|
||||
const value = String(raw || '').trim().replace(/\\/g, '/')
|
||||
if (!value) return ''
|
||||
if (value.length > 240) return ''
|
||||
if (value.includes('..') || value.startsWith('/') || /[\0\n\r]/.test(value)) return ''
|
||||
if (!/\.(safetensors|sft|ckpt|pt|bin)$/i.test(value)) return ''
|
||||
return value
|
||||
}
|
||||
|
||||
export function clampLoraStrength(raw: unknown) {
|
||||
const value = Number(raw)
|
||||
if (!Number.isFinite(value)) return LORA_STRENGTH_DEFAULT
|
||||
const snapped = Math.round(value / LORA_STRENGTH_STEP) * LORA_STRENGTH_STEP
|
||||
return Math.min(LORA_STRENGTH_MAX, Math.max(LORA_STRENGTH_MIN, Math.round(snapped * 100) / 100))
|
||||
}
|
||||
|
||||
export function formatLoraStrength(value: number) {
|
||||
const n = clampLoraStrength(value)
|
||||
return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/0$/, '')
|
||||
}
|
||||
|
||||
function loraKey(name: string) {
|
||||
return name.replace(/\\/g, '/').toLowerCase()
|
||||
}
|
||||
|
||||
export function makeLoraStackItem(
|
||||
name: string,
|
||||
strengthModel: unknown = LORA_STRENGTH_DEFAULT,
|
||||
strengthClip: unknown = LORA_STRENGTH_DEFAULT
|
||||
): LoraStackItem | null {
|
||||
const normalized = normalizeLoraName(name)
|
||||
if (!normalized) return null
|
||||
return {
|
||||
name: normalized,
|
||||
strengthModel: clampLoraStrength(strengthModel),
|
||||
strengthClip: clampLoraStrength(strengthClip)
|
||||
}
|
||||
}
|
||||
|
||||
function parseStackEntry(raw: unknown): LoraStackItem | null {
|
||||
if (typeof raw === 'string') return makeLoraStackItem(raw)
|
||||
if (!raw || typeof raw !== 'object') return null
|
||||
const rec = raw as Record<string, unknown>
|
||||
const name = rec.name ?? rec.loraName ?? rec.lora_name
|
||||
return makeLoraStackItem(
|
||||
name,
|
||||
rec.strengthModel ?? rec.strength_model ?? LORA_STRENGTH_DEFAULT,
|
||||
rec.strengthClip ?? rec.strength_clip ?? LORA_STRENGTH_DEFAULT
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeLoraStack(raw: unknown): LoraStackItem[] {
|
||||
if (raw == null || raw === '') return []
|
||||
let parsed: unknown = raw
|
||||
if (typeof raw === 'string') {
|
||||
const trimmed = raw.trim()
|
||||
if (!trimmed) return []
|
||||
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
||||
try {
|
||||
parsed = JSON.parse(trimmed)
|
||||
} catch {
|
||||
const item = makeLoraStackItem(trimmed)
|
||||
return item ? [item] : []
|
||||
}
|
||||
} else {
|
||||
const item = makeLoraStackItem(trimmed)
|
||||
return item ? [item] : []
|
||||
}
|
||||
}
|
||||
const entries = Array.isArray(parsed) ? parsed : [parsed]
|
||||
const seen = new Set<string>()
|
||||
const out: LoraStackItem[] = []
|
||||
for (const entry of entries) {
|
||||
if (out.length >= LORA_STACK_MAX) break
|
||||
const item = parseStackEntry(entry)
|
||||
if (!item) continue
|
||||
const key = loraKey(item.name)
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
out.push(item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function readLoraStack(value?: { loraStack?: unknown; loraName?: unknown } | string | unknown[] | null) {
|
||||
if (value == null || value === '') return [] as LoraStackItem[]
|
||||
if (typeof value === 'string' || Array.isArray(value)) return normalizeLoraStack(value)
|
||||
const stack = normalizeLoraStack(value.loraStack)
|
||||
if (stack.length) return stack
|
||||
return normalizeLoraStack(value.loraName)
|
||||
}
|
||||
|
||||
export function resolveLoraName(fallback?: string | null, override?: string | null) {
|
||||
const over = normalizeLoraName(override)
|
||||
if (over) return over
|
||||
return normalizeLoraName(fallback)
|
||||
}
|
||||
|
||||
export function resolveLoraStack(fallback?: unknown, override?: unknown) {
|
||||
const over = normalizeLoraStack(override)
|
||||
if (over.length) return over
|
||||
return normalizeLoraStack(fallback)
|
||||
}
|
||||
|
||||
export function primaryLoraName(stack?: LoraStackItem[] | null) {
|
||||
return stack?.[0]?.name || ''
|
||||
}
|
||||
|
||||
export function loraStackLabel(stack?: LoraStackItem[] | null) {
|
||||
return (stack || []).map(item => loraDisplayName(item.name)).join(' · ')
|
||||
}
|
||||
|
||||
export function persistLoraFields(stack?: unknown) {
|
||||
const items = normalizeLoraStack(stack)
|
||||
if (!items.length) return { loraName: undefined as string | undefined, loraStack: undefined as LoraStackItem[] | undefined }
|
||||
return { loraName: items[0].name, loraStack: items }
|
||||
}
|
||||
|
||||
export function loraStacksEqual(a?: unknown, b?: unknown) {
|
||||
const left = normalizeLoraStack(a)
|
||||
const right = normalizeLoraStack(b)
|
||||
if (left.length !== right.length) return false
|
||||
return left.every((item, index) => (
|
||||
loraKey(item.name) === loraKey(right[index].name)
|
||||
&& item.strengthModel === right[index].strengthModel
|
||||
&& item.strengthClip === right[index].strengthClip
|
||||
))
|
||||
}
|
||||
|
||||
export function filterLorasForStudio(names: string[], xaigenStudio: boolean) {
|
||||
const seen = new Set<string>()
|
||||
const out: string[] = []
|
||||
for (const raw of names) {
|
||||
const name = String(raw || '').trim()
|
||||
if (!name || seen.has(name)) continue
|
||||
if (!xaigenStudio && isXaigenOnlyLora(name)) continue
|
||||
seen.add(name)
|
||||
out.push(name)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function filterLoraStackForStudio(stack: unknown, xaigenStudio: boolean) {
|
||||
return normalizeLoraStack(stack).filter(item => xaigenStudio || !isXaigenOnlyLora(item.name))
|
||||
}
|
||||
Reference in New Issue
Block a user