Follow sampler conditioning and enabled group presets

This commit is contained in:
Adachi
2026-11-09 15:09:44 +08:00
parent 1465bd8d7e
commit 4cb1be78ff
4 changed files with 106 additions and 13 deletions
+15 -4
View File
@@ -16,13 +16,13 @@ add(553,'Power Lora Loader (rgthree)',[
{name:'lora_7',value:{on:true,lora:'old7.safetensors',strength:1}}
]);
add(360,'Power Lora Loader (rgthree)');
const preset=add(524,'AdachiGroupPresets');
const preset=add(524,'AdachiGroupPresets');preset.mode=0;
const applied=[];
preset.adachiGroupPresets={model:{node:preset,data:{activeId:'anima',presets:[{id:'txt',name:'anima文生图'},{id:'anima',name:'anima修图'},{id:'wai',name:'wai修图'}]},apply:id=>applied.push(id)}};
preset.adachiGroupPresets={model:{node:preset,data:{activeId:'anima',presets:[{id:'txt',name:'anima文生图'},{id:'anima',name:'anima修图'},{id:'wai',name:'wai修图'}]},apply:id=>{applied.push(id);preset.adachiGroupPresets.model.data.activeId=id;}}};
const graph={_nodes:nodes,getNodeById:id=>nodes.find(n=>String(n.id)===String(id)),links:{}};
const bindings=new WorkspaceBindings({graph,sampler});
assert.equal(bindings.profiles.length,3);
assert.equal(bindings.profileId,'wai');
assert.equal(bindings.profileId,'anima');bindings.selectProfile('wai');
let result=bindings.getExecutionBinding();
assert.deepEqual(result.samplerInputs,{model:['518',0],vae:['518',2],positive:['521',0],negative:['520',0]});
assert.deepEqual(result.inputOverrides['521'],{clip:['518',1]});
@@ -37,10 +37,21 @@ assert.equal(result.inputOverrides['553'].lora_7.on,false,'old enabled LoRA rows
bindings.settings.positive='new prompt';
assert.equal(bindings.getExecutionBinding().inputOverrides['521'].text,'new prompt');
bindings.selectProfile('anima');
assert.deepEqual(applied,['anima']);
assert.deepEqual(applied,['wai','anima']);
assert.deepEqual(bindings.getExecutionBinding().samplerInputs.model,['359',0]);
assert.deepEqual(bindings.getExecutionBinding().inputOverrides['547'].clip,['357',0]);
bindings.selectProfile('wai');
assert.equal(bindings.settings.positive,'new prompt');
assert.equal(ckpt.widgets[0].value,'wai.safetensors','read-only discovery must not change a model widget');
console.log('Workspace bindings: existing presets, WAI/Anima mapping, no LoRA, LoRA strengths, prompt edits and profile isolation passed');
preset.mode=4;
const ignored=new WorkspaceBindings({graph,sampler});
assert.equal(ignored.controller,undefined,'ignored group controller must not create preset buttons');
assert.deepEqual(ignored.profiles.map(p=>p.id),['current']);
preset.mode=0;
preset.adachiGroupPresets.model.data.presets.push({id:'fourth',name:'custom branch'});
ignored.refreshProfiles();assert.equal(ignored.profiles.length,4,'preset count comes from active controller');
console.log('Group controller: ignored/missing fallback and dynamic preset count pass');
preset.adachiGroupPresets.model.data.activeId='anima';bindings.refreshProfiles();assert.equal(bindings.profileId,'anima','external preset switching is reflected');
+10
View File
@@ -0,0 +1,10 @@
import assert from 'node:assert/strict';import {conditioningSource} from '../web/workspace_routes.js';
const nodes=[];const add=(id,type,inputs=[],value)=>{const n={id,type,mode:0,inputs,widgets:[{value}]};nodes.push(n);return n};
const a=add(1,'CLIPTextEncode'),b=add(2,'CLIPTextEncode');const sa=add(3,'SetNode',[{name:'x',link:1}],'p');const ga=add(4,'GetNode',[],'p');const sb=add(5,'SetNode',[{name:'x',link:2}],'p2');const gb=add(6,'GetNode',[],'p2');const sw=add(7,'AdachiExclusiveSwitch',[{name:'input_a',link:3},{name:'input_b',link:4}]);const sampler=add(8,'AdachiWebUIInpaint',[{name:'positive',link:5},{name:'negative',link:6}]);
const links={1:{origin_id:1,origin_slot:0},2:{origin_id:2,origin_slot:0},3:{origin_id:4,origin_slot:0},4:{origin_id:6,origin_slot:0},5:{origin_id:7,origin_slot:0},6:{origin_id:2,origin_slot:0}};
const g={_nodes:nodes,links,getNodeById:id=>nodes.find(n=>n.id==id)};nodes.forEach(n=>n.graph=g);
assert.equal(conditioningSource(g,sampler,'positive'),null,'ambiguous branches must not be guessed');
assert.equal(conditioningSource(g,sampler,'positive',new Map([[sb,4]])),a);
assert.equal(conditioningSource(g,sampler,'negative'),b);
sa.inputs[0].link=3;assert.equal(conditioningSource(g,sampler,'positive',new Map([[sb,4]])),null,'cycle must stop');
console.log('Conditioning discovery: actual sampler route, Get/Set, preset state, ambiguity and cycles pass');
+46 -9
View File
@@ -1,3 +1,4 @@
import { conditioningSource } from './workspace_routes.js';
// Bind the workbench to existing graph widgets; no model or LoRA is loaded here.
const widget = (node, name) => node?.widgets?.find(w => w.name === name);
const nodeList = graph => Array.from(graph?._nodes || []);
@@ -12,14 +13,14 @@ export class WorkspaceBindings {
constructor({graph, sampler, onChange = () => {}, markChanged = () => {}}) {
Object.assign(this, {graph, sampler, onChange, markChanged});
this.nodes = nodeList(graph);
this.controller = this.nodes.find(n => n.type === 'AdachiGroupPresets')?.adachiGroupPresets?.model;
this.controller = this.nodes.find(n => n.type === 'AdachiGroupPresets' && n.mode === 0)?.adachiGroupPresets?.model;
this.owner = this.controller?.node || sampler;
this.owner.properties ??= {};
this.owner.properties[stateKey] ??= {profiles:{}};
this.catalog = {checkpoints:[], loras:[]};
this.profiles = this.controller?.data?.presets?.map(p => ({id:p.id, name:p.name, preset:p})) || [];
if (!this.profiles.length) this.profiles = [{id:'current',name:'当前工作流'}];
this.profileId = this.profiles.find(p => /wai/i.test(p.name))?.id || this.controller?.data?.activeId || this.profiles[0].id;
this.profileId = this.controller?.data?.activeId || this.profiles[0].id;
this.resolve();
}
resolve() {
@@ -47,6 +48,14 @@ export class WorkspaceBindings {
this.vae=this.model?.type==='CheckpointLoaderSimple'?this.model:null;this.vaeSlot=2;
this.lora=this.nodes.find(n=>n.type==='Power Lora Loader (rgthree)' && n.mode===0) || this.nodes.find(n=>n.type==='LoraLoader' && n.mode===0);
}
const planned = this.controller?.planStates && profile.preset ? this.controller.planStates(profile.preset).plan : new Map();
this.promptSources = {};
for (const kind of ['positive','negative']) {
const traced = conditioningSource(this.graph,this.sampler,kind,planned);
if (traced) { this[kind] = traced; this.promptSources[kind] = '实际采样器连线'; }
else if (this.isTemplate && profile.preset) this.promptSources[kind] = '当前模板方案';
else { this[kind] = null; this.promptSources[kind] = '由原工作流控制'; }
}
const all=this.owner.properties[stateKey].profiles;
all[this.profileId] ??= {loras:[]};
this.settings=all[this.profileId];
@@ -54,19 +63,40 @@ export class WorkspaceBindings {
return profile;
}
changed() { this.markChanged(); this.onChange(this); }
refreshProfiles() {
this.nodes=nodeList(this.graph);
const controller = this.nodes.find(n=>n.type==='AdachiGroupPresets' && n.mode===0)?.adachiGroupPresets?.model;
const signature = JSON.stringify([controller?.node?.id, controller?.data?.activeId, controller?.data?.presets?.map(p=>[p.id,p.name])]);
if(signature===this.presetSignature)return;
this.presetSignature=signature;
this.controller=controller;
this.owner=controller?.node || this.sampler;
this.owner.properties ??= {};
this.owner.properties[stateKey] ??= {profiles:{}};
this.profiles=controller?.data?.presets?.map(p=>({id:p.id,name:p.name,preset:p})) || [];
if(!this.profiles.length)this.profiles=[{id:'current',name:'当前工作流'}];
this.profileId=controller?.data?.activeId || (this.profiles.some(p=>p.id===this.profileId)?this.profileId:this.profiles[0].id);
this.resolve();this.render();this.onChange(this);
}
selectProfile(id) {
if (!this.profiles.some(p=>p.id===id)) throw Error('分组预设不存在');
this.profileId=id;
this.controller?.apply(id);
this.resolve(); this.render(); this.changed();
}
getExecutionBinding(id=this.profileId) {
if (id!==this.profileId) { this.profileId=id; this.resolve(); }
getExecutionBinding(id) {
this.refreshProfiles();
if (id!==undefined && id!==this.profileId) { this.profileId=id; this.resolve(); }
const binding={enableNodes:[],samplerInputs:{},inputOverrides:{},removeInputs:{}};
const add=node=>{if(node && !binding.enableNodes.includes(node))binding.enableNodes.push(node);};
// Generic graphs keep their original routing unless complete core
// model/conditioning nodes can be identified.
if (!this.model || !this.clip || !this.vae || this.positive?.type!=='CLIPTextEncode' || this.negative?.type!=='CLIPTextEncode') return binding;
if (!this.model || !this.clip || !this.vae || this.positive?.type!=='CLIPTextEncode' || this.negative?.type!=='CLIPTextEncode') {
for (const kind of ['positive','negative']) if(this[kind] && Object.hasOwn(this.settings,kind)) {
add(this[kind]); binding.inputOverrides[String(this[kind].id)] = {text:this.settings[kind]};
}
return binding;
}
for(const n of [this.model,this.clip,this.vae,this.positive,this.negative])add(n);
let model=ref(this.model),clip=ref(this.clip,this.clipSlot);
const selected=this.settings.loras.filter(l=>l.name && l.on!==false);
@@ -112,7 +142,9 @@ export class WorkspaceBindings {
const select=(values,current,change)=>{const s=el('select');for(const [value,label] of values){const o=el('option',label);o.value=value;s.append(o);}s.value=current;s.onchange=()=>change(s.value);return s;};
this.root.replaceChildren();
const presets=el('div');presets.className='aw-presets';
for(const p of this.profiles){const b=el('button',p.name);b.type='button';b.setAttribute('aria-pressed',String(p.id===this.profileId));b.onclick=()=>this.selectProfile(p.id);presets.append(b);}
const controllerActive = this.controller && this.controller.node.mode === 0;
if (!controllerActive) {const warning=el('p','未启用组忽略预设节点,当前使用原工作流设置。');warning.className='aw-profile-warning';warning.setAttribute('role','status');this.root.append(warning);}
for(const p of controllerActive ? this.profiles : []){const b=el('button',p.name);b.type='button';b.setAttribute('aria-pressed',String(p.id===this.profileId));b.onclick=()=>this.selectProfile(p.id);presets.append(b);}
this.root.append(presets);
const details=el('details'),summary=el('summary','主模型与 LoRA');details.append(summary);details.open=!!this.detailsOpen;details.ontoggle=()=>{this.detailsOpen=details.open;};
const modelWidget=widget(this.model,'ckpt_name') || widget(this.model,'unet_name');
@@ -135,12 +167,17 @@ export class WorkspaceBindings {
if(this.catalogError)details.append(el('small',this.catalogError));
this.root.append(details);
for(const [kind,node,label] of [['positive',this.positive,'正面提示词'],['negative',this.negative,'负面提示词']]) {
const w=widget(node,'text');if(!w)continue;
const w=widget(node,'text');
if(!w){const source=el('p',label+':由原工作流控制');source.title='未找到唯一的 CLIP 编码节点,请在原工作流调整该条件';this.root.append(source);continue;}
const linked=node.inputs?.some(i=>i.name==='text' && i.link!=null);
const text=el('textarea');text.rows=kind==='positive'?6:3;
text.value=this.settings[kind] ?? w.value ?? '';
text.value=this.settings[kind] ?? (linked ? '' : w.value) ?? '';
text.placeholder=node.inputs?.some(i=>i.name==='text' && i.link!=null)?'保持原工作流提示词;输入后以这里为准':'';
text.oninput=()=>{this.settings[kind]=text.value;this.changed();};
this.root.append(row(label,text));
text.setAttribute('aria-label',label);
const field=row(label,text);field.title=`${this.promptSources[kind]} · ${node.title || node.type} #${node.id}。${linked ? '文本来自上游节点;留空且未编辑时沿用上游,输入后覆盖。' : '编辑后作为本方案的提示词。'}`;
const reset=el('button','↶');reset.type='button';reset.title='恢复工作流中的'+label;reset.onclick=()=>{delete this.settings[kind];this.render();this.changed();};field.append(reset);
this.root.append(field);
}
}
destroy() {this.destroyed=true;this.root?.remove();}
+35
View File
@@ -0,0 +1,35 @@
const linkAt = (graph, input) => input?.link == null ? null
: graph.getLink?.(input.link) ?? graph.links?.get?.(input.link) ?? graph.links?.[input.link];
// Follow the actual sampler conditioning inputs, including KJ Get/Set routes.
// Ambiguous processing graphs are left under the workflow's control.
export function conditioningSource(graph, sampler, inputName, modes = new Map()) {
const mode = node => modes.get(node) ?? node.mode ?? 0;
const visit = (node, slot, seen = new Set()) => {
if (!node || seen.has(node) || mode(node) === 2) return null;
seen = new Set(seen); seen.add(node);
const follow = input => {
const link = linkAt(node.graph || graph, input);
return link ? visit((node.graph || graph).getNodeById(link.origin_id), link.origin_slot, seen) : null;
};
if (node.type === 'AdachiExclusiveSwitch') {
const sources = (node.inputs || []).filter(i=>['input_a','input_b'].includes(i.name)).map(follow).filter(Boolean);
return sources.length === 1 ? sources[0] : null;
}
if (node.type === 'GetNode') {
if (mode(node) !== 0) return null;
const name = node.widgets?.[0]?.value;
const setter = node.findSetter?.(node.graph || graph) || Array.from(graph._nodes || []).find(n=>n.type === 'SetNode' && n.widgets?.[0]?.value === name);
return setter ? visit(setter,slot,seen) : null;
}
if (node.type === 'SetNode' || node.type === 'Reroute') {
if (mode(node) !== 0) return null;
return follow(node.inputs?.[0]);
}
if (node.type === 'CLIPTextEncode') return mode(node) === 0 ? node : null;
const resolved = node.resolveVirtualOutput?.(slot);
if (resolved?.node) return visit(resolved.node,resolved.slot ?? 0,seen);
return null;
};
const link = linkAt(graph, sampler.inputs?.find(i=>i.name===inputName));
return link ? visit(graph.getNodeById(link.origin_id),link.origin_slot) : null;
}