Keep Image v2 LoRA names and Flux/Krea chains when Comfy is asleep.
The Add picker vanished after the listing cache went empty, and switching engines wrote one stack over the other. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -80,24 +80,23 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<p v-if="!clipStrength && model.length" class="mt-1 text-[11px] text-zinc-500">CLIP unused on this graph — model strength only.</p>
|
<p v-if="!clipStrength && model.length" class="mt-1 text-[11px] text-zinc-500">CLIP unused on this graph — model strength only.</p>
|
||||||
<select
|
<select
|
||||||
v-if="availableNames.length && model.length < LORA_STACK_MAX"
|
v-if="model.length < LORA_STACK_MAX"
|
||||||
class="w-full rounded-xl border border-white/10 bg-zinc-950 outline-none ring-amber-300/40 focus:ring-2 disabled:opacity-50"
|
class="w-full rounded-xl border border-white/10 bg-zinc-950 outline-none ring-amber-300/40 focus:ring-2 disabled:opacity-50"
|
||||||
:class="[
|
:class="[
|
||||||
compact ? 'px-2 py-1 text-xs' : 'px-3 py-2 text-sm',
|
compact ? 'px-2 py-1 text-xs' : 'px-3 py-2 text-sm',
|
||||||
model.length ? 'mt-2' : ''
|
model.length ? 'mt-2' : ''
|
||||||
]"
|
]"
|
||||||
:disabled="disabled"
|
:disabled="disabled || !availableNames.length"
|
||||||
:value="''"
|
:value="''"
|
||||||
@change="onAdd"
|
@change="onAdd"
|
||||||
>
|
>
|
||||||
<option value="">{{ model.length ? addLabel : emptyLabel }}</option>
|
<option value="">{{ selectPlaceholder }}</option>
|
||||||
<option
|
<option
|
||||||
v-for="name in availableNames"
|
v-for="name in availableNames"
|
||||||
:key="name"
|
:key="name"
|
||||||
:value="name"
|
:value="name"
|
||||||
>{{ loraDisplayName(name) }}</option>
|
>{{ loraDisplayName(name) }}</option>
|
||||||
</select>
|
</select>
|
||||||
<p v-else-if="!model.length" class="text-[11px] text-zinc-500">{{ emptyLabel }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -120,6 +119,7 @@ const props = withDefaults(defineProps<{
|
|||||||
label?: string
|
label?: string
|
||||||
emptyLabel?: string
|
emptyLabel?: string
|
||||||
addLabel?: string
|
addLabel?: string
|
||||||
|
missingLabel?: string
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
clipStrength?: boolean
|
clipStrength?: boolean
|
||||||
@@ -127,6 +127,7 @@ const props = withDefaults(defineProps<{
|
|||||||
label: 'LoRA',
|
label: 'LoRA',
|
||||||
emptyLabel: 'None',
|
emptyLabel: 'None',
|
||||||
addLabel: 'Add LoRA',
|
addLabel: 'Add LoRA',
|
||||||
|
missingLabel: 'Wake GPU to load LoRAs',
|
||||||
compact: false,
|
compact: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
clipStrength: true
|
clipStrength: true
|
||||||
@@ -142,6 +143,13 @@ const availableNames = computed(() => {
|
|||||||
return props.names.filter(name => !taken.has(loraIdentityKey(name)))
|
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) {
|
function onAdd(event: Event) {
|
||||||
const name = (event.target as HTMLSelectElement).value
|
const name = (event.target as HTMLSelectElement).value
|
||||||
;(event.target as HTMLSelectElement).value = ''
|
;(event.target as HTMLSelectElement).value = ''
|
||||||
|
|||||||
+65
-14
@@ -761,10 +761,11 @@
|
|||||||
v-model="imageV2LoraStack"
|
v-model="imageV2LoraStack"
|
||||||
:names="imageV2LoraOptions"
|
:names="imageV2LoraOptions"
|
||||||
label="LoRA"
|
label="LoRA"
|
||||||
empty-label="None"
|
empty-label="Add LoRA"
|
||||||
|
missing-label="Wake GPU to load LoRAs"
|
||||||
compact
|
compact
|
||||||
/>
|
/>
|
||||||
<p v-if="v2Engine === 'krea'" class="mt-1 text-[11px] text-zinc-500">Flux LoRAs stay off on Krea.</p>
|
<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>
|
</div>
|
||||||
<div v-if="v2Mode === 'generate'" class="space-y-3 rounded-2xl border border-white/10 bg-zinc-950/40 p-3">
|
<div v-if="v2Mode === 'generate'" class="space-y-3 rounded-2xl border border-white/10 bg-zinc-950/40 p-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -3256,10 +3257,37 @@ function applyComfyHealth(health: {
|
|||||||
imageSidecarHost.value = String(health?.imageComfy?.sidecar?.host || imageSidecarHost.value || '')
|
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() {
|
async function pollLoras() {
|
||||||
const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras').catch(() => ({ image: [], video: [] }))
|
const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras').catch(() => ({ image: [], video: [] }))
|
||||||
videoLoras.value = listed.video || []
|
const nextVideo = listed.video || []
|
||||||
imageLoras.value = listed.image || []
|
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() {
|
async function pollComfyHealth() {
|
||||||
@@ -3315,7 +3343,9 @@ onMounted(async () => {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
videoLoraStack.value = readStoredLoraStack('aigen-video-lora')
|
videoLoraStack.value = readStoredLoraStack('aigen-video-lora')
|
||||||
imageLoraStack.value = readStoredLoraStack('aigen-image-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()
|
seedImageV2LorasIfEmpty()
|
||||||
globalLocks.value = localStorage.getItem(LOCKS_STORE) || ''
|
globalLocks.value = localStorage.getItem(LOCKS_STORE) || ''
|
||||||
try {
|
try {
|
||||||
@@ -3482,7 +3512,7 @@ watch(imageLoraStack, (value) => {
|
|||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
watch(imageV2LoraStack, (value) => {
|
watch(imageV2LoraStack, (value) => {
|
||||||
try { localStorage.setItem('aigen-image-v2-lora', JSON.stringify(value)) } catch { /* ignore */ }
|
persistImageV2LoraStack(value, v2Engine.value)
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
function clampDuration(value: number) {
|
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[]) {
|
function mergeLoraOptions(listed: string[], extra: string[]) {
|
||||||
const names = [...listed]
|
const names = [...listed]
|
||||||
for (const name of extra) {
|
for (const name of extra) {
|
||||||
@@ -3718,6 +3768,7 @@ function applyGenerationPreset(preset: GenerationPreset) {
|
|||||||
const skipped = preset.loraStack.length - stack.length
|
const skipped = preset.loraStack.length - stack.length
|
||||||
if (preset.kind === 'imagev2') {
|
if (preset.kind === 'imagev2') {
|
||||||
const settings = preset.settings as ImageV2PresetSettings
|
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'
|
v2Mode.value = settings.mode === 'compose' ? 'compose' : settings.mode === 'refine' ? 'refine' : settings.mode === 'generate' ? 'generate' : 'edit'
|
||||||
v2Engine.value = parseImageV2Engine(settings.engine, 'flux')
|
v2Engine.value = parseImageV2Engine(settings.engine, 'flux')
|
||||||
v2Task.value = v2Mode.value === 'compose' ? (settings.task || 'scene') : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'generate' ? 't2i' : 'scene'
|
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
|
? settings.mode
|
||||||
: 'edit'
|
: 'edit'
|
||||||
setV2Mode(mode)
|
setV2Mode(mode)
|
||||||
|
persistImageV2LoraStack(imageV2LoraStack.value, v2Engine.value)
|
||||||
v2Engine.value = parseImageV2Engine(settings?.engine, 'flux')
|
v2Engine.value = parseImageV2Engine(settings?.engine, 'flux')
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
if (typeof settings.negative === 'string') v2Negative.value = settings.negative
|
if (typeof settings.negative === 'string') v2Negative.value = settings.negative
|
||||||
@@ -5638,19 +5690,19 @@ function setV2Mode(mode: ImageV2Mode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setV2Engine(engine: ImageV2Engine) {
|
function setV2Engine(engine: ImageV2Engine) {
|
||||||
|
if (engine === v2Engine.value) return
|
||||||
const previous = v2Engine.value
|
const previous = v2Engine.value
|
||||||
|
const previousStack = imageV2LoraStack.value
|
||||||
|
persistImageV2LoraStack(previousStack, previous)
|
||||||
v2Engine.value = engine
|
v2Engine.value = engine
|
||||||
|
imageV2LoraStack.value = readImageV2LoraStack(engine)
|
||||||
|
seedImageV2LorasIfEmpty()
|
||||||
if (engine === 'krea') {
|
if (engine === 'krea') {
|
||||||
v2Steps.value = IMAGE_V2_KREA_STEPS
|
v2Steps.value = IMAGE_V2_KREA_STEPS
|
||||||
v2Cfg.value = IMAGE_V2_KREA_CFG
|
v2Cfg.value = IMAGE_V2_KREA_CFG
|
||||||
v2Turbo.value = false
|
v2Turbo.value = false
|
||||||
const dropped = imageV2LoraStack.value.filter(item => isKleinImageLora(item.name))
|
if (previousStack.some(item => isKleinImageLora(item.name))) {
|
||||||
if (dropped.length) {
|
toast('Flux LoRAs stay on the Flux engine. Switch back to restore that chain.')
|
||||||
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 (v2Mode.value === 'edit') v2Denoise.value = IMAGE_V2_KREA_EDIT_DENOISE
|
if (v2Mode.value === 'edit') v2Denoise.value = IMAGE_V2_KREA_EDIT_DENOISE
|
||||||
if (v2Mode.value === 'compose') v2Denoise.value = IMAGE_V2_KREA_COMPOSE_DENOISE
|
if (v2Mode.value === 'compose') v2Denoise.value = IMAGE_V2_KREA_COMPOSE_DENOISE
|
||||||
@@ -5659,7 +5711,6 @@ function setV2Engine(engine: ImageV2Engine) {
|
|||||||
if (previous === 'krea') {
|
if (previous === 'krea') {
|
||||||
v2Steps.value = IMAGE_V2_STEPS_DEFAULT
|
v2Steps.value = IMAGE_V2_STEPS_DEFAULT
|
||||||
v2Cfg.value = IMAGE_V2_CFG_DEFAULT
|
v2Cfg.value = IMAGE_V2_CFG_DEFAULT
|
||||||
seedImageV2LorasIfEmpty()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -161,6 +161,11 @@ async function loadLoraCache(): Promise<LoraCache> {
|
|||||||
image: uniqueNames([...image.image, ...video.image]),
|
image: uniqueNames([...image.image, ...video.image]),
|
||||||
video: uniqueNames([...video.video, ...image.video])
|
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
|
cache = next
|
||||||
return next
|
return next
|
||||||
})().finally(() => {
|
})().finally(() => {
|
||||||
@@ -170,15 +175,22 @@ async function loadLoraCache(): Promise<LoraCache> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function listStudioLoras() {
|
export async function listStudioLoras() {
|
||||||
|
const xaigen = isXaigenStudio()
|
||||||
try {
|
try {
|
||||||
const listed = await loadLoraCache()
|
const listed = await loadLoraCache()
|
||||||
const xaigen = isXaigenStudio()
|
|
||||||
return {
|
return {
|
||||||
at: listed.at,
|
at: listed.at,
|
||||||
image: filterLorasForStudio(listed.image, xaigen),
|
image: filterLorasForStudio(listed.image, xaigen),
|
||||||
video: filterLorasForStudio(listed.video, xaigen)
|
video: filterLorasForStudio(listed.video, xaigen)
|
||||||
}
|
}
|
||||||
} catch {
|
} 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[] }
|
return { at: 0, image: [] as string[], video: [] as string[] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user