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; }