@enslo/sd-metadata 1.5.0 → 1.6.1

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,13 @@ 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.CIVITAI_EXTRA}"`)) {
511
+ return "civitai";
512
+ }
513
+ if (promptText.includes(MARKERS.COMFYUI_NODE)) {
390
514
  return "comfyui";
391
515
  }
392
516
  }
@@ -400,25 +524,25 @@ function detectFromTextContent(text) {
400
524
  return detectFromA1111Format(text);
401
525
  }
402
526
  function detectFromJsonFormat(json) {
403
- if (json.includes("sui_image_params")) {
527
+ if (json.includes(MARKERS.SWARMUI)) {
404
528
  return "swarmui";
405
529
  }
406
- if (json.includes('"software":"RuinedFooocus"') || json.includes('"software": "RuinedFooocus"')) {
530
+ if (json.includes(`"software":"${MARKERS.RUINED_FOOOCUS}"`) || json.includes(`"software": "${MARKERS.RUINED_FOOOCUS}"`)) {
407
531
  return "ruined-fooocus";
408
532
  }
409
- if (json.includes('"use_stable_diffusion_model"')) {
533
+ if (json.includes(`"${MARKERS.EASYDIFFUSION}"`)) {
410
534
  return "easydiffusion";
411
535
  }
412
- if (json.includes("civitai:") || json.includes('"resource-stack"')) {
536
+ if (json.includes(MARKERS.CIVITAI_NS) || json.includes(`"${MARKERS.CIVITAI_EXTRA}"`)) {
413
537
  return "civitai";
414
538
  }
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\\"')) {
539
+ 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
540
  return "novelai";
417
541
  }
418
- if (json.includes('"Model"') && json.includes('"resolution"')) {
542
+ if (json.includes(MARKERS.HF_MODEL) && json.includes(MARKERS.HF_RESOLUTION)) {
419
543
  return "hf-space";
420
544
  }
421
- if (json.includes('"prompt"') && json.includes('"base_model"')) {
545
+ if (json.includes('"prompt"') && json.includes(MARKERS.FOOOCUS_BASE)) {
422
546
  return "fooocus";
423
547
  }
424
548
  if (json.includes('"prompt"') || json.includes('"nodes"')) {
@@ -427,7 +551,7 @@ function detectFromJsonFormat(json) {
427
551
  return null;
428
552
  }
429
553
  function detectFromA1111Format(text) {
430
- if (text.includes("sui_image_params") || text.includes("swarm_version")) {
554
+ if (text.includes(MARKERS.SWARMUI) || text.includes(MARKERS.SWARM_VERSION)) {
431
555
  return "swarmui";
432
556
  }
433
557
  const versionMatch = text.match(/Version:\s*([^\s,]+)/);
@@ -446,7 +570,7 @@ function detectFromA1111Format(text) {
446
570
  if (text.includes("App: SD.Next") || text.includes("App:SD.Next")) {
447
571
  return "sd-next";
448
572
  }
449
- if (text.includes("Civitai resources:")) {
573
+ if (text.includes(MARKERS.CIVITAI_RESOURCES)) {
450
574
  return "civitai";
451
575
  }
452
576
  if (text.includes("Steps:") && text.includes("Sampler:")) {
@@ -606,33 +730,23 @@ function parseHfSpace(entries) {
606
730
  negativePrompt: json.negative_prompt ?? "",
607
731
  width,
608
732
  height,
609
- model: {
733
+ model: trimObject({
610
734
  name: json.Model,
611
735
  hash: json["Model hash"]
612
- },
613
- sampling: {
736
+ }),
737
+ sampling: trimObject({
614
738
  sampler: json.sampler,
615
739
  steps: json.num_inference_steps,
616
740
  cfg: json.guidance_scale,
617
741
  seed: json.seed
618
- }
742
+ })
619
743
  };
620
744
  return Result.ok(metadata);
621
745
  }
622
746
 
623
747
  // src/parsers/invokeai.ts
624
748
  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);
749
+ return entryRecord.invokeai_metadata ?? extractFromCommentJson(entryRecord, "invokeai_metadata");
636
750
  }
637
751
  function parseInvokeAI(entries) {
638
752
  const entryRecord = buildEntryRecord(entries);
@@ -650,28 +764,23 @@ function parseInvokeAI(entries) {
650
764
  const data = parsed.value;
651
765
  const width = data.width ?? 0;
652
766
  const height = data.height ?? 0;
653
- const metadata = {
767
+ return Result.ok({
654
768
  software: "invokeai",
655
769
  prompt: data.positive_prompt ?? "",
656
770
  negativePrompt: data.negative_prompt ?? "",
657
771
  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 = {
772
+ height,
773
+ model: trimObject({
774
+ name: data.model?.name,
775
+ hash: data.model?.hash
776
+ }),
777
+ sampling: trimObject({
668
778
  seed: data.seed,
669
779
  steps: data.steps,
670
780
  cfg: data.cfg_scale,
671
781
  sampler: data.scheduler
672
- };
673
- }
674
- return Result.ok(metadata);
782
+ })
783
+ });
675
784
  }
676
785
 
677
786
  // src/parsers/novelai.ts
@@ -699,35 +808,31 @@ function parseNovelAI(entries) {
699
808
  const height = comment.height ?? 0;
700
809
  const prompt = comment.v4_prompt?.caption?.base_caption ?? comment.prompt ?? "";
701
810
  const negativePrompt = comment.v4_negative_prompt?.caption?.base_caption ?? comment.uc ?? "";
702
- const metadata = {
811
+ const charCaptions = comment.v4_prompt?.caption?.char_captions;
812
+ const characterPrompts = charCaptions && charCaptions.length > 0 ? charCaptions.map((cc) => {
813
+ if (!cc.char_caption) return null;
814
+ return {
815
+ prompt: cc.char_caption,
816
+ center: cc.centers?.[0]
817
+ };
818
+ }).filter((cp) => cp !== null) : void 0;
819
+ return Result.ok({
703
820
  software: "novelai",
704
821
  prompt,
705
822
  negativePrompt,
706
823
  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 = {
824
+ height,
825
+ sampling: trimObject({
711
826
  steps: comment.steps,
712
827
  cfg: comment.scale,
713
828
  seed: comment.seed,
714
829
  sampler: comment.sampler,
715
830
  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);
831
+ }),
832
+ characterPrompts,
833
+ useCoords: characterPrompts ? comment.v4_prompt?.use_coords : void 0,
834
+ useOrder: characterPrompts ? comment.v4_prompt?.use_order : void 0
835
+ });
731
836
  }
732
837
 
733
838
  // src/parsers/ruined-fooocus.ts
@@ -777,43 +882,18 @@ function parseStabilityMatrix(entries) {
777
882
  if (!comfyResult.ok || comfyResult.value.software !== "comfyui") {
778
883
  return Result.error({ type: "unsupportedFormat" });
779
884
  }
780
- const metadata = {
885
+ const jsonText = entryRecord["parameters-json"] ?? extractFromCommentJson(entryRecord, "parameters-json");
886
+ const parsed = jsonText ? parseJson(jsonText) : void 0;
887
+ const data = parsed?.ok ? parsed.value : void 0;
888
+ return Result.ok({
781
889
  ...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);
890
+ software: "stability-matrix",
891
+ // Override prompts from parameters-json (more complete than workflow)
892
+ prompt: data?.PositivePrompt ?? comfyResult.value.prompt,
893
+ negativePrompt: data?.NegativePrompt ?? comfyResult.value.negativePrompt,
894
+ // Override model if either name or hash is provided
895
+ model: data?.ModelName !== void 0 || data?.ModelHash !== void 0 ? { name: data?.ModelName, hash: data?.ModelHash } : comfyResult.value.model
896
+ });
817
897
  }
818
898
 
819
899
  // src/parsers/swarmui.ts
@@ -852,67 +932,37 @@ function parseSwarmUI(entries) {
852
932
  }
853
933
  const width = params.width ?? 0;
854
934
  const height = params.height ?? 0;
855
- const metadata = {
935
+ const promptSource = entryRecord.prompt || entryRecord.Make;
936
+ const promptParsed = promptSource ? parseJson(promptSource) : void 0;
937
+ const nodes = promptParsed?.ok ? promptParsed.value : void 0;
938
+ return Result.ok({
856
939
  software: "swarmui",
857
940
  prompt: params.prompt ?? "",
858
941
  negativePrompt: params.negativeprompt ?? "",
859
942
  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 = {
943
+ height,
944
+ nodes,
945
+ model: trimObject({ name: params.model }),
946
+ sampling: trimObject({
876
947
  seed: params.seed,
877
948
  steps: params.steps,
878
949
  cfg: params.cfgscale,
879
950
  sampler: params.sampler,
880
951
  scheduler: params.scheduler
881
- };
882
- }
883
- if (params.refinerupscale !== void 0 || params.refinerupscalemethod !== void 0 || params.refinercontrolpercentage !== void 0) {
884
- metadata.hires = {
952
+ }),
953
+ hires: trimObject({
885
954
  scale: params.refinerupscale,
886
955
  upscaler: params.refinerupscalemethod,
887
956
  denoise: params.refinercontrolpercentage
888
- };
889
- }
890
- return Result.ok(metadata);
957
+ })
958
+ });
891
959
  }
892
960
 
893
961
  // src/parsers/tensorart.ts
894
962
  function parseTensorArt(entries) {
895
963
  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
- }
964
+ const dataText = entryRecord.generation_data ?? extractFromCommentJson(entryRecord, "generation_data");
965
+ const promptChunk = entryRecord.prompt ?? extractFromCommentJson(entryRecord, "prompt");
916
966
  if (!dataText) {
917
967
  return Result.error({ type: "unsupportedFormat" });
918
968
  }
@@ -937,30 +987,26 @@ function parseTensorArt(entries) {
937
987
  message: "Invalid JSON in prompt chunk"
938
988
  });
939
989
  }
940
- const metadata = {
990
+ const baseSeed = data.seed ? Number(data.seed) : void 0;
991
+ const seed = baseSeed === -1 ? findActualSeed(promptParsed.value) : baseSeed;
992
+ return Result.ok({
941
993
  software: "tensorart",
942
994
  prompt: data.prompt ?? "",
943
995
  negativePrompt: data.negativePrompt ?? "",
944
996
  width,
945
997
  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,
998
+ nodes: promptParsed.value,
999
+ model: trimObject({
1000
+ name: data.baseModel?.modelFileName,
1001
+ hash: data.baseModel?.hash
1002
+ }),
1003
+ sampling: trimObject({
1004
+ seed,
958
1005
  steps: data.steps,
959
1006
  cfg: data.cfgScale,
960
1007
  clipSkip: data.clipSkip
961
- };
962
- }
963
- return Result.ok(metadata);
1008
+ })
1009
+ });
964
1010
  }
965
1011
  function findActualSeed(nodes) {
966
1012
  const samplerNode = findSamplerNode(nodes);
@@ -987,8 +1033,16 @@ function parseMetadata(entries) {
987
1033
  return parseHfSpace(entries);
988
1034
  case "civitai": {
989
1035
  const comfyResult = parseComfyUI(entries);
990
- if (comfyResult.ok) return comfyResult;
991
- return parseA1111(entries);
1036
+ if (comfyResult.ok) {
1037
+ comfyResult.value.software = "civitai";
1038
+ return comfyResult;
1039
+ }
1040
+ const a1111Result = parseA1111(entries);
1041
+ if (a1111Result.ok) {
1042
+ a1111Result.value.software = "civitai";
1043
+ return a1111Result;
1044
+ }
1045
+ return a1111Result;
992
1046
  }
993
1047
  case "comfyui": {
994
1048
  const comfyResult = parseComfyUI(entries);
@@ -1644,7 +1698,7 @@ function sourceToKeyword(source) {
1644
1698
  }
1645
1699
 
1646
1700
  // src/api/read.ts
1647
- function read(input) {
1701
+ function read(input, options) {
1648
1702
  const data = toUint8Array(input);
1649
1703
  const format = detectFormat(data);
1650
1704
  if (!format) {
@@ -1661,7 +1715,7 @@ function read(input) {
1661
1715
  return { status: "unrecognized", raw };
1662
1716
  }
1663
1717
  const metadata = parseResult.value;
1664
- if (metadata.width === 0 || metadata.height === 0) {
1718
+ if (!options?.strict && (metadata.width === 0 || metadata.height === 0)) {
1665
1719
  const dims = HELPERS[format].readDimensions(data);
1666
1720
  if (dims) {
1667
1721
  metadata.width = metadata.width || dims.width;
@@ -1852,6 +1906,43 @@ function convertA1111SegmentsToPng(segments) {
1852
1906
  return createEncodedChunk("parameters", userComment.data, "dynamic");
1853
1907
  }
1854
1908
 
1909
+ // src/converters/civitai.ts
1910
+ function convertCivitaiPngToSegments(chunks) {
1911
+ const parametersChunk = chunks.find((c) => c.keyword === "parameters");
1912
+ if (parametersChunk && !parametersChunk.text.trimStart().startsWith("{")) {
1913
+ return convertA1111PngToSegments(chunks);
1914
+ }
1915
+ const data = {};
1916
+ for (const chunk of chunks) {
1917
+ if (chunk.keyword === "prompt") {
1918
+ const parsed = parseJson(chunk.text);
1919
+ if (parsed.ok && typeof parsed.value === "object" && parsed.value) {
1920
+ Object.assign(data, parsed.value);
1921
+ }
1922
+ } else if (chunk.keyword === "extraMetadata") {
1923
+ data[chunk.keyword] = chunk.text;
1924
+ } else {
1925
+ const parsed = parseJson(chunk.text);
1926
+ data[chunk.keyword] = parsed.ok ? parsed.value : chunk.text;
1927
+ }
1928
+ }
1929
+ return [
1930
+ {
1931
+ source: { type: "exifUserComment" },
1932
+ data: JSON.stringify(data)
1933
+ }
1934
+ ];
1935
+ }
1936
+ function convertCivitaiSegmentsToPng(segments) {
1937
+ const userComment = findSegment(segments, "exifUserComment");
1938
+ if (!userComment) return [];
1939
+ const isJson = userComment.data.trimStart().startsWith("{");
1940
+ if (!isJson) {
1941
+ return convertA1111SegmentsToPng(segments);
1942
+ }
1943
+ return createEncodedChunk("prompt", userComment.data, "text-utf8-raw");
1944
+ }
1945
+
1855
1946
  // src/converters/base-json.ts
1856
1947
  function convertKvPngToSegments(chunks) {
1857
1948
  const data = {};
@@ -2101,7 +2192,7 @@ function convertMetadata(parseResult, targetFormat) {
2101
2192
  });
2102
2193
  }
2103
2194
  const raw = parseResult.raw;
2104
- if (raw.format === "png" && targetFormat === "png" || raw.format === "jpeg" && targetFormat === "jpeg" || raw.format === "webp" && targetFormat === "webp") {
2195
+ if (raw.format === targetFormat) {
2105
2196
  return Result.ok(raw);
2106
2197
  }
2107
2198
  const software = parseResult.metadata.software;
@@ -2166,15 +2257,20 @@ var convertHfSpace = createFormatConverter(
2166
2257
  createPngToSegments("parameters"),
2167
2258
  createSegmentsToPng("parameters", "text-unicode-escape")
2168
2259
  );
2260
+ var convertCivitai = createFormatConverter(
2261
+ convertCivitaiPngToSegments,
2262
+ convertCivitaiSegmentsToPng
2263
+ );
2169
2264
  var softwareConverters = {
2170
2265
  // NovelAI
2171
2266
  novelai: convertNovelai,
2172
- // A1111-format (sd-webui, forge, forge-neo, civitai, sd-next)
2267
+ // A1111-format (sd-webui, forge, forge-neo, sd-next)
2173
2268
  "sd-webui": convertA1111,
2174
2269
  "sd-next": convertA1111,
2175
2270
  forge: convertA1111,
2176
2271
  "forge-neo": convertA1111,
2177
- civitai: convertA1111,
2272
+ // CivitAI Orchestration format
2273
+ civitai: convertCivitai,
2178
2274
  // ComfyUI-format (comfyui, tensorart, stability-matrix)
2179
2275
  comfyui: convertComfyUI,
2180
2276
  tensorart: convertComfyUI,