fix(inspector): check only the negative the legacy parse read for a zero-out
Review I1/M2/M3/M5 of the zero-out legacy-fill guard. The guard decided over every node with a `negative` input, so a graph whose read sampler encodes "blurry" lost that legacy-filled negative when another branch zeroed its own (fewer than the legacy pages: below the parity floor). - comfy-metadata.js gains `legacyNegativeSource`, which returns the graph and sampler `negative` link that `extractGenerationParams` reads from. It reuses the parser's own sampler pick (`selectSampler`, factored out of `fromPromptGraph` without changing any reading). The workspace checks only that link: a direct zero-out shows the zeroed empty negative, a zero-out further up the path leaves it unfilled, and anything else keeps the legacy fill. - Workflow-only files go through the parser's own workflow conversion, so bypassed (mode 4) and muted (mode 2) zero-outs and unconnected ones behave as the legacy reading does. A subgraph instance whose definition runs a zero-out (directly or nested) leaves the negative unfilled. - One bounded walk with an index pointer replaces a walk per negative input with queue.shift(). - An ambiguous fact that the legacy parse knows (a grid's per-branch model) is still filled, documented: the legacy pages show it and this page never shows less. Tests: the reversed mixed graph shows "blurry"; the mixed graph whose read sampler is zeroed, the zero-out-only grid and a direct workflow zero-out show the zeroed empty negative; a combined zero-out and a zero-out subgraph instance stay unfilled; bypassed and unconnected workflow zero-outs keep the legacy reading. An API-graph Reroute in front of a zero-out gives the legacy parse no negative, so the server answer stays (that expectation changed from zeroed).
This commit is contained in:
@@ -212,12 +212,17 @@
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** The sampler the legacy parse reads: a custom sampler's guider, else the first sampler. */
|
||||
function selectSampler(ctx, nodes) {
|
||||
const custom = nodes.find(isCustomSampler);
|
||||
const [, guider] = custom ? enter(ctx, custom.inputs.guider, new Set()) : [];
|
||||
return { custom, sampler: isSampler(guider) ? guider : nodes.find(isSampler) };
|
||||
}
|
||||
|
||||
function fromPromptGraph(graph, workflow) {
|
||||
const ctx = createContext(graph, workflow);
|
||||
const nodes = Object.values(graph);
|
||||
const custom = nodes.find(isCustomSampler);
|
||||
const [, guider] = custom ? enter(ctx, custom.inputs.guider, new Set()) : [];
|
||||
const sampler = isSampler(guider) ? guider : nodes.find(isSampler);
|
||||
const { custom, sampler } = selectSampler(ctx, nodes);
|
||||
if (!sampler) {
|
||||
const loose = nodes.find((n) => n && n.inputs && /ksampler/i.test(n.class_type || ""));
|
||||
const inputs = (loose && loose.inputs) || {};
|
||||
@@ -402,6 +407,26 @@
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where `extractGenerationParams` reads its negative from (#249): the graph it used
|
||||
* (the API graph, else the workflow converted the same way) and the read sampler's
|
||||
* `negative` link, or null when it found no negative. It changes no reading.
|
||||
*/
|
||||
function legacyNegativeSource(comfyData) {
|
||||
const data = comfyData || {};
|
||||
const sources = [];
|
||||
if (data.prompt && typeof data.prompt === "object") sources.push([data.prompt, data.workflow]);
|
||||
if (data.workflow) sources.push([workflowToGraph(data.workflow), null]);
|
||||
for (const [graph, workflow] of sources) {
|
||||
const ctx = createContext(graph, workflow);
|
||||
const { sampler } = selectSampler(ctx, Object.values(graph));
|
||||
if (sampler && hasValue(traceConditioning(ctx, sampler.inputs.negative, "negative"))) {
|
||||
return { graph, link: sampler.inputs.negative };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const FACT_FIELDS = ["seed", "steps", "cfg", "sampler", "scheduler", "model"];
|
||||
const WORKFLOW_PASSTHROUGH = Symbol("inspection workflow passthrough");
|
||||
// These tables must equal inspection-semantics.json: SAMPLERS its `samplers` section
|
||||
@@ -761,7 +786,7 @@
|
||||
return factSet(metadataStatus === "available" || metadataStatus === "absent" ? "not-recorded" : "unresolved");
|
||||
}
|
||||
|
||||
const api = { extractGenerationParams, extractGenerationFacts, PLACEHOLDERS };
|
||||
const api = { extractGenerationParams, extractGenerationFacts, legacyNegativeSource, PLACEHOLDERS };
|
||||
// For the semantics sync test only; not part of the legacy page contract.
|
||||
Object.defineProperty(api, "_factsTables", {
|
||||
value: Object.freeze({
|
||||
|
||||
@@ -175,45 +175,52 @@ function comfyMetadata() {
|
||||
}
|
||||
|
||||
// #249: the legacy parse follows ConditioningZeroOut back to its input, so it reads the
|
||||
// positive text as the negative. The workspace shows the server's zeroed state instead
|
||||
// when every sampler's negative is a zero-out, and never fills the negative from the
|
||||
// legacy parse when a zero-out sits anywhere else in a negative path.
|
||||
// positive text as the negative. Only the negative it actually read is checked: a direct
|
||||
// zero-out shows the server's zeroed state; a zero-out elsewhere on
|
||||
// that path, or a subgraph instance whose definition holds one, leaves the negative
|
||||
// unfilled. Any other negative keeps the legacy fill, so this page never shows less.
|
||||
const ZERO_OUT = 'ConditioningZeroOut';
|
||||
const ZEROED_NEGATIVE = Object.freeze({ status: 'resolved', text: '', source: 'zeroed-conditioning' });
|
||||
const MAX_NEGATIVE_VISITS = 2000;
|
||||
const MODE_MUTED = 2, MODE_BYPASSED = 4;
|
||||
const isLink = value => Array.isArray(value) && value.length === 2 && ['string', 'number'].includes(typeof value[0]);
|
||||
|
||||
/** 'zeroed', 'undecided' (do not fill), or null (no zero-out: the legacy fill applies). */
|
||||
function zeroOutNegative(graph, workflow) {
|
||||
if (!graph) return Array.isArray(workflow?.nodes) && workflow.nodes.some(node => node?.type === ZERO_OUT) ? 'undecided' : null;
|
||||
const nodes = Object.values(graph).filter(object);
|
||||
if (!nodes.some(node => node.class_type === ZERO_OUT)) return null;
|
||||
const kinds = nodes.filter(node => object(node.inputs) && isLink(node.inputs.negative)).map(node => negativeKind(graph, node.inputs.negative));
|
||||
if (kinds.length && kinds.every(kind => kind === 'zeroed')) return 'zeroed';
|
||||
return kinds.every(kind => kind === 'plain') && kinds.length ? null : 'undecided';
|
||||
/** Subgraph definition IDs that run a zero-out, directly or through a nested instance. */
|
||||
function zeroOutSubgraphs(workflow) {
|
||||
const definitions = Array.isArray(workflow?.definitions?.subgraphs) ? workflow.definitions.subgraphs.filter(object) : [];
|
||||
const active = item => Array.isArray(item.nodes) ? item.nodes.filter(node => object(node) && ![MODE_MUTED, MODE_BYPASSED].includes(node.mode)) : [];
|
||||
const found = new Set();
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const item of definitions) {
|
||||
if (found.has(item.id) || !active(item).some(node => node.type === ZERO_OUT || found.has(node.type))) continue;
|
||||
found.add(item.id); changed = true;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function negativeKind(graph, link) {
|
||||
let node = graph[String(link[0])];
|
||||
for (let hops = 0; object(node) && node.class_type === 'Reroute' && hops < MAX_NEGATIVE_VISITS; hops++) {
|
||||
const next = object(node.inputs) ? node.inputs.value : undefined;
|
||||
node = isLink(next) ? graph[String(next[0])] : undefined;
|
||||
}
|
||||
/** 'zeroed', 'undecided' (do not fill), or null (the legacy fill applies). */
|
||||
function zeroOutNegative(source, workflow) {
|
||||
if (!source || !object(source.graph) || !isLink(source.link)) return null;
|
||||
const { graph } = source, subgraphs = zeroOutSubgraphs(workflow);
|
||||
const zeroing = node => node.class_type === ZERO_OUT || subgraphs.has(node.class_type);
|
||||
if (!Object.values(graph).some(node => object(node) && zeroing(node))) return null;
|
||||
const node = graph[String(source.link[0])];
|
||||
if (!object(node)) return 'undecided';
|
||||
if (node.class_type === ZERO_OUT) return 'zeroed';
|
||||
// Any zero-out further up (combined, averaged, ...) leaves the negative undecided.
|
||||
// One bounded walk upstream; an index pointer keeps it linear in the nodes visited.
|
||||
const queue = [node], seen = new Set();
|
||||
while (queue.length) {
|
||||
const current = queue.shift();
|
||||
if (current.class_type === ZERO_OUT) return 'undecided';
|
||||
if (seen.size >= MAX_NEGATIVE_VISITS) return 'undecided';
|
||||
for (let index = 0; index < queue.length; index++) {
|
||||
const current = queue[index];
|
||||
if (zeroing(current) || seen.size >= MAX_NEGATIVE_VISITS) return 'undecided';
|
||||
for (const value of object(current.inputs) ? Object.values(current.inputs) : []) {
|
||||
const id = isLink(value) ? String(value[0]) : null;
|
||||
if (id === null || seen.has(id) || !object(graph[id])) continue;
|
||||
seen.add(id); queue.push(graph[id]);
|
||||
}
|
||||
}
|
||||
return 'plain';
|
||||
return null;
|
||||
}
|
||||
|
||||
function withWorkflowParams(record, parser = comfyMetadata()) {
|
||||
@@ -226,7 +233,9 @@ function withWorkflowParams(record, parser = comfyMetadata()) {
|
||||
const found = key => typeof params[key] === 'string' || typeof params[key] === 'number' ?
|
||||
String(params[key]) !== '' && params[key] !== parser.PLACEHOLDERS?.[key] : false;
|
||||
const prompts = { ...record.prompts };
|
||||
const zeroed = prompts.negative?.status === 'resolved' ? null : zeroOutNegative(graph, workflow);
|
||||
const read = prompts.negative?.status === 'resolved' || typeof parser.legacyNegativeSource !== 'function' ? null :
|
||||
parser.legacyNegativeSource({ prompt: graph, workflow });
|
||||
const zeroed = zeroOutNegative(read, workflow);
|
||||
for (const [role, key] of [['positive', 'positivePrompt'], ['negative', 'negativePrompt']]) {
|
||||
const current = prompts[role];
|
||||
if (current?.status === 'resolved') continue;
|
||||
@@ -240,6 +249,8 @@ function withWorkflowParams(record, parser = comfyMetadata()) {
|
||||
record.prompts = prompts;
|
||||
for (const [field, key] of LEGACY_FACTS) {
|
||||
// Only open answers are filled: a known value or a definite "not recorded" stays.
|
||||
// An ambiguous field (e.g. a compare grid's per-branch model) is filled too, on
|
||||
// purpose: the legacy pages show this value, and this page never shows less.
|
||||
if (!found(key) || !['unresolved', 'ambiguous'].includes(record.generation[field]?.status)) continue;
|
||||
record.generation[field] = { status: 'known', value: String(params[key]), source: graph ? 'api-graph' : 'workflow', evidence: 'configuration', reason: null, sampler_id: null };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user