diff --git a/components/LoraSelect.vue b/components/LoraSelect.vue index ed42a91..d1518b8 100644 --- a/components/LoraSelect.vue +++ b/components/LoraSelect.vue @@ -80,24 +80,23 @@

CLIP unused on this graph — model strength only.

-

{{ emptyLabel }}

@@ -120,6 +119,7 @@ const props = withDefaults(defineProps<{ label?: string emptyLabel?: string addLabel?: string + missingLabel?: string compact?: boolean disabled?: boolean clipStrength?: boolean @@ -127,6 +127,7 @@ const props = withDefaults(defineProps<{ label: 'LoRA', emptyLabel: 'None', addLabel: 'Add LoRA', + missingLabel: 'Wake GPU to load LoRAs', compact: false, disabled: false, clipStrength: true @@ -142,6 +143,13 @@ const availableNames = computed(() => { return props.names.filter(name => !taken.has(loraIdentityKey(name))) }) +const selectPlaceholder = computed(() => { + if (!availableNames.value.length) { + return props.names.length ? 'All listed LoRAs are in the chain' : props.missingLabel + } + return model.value.length ? props.addLabel : props.emptyLabel +}) + function onAdd(event: Event) { const name = (event.target as HTMLSelectElement).value ;(event.target as HTMLSelectElement).value = '' diff --git a/pages/index.vue b/pages/index.vue index 4376512..2b22d0a 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -761,10 +761,11 @@ v-model="imageV2LoraStack" :names="imageV2LoraOptions" label="LoRA" - empty-label="None" + empty-label="Add LoRA" + missing-label="Wake GPU to load LoRAs" compact /> -

Flux LoRAs stay off on Krea.

+

Flux LoRAs stay on the Flux engine. Switch back to restore that chain.

@@ -3256,10 +3257,37 @@ function applyComfyHealth(health: { imageSidecarHost.value = String(health?.imageComfy?.sidecar?.host || imageSidecarHost.value || '') } +function readStoredLoraNames(key: string) { + try { + const parsed = JSON.parse(localStorage.getItem(key) || '[]') + if (!Array.isArray(parsed)) return [] as string[] + return parsed.filter((name): name is string => typeof name === 'string' && Boolean(name.trim())) + } catch { + return [] as string[] + } +} + +function rememberLoraNames(key: string, names: string[]) { + if (!names.length) return + try { localStorage.setItem(key, JSON.stringify(names)) } catch { /* ignore */ } +} + async function pollLoras() { const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras').catch(() => ({ image: [], video: [] })) - videoLoras.value = listed.video || [] - imageLoras.value = listed.image || [] + const nextVideo = listed.video || [] + const nextImage = listed.image || [] + if (nextVideo.length) { + videoLoras.value = nextVideo + rememberLoraNames('aigen-lora-names-video', nextVideo) + } else if (!videoLoras.value.length) { + videoLoras.value = readStoredLoraNames('aigen-lora-names-video') + } + if (nextImage.length) { + imageLoras.value = nextImage + rememberLoraNames('aigen-lora-names-image', nextImage) + } else if (!imageLoras.value.length) { + imageLoras.value = readStoredLoraNames('aigen-lora-names-image') + } } async function pollComfyHealth() { @@ -3315,7 +3343,9 @@ onMounted(async () => { } catch { /* ignore */ } videoLoraStack.value = readStoredLoraStack('aigen-video-lora') imageLoraStack.value = readStoredLoraStack('aigen-image-lora') - imageV2LoraStack.value = readStoredLoraStack('aigen-image-v2-lora') + videoLoras.value = readStoredLoraNames('aigen-lora-names-video') + imageLoras.value = readStoredLoraNames('aigen-lora-names-image') + imageV2LoraStack.value = readImageV2LoraStack(v2Engine.value) seedImageV2LorasIfEmpty() globalLocks.value = localStorage.getItem(LOCKS_STORE) || '' try { @@ -3482,7 +3512,7 @@ watch(imageLoraStack, (value) => { }, { deep: true }) watch(imageV2LoraStack, (value) => { - try { localStorage.setItem('aigen-image-v2-lora', JSON.stringify(value)) } catch { /* ignore */ } + persistImageV2LoraStack(value, v2Engine.value) }, { deep: true }) function clampDuration(value: number) { @@ -3536,6 +3566,26 @@ function readStoredLoraStack(key: string) { } } +function imageV2LoraStoreKey(engine: ImageV2Engine) { + return `aigen-image-v2-lora-${engine}` +} + +function persistImageV2LoraStack(stack: LoraStackItem[], engine: ImageV2Engine) { + try { + const payload = JSON.stringify(stack) + localStorage.setItem(imageV2LoraStoreKey(engine), payload) + localStorage.setItem('aigen-image-v2-lora', payload) + } catch { /* ignore */ } +} + +function readImageV2LoraStack(engine: ImageV2Engine) { + const keyed = readStoredLoraStack(imageV2LoraStoreKey(engine)) + if (keyed.length) return filterLoraStackForImageEngine(keyed, engine) + const legacy = readStoredLoraStack('aigen-image-v2-lora') + if (!legacy.length) return [] as LoraStackItem[] + return filterLoraStackForImageEngine(legacy, engine) +} + function mergeLoraOptions(listed: string[], extra: string[]) { const names = [...listed] for (const name of extra) { @@ -3718,6 +3768,7 @@ function applyGenerationPreset(preset: GenerationPreset) { const skipped = preset.loraStack.length - stack.length if (preset.kind === 'imagev2') { const settings = preset.settings as ImageV2PresetSettings + persistImageV2LoraStack(imageV2LoraStack.value, v2Engine.value) v2Mode.value = settings.mode === 'compose' ? 'compose' : settings.mode === 'refine' ? 'refine' : settings.mode === 'generate' ? 'generate' : 'edit' v2Engine.value = parseImageV2Engine(settings.engine, 'flux') v2Task.value = v2Mode.value === 'compose' ? (settings.task || 'scene') : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'generate' ? 't2i' : 'scene' @@ -5006,6 +5057,7 @@ function applyStillSettings(still: LibraryStill) { ? settings.mode : 'edit' setV2Mode(mode) + persistImageV2LoraStack(imageV2LoraStack.value, v2Engine.value) v2Engine.value = parseImageV2Engine(settings?.engine, 'flux') if (!settings) return if (typeof settings.negative === 'string') v2Negative.value = settings.negative @@ -5638,19 +5690,19 @@ function setV2Mode(mode: ImageV2Mode) { } function setV2Engine(engine: ImageV2Engine) { + if (engine === v2Engine.value) return const previous = v2Engine.value + const previousStack = imageV2LoraStack.value + persistImageV2LoraStack(previousStack, previous) v2Engine.value = engine + imageV2LoraStack.value = readImageV2LoraStack(engine) + seedImageV2LorasIfEmpty() if (engine === 'krea') { v2Steps.value = IMAGE_V2_KREA_STEPS v2Cfg.value = IMAGE_V2_KREA_CFG v2Turbo.value = false - const dropped = imageV2LoraStack.value.filter(item => isKleinImageLora(item.name)) - if (dropped.length) { - imageV2LoraStack.value = filterLoraStackForImageEngine(imageV2LoraStack.value, 'krea') - seedImageV2LorasIfEmpty() - toast(dropped.length === 1 - ? kleinLoraOnKreaMessage(dropped[0].name) - : 'Flux LoRAs were removed. They cannot be used with Krea.') + if (previousStack.some(item => isKleinImageLora(item.name))) { + toast('Flux LoRAs stay on the Flux engine. Switch back to restore that chain.') } if (v2Mode.value === 'edit') v2Denoise.value = IMAGE_V2_KREA_EDIT_DENOISE if (v2Mode.value === 'compose') v2Denoise.value = IMAGE_V2_KREA_COMPOSE_DENOISE @@ -5659,7 +5711,6 @@ function setV2Engine(engine: ImageV2Engine) { if (previous === 'krea') { v2Steps.value = IMAGE_V2_STEPS_DEFAULT v2Cfg.value = IMAGE_V2_CFG_DEFAULT - seedImageV2LorasIfEmpty() } } diff --git a/server/utils/loras.ts b/server/utils/loras.ts index 3ee9944..221b092 100644 --- a/server/utils/loras.ts +++ b/server/utils/loras.ts @@ -161,6 +161,11 @@ async function loadLoraCache(): Promise { image: uniqueNames([...image.image, ...video.image]), video: uniqueNames([...video.video, ...image.video]) } + // Comfy asleep / unreachable returns empty. Keep the last good list so the picker does not vanish. + if (!next.image.length && !next.video.length && cache && (cache.image.length || cache.video.length)) { + cache = { ...cache, at: Date.now() } + return cache + } cache = next return next })().finally(() => { @@ -170,15 +175,22 @@ async function loadLoraCache(): Promise { } export async function listStudioLoras() { + const xaigen = isXaigenStudio() try { const listed = await loadLoraCache() - const xaigen = isXaigenStudio() return { at: listed.at, image: filterLorasForStudio(listed.image, xaigen), video: filterLorasForStudio(listed.video, xaigen) } } catch { + if (cache) { + return { + at: cache.at, + image: filterLorasForStudio(cache.image, xaigen), + video: filterLorasForStudio(cache.video, xaigen) + } + } return { at: 0, image: [] as string[], video: [] as string[] } } }