diff --git a/components/LoraSelect.vue b/components/LoraSelect.vue index ca62334..1a6f385 100644 --- a/components/LoraSelect.vue +++ b/components/LoraSelect.vue @@ -32,6 +32,12 @@ ⋮⋮ {{ loraDisplayName(item.name) }} + }>(), { label: 'LoRA', emptyLabel: 'None', @@ -119,9 +127,15 @@ const props = withDefaults(defineProps<{ missingLabel: 'Wake GPU to load LoRAs', compact: false, disabled: false, - clipStrength: true + clipStrength: true, + triggers: () => ({}) }) +const emit = defineEmits<{ + 'insert-trigger': [text: string] + 'update-trigger': [name: string, trigger: string] +}>() + const model = defineModel({ default: () => [] }) const draggingIndex = ref(null) @@ -201,4 +215,12 @@ function setStrength(index: number, key: 'strengthModel' | 'strengthClip', value const next = clampLoraStrength(value) model.value = model.value.map((item, i) => i === index ? { ...item, [key]: next } : item) } + +function triggerFor(name: string) { + return loraTriggerFor(name, props.triggers) +} + +function saveTrigger(name: string, trigger: string) { + emit('update-trigger', name, trigger) +} diff --git a/components/LoraTriggerTip.vue b/components/LoraTriggerTip.vue new file mode 100644 index 0000000..54dda28 --- /dev/null +++ b/components/LoraTriggerTip.vue @@ -0,0 +1,82 @@ + + + + + + + + + + + Activation + {{ text }} + No word yet. Type one, then insert it into the prompt. + + + + Insert into prompt + + Save + + + + + + + diff --git a/components/SettingsModal.vue b/components/SettingsModal.vue index 6675458..bf92760 100644 --- a/components/SettingsModal.vue +++ b/components/SettingsModal.vue @@ -159,6 +159,7 @@ Short names used in auto-generated output titles. Leave blank to use the file stem (XAIgen-cline becomes cline). + Activation is the word or phrase the i on a LoRA slider inserts into the prompt. Loading LoRAs… @@ -172,13 +173,22 @@ {{ row.file }} {{ row.kind === 'both' ? 'Image · Video' : row.kind }} - + + + + @@ -279,7 +289,7 @@ const newPresetName = ref('') const newPresetPrompt = ref('') const promptPresets = ref>([]) const loraRowsLoading = ref(true) -const loraRows = ref>([]) +const loraRows = ref>([]) const loginUser = ref(props.username || '') const loginCurrent = ref('') const loginNext = ref('') @@ -462,9 +472,10 @@ async function loadLoraKeys() { loraRowsLoading.value = true try { const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras').catch(() => ({ image: [], video: [] })) - const library = await $fetch<{ loraAliases?: Record }>('/api/library?light=1').catch(() => ({ loraAliases: {} })) + const library = await $fetch<{ loraAliases?: Record; loraTriggers?: Record }>('/api/library?light=1').catch(() => ({ loraAliases: {}, loraTriggers: {} })) const aliases = library.loraAliases || {} - const map = new Map() + const triggers = library.loraTriggers || {} + const map = new Map() const add = (name: string, kind: 'image' | 'video') => { if (isFilenameSystemLora(name)) return const key = loraIdentityKey(name) @@ -479,6 +490,7 @@ async function loadLoraKeys() { file: loraDisplayName(name), kind, draft: aliases[key] || '', + trigger: triggers[key] || '', placeholder: defaultLoraFilenameKey(name) }) } @@ -501,6 +513,17 @@ async function saveLoraAlias(row: { key: string; draft: string }) { } } +async function saveLoraTrigger(row: { key: string; trigger: string }) { + try { + emit('updated', await $fetch('/api/library/lora-triggers', { + method: 'PUT', + body: { key: row.key, trigger: row.trigger } + })) + } catch (error: any) { + emit('error', error?.data?.statusMessage || 'Could not save LoRA activation word') + } +} + async function addPreset() { const name = newPresetName.value.trim() const systemPrompt = newPresetPrompt.value.trim() diff --git a/pages/index.vue b/pages/index.vue index c5edd51..84e9c29 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -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" /> - + Flux LoRAs stay on the Flux engine. Switch back to restore that chain. @@ -897,7 +916,16 @@ {{ imageAspectHelp }} - + @@ -1345,7 +1373,17 @@ label="Extension duration" hint="Type any length, or hover the wheel and scroll. Shift-scroll jumps by 1s." /> - + ([]) const stills = ref([]) const drafts = ref([]) const loraAliases = ref>({}) +const loraTriggers = ref>({}) const folderId = ref('') const browseFolderId = ref('') const renamingId = ref('') @@ -2578,17 +2621,23 @@ const minimaxGraph = ref('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 } | unknown) { - const payload = (data || {}) as { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record } +function applyLibrary(data: { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record; loraTriggers?: Record } | unknown) { + const payload = (data || {}) as { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record; loraTriggers?: Record } 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 }>(light ? '/api/library?light=1' : '/api/library') + const data = await $fetch<{ folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record; loraTriggers?: Record }>(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) diff --git a/pages/queue.vue b/pages/queue.vue index 8c91ae7..22df9cd 100644 --- a/pages/queue.vue +++ b/pages/queue.vue @@ -193,12 +193,15 @@ class="mt-3 max-w-md" :model-value="readLoraStack(segment)" :names="videoLoraOptions" + :triggers="loraTriggers" label="LoRA" empty-label="Default" add-label="Add LoRA" compact :clip-strength="false" @update:model-value="(value) => { setSegmentStack(segment, value); saveSegment(job.shots, segment) }" + @insert-trigger="insertSegmentTrigger(job.shots, segment, $event)" + @update-trigger="saveLoraTrigger" /> {{ segmentLoraLabel(job.shots, segment) }} @@ -335,12 +338,15 @@ class="mt-3 max-w-md" :model-value="readLoraStack(segment)" :names="videoLoraOptions" + :triggers="loraTriggers" label="LoRA" empty-label="Default" add-label="Add LoRA" compact :clip-strength="false" @update:model-value="(value) => { setSegmentStack(segment, value); saveSegment(queue, segment) }" + @insert-trigger="insertSegmentTrigger(queue, segment, $event)" + @update-trigger="saveLoraTrigger" /> {{ segmentLoraLabel(queue, segment) }} @@ -397,7 +403,7 @@
Activation
{{ text }}
No word yet. Type one, then insert it into the prompt.
Short names used in auto-generated output titles. Leave blank to use the file stem (XAIgen-cline becomes cline). + Activation is the word or phrase the i on a LoRA slider inserts into the prompt.
Loading LoRAs…
{{ row.file }}
{{ row.kind === 'both' ? 'Image · Video' : row.kind }}
Flux LoRAs stay on the Flux engine. Switch back to restore that chain.
{{ imageAspectHelp }}
{{ segmentLoraLabel(job.shots, segment) }}
{{ segmentLoraLabel(queue, segment) }}