@enslo/sd-metadata 1.0.1 → 1.1.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
@@ -407,23 +407,45 @@ function convertSwarmUIPngToSegments(chunks) {
407
407
  }
408
408
  const parsed = parseJson(parametersChunk.text);
409
409
  const data = parsed.ok ? parsed.value : parametersChunk.text;
410
- return [
410
+ const segments = [
411
411
  {
412
412
  source: { type: "exifUserComment" },
413
413
  data: typeof data === "string" ? data : JSON.stringify(data)
414
414
  }
415
415
  ];
416
+ const promptChunk = chunks.find((c) => c.keyword === "prompt");
417
+ if (promptChunk) {
418
+ segments.push({
419
+ source: { type: "exifMake" },
420
+ data: promptChunk.text
421
+ });
422
+ }
423
+ return segments;
416
424
  }
417
425
  function convertSwarmUISegmentsToPng(segments) {
418
426
  const userComment = findSegment(segments, "exifUserComment");
419
427
  if (!userComment) {
420
428
  return [];
421
429
  }
422
- return createEncodedChunk(
423
- "parameters",
424
- userComment.data,
425
- getEncodingStrategy("swarmui")
430
+ const chunks = [];
431
+ const make = findSegment(segments, "exifMake");
432
+ if (make) {
433
+ chunks.push(
434
+ ...createEncodedChunk(
435
+ "prompt",
436
+ make.data,
437
+ getEncodingStrategy("swarmui")
438
+ )
439
+ );
440
+ }
441
+ chunks.push(
442
+ ...createEncodedChunk(
443
+ "parameters",
444
+ userComment.data,
445
+ getEncodingStrategy("swarmui")
446
+ )
426
447
  );
448
+ return chunks;
427
449
  }
428
450
 
429
451
  // src/converters/index.ts
@@ -548,6 +570,10 @@ function parseA1111(entries) {
548
570
  return Result.error({ type: "unsupportedFormat" });
549
571
  }
550
572
  const text = parametersEntry.text;
573
+ const hasAIMarkers = text.includes("Steps:") || text.includes("Sampler:") || text.includes("Negative prompt:");
574
+ if (!hasAIMarkers) {
575
+ return Result.error({ type: "unsupportedFormat" });
576
+ }
551
577
  const { prompt, negativePrompt, settings } = parseParametersText(text);
552
578
  const settingsMap = parseSettings(settings);
553
579
  const size = settingsMap.get("Size") ?? "0x0";
@@ -556,7 +582,6 @@ function parseA1111(entries) {
556
582
  const app = settingsMap.get("App");
557
583
  const software = detectSoftwareVariant(version, app);
558
584
  const metadata = {
559
- type: "a1111",
560
585
  software,
561
586
  prompt,
562
587
  negativePrompt,
@@ -705,12 +730,13 @@ function parseComfyUI(entries) {
705
730
  const width = latentWidth || extraMeta?.width || 0;
706
731
  const height = latentHeight || extraMeta?.height || 0;
707
732
  const metadata = {
708
- type: "comfyui",
709
733
  software: "comfyui",
710
734
  prompt: positiveText,
711
735
  negativePrompt: negativeText,
712
736
  width,
713
- height
737
+ height,
738
+ nodes: prompt
739
+ // Store the parsed node graph
714
740
  };
715
741
  const checkpoint = findNode(prompt, ["CheckpointLoader_Base"])?.inputs?.ckpt_name;
716
742
  if (checkpoint) {
@@ -778,7 +804,7 @@ function parseComfyUI(entries) {
778
804
  }
779
805
  function findPromptJson(entryRecord) {
780
806
  if (entryRecord.prompt) {
781
- return entryRecord.prompt;
807
+ return entryRecord.prompt.replace(/:\s*NaN\b/g, ": null");
782
808
  }
783
809
  const candidates = [
784
810
  entryRecord.Comment,
@@ -792,7 +818,7 @@ function findPromptJson(entryRecord) {
792
818
  for (const candidate of candidates) {
793
819
  if (!candidate) continue;
794
820
  if (candidate.startsWith("{")) {
795
- const cleaned = candidate.replace(/\0+$/, "");
821
+ const cleaned = candidate.replace(/\0+$/, "").replace(/:\s*NaN\b/g, ": null");
796
822
  const parsed = parseJson(cleaned);
797
823
  if (!parsed.ok) continue;
798
824
  if (parsed.value.prompt && typeof parsed.value.prompt === "object") {
@@ -800,7 +826,7 @@ function findPromptJson(entryRecord) {
800
826
  }
801
827
  const values = Object.values(parsed.value);
802
828
  if (values.some((v) => v && typeof v === "object" && "class_type" in v)) {
803
- return candidate;
829
+ return cleaned;
804
830
  }
805
831
  }
806
832
  }
@@ -822,11 +848,17 @@ function extractExtraMetadata(prompt) {
822
848
  // src/parsers/detect.ts
823
849
  function detectSoftware(entries) {
824
850
  const entryRecord = buildEntryRecord(entries);
825
- const keywordResult = detectFromKeywords(entryRecord);
826
- if (keywordResult) return keywordResult;
827
- return detectFromContent(entryRecord);
851
+ const uniqueResult = detectUniqueKeywords(entryRecord);
852
+ if (uniqueResult) return uniqueResult;
853
+ const comfyResult = detectComfyUIEntries(entryRecord);
854
+ if (comfyResult) return comfyResult;
855
+ const text = entryRecord.parameters ?? entryRecord.Comment ?? "";
856
+ if (text) {
857
+ return detectFromTextContent(text);
858
+ }
859
+ return null;
828
860
  }
829
- function detectFromKeywords(entryRecord) {
861
+ function detectUniqueKeywords(entryRecord) {
830
862
  if (entryRecord.Software === "NovelAI") {
831
863
  return "novelai";
832
864
  }
@@ -842,56 +874,80 @@ function detectFromKeywords(entryRecord) {
842
874
  if ("negative_prompt" in entryRecord || "Negative Prompt" in entryRecord) {
843
875
  return "easydiffusion";
844
876
  }
877
+ const parameters = entryRecord.parameters;
878
+ if (parameters?.includes("sui_image_params")) {
879
+ return "swarmui";
880
+ }
845
881
  const comment = entryRecord.Comment;
846
882
  if (comment?.startsWith("{")) {
847
- try {
848
- const parsed = JSON.parse(comment);
849
- if ("invokeai_metadata" in parsed) {
850
- return "invokeai";
851
- }
852
- if ("prompt" in parsed && "workflow" in parsed) {
853
- const workflow = parsed.workflow;
854
- const prompt = parsed.prompt;
855
- const isObject = typeof workflow === "object" || typeof prompt === "object";
856
- const isJsonString = typeof workflow === "string" && workflow.startsWith("{") || typeof prompt === "string" && prompt.startsWith("{");
857
- if (isObject || isJsonString) {
858
- return "comfyui";
859
- }
883
+ return detectFromCommentJson(comment);
884
+ }
885
+ return null;
886
+ }
887
+ function detectFromCommentJson(comment) {
888
+ try {
889
+ const parsed = JSON.parse(comment);
890
+ if ("invokeai_metadata" in parsed) {
891
+ return "invokeai";
892
+ }
893
+ if ("prompt" in parsed && "workflow" in parsed) {
894
+ const workflow = parsed.workflow;
895
+ const prompt = parsed.prompt;
896
+ const isObject = typeof workflow === "object" || typeof prompt === "object";
897
+ const isJsonString = typeof workflow === "string" && workflow.startsWith("{") || typeof prompt === "string" && prompt.startsWith("{");
898
+ if (isObject || isJsonString) {
899
+ return "comfyui";
860
900
  }
861
- if ("sui_image_params" in parsed) {
901
+ }
902
+ if ("sui_image_params" in parsed) {
903
+ return "swarmui";
904
+ }
905
+ if ("prompt" in parsed && "parameters" in parsed) {
906
+ const params = String(parsed.parameters || "");
907
+ if (params.includes("sui_image_params") || params.includes("swarm_version")) {
862
908
  return "swarmui";
863
909
  }
864
- if ("prompt" in parsed && "parameters" in parsed) {
865
- const params = String(parsed.parameters || "");
866
- if (params.includes("sui_image_params") || params.includes("swarm_version")) {
867
- return "swarmui";
868
- }
869
- }
870
- } catch {
871
910
  }
911
+ } catch {
872
912
  }
873
913
  return null;
874
914
  }
875
- function detectFromContent(entryRecord) {
915
+ function detectComfyUIEntries(entryRecord) {
876
916
  if ("prompt" in entryRecord && "workflow" in entryRecord) {
877
917
  return "comfyui";
878
918
  }
879
- const text = entryRecord.parameters ?? entryRecord.Comment ?? "";
880
- if (!text) {
881
- if ("workflow" in entryRecord) {
882
- return "comfyui";
919
+ if ("workflow" in entryRecord) {
920
+ return "comfyui";
921
+ }
922
+ if ("prompt" in entryRecord) {
923
+ const promptText = entryRecord.prompt;
924
+ if (promptText?.startsWith("{")) {
925
+ if (promptText.includes("sui_image_params")) {
926
+ return "swarmui";
927
+ }
928
+ if (promptText.includes("class_type")) {
929
+ return "comfyui";
930
+ }
883
931
  }
884
- return null;
885
932
  }
933
+ return null;
934
+ }
935
+ function detectFromTextContent(text) {
886
936
  if (text.startsWith("{")) {
887
- return detectFromJson(text);
937
+ return detectFromJsonFormat(text);
888
938
  }
889
- return detectFromA1111Text(text);
939
+ return detectFromA1111Format(text);
890
940
  }
891
- function detectFromJson(json) {
941
+ function detectFromJsonFormat(json) {
892
942
  if (json.includes("sui_image_params")) {
893
943
  return "swarmui";
894
944
  }
945
+ if (json.includes('"software":"RuinedFooocus"') || json.includes('"software": "RuinedFooocus"')) {
946
+ return "ruined-fooocus";
947
+ }
948
+ if (json.includes('"use_stable_diffusion_model"')) {
949
+ return "easydiffusion";
950
+ }
895
951
  if (json.includes("civitai:") || json.includes('"resource-stack"')) {
896
952
  return "civitai";
897
953
  }
@@ -901,12 +957,6 @@ function detectFromJson(json) {
901
957
  if (json.includes('"Model"') && json.includes('"resolution"')) {
902
958
  return "hf-space";
903
959
  }
904
- if (json.includes('"use_stable_diffusion_model"')) {
905
- return "easydiffusion";
906
- }
907
- if (json.includes('"software":"RuinedFooocus"') || json.includes('"software": "RuinedFooocus"')) {
908
- return "ruined-fooocus";
909
- }
910
960
  if (json.includes('"prompt"') && json.includes('"base_model"')) {
911
961
  return "fooocus";
912
962
  }
@@ -915,11 +965,8 @@ function detectFromJson(json) {
915
965
  }
916
966
  return null;
917
967
  }
918
- function detectFromA1111Text(text) {
919
- if (text.includes("sui_image_params")) {
920
- return "swarmui";
921
- }
922
- if (text.includes("swarm_version")) {
968
+ function detectFromA1111Format(text) {
969
+ if (text.includes("sui_image_params") || text.includes("swarm_version")) {
923
970
  return "swarmui";
924
971
  }
925
972
  const versionMatch = text.match(/Version:\s*([^\s,]+)/);
@@ -981,7 +1028,6 @@ function parseFromEntries(entryRecord) {
981
1028
  const width = Number(entryRecord.width ?? entryRecord.Width) || 0;
982
1029
  const height = Number(entryRecord.height ?? entryRecord.Height) || 0;
983
1030
  const metadata = {
984
- type: "a1111",
985
1031
  software: "easydiffusion",
986
1032
  prompt: prompt.trim(),
987
1033
  negativePrompt: negativePrompt.trim(),
@@ -1012,7 +1058,6 @@ function parseFromJson(json) {
1012
1058
  const width = getValue(json, "width", "Width") ?? 0;
1013
1059
  const height = getValue(json, "height", "Height") ?? 0;
1014
1060
  const metadata = {
1015
- type: "a1111",
1016
1061
  software: "easydiffusion",
1017
1062
  prompt: prompt.trim(),
1018
1063
  negativePrompt: negativePrompt.trim(),
@@ -1052,7 +1097,6 @@ function parseFooocus(entries) {
1052
1097
  return Result.error({ type: "unsupportedFormat" });
1053
1098
  }
1054
1099
  const metadata = {
1055
- type: "a1111",
1056
1100
  software: "fooocus",
1057
1101
  prompt: json.prompt?.trim() ?? "",
1058
1102
  negativePrompt: json.negative_prompt?.trim() ?? "",
@@ -1096,7 +1140,6 @@ function parseHfSpace(entries) {
1096
1140
  };
1097
1141
  const { width, height } = parseResolution(json.resolution);
1098
1142
  const metadata = {
1099
- type: "a1111",
1100
1143
  software: "hf-space",
1101
1144
  prompt: json.prompt ?? "",
1102
1145
  negativePrompt: json.negative_prompt ?? "",
@@ -1147,7 +1190,6 @@ function parseInvokeAI(entries) {
1147
1190
  const width = data.width ?? 0;
1148
1191
  const height = data.height ?? 0;
1149
1192
  const metadata = {
1150
- type: "invokeai",
1151
1193
  software: "invokeai",
1152
1194
  prompt: data.positive_prompt ?? "",
1153
1195
  negativePrompt: data.negative_prompt ?? "",
@@ -1197,7 +1239,6 @@ function parseNovelAI(entries) {
1197
1239
  const prompt = comment.v4_prompt?.caption?.base_caption ?? comment.prompt ?? "";
1198
1240
  const negativePrompt = comment.v4_negative_prompt?.caption?.base_caption ?? comment.uc ?? "";
1199
1241
  const metadata = {
1200
- type: "novelai",
1201
1242
  software: "novelai",
1202
1243
  prompt,
1203
1244
  negativePrompt,
@@ -1247,7 +1288,6 @@ function parseRuinedFooocus(entries) {
1247
1288
  return Result.error({ type: "unsupportedFormat" });
1248
1289
  }
1249
1290
  const metadata = {
1250
- type: "a1111",
1251
1291
  software: "ruined-fooocus",
1252
1292
  prompt: json.Prompt?.trim() ?? "",
1253
1293
  negativePrompt: json.Negative?.trim() ?? "",
@@ -1273,12 +1313,11 @@ function parseRuinedFooocus(entries) {
1273
1313
  function parseStabilityMatrix(entries) {
1274
1314
  const entryRecord = buildEntryRecord(entries);
1275
1315
  const comfyResult = parseComfyUI(entries);
1276
- if (!comfyResult.ok) {
1316
+ if (!comfyResult.ok || comfyResult.value.software !== "comfyui") {
1277
1317
  return Result.error({ type: "unsupportedFormat" });
1278
1318
  }
1279
1319
  const metadata = {
1280
1320
  ...comfyResult.value,
1281
- type: "comfyui",
1282
1321
  software: "stability-matrix"
1283
1322
  };
1284
1323
  const jsonText = entryRecord["parameters-json"];
@@ -1340,13 +1379,19 @@ function parseSwarmUI(entries) {
1340
1379
  const width = params.width ?? 0;
1341
1380
  const height = params.height ?? 0;
1342
1381
  const metadata = {
1343
- type: "swarmui",
1344
1382
  software: "swarmui",
1345
1383
  prompt: params.prompt ?? "",
1346
1384
  negativePrompt: params.negativeprompt ?? "",
1347
1385
  width,
1348
1386
  height
1349
1387
  };
1388
+ const promptSource = entryRecord.prompt || entryRecord.Make;
1389
+ if (promptSource) {
1390
+ const promptParsed = parseJson(promptSource);
1391
+ if (promptParsed.ok) {
1392
+ metadata.nodes = promptParsed.value;
1393
+ }
1394
+ }
1350
1395
  if (params.model) {
1351
1396
  metadata.model = {
1352
1397
  name: params.model
@@ -1389,13 +1434,24 @@ function parseTensorArt(entries) {
1389
1434
  const data = parsed.value;
1390
1435
  const width = data.width ?? 0;
1391
1436
  const height = data.height ?? 0;
1437
+ const promptChunk = entryRecord.prompt;
1438
+ if (!promptChunk) {
1439
+ return Result.error({ type: "unsupportedFormat" });
1440
+ }
1441
+ const promptParsed = parseJson(promptChunk);
1442
+ if (!promptParsed.ok) {
1443
+ return Result.error({
1444
+ type: "parseError",
1445
+ message: "Invalid JSON in prompt chunk"
1446
+ });
1447
+ }
1392
1448
  const metadata = {
1393
- type: "comfyui",
1394
1449
  software: "tensorart",
1395
1450
  prompt: data.prompt ?? "",
1396
1451
  negativePrompt: data.negativePrompt ?? "",
1397
1452
  width,
1398
- height
1453
+ height,
1454
+ nodes: promptParsed.value
1399
1455
  };
1400
1456
  if (data.baseModel?.modelFileName || data.baseModel?.hash) {
1401
1457
  metadata.model = {
@@ -1404,8 +1460,9 @@ function parseTensorArt(entries) {
1404
1460
  };
1405
1461
  }
1406
1462
  if (data.seed !== void 0 || data.steps !== void 0 || data.cfgScale !== void 0 || data.clipSkip !== void 0) {
1463
+ const baseSeed = data.seed ? Number(data.seed) : void 0;
1407
1464
  metadata.sampling = {
1408
- seed: data.seed ? Number(data.seed) : void 0,
1465
+ seed: baseSeed === -1 ? findActualSeed(promptParsed.value) : baseSeed,
1409
1466
  steps: data.steps,
1410
1467
  cfg: data.cfgScale,
1411
1468
  clipSkip: data.clipSkip
@@ -1413,6 +1470,15 @@ function parseTensorArt(entries) {
1413
1470
  }
1414
1471
  return Result.ok(metadata);
1415
1472
  }
1473
+ function findActualSeed(nodes) {
1474
+ const samplerNode = findSamplerNode(nodes);
1475
+ return samplerNode && typeof samplerNode.inputs.seed === "number" ? samplerNode.inputs.seed : -1;
1476
+ }
1477
+ function findSamplerNode(nodes) {
1478
+ return Object.values(nodes).find(
1479
+ (node) => node.class_type === "KSampler" || node.class_type.toLowerCase().includes("sampler")
1480
+ );
1481
+ }
1416
1482
 
1417
1483
  // src/parsers/index.ts
1418
1484
  function parseMetadata(entries) {