Show the Qwen LoRA card with qwen-stem picks and apply them on generate.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-28 22:47:59 -05:00
co-authored by Cursor
parent fe560d7e40
commit c4e74628ee
5 changed files with 29 additions and 11 deletions
+13 -4
View File
@@ -171,9 +171,20 @@
</div> </div>
</div> </div>
<div v-if="form.engine!=='qwen21'"> <div v-if="['flux','krea','qwen21'].includes(form.engine)">
<span class="field-label">LoRA</span> <span class="field-label">LoRA</span>
<LoraSelect v-model="form.settings.loraStack" compact :names="pickerLoras" :use="currentLoraUse" :clip-strength="!video" filename-subtitle :aliases="{...loraNames.aliases,...library.loraAliases}" :triggers="{...loraNames.triggers,...library.loraTriggers}" missing-label="None" @insert-trigger="form.promptSections.action += ' '+$event" /> <LoraSelect
v-model="form.settings.loraStack"
compact
:names="pickerLoras"
:use="currentLoraUse"
:clip-strength="form.engine!=='qwen21' && !video"
filename-subtitle
:aliases="{...loraNames.aliases,...library.loraAliases}"
:triggers="{...loraNames.triggers,...library.loraTriggers}"
missing-label="None"
@insert-trigger="form.promptSections.action += ' '+$event"
/>
</div> </div>
<div> <div>
@@ -532,7 +543,6 @@ watch(()=>form.engine,(engine,prev)=>{
form.settings.cfg=1 form.settings.cfg=1
form.lockFace=false form.lockFace=false
form.lockOutfit=false form.lockOutfit=false
form.settings.loraStack=[]
} }
}) })
watch(()=>form.enhancePrompt,on=>writeStoredEnhancePrompt(!!on)) watch(()=>form.enhancePrompt,on=>writeStoredEnhancePrompt(!!on))
@@ -548,7 +558,6 @@ watch(()=>[form.mode,form.engine],([mode,engine])=>{
form.settings.cfg=1 form.settings.cfg=1
form.lockFace=false form.lockFace=false
form.lockOutfit=false form.lockOutfit=false
form.settings.loraStack=[]
} }
const plan=photosPlan({mode,engine}) const plan=photosPlan({mode,engine})
if(!plan.show){form.photos=[];form.imageAId='';form.imageBId='';if(engine==='qwen21')form.identityStillId='';return} if(!plan.show){form.photos=[];form.imageAId='';form.imageBId='';if(engine==='qwen21')form.identityStillId='';return}
+1 -1
View File
@@ -259,7 +259,7 @@
<template v-else> <template v-else>
<div> <div>
<span class="m-label">LoRA</span> <span class="m-label">LoRA</span>
<LoraSelect v-model="ctx.form.settings.loraStack" compact :names="ctx.pickerLoras" :use="ctx.currentLoraUse" :clip-strength="!ctx.video" filename-subtitle :aliases="ctx.loraAliases" :triggers="ctx.loraTriggers" missing-label="None" @insert-trigger="ctx.form.promptSections.action += ' '+$event" /> <LoraSelect v-model="ctx.form.settings.loraStack" compact :names="ctx.pickerLoras" :use="ctx.currentLoraUse" :clip-strength="ctx.form.engine!=='qwen21' && !ctx.video" filename-subtitle :aliases="ctx.loraAliases" :triggers="ctx.loraTriggers" missing-label="None" @insert-trigger="ctx.form.promptSections.action += ' '+$event" />
</div> </div>
<label class="m-label">Seed <label class="m-label">Seed
<div class="m-seed"> <div class="m-seed">
+2
View File
@@ -406,6 +406,8 @@ async function prepareGraph(r: any) {
r.sampleDenoise = null r.sampleDenoise = null
r.heroReferenceAttached = false r.heroReferenceAttached = false
} }
await ensureComfyLoraNames('image')
applyUserLoraToGraph(graph, s.loraStack)
} }
else { else {
const mode = identityPlan?.mode ?? (q.mode === 'iterate' ? (a ? 'edit' : 'generate') : q.mode); const mode = identityPlan?.mode ?? (q.mode === 'iterate' ? (a ? 'edit' : 'generate') : q.mode);
+1 -1
View File
@@ -79,7 +79,7 @@ export function validateRequest(raw, xaigen = false) {
photos: Array.isArray(raw.photos) ? raw.photos : [], photos: Array.isArray(raw.photos) ? raw.photos : [],
startFrameSource: raw.startClipId ? {kind:'previous-last-frame',clipId:raw.startClipId} : {kind:'still',stillId:raw.imageAId || ''}, startFrameSource: raw.startClipId ? {kind:'previous-last-frame',clipId:raw.startClipId} : {kind:'still',stillId:raw.imageAId || ''},
promptSections, compiledPrompt:compilePrompt(promptSections,video,{...raw,engine,lockFace:qwen?false:raw.lockFace,lockOutfit:qwen?false:raw.lockOutfit}), promptSections, compiledPrompt:compilePrompt(promptSections,video,{...raw,engine,lockFace:qwen?false:raw.lockFace,lockOutfit:qwen?false:raw.lockOutfit}),
settings: { aspect, fps:number('fps',24,1,60), width, height, steps, cfg, seedMode:raw.settings?.seedMode==='locked'?'locked':'random',seed:shotSeeds[0],duration:number('duration',5,1,15),turbo:raw.settings?.turbo !== false, loraStack:qwen?[]:(Array.isArray(raw.settings?.loraStack)?raw.settings.loraStack:[]) }, settings: { aspect, fps:number('fps',24,1,60), width, height, steps, cfg, seedMode:raw.settings?.seedMode==='locked'?'locked':'random',seed:shotSeeds[0],duration:number('duration',5,1,15),turbo:raw.settings?.turbo !== false, loraStack:Array.isArray(raw.settings?.loraStack)?raw.settings.loraStack:[] },
guides: (Array.isArray(raw.guides)?raw.guides:[]).slice(0,8).map(g=>{ if (!g.stillId || !Number.isInteger(g.frame) || g.frame<1 || g.frame>=frames) fail('Each guide requires a still and positive frame index.'); return {stillId:String(g.stillId),frame:g.frame} }), guides: (Array.isArray(raw.guides)?raw.guides:[]).slice(0,8).map(g=>{ if (!g.stillId || !Number.isInteger(g.frame) || g.frame<1 || g.frame>=frames) fail('Each guide requires a still and positive frame index.'); return {stillId:String(g.stillId),frame:g.frame} }),
batch: (Array.isArray(raw.batch)?raw.batch:[]).slice(0,30).map(s=>{const next=sectionsFrom({...promptSections,...(typeof s==='string'?{action:s}:s)});if(!next.action.trim())fail('Every shot needs an action prompt.');return video?next:{action:next.action}}) } batch: (Array.isArray(raw.batch)?raw.batch:[]).slice(0,30).map(s=>{const next=sectionsFrom({...promptSections,...(typeof s==='string'?{action:s}:s)});if(!next.action.trim())fail('Every shot needs an action prompt.');return video?next:{action:next.action}}) }
} }
+12 -5
View File
@@ -305,23 +305,30 @@ export function inferLoraUse(name: string): LoraUse[] | null {
return uses.length ? uses : null return uses.length ? uses : null
} }
/** Qwen 2.1 user adapters: basename stem starts with qwen or qwen2 (not klein). */
export function isQwenImageLora(name: string) {
const stem = loraIdentityKey(name).replace(/\.(safetensors|sft|ckpt|pt|bin)$/i, '')
return /^(qwen2|qwen)/i.test(stem)
}
export function loraMatchesUse(name: string, use: LoraUse) { export function loraMatchesUse(name: string, use: LoraUse) {
if (use === 'qwen21') return false if (isSystemGraphLora(name)) return false
return !isSystemGraphLora(name) && !!inferLoraUse(name)?.includes(use) if (use === 'qwen21') return isQwenImageLora(name)
return !!inferLoraUse(name)?.includes(use)
} }
/** Saved stacks may contain unlabelled adapters; never carry a known wrong engine. */ /** Saved stacks may contain unlabelled adapters; never carry a known wrong engine. */
export function filterLoraStackForUse(stack: unknown, use: LoraUse, xaigen: boolean) { export function filterLoraStackForUse(stack: unknown, use: LoraUse, xaigen: boolean) {
if (use === 'ltx' && !xaigen) return [] if (use === 'ltx' && !xaigen) return []
if (use === 'qwen21') return []
return filterLoraStackForStudio(stack, xaigen).filter(item => { return filterLoraStackForStudio(stack, xaigen).filter(item => {
if (isSystemGraphLora(item.name)) return false
if (use === 'qwen21') return isQwenImageLora(item.name)
const uses = inferLoraUse(item.name) const uses = inferLoraUse(item.name)
return !isSystemGraphLora(item.name) && (!uses || uses.includes(use)) return !uses || uses.includes(use)
}) })
} }
export function filterLorasForUse(names: string[], use: LoraUse) { export function filterLorasForUse(names: string[], use: LoraUse) {
if (use === 'qwen21') return []
return names.filter(name => loraMatchesUse(name, use)) return names.filter(name => loraMatchesUse(name, use))
} }