Files
Azornes-Comfyui-Model-Resolver/web/resolver/utils/workflow_metadata.js
T
Dariusz L 2d6be246b4 Extract and display copyable CivitAI ComfyUI workflow metadata in model previews
- Preserve existing image metadata when re-normalizing CivitAI model images
- Add extractComfyWorkflow helper to recover pasteable editor workflows from image metadata, including nested/double-encoded JSON and non-finite number tolerance
- Surface recovered workflows as a copyable button in the model info image preview under Other metadata
- Add focused tests for workflow extraction, metadata preservation, and edge cases
2026-07-26 20:40:41 +02:00

123 lines
3.2 KiB
JavaScript

const WORKFLOW_CONTAINER_KEYS = new Set([
'comfy',
'comfyui',
'extra_metadata',
'extra_pnginfo',
'workflow'
]);
function normalizeNonFiniteJsonNumbers(text) {
let normalized = '';
let inString = false;
let escaped = false;
const tokens = ['-Infinity', '+Infinity', 'Infinity', 'NaN'];
for (let index = 0; index < text.length; index += 1) {
const character = text[index];
if (inString) {
normalized += character;
if (escaped) {
escaped = false;
} else if (character === '\\') {
escaped = true;
} else if (character === '"') {
inString = false;
}
continue;
}
if (character === '"') {
inString = true;
normalized += character;
continue;
}
const token = tokens.find(candidate => text.startsWith(candidate, index));
if (token) {
const previous = index > 0 ? text[index - 1] : '';
const next = text[index + token.length] || '';
const validPrevious = !previous || /[\s[{:;,]/.test(previous);
const validNext = !next || /[\s\]},;]/.test(next);
if (validPrevious && validNext) {
normalized += 'null';
index += token.length - 1;
continue;
}
}
normalized += character;
}
return normalized;
}
function parseJsonStrings(value) {
let parsed = value;
for (let attempt = 0; attempt < 3 && typeof parsed === 'string'; attempt += 1) {
const text = parsed.trim();
if (!text) return null;
try {
parsed = JSON.parse(text);
} catch {
const normalizedText = normalizeNonFiniteJsonNumbers(text);
if (normalizedText === text) return null;
try {
parsed = JSON.parse(normalizedText);
} catch {
return null;
}
}
}
return parsed;
}
function findWorkflow(value, seen, depth = 0) {
if (depth > 6) return null;
const parsed = parseJsonStrings(value);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return null;
}
if (seen.has(parsed)) return null;
seen.add(parsed);
if (Array.isArray(parsed.nodes)) {
return parsed;
}
for (const [key, nestedValue] of Object.entries(parsed)) {
if (!WORKFLOW_CONTAINER_KEYS.has(String(key).toLowerCase())) continue;
const workflow = findWorkflow(nestedValue, seen, depth + 1);
if (workflow) return workflow;
}
return null;
}
/**
* Extract a pasteable ComfyUI editor workflow from CivitAI image metadata.
*/
export function extractComfyWorkflow(image = {}) {
const candidates = [
image.workflow,
image.comfy,
image.meta,
image.metadata,
image.extra_metadata,
image.extra_pnginfo
];
for (const candidate of candidates) {
const workflow = findWorkflow(candidate, new WeakSet());
if (!workflow) continue;
return {
workflow,
nodeCount: workflow.nodes.length,
text: JSON.stringify(workflow)
};
}
return null;
}