Add LoRA activation words and hide LoRAs that do not match the current engine.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-31 20:32:12 -05:00
co-authored by Cursor
parent 6926538589
commit 529598cef2
8 changed files with 392 additions and 31 deletions
+95
View File
@@ -234,3 +234,98 @@ export function filterLoraStackForImageEngine(stack: unknown, engine: 'flux' | '
export function kleinLoraOnKreaMessage(name: string) {
return `${loraDisplayName(name)} is a Flux LoRA and cannot be used with Krea.`
}
export type LoraUse = 'minimax' | 'ltx' | 'flux' | 'krea'
export function isSystemGraphLora(name: string) {
const id = loraIdentityKey(name)
if (id === loraIdentityKey(MINIMAX_TURBO_LORA)) return true
if (/flux2-klein-9b-consistency/i.test(id)) return true
if (/ltx-2\.3-22b-distilled/i.test(id)) return true
return false
}
/** Filename hints. `null` means the LoRA can appear in any picker. */
export function inferLoraUse(name: string): LoraUse[] | null {
const id = loraIdentityKey(name)
if (/snofs_krea|(^|[_-])krea([_-]|$)/i.test(id)) return ['krea']
if (isKleinImageLora(name) || /flux-?2-klein|flux2-klein/i.test(id)) return ['flux']
if (/(^|[_-])ltx([_-]|$)|ltx-2/i.test(id)) return ['ltx']
if (/minimax|h3_fl2v|fl2v_turbo/i.test(id)) return ['minimax']
return null
}
export function loraMatchesUse(name: string, use: LoraUse) {
if (isSystemGraphLora(name)) return false
const inferred = inferLoraUse(name)
if (!inferred) return true
return inferred.includes(use)
}
export function filterLorasForUse(names: string[], use: LoraUse) {
return names.filter(name => loraMatchesUse(name, use))
}
export function sanitizeLoraTrigger(raw: unknown) {
return String(raw || '')
.replace(/\r\n/g, '\n')
.replace(/[\0\r]/g, '')
.replace(/[ \t]+\n/g, '\n')
.trim()
.slice(0, 240)
}
export function normalizeLoraTriggers(raw: unknown): Record<string, string> {
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
const out: Record<string, string> = {}
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
const id = loraIdentityKey(key) || String(key || '').trim().toLowerCase()
const trigger = sanitizeLoraTrigger(value)
if (!id || !trigger) continue
out[id] = trigger
}
return out
}
export function loraTriggerFor(name: string, triggers: Record<string, string> = {}) {
const id = loraIdentityKey(name)
return String(triggers[id] || triggers[loraBasename(name).toLowerCase()] || '').trim()
}
export function insertActivationPhrase(text: string, phrase: string) {
const src = String(text || '')
const token = sanitizeLoraTrigger(phrase)
if (!token) return src
if (src.includes(token)) return src
if (!src.trim()) return token
const needsSpace = !/\s$/.test(src) && !/^\s/.test(token)
return needsSpace ? `${src} ${token}` : `${src}${token}`
}
export function insertActivationInShotScript(script: string, shotN: number, phrase: string) {
const src = String(script || '')
const token = sanitizeLoraTrigger(phrase)
if (!token || src.includes(token)) return src
const marker = new RegExp(`(^[ \\t]*shot\\s+${Number(shotN)}\\b[^\\n]*\\n?)`, 'im')
const match = marker.exec(src)
if (!match || match.index == null) {
if (Number(shotN) === 1) {
const nextShot = src.search(/^[ \t]*shot\s+\d+\b/m)
if (nextShot > 0) {
const head = src.slice(0, nextShot).replace(/\s+$/, '')
const gap = src.slice(0, nextShot).match(/\s+$/)?.[0] || '\n\n'
return insertActivationPhrase(head, token) + gap + src.slice(nextShot)
}
}
return insertActivationPhrase(src, token)
}
const start = match.index + match[0].length
const rest = src.slice(start)
const next = rest.search(/^[ \t]*shot\s+\d+\b/m)
const end = next < 0 ? src.length : start + next
const block = src.slice(start, end)
const lead = block.match(/^\s*/)?.[0] || ''
const body = block.slice(lead.length).replace(/\s+$/, '')
const tail = block.match(/\s+$/)?.[0] ?? (next < 0 ? '' : '\n')
return src.slice(0, start) + lead + insertActivationPhrase(body, token) + tail + src.slice(end)
}