From 91bd060344f0a402b6a1272a83332584e4e01361 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 19 Oct 2026 18:55:51 -0700 Subject: [PATCH] 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. --- tests/js/support/inspection-parity-facts.mjs | 4 +-- tests/js/workspace-inspection-state.test.js | 26 ++++++++++++++++++++ tools/inspection_corpus_facts.mjs | 4 +-- web/js/workspace/inspection-state.mjs | 15 ++++++----- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/tests/js/support/inspection-parity-facts.mjs b/tests/js/support/inspection-parity-facts.mjs index 8a8bb07..d2bc141 100644 --- a/tests/js/support/inspection-parity-facts.mjs +++ b/tests/js/support/inspection-parity-facts.mjs @@ -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)); diff --git a/tests/js/workspace-inspection-state.test.js b/tests/js/workspace-inspection-state.test.js index bb884be..02bd499 100644 --- a/tests/js/workspace-inspection-state.test.js +++ b/tests/js/workspace-inspection-state.test.js @@ -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'); +}); diff --git a/tools/inspection_corpus_facts.mjs b/tools/inspection_corpus_facts.mjs index b0e9024..62ea4d4 100644 --- a/tools/inspection_corpus_facts.mjs +++ b/tools/inspection_corpus_facts.mjs @@ -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' }; diff --git a/web/js/workspace/inspection-state.mjs b/web/js/workspace/inspection-state.mjs index 638cd8a..1735bc2 100644 --- a/web/js/workspace/inspection-state.mjs +++ b/web/js/workspace/inspection-state.mjs @@ -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; }