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
+98 -14
View File
@@ -451,12 +451,15 @@
class="mt-2"
:model-value="shotLoraStacks[shot.n] || []"
:names="videoLoraOptions"
:triggers="loraTriggers"
label="LoRA"
empty-label="Default"
add-label="Add LoRA"
compact
:clip-strength="false"
@update:model-value="setShotLoraStack(shot.n, $event)"
@insert-trigger="insertLoraTrigger($event, { shot: shot.n })"
@update-trigger="saveLoraTrigger"
/>
<PermanenceRefStrip
class="mt-2"
@@ -504,12 +507,15 @@
class="mt-3"
:model-value="item.loraStack || []"
:names="videoLoraOptions"
:triggers="loraTriggers"
label="LoRA"
empty-label="Default"
add-label="Add LoRA"
compact
:clip-strength="false"
@update:model-value="item.loraStack = $event"
@insert-trigger="insertLoraTrigger($event, { extensionId: item.id })"
@update-trigger="saveLoraTrigger"
/>
<PermanenceRefStrip
class="mt-3"
@@ -724,7 +730,17 @@
</div>
<div v-if="studioMode === 'video'" class="max-w-md space-y-2">
<LoraSelect v-model="videoLoraStack" :names="videoLoraOptions" label="LoRA" empty-label="None" compact :clip-strength="false" />
<LoraSelect
v-model="videoLoraStack"
:names="videoLoraOptions"
:triggers="loraTriggers"
label="LoRA"
empty-label="None"
compact
:clip-strength="false"
@insert-trigger="insertLoraTrigger($event)"
@update-trigger="saveLoraTrigger"
/>
<GenerationPresetBar
:presets="videoGenerationPresets"
v-model:selected-id="videoPresetId"
@@ -779,10 +795,13 @@
<LoraSelect
v-model="imageV2LoraStack"
:names="imageV2LoraOptions"
:triggers="loraTriggers"
label="LoRA"
empty-label="Add LoRA"
missing-label="Wake GPU to load LoRAs"
compact
@insert-trigger="insertLoraTrigger($event)"
@update-trigger="saveLoraTrigger"
/>
<p v-if="v2Engine === 'krea'" class="mt-1 text-[11px] text-zinc-500">Flux LoRAs stay on the Flux engine. Switch back to restore that chain.</p>
</div>
@@ -897,7 +916,16 @@
<p class="mt-2 text-xs text-zinc-500">{{ imageAspectHelp }}</p>
</div>
<div class="max-w-md space-y-2">
<LoraSelect v-model="imageLoraStack" :names="imageLoraOptions" label="LoRA" empty-label="None" compact />
<LoraSelect
v-model="imageLoraStack"
:names="imageLoraOptions"
:triggers="loraTriggers"
label="LoRA"
empty-label="None"
compact
@insert-trigger="insertLoraTrigger($event)"
@update-trigger="saveLoraTrigger"
/>
<label class="flex cursor-pointer items-center gap-2 text-xs text-zinc-400">
<span class="relative inline-flex h-5 w-9 shrink-0 items-center">
<input v-model="editScaleToTotalPixels" type="checkbox" class="peer sr-only" role="switch" aria-label="Scale to 1 MP">
@@ -1345,7 +1373,17 @@
label="Extension duration"
hint="Type any length, or hover the wheel and scroll. Shift-scroll jumps by 1s."
/>
<LoraSelect v-model="extendLoraStack" :names="videoLoraOptions" label="LoRA" empty-label="Same as clip / none" compact :clip-strength="false" />
<LoraSelect
v-model="extendLoraStack"
:names="videoLoraOptions"
:triggers="loraTriggers"
label="LoRA"
empty-label="Same as clip / none"
compact
:clip-strength="false"
@insert-trigger="insertLoraTrigger($event, 'extend')"
@update-trigger="saveLoraTrigger"
/>
<button
type="button"
class="w-full rounded-2xl bg-amber-400 px-4 py-2.5 text-sm font-semibold text-zinc-950 transition hover:bg-amber-300 disabled:cursor-not-allowed disabled:opacity-50"
@@ -2043,8 +2081,12 @@ import {
filterLoraStackForImageEngine,
filterLoraStackForStudio,
filterLorasForImageEngine,
filterLorasForUse,
insertActivationInShotScript,
insertActivationPhrase,
isKleinImageLora,
kleinLoraOnKreaMessage,
loraIdentityKey,
loraStacksEqual,
normalizeLoraStack,
persistLoraFields,
@@ -2524,6 +2566,7 @@ const clips = ref<LibraryClip[]>([])
const stills = ref<LibraryStill[]>([])
const drafts = ref<RetryDraft[]>([])
const loraAliases = ref<Record<string, string>>({})
const loraTriggers = ref<Record<string, string>>({})
const folderId = ref('')
const browseFolderId = ref('')
const renamingId = ref('')
@@ -2578,17 +2621,23 @@ const minimaxGraph = ref<MiniMaxGraphId>('v2')
const videoWorkflow = computed(() => composeVideoWorkflow(videoEngine.value, videoStart.value, minimaxGraph.value))
const textToVideo = computed(() => isTextToVideo(videoWorkflow.value))
const ltxVideo = computed(() => isLtxWorkflow(videoWorkflow.value))
const videoLoraOptions = computed(() => mergeLoraOptions(videoLoras.value, [
...videoLoraStack.value.map(item => item.name),
...extendLoraStack.value.map(item => item.name),
...Object.values(shotLoraStacks.value).flatMap(stack => stack.map(item => item.name)),
...extensionQueue.value.flatMap(item => (item.loraStack || []).map(entry => entry.name))
]))
const imageLoraOptions = computed(() => mergeLoraOptions(imageLoras.value, imageLoraStack.value.map(item => item.name)))
const videoLoraOptions = computed(() => mergeLoraOptions(
filterLorasForUse(videoLoras.value, ltxVideo.value ? 'ltx' : 'minimax'),
[
...videoLoraStack.value.map(item => item.name),
...extendLoraStack.value.map(item => item.name),
...Object.values(shotLoraStacks.value).flatMap(stack => stack.map(item => item.name)),
...extensionQueue.value.flatMap(item => (item.loraStack || []).map(entry => entry.name))
]
))
const imageLoraOptions = computed(() => mergeLoraOptions(
filterLorasForUse(imageLoras.value, 'flux'),
imageLoraStack.value.map(item => item.name)
))
const imageV2ConceptFile = computed(() => imageV2ConceptName())
const imageV2SnofsOn = computed(() => imageV2LoraStack.value.some(item => isImageV2ConceptLora(item.name, v2Engine.value)))
const imageV2LoraOptions = computed(() => mergeLoraOptions(
filterLorasForImageEngine(imageLoras.value, v2Engine.value),
filterLorasForUse(filterLorasForImageEngine(imageLoras.value, v2Engine.value), v2Engine.value),
filterLorasForImageEngine([
...imageV2LoraStack.value.map(item => item.name),
...(v2ConceptLoraEnabled.value && imageV2ConceptFile.value ? [imageV2ConceptFile.value] : [])
@@ -3454,8 +3503,8 @@ function applyInstancePrivacy() {
hideThumbnail.value = readPrivacyToggle('aigen-hide-output-preview', true)
}
function applyLibrary(data: { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record<string, string> } | unknown) {
const payload = (data || {}) as { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record<string, string> }
function applyLibrary(data: { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record<string, string>; loraTriggers?: Record<string, string> } | unknown) {
const payload = (data || {}) as { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record<string, string>; loraTriggers?: Record<string, string> }
folders.value = payload.folders || []
clips.value = payload.clips || []
stills.value = payload.stills || []
@@ -3463,6 +3512,9 @@ function applyLibrary(data: { folders?: LibraryFolder[]; clips?: LibraryClip[];
if (payload.loraAliases && typeof payload.loraAliases === 'object') {
loraAliases.value = payload.loraAliases
}
if (payload.loraTriggers && typeof payload.loraTriggers === 'object') {
loraTriggers.value = payload.loraTriggers
}
if (!folderId.value || !folders.value.some(folder => folder.id === folderId.value)) {
folderId.value = folders.value[0]?.id || ''
}
@@ -3472,7 +3524,7 @@ function applyLibrary(data: { folders?: LibraryFolder[]; clips?: LibraryClip[];
}
async function loadLibrary(light = false) {
const data = await $fetch<{ folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record<string, string> }>(light ? '/api/library?light=1' : '/api/library')
const data = await $fetch<{ folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record<string, string>; loraTriggers?: Record<string, string> }>(light ? '/api/library?light=1' : '/api/library')
applyLibrary(data)
if (light) return
const queued = await $fetch<{ pendingCount?: number }>('/api/queues').catch(() => ({ pendingCount: 0 }))
@@ -3933,6 +3985,38 @@ function mergeLoraOptions(listed: string[], extra: string[]) {
return names
}
async function saveLoraTrigger(name: string, trigger: string) {
try {
applyLibrary(await $fetch('/api/library/lora-triggers', {
method: 'PUT',
body: { key: loraIdentityKey(name), trigger }
}))
} catch (error: any) {
toast(error?.data?.statusMessage || 'Could not save activation word')
}
}
function insertLoraTrigger(phrase: string, target: 'main' | 'extend' | { shot: number } | { extensionId: string } = 'main') {
const token = String(phrase || '').trim()
if (!token) return
if (target === 'extend') {
extendPrompt.value = insertActivationPhrase(extendPrompt.value, token)
return
}
if (typeof target === 'object' && 'extensionId' in target) {
const item = extensionQueue.value.find(entry => entry.id === target.extensionId)
if (item) item.prompt = insertActivationPhrase(item.prompt, token)
return
}
if (typeof target === 'object' && 'shot' in target) {
prompt.value = insertActivationInShotScript(prompt.value, target.shot, token)
promptDrawerOpen.value = true
return
}
prompt.value = insertActivationPhrase(prompt.value, token)
promptDrawerOpen.value = true
}
function setShotLoraStack(shotN: number, stack: LoraStackItem[]) {
const next = { ...shotLoraStacks.value }
const items = normalizeLoraStack(stack)