@enslo/sd-metadata 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,6 +4,14 @@ var Result = {
4
4
  error: (error) => ({ ok: false, error })
5
5
  };
6
6
 
7
+ // src/utils/object.ts
8
+ function trimObject(obj) {
9
+ const result = Object.fromEntries(
10
+ Object.entries(obj).filter(([, value]) => value !== void 0)
11
+ );
12
+ return Object.keys(result).length === 0 ? void 0 : result;
13
+ }
14
+
7
15
  // src/parsers/a1111.ts
8
16
  function parseA1111(entries) {
9
17
  const parametersEntry = entries.find(
@@ -24,21 +32,8 @@ function parseA1111(entries) {
24
32
  const version = settingsMap.get("Version");
25
33
  const app = settingsMap.get("App");
26
34
  const software = detectSoftwareVariant(version, app);
27
- const metadata = {
28
- software,
29
- prompt,
30
- negativePrompt,
31
- width,
32
- height
33
- };
34
35
  const modelName = settingsMap.get("Model");
35
36
  const modelHash = settingsMap.get("Model hash");
36
- if (modelName || modelHash) {
37
- metadata.model = {
38
- name: modelName,
39
- hash: modelHash
40
- };
41
- }
42
37
  const sampler = settingsMap.get("Sampler");
43
38
  const scheduler = settingsMap.get("Schedule type");
44
39
  const steps = parseNumber(settingsMap.get("Steps"));
@@ -47,29 +42,30 @@ function parseA1111(entries) {
47
42
  );
48
43
  const seed = parseNumber(settingsMap.get("Seed"));
49
44
  const clipSkip = parseNumber(settingsMap.get("Clip skip"));
50
- if (sampler !== void 0 || scheduler !== void 0 || steps !== void 0 || cfg !== void 0 || seed !== void 0 || clipSkip !== void 0) {
51
- metadata.sampling = {
45
+ const hiresScale = parseNumber(settingsMap.get("Hires upscale"));
46
+ const upscaler = settingsMap.get("Hires upscaler");
47
+ const hiresSteps = parseNumber(settingsMap.get("Hires steps"));
48
+ const denoise = parseNumber(settingsMap.get("Denoising strength"));
49
+ const hiresSize = settingsMap.get("Hires size");
50
+ const [hiresWidth] = parseSize(hiresSize ?? "");
51
+ const scale = hiresScale ?? (hiresWidth > 0 ? hiresWidth / width : void 0);
52
+ return Result.ok({
53
+ software,
54
+ prompt,
55
+ negativePrompt,
56
+ width,
57
+ height,
58
+ model: trimObject({ name: modelName, hash: modelHash }),
59
+ sampling: trimObject({
52
60
  sampler,
53
61
  scheduler,
54
62
  steps,
55
63
  cfg,
56
64
  seed,
57
65
  clipSkip
58
- };
59
- }
60
- const hiresScale = parseNumber(settingsMap.get("Hires upscale"));
61
- const upscaler = settingsMap.get("Hires upscaler");
62
- const hiresSteps = parseNumber(settingsMap.get("Hires steps"));
63
- const denoise = parseNumber(settingsMap.get("Denoising strength"));
64
- const hiresSize = settingsMap.get("Hires size");
65
- if ([hiresScale, hiresSize, upscaler, hiresSteps, denoise].some(
66
- (v) => v !== void 0
67
- )) {
68
- const [hiresWidth] = parseSize(hiresSize ?? "");
69
- const scale = hiresScale ?? hiresWidth / width;
70
- metadata.hires = { scale, upscaler, steps: hiresSteps, denoise };
71
- }
72
- return Result.ok(metadata);
66
+ }),
67
+ hires: trimObject({ scale, upscaler, steps: hiresSteps, denoise })
68
+ });
73
69
  }
74
70
  function parseParametersText(text) {
75
71
  const negativeIndex = text.indexOf("Negative prompt:");
@@ -133,13 +129,6 @@ function detectSoftwareVariant(version, app) {
133
129
  return "sd-webui";
134
130
  }
135
131
 
136
- // src/utils/entries.ts
137
- function buildEntryRecord(entries) {
138
- return Object.freeze(
139
- Object.fromEntries(entries.map((e) => [e.keyword, e.text]))
140
- );
141
- }
142
-
143
132
  // src/utils/json.ts
144
133
  function parseJson(text) {
145
134
  try {
@@ -152,7 +141,37 @@ function parseJson(text) {
152
141
  }
153
142
  }
154
143
 
144
+ // src/utils/entries.ts
145
+ function buildEntryRecord(entries) {
146
+ return Object.freeze(
147
+ Object.fromEntries(entries.map((e) => [e.keyword, e.text]))
148
+ );
149
+ }
150
+ function extractFromCommentJson(entryRecord, key) {
151
+ if (!entryRecord.Comment?.startsWith("{")) return void 0;
152
+ const parsed = parseJson(entryRecord.Comment);
153
+ if (!parsed.ok) return void 0;
154
+ const value = parsed.value[key];
155
+ if (typeof value === "string") return value;
156
+ if (typeof value === "object" && value !== null) return JSON.stringify(value);
157
+ return void 0;
158
+ }
159
+
155
160
  // src/parsers/comfyui.ts
161
+ var CIVITAI_EXTENSION_KEYS = ["extra", "extraMetadata", "resource-stack"];
162
+ var COMFYUI_NODE_KEYS = {
163
+ sampler: ["Sampler"],
164
+ positiveClip: ["PositiveCLIP_Base"],
165
+ negativeClip: ["NegativeCLIP_Base"],
166
+ latentImage: ["EmptyLatentImage"],
167
+ checkpoint: ["CheckpointLoader_Base"],
168
+ hiresModelUpscale: [
169
+ "HiresFix_ModelUpscale_UpscaleModelLoader",
170
+ "PostUpscale_ModelUpscale_UpscaleModelLoader"
171
+ ],
172
+ hiresImageScale: ["HiresFix_ImageScale", "PostUpscale_ImageScale"],
173
+ hiresSampler: ["HiresFix_Sampler"]
174
+ };
156
175
  function parseComfyUI(entries) {
157
176
  const entryRecord = buildEntryRecord(entries);
158
177
  const promptText = findPromptJson(entryRecord);
@@ -167,99 +186,35 @@ function parseComfyUI(entries) {
167
186
  });
168
187
  }
169
188
  const prompt = parsed.value;
170
- const nodes = Object.values(prompt);
171
- if (!nodes.some((node) => "class_type" in node)) {
189
+ if (!Object.values(prompt).some((node) => "class_type" in node)) {
172
190
  return Result.error({ type: "unsupportedFormat" });
173
191
  }
174
- const ksampler = findNode(prompt, ["Sampler"]);
175
- const positiveClip = findNode(prompt, ["PositiveCLIP_Base"]);
176
- const negativeClip = findNode(prompt, ["NegativeCLIP_Base"]);
177
- const clipPositiveText = extractText(positiveClip);
178
- const clipNegativeText = extractText(negativeClip);
179
- const latentImage = findNode(prompt, ["EmptyLatentImage"]);
180
- const latentWidth = latentImage ? Number(latentImage.inputs.width) || 0 : 0;
181
- const latentHeight = latentImage ? Number(latentImage.inputs.height) || 0 : 0;
182
- const extraMeta = extractExtraMetadata(prompt);
183
- const positiveText = clipPositiveText || extraMeta?.prompt || "";
184
- const negativeText = clipNegativeText || extraMeta?.negativePrompt || "";
185
- const width = latentWidth || extraMeta?.width || 0;
186
- const height = latentHeight || extraMeta?.height || 0;
187
- const metadata = {
192
+ const nodes = Object.fromEntries(
193
+ Object.entries(prompt).filter(
194
+ ([key]) => !CIVITAI_EXTENSION_KEYS.includes(key)
195
+ )
196
+ );
197
+ const comfyMetadata = extractComfyUIMetadata(prompt);
198
+ const civitaiMetadata = extractCivitaiMetadata(
199
+ extractExtraMetadata(prompt, entryRecord)
200
+ );
201
+ const merged = mergeMetadata(civitaiMetadata, comfyMetadata);
202
+ return Result.ok({
188
203
  software: "comfyui",
189
- prompt: positiveText,
190
- negativePrompt: negativeText,
191
- width,
192
- height,
193
- nodes: prompt
194
- // Store the parsed node graph
195
- };
196
- const checkpoint = findNode(prompt, ["CheckpointLoader_Base"])?.inputs?.ckpt_name;
197
- if (checkpoint) {
198
- metadata.model = { name: String(checkpoint) };
199
- } else if (extraMeta?.baseModel) {
200
- metadata.model = { name: extraMeta.baseModel };
201
- }
202
- if (ksampler) {
203
- metadata.sampling = {
204
- seed: ksampler.inputs.seed,
205
- steps: ksampler.inputs.steps,
206
- cfg: ksampler.inputs.cfg,
207
- sampler: ksampler.inputs.sampler_name,
208
- scheduler: ksampler.inputs.scheduler
209
- };
210
- } else if (extraMeta) {
211
- metadata.sampling = {
212
- seed: extraMeta.seed,
213
- steps: extraMeta.steps,
214
- cfg: extraMeta.cfgScale,
215
- sampler: extraMeta.sampler
216
- };
217
- }
218
- const hiresModel = findNode(prompt, [
219
- "HiresFix_ModelUpscale_UpscaleModelLoader",
220
- "PostUpscale_ModelUpscale_UpscaleModelLoader"
221
- ])?.inputs;
222
- const hiresScale = findNode(prompt, [
223
- "HiresFix_ImageScale",
224
- "PostUpscale_ImageScale"
225
- ])?.inputs;
226
- const hiresSampler = findNode(prompt, ["HiresFix_Sampler"])?.inputs;
227
- if (hiresModel && hiresScale) {
228
- const hiresWidth = hiresScale.width;
229
- const scale = latentWidth > 0 ? Math.round(hiresWidth / latentWidth * 100) / 100 : void 0;
230
- if (hiresSampler) {
231
- metadata.hires = {
232
- upscaler: hiresModel.model_name,
233
- scale,
234
- steps: hiresSampler.steps,
235
- denoise: hiresSampler.denoise
236
- };
237
- } else {
238
- metadata.upscale = {
239
- upscaler: hiresModel.model_name,
240
- scale
241
- };
242
- }
243
- }
244
- if (extraMeta?.transformations) {
245
- const upscaleTransform = extraMeta.transformations.find(
246
- (t) => t.type === "upscale"
247
- );
248
- if (upscaleTransform) {
249
- const originalWidth = extraMeta.width ?? width;
250
- if (originalWidth > 0 && upscaleTransform.upscaleWidth) {
251
- const scale = upscaleTransform.upscaleWidth / originalWidth;
252
- metadata.upscale = {
253
- scale: Math.round(scale * 100) / 100
254
- };
255
- }
256
- }
257
- }
258
- return Result.ok(metadata);
204
+ nodes,
205
+ ...merged
206
+ });
207
+ }
208
+ function cleanJsonString(json) {
209
+ return json.replace(/\0+$/, "").replace(/:\s*NaN\b/g, ": null");
210
+ }
211
+ function calculateScale(targetWidth, baseWidth) {
212
+ if (baseWidth <= 0 || targetWidth <= 0) return void 0;
213
+ return Math.round(targetWidth / baseWidth * 100) / 100;
259
214
  }
260
215
  function findPromptJson(entryRecord) {
261
216
  if (entryRecord.prompt) {
262
- return entryRecord.prompt.replace(/:\s*NaN\b/g, ": null");
217
+ return cleanJsonString(entryRecord.prompt);
263
218
  }
264
219
  const candidates = [
265
220
  entryRecord.Comment,
@@ -273,7 +228,7 @@ function findPromptJson(entryRecord) {
273
228
  for (const candidate of candidates) {
274
229
  if (!candidate) continue;
275
230
  if (candidate.startsWith("{")) {
276
- const cleaned = candidate.replace(/\0+$/, "").replace(/:\s*NaN\b/g, ": null");
231
+ const cleaned = cleanJsonString(candidate);
277
232
  const parsed = parseJson(cleaned);
278
233
  if (!parsed.ok) continue;
279
234
  if (parsed.value.prompt && typeof parsed.value.prompt === "object") {
@@ -293,14 +248,174 @@ function findNode(prompt, keys) {
293
248
  function extractText(node) {
294
249
  return typeof node?.inputs.text === "string" ? node.inputs.text : "";
295
250
  }
296
- function extractExtraMetadata(prompt) {
251
+ function extractPromptTexts(prompt) {
252
+ const positiveClip = findNode(prompt, COMFYUI_NODE_KEYS.positiveClip);
253
+ const negativeClip = findNode(prompt, COMFYUI_NODE_KEYS.negativeClip);
254
+ return {
255
+ promptText: extractText(positiveClip),
256
+ negativeText: extractText(negativeClip)
257
+ };
258
+ }
259
+ function extractDimensions(prompt) {
260
+ const latentImage = findNode(prompt, COMFYUI_NODE_KEYS.latentImage);
261
+ return {
262
+ width: latentImage ? Number(latentImage.inputs.width) || 0 : 0,
263
+ height: latentImage ? Number(latentImage.inputs.height) || 0 : 0
264
+ };
265
+ }
266
+ function extractSamplingFromKSampler(ksampler) {
267
+ if (!ksampler) return void 0;
268
+ return {
269
+ seed: ksampler.inputs.seed,
270
+ steps: ksampler.inputs.steps,
271
+ cfg: ksampler.inputs.cfg,
272
+ sampler: ksampler.inputs.sampler_name,
273
+ scheduler: ksampler.inputs.scheduler
274
+ };
275
+ }
276
+ function extractModelFromCheckpoint(checkpoint) {
277
+ if (!checkpoint?.inputs?.ckpt_name) return void 0;
278
+ return { name: String(checkpoint.inputs.ckpt_name) };
279
+ }
280
+ function extractComfyUIMetadata(prompt) {
281
+ const { promptText, negativeText } = extractPromptTexts(prompt);
282
+ const { width, height } = extractDimensions(prompt);
283
+ const ksampler = findNode(prompt, COMFYUI_NODE_KEYS.sampler);
284
+ const checkpoint = findNode(prompt, COMFYUI_NODE_KEYS.checkpoint);
285
+ const hiresModel = findNode(
286
+ prompt,
287
+ COMFYUI_NODE_KEYS.hiresModelUpscale
288
+ )?.inputs;
289
+ const hiresScale = findNode(
290
+ prompt,
291
+ COMFYUI_NODE_KEYS.hiresImageScale
292
+ )?.inputs;
293
+ const hiresSampler = findNode(prompt, COMFYUI_NODE_KEYS.hiresSampler)?.inputs;
294
+ return trimObject({
295
+ prompt: promptText || void 0,
296
+ negativePrompt: negativeText || void 0,
297
+ width: width > 0 ? width : void 0,
298
+ height: height > 0 ? height : void 0,
299
+ model: extractModelFromCheckpoint(checkpoint),
300
+ sampling: extractSamplingFromKSampler(ksampler),
301
+ ...buildHiresOrUpscale(hiresModel, hiresScale, hiresSampler, width)
302
+ });
303
+ }
304
+ function buildHiresOrUpscale(hiresModel, hiresScale, hiresSampler, baseWidth) {
305
+ if (!hiresModel || !hiresScale) return {};
306
+ const hiresWidth = hiresScale.width;
307
+ const scale = calculateScale(hiresWidth, baseWidth);
308
+ if (hiresSampler) {
309
+ return {
310
+ hires: {
311
+ upscaler: hiresModel.model_name,
312
+ scale,
313
+ steps: hiresSampler.steps,
314
+ denoise: hiresSampler.denoise
315
+ }
316
+ };
317
+ }
318
+ return {
319
+ upscale: {
320
+ upscaler: hiresModel.model_name,
321
+ scale
322
+ }
323
+ };
324
+ }
325
+ function extractExtraMetadata(prompt, entryRecord) {
297
326
  const extraMetaField = prompt.extraMetadata;
298
- if (typeof extraMetaField !== "string") return void 0;
299
- const parsed = parseJson(extraMetaField);
300
- return parsed.ok ? parsed.value : void 0;
327
+ if (typeof extraMetaField === "string") {
328
+ const parsed = parseJson(extraMetaField);
329
+ if (parsed.ok) return parsed.value;
330
+ }
331
+ if (entryRecord?.extraMetadata) {
332
+ const parsed = parseJson(entryRecord.extraMetadata);
333
+ if (parsed.ok) return parsed.value;
334
+ }
335
+ return void 0;
336
+ }
337
+ function extractCivitaiMetadata(extraMeta) {
338
+ if (!extraMeta) return void 0;
339
+ const upscale = buildCivitaiUpscale(extraMeta);
340
+ const sampling = buildCivitaiSampling(extraMeta);
341
+ return trimObject({
342
+ prompt: extraMeta.prompt,
343
+ negativePrompt: extraMeta.negativePrompt,
344
+ width: extraMeta.width,
345
+ height: extraMeta.height,
346
+ model: extraMeta.baseModel ? { name: extraMeta.baseModel } : void 0,
347
+ ...sampling,
348
+ ...upscale
349
+ });
350
+ }
351
+ function buildCivitaiUpscale(extraMeta) {
352
+ if (!extraMeta.transformations) return {};
353
+ const upscaleTransform = extraMeta.transformations.find(
354
+ (t) => t.type === "upscale"
355
+ );
356
+ if (!upscaleTransform?.upscaleWidth) return {};
357
+ const scale = calculateScale(
358
+ upscaleTransform.upscaleWidth,
359
+ extraMeta.width ?? 0
360
+ );
361
+ if (scale === void 0) return {};
362
+ return {
363
+ upscale: { scale }
364
+ };
365
+ }
366
+ function buildCivitaiSampling(extraMeta) {
367
+ if (extraMeta.seed === void 0 && extraMeta.steps === void 0 && extraMeta.cfgScale === void 0 && extraMeta.sampler === void 0) {
368
+ return {};
369
+ }
370
+ return {
371
+ sampling: {
372
+ seed: extraMeta.seed,
373
+ steps: extraMeta.steps,
374
+ cfg: extraMeta.cfgScale,
375
+ sampler: extraMeta.sampler
376
+ }
377
+ };
378
+ }
379
+ function mergeMetadata(base, override) {
380
+ const merged = { ...base, ...override };
381
+ return {
382
+ // Required fields with defaults
383
+ prompt: merged.prompt ?? "",
384
+ negativePrompt: merged.negativePrompt ?? "",
385
+ width: merged.width ?? 0,
386
+ height: merged.height ?? 0,
387
+ // Optional fields - only include if defined
388
+ ...trimObject({
389
+ model: merged.model,
390
+ sampling: merged.sampling,
391
+ hires: merged.hires,
392
+ upscale: merged.upscale
393
+ })
394
+ };
301
395
  }
302
396
 
303
397
  // src/parsers/detect.ts
398
+ var MARKERS = {
399
+ // Unique chunk keywords
400
+ INVOKEAI: "invokeai_metadata",
401
+ TENSORART: "generation_data",
402
+ STABILITY_MATRIX: "smproj",
403
+ EASYDIFFUSION: "use_stable_diffusion_model",
404
+ CIVITAI_EXTRA: "extraMetadata",
405
+ // Content patterns
406
+ SWARMUI: "sui_image_params",
407
+ SWARM_VERSION: "swarm_version",
408
+ COMFYUI_NODE: "class_type",
409
+ NOVELAI_SCHEDULE: "noise_schedule",
410
+ NOVELAI_V4: "v4_prompt",
411
+ NOVELAI_UNCOND: "uncond_scale",
412
+ CIVITAI_NS: "civitai:",
413
+ CIVITAI_RESOURCES: "Civitai resources:",
414
+ RUINED_FOOOCUS: "RuinedFooocus",
415
+ HF_MODEL: '"Model"',
416
+ HF_RESOLUTION: '"resolution"',
417
+ FOOOCUS_BASE: '"base_model"'
418
+ };
304
419
  function detectSoftware(entries) {
305
420
  const entryRecord = buildEntryRecord(entries);
306
421
  const uniqueResult = detectUniqueKeywords(entryRecord);
@@ -317,20 +432,23 @@ function detectUniqueKeywords(entryRecord) {
317
432
  if (entryRecord.Software?.startsWith("NovelAI")) {
318
433
  return "novelai";
319
434
  }
320
- if ("invokeai_metadata" in entryRecord) {
435
+ if (MARKERS.INVOKEAI in entryRecord) {
321
436
  return "invokeai";
322
437
  }
323
- if ("generation_data" in entryRecord) {
438
+ if (MARKERS.TENSORART in entryRecord) {
324
439
  return "tensorart";
325
440
  }
326
- if ("smproj" in entryRecord) {
441
+ if (MARKERS.STABILITY_MATRIX in entryRecord) {
327
442
  return "stability-matrix";
328
443
  }
329
444
  if ("negative_prompt" in entryRecord || "Negative Prompt" in entryRecord) {
330
445
  return "easydiffusion";
331
446
  }
447
+ if (MARKERS.CIVITAI_EXTRA in entryRecord) {
448
+ return "civitai";
449
+ }
332
450
  const parameters = entryRecord.parameters;
333
- if (parameters?.includes("sui_image_params")) {
451
+ if (parameters?.includes(MARKERS.SWARMUI)) {
334
452
  return "swarmui";
335
453
  }
336
454
  const comment = entryRecord.Comment;
@@ -342,15 +460,18 @@ function detectUniqueKeywords(entryRecord) {
342
460
  function detectFromCommentJson(comment) {
343
461
  try {
344
462
  const parsed = JSON.parse(comment);
345
- if ("invokeai_metadata" in parsed) {
463
+ if (MARKERS.INVOKEAI in parsed) {
346
464
  return "invokeai";
347
465
  }
348
- if ("generation_data" in parsed) {
466
+ if (MARKERS.TENSORART in parsed) {
349
467
  return "tensorart";
350
468
  }
351
- if ("smproj" in parsed) {
469
+ if (MARKERS.STABILITY_MATRIX in parsed) {
352
470
  return "stability-matrix";
353
471
  }
472
+ if (MARKERS.CIVITAI_EXTRA in parsed) {
473
+ return "civitai";
474
+ }
354
475
  if ("prompt" in parsed && "workflow" in parsed) {
355
476
  const workflow = parsed.workflow;
356
477
  const prompt = parsed.prompt;
@@ -360,12 +481,12 @@ function detectFromCommentJson(comment) {
360
481
  return "comfyui";
361
482
  }
362
483
  }
363
- if ("sui_image_params" in parsed) {
484
+ if (MARKERS.SWARMUI in parsed) {
364
485
  return "swarmui";
365
486
  }
366
487
  if ("prompt" in parsed && "parameters" in parsed) {
367
488
  const params = String(parsed.parameters || "");
368
- if (params.includes("sui_image_params") || params.includes("swarm_version")) {
489
+ if (params.includes(MARKERS.SWARMUI) || params.includes(MARKERS.SWARM_VERSION)) {
369
490
  return "swarmui";
370
491
  }
371
492
  }
@@ -383,10 +504,10 @@ function detectComfyUIEntries(entryRecord) {
383
504
  if ("prompt" in entryRecord) {
384
505
  const promptText = entryRecord.prompt;
385
506
  if (promptText?.startsWith("{")) {
386
- if (promptText.includes("sui_image_params")) {
507
+ if (promptText.includes(MARKERS.SWARMUI)) {
387
508
  return "swarmui";
388
509
  }
389
- if (promptText.includes("class_type")) {
510
+ if (promptText.includes(MARKERS.COMFYUI_NODE)) {
390
511
  return "comfyui";
391
512
  }
392
513
  }
@@ -400,25 +521,25 @@ function detectFromTextContent(text) {
400
521
  return detectFromA1111Format(text);
401
522
  }
402
523
  function detectFromJsonFormat(json) {
403
- if (json.includes("sui_image_params")) {
524
+ if (json.includes(MARKERS.SWARMUI)) {
404
525
  return "swarmui";
405
526
  }
406
- if (json.includes('"software":"RuinedFooocus"') || json.includes('"software": "RuinedFooocus"')) {
527
+ if (json.includes(`"software":"${MARKERS.RUINED_FOOOCUS}"`) || json.includes(`"software": "${MARKERS.RUINED_FOOOCUS}"`)) {
407
528
  return "ruined-fooocus";
408
529
  }
409
- if (json.includes('"use_stable_diffusion_model"')) {
530
+ if (json.includes(`"${MARKERS.EASYDIFFUSION}"`)) {
410
531
  return "easydiffusion";
411
532
  }
412
- if (json.includes("civitai:") || json.includes('"resource-stack"')) {
533
+ if (json.includes(MARKERS.CIVITAI_NS) || json.includes(`"${MARKERS.CIVITAI_EXTRA}"`)) {
413
534
  return "civitai";
414
535
  }
415
- if (json.includes('"v4_prompt"') || json.includes('"noise_schedule"') || json.includes('"uncond_scale"') || json.includes('"Software":"NovelAI"') || json.includes('\\"noise_schedule\\"') || json.includes('\\"v4_prompt\\"')) {
536
+ if (json.includes(`"${MARKERS.NOVELAI_V4}"`) || json.includes(`"${MARKERS.NOVELAI_SCHEDULE}"`) || json.includes(`"${MARKERS.NOVELAI_UNCOND}"`) || json.includes('"Software":"NovelAI"') || json.includes(`\\"${MARKERS.NOVELAI_SCHEDULE}\\"`) || json.includes(`\\"${MARKERS.NOVELAI_V4}\\"`)) {
416
537
  return "novelai";
417
538
  }
418
- if (json.includes('"Model"') && json.includes('"resolution"')) {
539
+ if (json.includes(MARKERS.HF_MODEL) && json.includes(MARKERS.HF_RESOLUTION)) {
419
540
  return "hf-space";
420
541
  }
421
- if (json.includes('"prompt"') && json.includes('"base_model"')) {
542
+ if (json.includes('"prompt"') && json.includes(MARKERS.FOOOCUS_BASE)) {
422
543
  return "fooocus";
423
544
  }
424
545
  if (json.includes('"prompt"') || json.includes('"nodes"')) {
@@ -427,7 +548,7 @@ function detectFromJsonFormat(json) {
427
548
  return null;
428
549
  }
429
550
  function detectFromA1111Format(text) {
430
- if (text.includes("sui_image_params") || text.includes("swarm_version")) {
551
+ if (text.includes(MARKERS.SWARMUI) || text.includes(MARKERS.SWARM_VERSION)) {
431
552
  return "swarmui";
432
553
  }
433
554
  const versionMatch = text.match(/Version:\s*([^\s,]+)/);
@@ -446,7 +567,7 @@ function detectFromA1111Format(text) {
446
567
  if (text.includes("App: SD.Next") || text.includes("App:SD.Next")) {
447
568
  return "sd-next";
448
569
  }
449
- if (text.includes("Civitai resources:")) {
570
+ if (text.includes(MARKERS.CIVITAI_RESOURCES)) {
450
571
  return "civitai";
451
572
  }
452
573
  if (text.includes("Steps:") && text.includes("Sampler:")) {
@@ -606,33 +727,23 @@ function parseHfSpace(entries) {
606
727
  negativePrompt: json.negative_prompt ?? "",
607
728
  width,
608
729
  height,
609
- model: {
730
+ model: trimObject({
610
731
  name: json.Model,
611
732
  hash: json["Model hash"]
612
- },
613
- sampling: {
733
+ }),
734
+ sampling: trimObject({
614
735
  sampler: json.sampler,
615
736
  steps: json.num_inference_steps,
616
737
  cfg: json.guidance_scale,
617
738
  seed: json.seed
618
- }
739
+ })
619
740
  };
620
741
  return Result.ok(metadata);
621
742
  }
622
743
 
623
744
  // src/parsers/invokeai.ts
624
745
  function extractInvokeAIMetadata(entryRecord) {
625
- if (entryRecord.invokeai_metadata) {
626
- return entryRecord.invokeai_metadata;
627
- }
628
- if (!entryRecord.Comment) {
629
- return void 0;
630
- }
631
- const commentParsed = parseJson(entryRecord.Comment);
632
- if (!commentParsed.ok || !("invokeai_metadata" in commentParsed.value)) {
633
- return void 0;
634
- }
635
- return JSON.stringify(commentParsed.value.invokeai_metadata);
746
+ return entryRecord.invokeai_metadata ?? extractFromCommentJson(entryRecord, "invokeai_metadata");
636
747
  }
637
748
  function parseInvokeAI(entries) {
638
749
  const entryRecord = buildEntryRecord(entries);
@@ -650,28 +761,23 @@ function parseInvokeAI(entries) {
650
761
  const data = parsed.value;
651
762
  const width = data.width ?? 0;
652
763
  const height = data.height ?? 0;
653
- const metadata = {
764
+ return Result.ok({
654
765
  software: "invokeai",
655
766
  prompt: data.positive_prompt ?? "",
656
767
  negativePrompt: data.negative_prompt ?? "",
657
768
  width,
658
- height
659
- };
660
- if (data.model?.name || data.model?.hash) {
661
- metadata.model = {
662
- name: data.model.name,
663
- hash: data.model.hash
664
- };
665
- }
666
- if (data.seed !== void 0 || data.steps !== void 0 || data.cfg_scale !== void 0 || data.scheduler !== void 0) {
667
- metadata.sampling = {
769
+ height,
770
+ model: trimObject({
771
+ name: data.model?.name,
772
+ hash: data.model?.hash
773
+ }),
774
+ sampling: trimObject({
668
775
  seed: data.seed,
669
776
  steps: data.steps,
670
777
  cfg: data.cfg_scale,
671
778
  sampler: data.scheduler
672
- };
673
- }
674
- return Result.ok(metadata);
779
+ })
780
+ });
675
781
  }
676
782
 
677
783
  // src/parsers/novelai.ts
@@ -699,35 +805,31 @@ function parseNovelAI(entries) {
699
805
  const height = comment.height ?? 0;
700
806
  const prompt = comment.v4_prompt?.caption?.base_caption ?? comment.prompt ?? "";
701
807
  const negativePrompt = comment.v4_negative_prompt?.caption?.base_caption ?? comment.uc ?? "";
702
- const metadata = {
808
+ const charCaptions = comment.v4_prompt?.caption?.char_captions;
809
+ const characterPrompts = charCaptions && charCaptions.length > 0 ? charCaptions.map((cc) => {
810
+ if (!cc.char_caption) return null;
811
+ return {
812
+ prompt: cc.char_caption,
813
+ center: cc.centers?.[0]
814
+ };
815
+ }).filter((cp) => cp !== null) : void 0;
816
+ return Result.ok({
703
817
  software: "novelai",
704
818
  prompt,
705
819
  negativePrompt,
706
820
  width,
707
- height
708
- };
709
- if (comment.steps !== void 0 || comment.scale !== void 0 || comment.seed !== void 0 || comment.noise_schedule !== void 0 || comment.sampler !== void 0) {
710
- metadata.sampling = {
821
+ height,
822
+ sampling: trimObject({
711
823
  steps: comment.steps,
712
824
  cfg: comment.scale,
713
825
  seed: comment.seed,
714
826
  sampler: comment.sampler,
715
827
  scheduler: comment.noise_schedule
716
- };
717
- }
718
- const charCaptions = comment.v4_prompt?.caption?.char_captions;
719
- if (charCaptions && charCaptions.length > 0) {
720
- metadata.characterPrompts = charCaptions.map((cc) => {
721
- if (!cc.char_caption) return null;
722
- return {
723
- prompt: cc.char_caption,
724
- center: cc.centers?.[0]
725
- };
726
- }).filter((cp) => cp !== null);
727
- metadata.useCoords = comment.v4_prompt?.use_coords;
728
- metadata.useOrder = comment.v4_prompt?.use_order;
729
- }
730
- return Result.ok(metadata);
828
+ }),
829
+ characterPrompts,
830
+ useCoords: characterPrompts ? comment.v4_prompt?.use_coords : void 0,
831
+ useOrder: characterPrompts ? comment.v4_prompt?.use_order : void 0
832
+ });
731
833
  }
732
834
 
733
835
  // src/parsers/ruined-fooocus.ts
@@ -777,43 +879,18 @@ function parseStabilityMatrix(entries) {
777
879
  if (!comfyResult.ok || comfyResult.value.software !== "comfyui") {
778
880
  return Result.error({ type: "unsupportedFormat" });
779
881
  }
780
- const metadata = {
882
+ const jsonText = entryRecord["parameters-json"] ?? extractFromCommentJson(entryRecord, "parameters-json");
883
+ const parsed = jsonText ? parseJson(jsonText) : void 0;
884
+ const data = parsed?.ok ? parsed.value : void 0;
885
+ return Result.ok({
781
886
  ...comfyResult.value,
782
- software: "stability-matrix"
783
- };
784
- let jsonText = entryRecord["parameters-json"];
785
- if (!jsonText && entryRecord.Comment?.startsWith("{")) {
786
- const commentParsed = parseJson(
787
- entryRecord.Comment
788
- );
789
- if (commentParsed.ok) {
790
- const commentData = commentParsed.value;
791
- if (typeof commentData["parameters-json"] === "string") {
792
- jsonText = commentData["parameters-json"];
793
- } else if (typeof commentData["parameters-json"] === "object") {
794
- jsonText = JSON.stringify(commentData["parameters-json"]);
795
- }
796
- }
797
- }
798
- if (jsonText) {
799
- const parsed = parseJson(jsonText);
800
- if (parsed.ok) {
801
- const data = parsed.value;
802
- if (data.PositivePrompt !== void 0) {
803
- metadata.prompt = data.PositivePrompt;
804
- }
805
- if (data.NegativePrompt !== void 0) {
806
- metadata.negativePrompt = data.NegativePrompt;
807
- }
808
- if (data.ModelName !== void 0 || data.ModelHash !== void 0) {
809
- metadata.model = {
810
- name: data.ModelName,
811
- hash: data.ModelHash
812
- };
813
- }
814
- }
815
- }
816
- return Result.ok(metadata);
887
+ software: "stability-matrix",
888
+ // Override prompts from parameters-json (more complete than workflow)
889
+ prompt: data?.PositivePrompt ?? comfyResult.value.prompt,
890
+ negativePrompt: data?.NegativePrompt ?? comfyResult.value.negativePrompt,
891
+ // Override model if either name or hash is provided
892
+ model: data?.ModelName !== void 0 || data?.ModelHash !== void 0 ? { name: data?.ModelName, hash: data?.ModelHash } : comfyResult.value.model
893
+ });
817
894
  }
818
895
 
819
896
  // src/parsers/swarmui.ts
@@ -852,67 +929,37 @@ function parseSwarmUI(entries) {
852
929
  }
853
930
  const width = params.width ?? 0;
854
931
  const height = params.height ?? 0;
855
- const metadata = {
932
+ const promptSource = entryRecord.prompt || entryRecord.Make;
933
+ const promptParsed = promptSource ? parseJson(promptSource) : void 0;
934
+ const nodes = promptParsed?.ok ? promptParsed.value : void 0;
935
+ return Result.ok({
856
936
  software: "swarmui",
857
937
  prompt: params.prompt ?? "",
858
938
  negativePrompt: params.negativeprompt ?? "",
859
939
  width,
860
- height
861
- };
862
- const promptSource = entryRecord.prompt || entryRecord.Make;
863
- if (promptSource) {
864
- const promptParsed = parseJson(promptSource);
865
- if (promptParsed.ok) {
866
- metadata.nodes = promptParsed.value;
867
- }
868
- }
869
- if (params.model) {
870
- metadata.model = {
871
- name: params.model
872
- };
873
- }
874
- if (params.seed !== void 0 || params.steps !== void 0 || params.cfgscale !== void 0 || params.sampler !== void 0 || params.scheduler !== void 0) {
875
- metadata.sampling = {
940
+ height,
941
+ nodes,
942
+ model: trimObject({ name: params.model }),
943
+ sampling: trimObject({
876
944
  seed: params.seed,
877
945
  steps: params.steps,
878
946
  cfg: params.cfgscale,
879
947
  sampler: params.sampler,
880
948
  scheduler: params.scheduler
881
- };
882
- }
883
- if (params.refinerupscale !== void 0 || params.refinerupscalemethod !== void 0 || params.refinercontrolpercentage !== void 0) {
884
- metadata.hires = {
949
+ }),
950
+ hires: trimObject({
885
951
  scale: params.refinerupscale,
886
952
  upscaler: params.refinerupscalemethod,
887
953
  denoise: params.refinercontrolpercentage
888
- };
889
- }
890
- return Result.ok(metadata);
954
+ })
955
+ });
891
956
  }
892
957
 
893
958
  // src/parsers/tensorart.ts
894
959
  function parseTensorArt(entries) {
895
960
  const entryRecord = buildEntryRecord(entries);
896
- let dataText = entryRecord.generation_data;
897
- let promptChunk = entryRecord.prompt;
898
- if (!dataText && entryRecord.Comment?.startsWith("{")) {
899
- const commentParsed = parseJson(
900
- entryRecord.Comment
901
- );
902
- if (commentParsed.ok) {
903
- const commentData = commentParsed.value;
904
- if (typeof commentData.generation_data === "string") {
905
- dataText = commentData.generation_data;
906
- } else if (typeof commentData.generation_data === "object") {
907
- dataText = JSON.stringify(commentData.generation_data);
908
- }
909
- if (typeof commentData.prompt === "string") {
910
- promptChunk = commentData.prompt;
911
- } else if (typeof commentData.prompt === "object") {
912
- promptChunk = JSON.stringify(commentData.prompt);
913
- }
914
- }
915
- }
961
+ const dataText = entryRecord.generation_data ?? extractFromCommentJson(entryRecord, "generation_data");
962
+ const promptChunk = entryRecord.prompt ?? extractFromCommentJson(entryRecord, "prompt");
916
963
  if (!dataText) {
917
964
  return Result.error({ type: "unsupportedFormat" });
918
965
  }
@@ -937,30 +984,26 @@ function parseTensorArt(entries) {
937
984
  message: "Invalid JSON in prompt chunk"
938
985
  });
939
986
  }
940
- const metadata = {
987
+ const baseSeed = data.seed ? Number(data.seed) : void 0;
988
+ const seed = baseSeed === -1 ? findActualSeed(promptParsed.value) : baseSeed;
989
+ return Result.ok({
941
990
  software: "tensorart",
942
991
  prompt: data.prompt ?? "",
943
992
  negativePrompt: data.negativePrompt ?? "",
944
993
  width,
945
994
  height,
946
- nodes: promptParsed.value
947
- };
948
- if (data.baseModel?.modelFileName || data.baseModel?.hash) {
949
- metadata.model = {
950
- name: data.baseModel.modelFileName,
951
- hash: data.baseModel.hash
952
- };
953
- }
954
- if (data.seed !== void 0 || data.steps !== void 0 || data.cfgScale !== void 0 || data.clipSkip !== void 0) {
955
- const baseSeed = data.seed ? Number(data.seed) : void 0;
956
- metadata.sampling = {
957
- seed: baseSeed === -1 ? findActualSeed(promptParsed.value) : baseSeed,
995
+ nodes: promptParsed.value,
996
+ model: trimObject({
997
+ name: data.baseModel?.modelFileName,
998
+ hash: data.baseModel?.hash
999
+ }),
1000
+ sampling: trimObject({
1001
+ seed,
958
1002
  steps: data.steps,
959
1003
  cfg: data.cfgScale,
960
1004
  clipSkip: data.clipSkip
961
- };
962
- }
963
- return Result.ok(metadata);
1005
+ })
1006
+ });
964
1007
  }
965
1008
  function findActualSeed(nodes) {
966
1009
  const samplerNode = findSamplerNode(nodes);
@@ -987,8 +1030,16 @@ function parseMetadata(entries) {
987
1030
  return parseHfSpace(entries);
988
1031
  case "civitai": {
989
1032
  const comfyResult = parseComfyUI(entries);
990
- if (comfyResult.ok) return comfyResult;
991
- return parseA1111(entries);
1033
+ if (comfyResult.ok) {
1034
+ comfyResult.value.software = "civitai";
1035
+ return comfyResult;
1036
+ }
1037
+ const a1111Result = parseA1111(entries);
1038
+ if (a1111Result.ok) {
1039
+ a1111Result.value.software = "civitai";
1040
+ return a1111Result;
1041
+ }
1042
+ return a1111Result;
992
1043
  }
993
1044
  case "comfyui": {
994
1045
  const comfyResult = parseComfyUI(entries);
@@ -1644,7 +1695,7 @@ function sourceToKeyword(source) {
1644
1695
  }
1645
1696
 
1646
1697
  // src/api/read.ts
1647
- function read(input) {
1698
+ function read(input, options) {
1648
1699
  const data = toUint8Array(input);
1649
1700
  const format = detectFormat(data);
1650
1701
  if (!format) {
@@ -1661,7 +1712,7 @@ function read(input) {
1661
1712
  return { status: "unrecognized", raw };
1662
1713
  }
1663
1714
  const metadata = parseResult.value;
1664
- if (metadata.width === 0 || metadata.height === 0) {
1715
+ if (!options?.strict && (metadata.width === 0 || metadata.height === 0)) {
1665
1716
  const dims = HELPERS[format].readDimensions(data);
1666
1717
  if (dims) {
1667
1718
  metadata.width = metadata.width || dims.width;
@@ -1852,6 +1903,69 @@ function convertA1111SegmentsToPng(segments) {
1852
1903
  return createEncodedChunk("parameters", userComment.data, "dynamic");
1853
1904
  }
1854
1905
 
1906
+ // src/converters/civitai.ts
1907
+ var CIVITAI_SPECIAL_KEYS = ["extra", "extraMetadata", "resource-stack"];
1908
+ function convertCivitaiPngToSegments(chunks) {
1909
+ const parametersChunk = chunks.find((c) => c.keyword === "parameters");
1910
+ if (parametersChunk && !parametersChunk.text.trimStart().startsWith("{")) {
1911
+ return convertA1111PngToSegments(chunks);
1912
+ }
1913
+ const data = {};
1914
+ for (const chunk of chunks) {
1915
+ if (chunk.keyword === "prompt") {
1916
+ const parsed = parseJson(chunk.text);
1917
+ if (parsed.ok && typeof parsed.value === "object" && parsed.value) {
1918
+ Object.assign(data, parsed.value);
1919
+ }
1920
+ } else if (chunk.keyword === "extraMetadata") {
1921
+ data[chunk.keyword] = chunk.text;
1922
+ } else {
1923
+ const parsed = parseJson(chunk.text);
1924
+ data[chunk.keyword] = parsed.ok ? parsed.value : chunk.text;
1925
+ }
1926
+ }
1927
+ return [
1928
+ {
1929
+ source: { type: "exifUserComment" },
1930
+ data: JSON.stringify(data)
1931
+ }
1932
+ ];
1933
+ }
1934
+ function convertCivitaiSegmentsToPng(segments) {
1935
+ const userComment = findSegment(segments, "exifUserComment");
1936
+ if (!userComment) return [];
1937
+ const isJson = userComment.data.trimStart().startsWith("{");
1938
+ if (!isJson) {
1939
+ return convertA1111SegmentsToPng(segments);
1940
+ }
1941
+ const parsed = parseJson(userComment.data);
1942
+ if (!parsed.ok) {
1943
+ return convertA1111SegmentsToPng(segments);
1944
+ }
1945
+ const data = parsed.value;
1946
+ const promptData = {};
1947
+ const chunks = [];
1948
+ for (const [key, value] of Object.entries(data)) {
1949
+ if (CIVITAI_SPECIAL_KEYS.includes(key)) {
1950
+ chunks.push(
1951
+ ...createEncodedChunk(key, stringify(value), "text-utf8-raw")
1952
+ );
1953
+ } else {
1954
+ promptData[key] = value;
1955
+ }
1956
+ }
1957
+ if (Object.keys(promptData).length > 0) {
1958
+ chunks.unshift(
1959
+ ...createEncodedChunk(
1960
+ "prompt",
1961
+ JSON.stringify(promptData),
1962
+ "text-utf8-raw"
1963
+ )
1964
+ );
1965
+ }
1966
+ return chunks;
1967
+ }
1968
+
1855
1969
  // src/converters/base-json.ts
1856
1970
  function convertKvPngToSegments(chunks) {
1857
1971
  const data = {};
@@ -2101,7 +2215,7 @@ function convertMetadata(parseResult, targetFormat) {
2101
2215
  });
2102
2216
  }
2103
2217
  const raw = parseResult.raw;
2104
- if (raw.format === "png" && targetFormat === "png" || raw.format === "jpeg" && targetFormat === "jpeg" || raw.format === "webp" && targetFormat === "webp") {
2218
+ if (raw.format === targetFormat) {
2105
2219
  return Result.ok(raw);
2106
2220
  }
2107
2221
  const software = parseResult.metadata.software;
@@ -2166,15 +2280,20 @@ var convertHfSpace = createFormatConverter(
2166
2280
  createPngToSegments("parameters"),
2167
2281
  createSegmentsToPng("parameters", "text-unicode-escape")
2168
2282
  );
2283
+ var convertCivitai = createFormatConverter(
2284
+ convertCivitaiPngToSegments,
2285
+ convertCivitaiSegmentsToPng
2286
+ );
2169
2287
  var softwareConverters = {
2170
2288
  // NovelAI
2171
2289
  novelai: convertNovelai,
2172
- // A1111-format (sd-webui, forge, forge-neo, civitai, sd-next)
2290
+ // A1111-format (sd-webui, forge, forge-neo, sd-next)
2173
2291
  "sd-webui": convertA1111,
2174
2292
  "sd-next": convertA1111,
2175
2293
  forge: convertA1111,
2176
2294
  "forge-neo": convertA1111,
2177
- civitai: convertA1111,
2295
+ // CivitAI Orchestration format
2296
+ civitai: convertCivitai,
2178
2297
  // ComfyUI-format (comfyui, tensorart, stability-matrix)
2179
2298
  comfyui: convertComfyUI,
2180
2299
  tensorart: convertComfyUI,