134 lines
9.4 KiB
JavaScript
134 lines
9.4 KiB
JavaScript
import {T} from './i18n.js';
|
|
const link=value=>Array.isArray(value) && value.length===2 && ['string','number'].includes(typeof value[0]) && Number.isInteger(value[1]);
|
|
const asRef=value=>link(value)?[String(value[0]),value[1]]:null;
|
|
const loraTypes=new Set(['LoraLoader','LoraLoaderModelOnly','Power Lora Loader (rgthree)']);
|
|
const staticRouter=api=>api.class_type==='AdachiExclusiveSwitch'||(['ComfySwitchNode','ComfySoftSwitchNode'].includes(api.class_type)&&typeof api.inputs?.switch==='boolean');
|
|
const loaderTypes={MODEL:new Set(['CheckpointLoaderSimple','CheckpointLoader','UNETLoader','UNETLoaderGGUF','CheckpointLoaderNF4']),
|
|
CLIP:new Set(['CheckpointLoaderSimple','CheckpointLoader','CLIPLoader','DualCLIPLoader','TripleCLIPLoader','CLIPLoaderGGUF','DualCLIPLoaderGGUF']),
|
|
VAE:new Set(['CheckpointLoaderSimple','CheckpointLoader','VAELoader'])};
|
|
|
|
function sourceInputs(api) {
|
|
if(['ComfySwitchNode','ComfySoftSwitchNode'].includes(api.class_type)&&typeof api.inputs?.switch==='boolean'){
|
|
const inputs=api.inputs;if(api.class_type==='ComfySoftSwitchNode'){
|
|
if(!Object.hasOwn(inputs,'on_false'))return [inputs.on_true];if(!Object.hasOwn(inputs,'on_true'))return [inputs.on_false];
|
|
}
|
|
return [inputs[inputs.switch?'on_true':'on_false']];
|
|
}
|
|
if(api.class_type==='AdachiExclusiveSwitch') {
|
|
const mask=Number(api.inputs?.active_inputs);
|
|
if(mask===1)return [api.inputs.input_a];if(mask===2)return [api.inputs.input_b];
|
|
}
|
|
return Object.values(api.inputs||{});
|
|
}
|
|
function ancestors(prompt,refs) {
|
|
const found=new Set(),queue=refs.map(asRef).filter(Boolean);
|
|
while(queue.length){const [id]=queue.pop();if(found.has(id)||!prompt[id])continue;found.add(id);queue.push(...sourceInputs(prompt[id]).map(asRef).filter(Boolean));}
|
|
return found;
|
|
}
|
|
function origins(prompt,start,type,lookup) {
|
|
const found=[],seen=new Set(),queue=[asRef(start)].filter(Boolean);
|
|
while(queue.length){const [id,slot]=queue.shift(),key=`${id}:${slot}`;if(seen.has(key))continue;seen.add(key);
|
|
const api=prompt[id],node=lookup(id);if(!api)continue;
|
|
if(loaderTypes[type].has(api.class_type)){found.push({id,node,api});continue;}
|
|
if(staticRouter(api)){queue.push(...sourceInputs(api).map(asRef).filter(Boolean));continue;}
|
|
if(loraTypes.has(api.class_type)){const ref=asRef(api.inputs?.[type==='MODEL'?'model':type==='CLIP'?'clip':'vae']);if(ref)queue.push(ref);continue;}
|
|
const typed=(node?.inputs||[]).filter(i=>i.type===type).map(i=>i.name);
|
|
const names=typed.length?typed:Object.keys(api.inputs||{}).filter(name=>new RegExp(`^${type.toLowerCase()}(?:$|[_\\d])`,'i').test(name));
|
|
queue.push(...names.map(name=>asRef(api.inputs[name])).filter(Boolean));
|
|
}
|
|
return found.filter((item,i,all)=>all.findIndex(x=>x.id===item.id)===i);
|
|
}
|
|
function encoders(prompt,start,lookup) {
|
|
const found=[],seen=new Set(),queue=[asRef(start)].filter(Boolean);
|
|
while(queue.length){const [id,slot]=queue.shift(),key=`${id}:${slot}`;if(seen.has(key))continue;seen.add(key);
|
|
const api=prompt[id],node=lookup(id);if(!api)continue;
|
|
const fields=(node?.widgets||[]).filter(w=>/^(text(?:_[gl])?|prompt)$/i.test(w.name) && typeof w.value==='string').map(w=>w.name);
|
|
const output=node?.outputs?.[slot];
|
|
if(fields.length && (output?.type==='CONDITIONING'||/^CLIPTextEncode/.test(api.class_type))){found.push({id,node,api,fields});continue;}
|
|
if(staticRouter(api)){queue.push(...sourceInputs(api).map(asRef).filter(Boolean));continue;}
|
|
const selected=output?.name && /^(positive|negative)$/i.test(output.name) && asRef(api.inputs?.[output.name]);
|
|
if(selected){queue.push(selected);continue;}
|
|
const names=(node?.inputs||[]).filter(i=>i.type==='CONDITIONING').map(i=>i.name);
|
|
const fallback=Object.keys(api.inputs||{}).filter(name=>/^(positive|negative|conditioning(?:_\d+)?)$/i.test(name));
|
|
queue.push(...(names.length?names:fallback).map(name=>asRef(api.inputs[name])).filter(Boolean));
|
|
}
|
|
return found.filter((item,i,all)=>all.findIndex(x=>x.id===item.id)===i);
|
|
}
|
|
|
|
export function inspectExecutionFlow(prompt,samplerId,lookup=()=>null) {
|
|
const api=prompt[String(samplerId)],refs={};
|
|
for(const name of ['model','positive','negative','vae'])refs[name]=asRef(api?.inputs?.[name]);
|
|
const positive=encoders(prompt,refs.positive,lookup),negative=encoders(prompt,refs.negative,lookup);
|
|
const allEncoders=[...positive,...negative].filter((item,i,all)=>all.findIndex(x=>x.id===item.id)===i);
|
|
const consumers=allEncoders.filter(e=>asRef(e.api.inputs?.clip)).map(e=>({id:e.id,input:'clip'}));
|
|
const clipNodes=consumers.flatMap(e=>origins(prompt,prompt[e.id].inputs[e.input],'CLIP',lookup)).filter((e,i,all)=>all.findIndex(x=>x.id===e.id)===i);
|
|
return {refs,modelNodes:origins(prompt,refs.model,'MODEL',lookup),clipNodes,
|
|
vaeNodes:origins(prompt,refs.vae,'VAE',lookup),encoders:{positive,negative},clipConsumers:consumers,
|
|
loras:[...ancestors(prompt,Object.values(refs))].filter(id=>loraTypes.has(prompt[id].class_type)).map(id=>({id,node:lookup(id),api:prompt[id]}))};
|
|
}
|
|
|
|
export function executionNode(graph,id) {
|
|
const path=String(id).split(':');let node;
|
|
for(let i=0;i<path.length;i++){node=graph?.getNodeById(path[i]);if(!node && /^-?\d+$/.test(path[i]))node=graph?.getNodeById(Number(path[i]));if(!node)return null;if(i<path.length-1)graph=node.subgraph;}
|
|
return node;
|
|
}
|
|
|
|
export function graphNodeEntries(graph,prefix=[],seen=new Set()) {
|
|
if(!graph || seen.has(graph))return [];seen.add(graph);
|
|
return Array.from(graph._nodes||[]).flatMap(node=>{
|
|
const path=[...prefix,String(node.id)];
|
|
return [{id:path.join(':'),node,graph},...graphNodeEntries(node.subgraph,path,seen)];
|
|
});
|
|
}
|
|
export const executionId=(graph,node)=>graphNodeEntries(graph).find(entry=>entry.node===node)?.id;
|
|
|
|
// Rewrite only the chosen sampler's ancestry. Other output branches keep their
|
|
// graph definitions; the backend subsequently queues only the comparison target.
|
|
export function applyExecutionBinding(original,samplerId,binding={}) {
|
|
// Native widget values can be Vue proxies (for example rgthree LoRA
|
|
// rows). Copy using the same JSON boundary as ComfyUI's queue request.
|
|
const prompt=JSON.parse(JSON.stringify(original)),id=String(samplerId),sampler=prompt[id];
|
|
if(!sampler)throw Error(T("The selected inpaint node is missing from the execution graph"));
|
|
Object.assign(sampler.inputs,binding.samplerInputs||{});
|
|
for(const [key,inputs] of Object.entries(binding.inputOverrides||{})){
|
|
if(!prompt[key])throw Error(T("The selected parameter node is missing from the execution graph #{id}",{id:key}));Object.assign(prompt[key].inputs,inputs);
|
|
}
|
|
for(const [key,names] of Object.entries(binding.removeInputs||{}))for(const name of names)if(prompt[key])delete prompt[key].inputs[name];
|
|
const scope=()=>new Set([id,...ancestors(prompt,Object.values(sampler.inputs))]);
|
|
const collapse=(candidates,choose)=>{
|
|
const resolve=(value,seen=new Set())=>{
|
|
const ref=asRef(value);if(!ref || !candidates.has(ref[0]))return ref;
|
|
if(seen.has(ref[0]))return null;seen=new Set(seen);seen.add(ref[0]);
|
|
const next=choose(prompt[ref[0]],ref[1]);return next?resolve(next,seen):null;
|
|
};
|
|
for(const key of scope())if(!candidates.has(key))for(const [name,value] of Object.entries(prompt[key]?.inputs||{})){
|
|
const ref=asRef(value);if(ref && candidates.has(ref[0])){const result=resolve(ref);if(result && prompt[result[0]])prompt[key].inputs[name]=result;}
|
|
}
|
|
};
|
|
const switches=new Set([...scope()].filter(key=>prompt[key]?.class_type==='AdachiExclusiveSwitch' && [1,2].includes(Number(prompt[key].inputs.active_inputs))));
|
|
collapse(switches,(api,slot)=>slot===0?api.inputs[Number(api.inputs.active_inputs)===1?'input_a':'input_b']:null);
|
|
if(binding.loraMode==='override') {
|
|
const loras=new Set([...scope()].filter(key=>loraTypes.has(prompt[key]?.class_type)));
|
|
collapse(loras,(api,slot)=>slot===0?api.inputs.model:slot===1?api.inputs.clip:null);
|
|
const selected=(binding.loras||[]).filter(l=>l.name && l.on!==false);
|
|
if(selected.length){
|
|
const baseModel=asRef(sampler.inputs.model);if(!baseModel)throw Error(T("Connect a main model before adding LoRAs"));
|
|
let nextId=-1000000;
|
|
const fresh=()=>{while(prompt[String(nextId)])nextId--;return String(nextId--);};
|
|
const consumers=(binding.clipConsumers||[]).filter(c=>asRef(prompt[c.id]?.inputs?.[c.input]));
|
|
const unique=new Map();for(const c of consumers){const ref=asRef(prompt[c.id].inputs[c.input]),key=JSON.stringify(ref);if(!unique.has(key))unique.set(key,{ref,consumers:[]});unique.get(key).consumers.push(c);}
|
|
const chains=unique.size?[...unique.values()]:[{ref:null,consumers:[]}];
|
|
chains.forEach((chain,index)=>{
|
|
let model=baseModel,clip=chain.ref;
|
|
for(const l of selected){const key=fresh(),inputs={model,lora_name:l.name,strength_model:Number(l.strength??1)};
|
|
if(clip)Object.assign(inputs,{clip,strength_clip:Number(l.clipStrength??l.strength??1)});
|
|
prompt[key]={class_type:clip?'LoraLoader':'LoraLoaderModelOnly',inputs};model=[key,0];if(clip)clip=[key,1];
|
|
}
|
|
if(index===0)sampler.inputs.model=model;
|
|
for(const c of chain.consumers)prompt[c.id].inputs[c.input]=clip;
|
|
});
|
|
}
|
|
}
|
|
return prompt;
|
|
}
|