Follow sampler conditioning and enabled group presets
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
@@ -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();}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user