fix(inspect): keep definite server prompts over the legacy parse

The legacy parser follows ConditioningZeroOut back to the positive text
node and shows the positive as the negative. Apply its result only to
roles and settings the server could not decide, so a definite answer
such as an explicitly empty negative is never replaced.

Node callers (the corpus facts tool and the parity test support) now
pass the parser explicitly instead of relying on a browser global.

On a 38,602-file output folder, prompts shown stay at 7,542 files. Where
the server and the legacy parser both answer, they differ only in
surrounding whitespace (793 roles), except 227 zeroed negatives the
legacy parser misread and one prompt that is a JSON block.
This commit is contained in:
Vito Sansevero
2026-10-19 18:55:51 -07:00
parent 5b3ddb887d
commit 91bd060344
4 changed files with 37 additions and 12 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ import path from 'node:path';
const here = path.dirname(fileURLToPath(import.meta.url));
const web = path.join(here, '..', '..', '..', 'web', 'js');
const require = createRequire(import.meta.url);
const { extractGenerationFacts } = require(path.join(web, 'comfy-metadata.js'));
const parser = require(path.join(web, 'comfy-metadata.js')), { extractGenerationFacts } = parser;
const { projectInspectionPayload } = await import(pathToFileURL(path.join(web, 'workspace', 'inspection-state.mjs')).href);
const chunks = [];
@@ -19,7 +19,7 @@ for await (const chunk of process.stdin) chunks.push(chunk);
const output = {};
for (const { name, body } of JSON.parse(Buffer.concat(chunks).toString('utf8'))) {
const identity = { kind: 'rooted-file', key: `0:${name}.png`, root_index: 0, path: `${name}.png`, root_scope: null };
const record = projectInspectionPayload(JSON.parse(body), identity, null, extractGenerationFacts);
const record = projectInspectionPayload(JSON.parse(body), identity, null, extractGenerationFacts, false, parser);
output[name] = Object.fromEntries(Object.entries(record.generation).map(([field, fact]) => [field, { status: fact.status, value: fact.value }]));
}
process.stdout.write(JSON.stringify(output));
@@ -518,3 +518,29 @@ test('recorded and saved prompts are never replaced by the workflow parse', t =>
assert.deepEqual(record.prompts.positive, { status: 'resolved', text: 'exact\npositive', source: 'execution' });
assert.equal(record.prompts.negative.text, 'blurry, watermark');
});
test('a definite server prompt is kept even when the legacy parser reads it differently', t => {
globalThis.ComfyMetadata = require('../../web/js/comfy-metadata.js');
t.after(() => { delete globalThis.ComfyMetadata; });
const entry = image(), data = envelope(entry);
// The legacy parser follows ConditioningZeroOut back to the positive text node.
const graph = detailerPipelineGraph();
graph[30] = { class_type: 'ConditioningZeroOut', inputs: { conditioning: ['25', 0] } };
graph[5].inputs.negative = ['30', 0];
data.metadata.prompt_graph = { status: 'parsed', value: graph };
data.prompts = { positive: { status: 'ambiguous', text: null, source: null }, negative: { status: 'resolved', text: '', source: 'zeroed-conditioning' } };
const record = contract('toInspectionRecord')(data, entry, extractGenerationFacts);
assert.deepEqual(record.prompts.negative, { status: 'resolved', text: '', source: 'zeroed-conditioning' });
assert.equal(record.prompts.positive.text, 'a lighthouse at dusk, oil painting');
});
test('callers without the browser global pass the legacy parser explicitly', () => {
const parser = require('../../web/js/comfy-metadata.js');
const entry = image(), data = envelope(entry);
data.prompts = { positive: { status: 'ambiguous', text: null, source: null }, negative: { status: 'ambiguous', text: null, source: null } };
data.metadata.prompt_graph = { status: 'parsed', value: detailerPipelineGraph() };
const identity = { kind: 'rooted-file', key: '0:x', root_index: 0, path: entry.path, root_scope: scopeA };
const record = contract('projectInspectionPayload')(data, identity, null, extractGenerationFacts, false, parser);
assert.equal(record.prompts.positive.text, 'a lighthouse at dusk, oil painting');
assert.equal(record.prompts.negative.text, 'blurry, watermark');
});
+2 -2
View File
@@ -10,13 +10,13 @@ import readline from 'node:readline';
const web = path.join(path.resolve(process.argv[2]), 'web', 'js');
const require = createRequire(import.meta.url);
const { extractGenerationFacts } = require(path.join(web, 'comfy-metadata.js'));
const parser = require(path.join(web, 'comfy-metadata.js')), { extractGenerationFacts } = parser;
const { projectInspectionPayload } = await import(pathToFileURL(path.join(web, 'workspace', 'inspection-state.mjs')).href);
const identity = { kind: 'rooted-file', key: '0:corpus.png', root_index: 0, path: 'corpus.png', root_scope: null };
function facts(line) {
try {
const record = projectInspectionPayload(JSON.parse(line), identity, null, extractGenerationFacts);
const record = projectInspectionPayload(JSON.parse(line), identity, null, extractGenerationFacts, false, parser);
return Object.fromEntries(Object.entries(record.generation).map(([field, fact]) => [field, { status: fact.status, reason: fact.reason ?? null }]));
} catch (error) {
return { error: error?.name || 'Error' };
+7 -8
View File
@@ -165,8 +165,7 @@ function libraryFor(value) {
// The legacy workflow parse (comfy-metadata.js, as of #170) also feeds the gallery and
// admin pages; it fills any prompt or setting the server could not decide, so this page
// never shows less than they do. Recorded and saved prompts are never replaced.
const KEPT_PROMPT_SOURCES = ['execution', 'library', 'saved-association'];
// never shows less than they do. A definite server answer is never replaced.
const LEGACY_FACTS = [['seed', 'seed'], ['steps', 'steps'], ['cfg', 'cfgScale'], ['sampler', 'sampler'], ['model', 'checkpoint']];
function comfyMetadata() {
@@ -183,12 +182,12 @@ function withWorkflowParams(record, parser = comfyMetadata()) {
const prompts = { ...record.prompts };
for (const [role, key] of [['positive', 'positivePrompt'], ['negative', 'negativePrompt']]) {
const current = prompts[role];
if (!found(key) || (current?.status === 'resolved' && KEPT_PROMPT_SOURCES.includes(current.source))) continue;
if (!found(key) || current?.status === 'resolved') continue;
prompts[role] = { status: 'resolved', text: params[key], source: 'graph-inferred' };
}
record.prompts = prompts;
for (const [field, key] of LEGACY_FACTS) {
if (!found(key)) continue;
if (!found(key) || record.generation[field]?.status === 'known') continue;
record.generation[field] = { status: 'known', value: String(params[key]), source: graph ? 'api-graph' : 'workflow', evidence: 'configuration', reason: null, sampler_id: null };
}
}
@@ -197,7 +196,7 @@ function unavailableFacts() {
return Object.fromEntries(FACT_FIELDS.map(key => [key, { status: 'unresolved', value: null, source: null, evidence: null, reason: null, sampler_id: null }]));
}
export function toInspectionRecord(result, image, extractFacts = globalThis.ComfyMetadata?.extractGenerationFacts || globalThis.window?.ComfyMetadata?.extractGenerationFacts) {
export function toInspectionRecord(result, image, extractFacts = globalThis.ComfyMetadata?.extractGenerationFacts || globalThis.window?.ComfyMetadata?.extractGenerationFacts, parser = comfyMetadata()) {
const scope = validateIdentity(image);
if (scope !== null && (!object(result) || result.root_scope !== scope)) throw new InspectionSourceChangedError();
if (!object(result) || result.success === false) throw new TypeError('Invalid inspection response');
@@ -207,11 +206,11 @@ export function toInspectionRecord(result, image, extractFacts = globalThis.Comf
if ((!legacy || result.image !== undefined) && (!object(result.image) || result.image.root_index !== image.root_index || result.image.path !== image.path || result.image.filename !== image.path.split('/').at(-1))) throw new TypeError('Mismatched inspection identity');
return projectInspectionPayload(result,
{ kind: 'rooted-file', key: imageKey(image), root_index: image.root_index, path: image.path, root_scope: scope },
downloadFor(image, !legacy || scope !== null), extractFacts, legacy);
downloadFor(image, !legacy || scope !== null), extractFacts, legacy, parser);
}
/** Shared bounded payload projection; adapters validate identity before entering here. */
export function projectInspectionPayload(result, identity, download, extractFacts = globalThis.ComfyMetadata?.extractGenerationFacts || globalThis.window?.ComfyMetadata?.extractGenerationFacts, legacy = false) {
export function projectInspectionPayload(result, identity, download, extractFacts = globalThis.ComfyMetadata?.extractGenerationFacts || globalThis.window?.ComfyMetadata?.extractGenerationFacts, legacy = false, parser = comfyMetadata()) {
const record = {
identity, file: null, prompts: promptsFor(result.prompts), library: null, metadata: null,
generation: unavailableFacts(), historyTarget: null, download,
@@ -224,7 +223,7 @@ export function projectInspectionPayload(result, identity, download, extractFact
if (typeof extractFacts !== 'function') throw new TypeError('Inspection facts parser unavailable');
record.generation = extractFacts({ prompt_graph: record.metadata.prompt_graph, workflow_graph: record.metadata.workflow_graph,
parameters: record.metadata.parameters, recordFormats: record.metadata.record_formats, metadataStatus: record.metadata.status, resolution });
withWorkflowParams(record);
withWorkflowParams(record, parser);
if (record.library.status === 'linked' && record.library.positive_id !== null) record.historyTarget = `#/library/${record.library.positive_id}`;
return record;
}