@enslo/sd-metadata 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # sd-metadata
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@enslo/sd-metadata.svg)](https://www.npmjs.com/package/@enslo/sd-metadata)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@enslo/sd-metadata.svg)](https://www.npmjs.com/package/@enslo/sd-metadata)
5
+ [![license](https://img.shields.io/npm/l/@enslo/sd-metadata.svg)](https://github.com/enslo/sd-metadata/blob/main/LICENSE)
6
+
3
7
  A TypeScript library to read and write metadata embedded in AI-generated images.
4
8
 
5
9
  ## Features
@@ -43,7 +47,7 @@ npm install @enslo/sd-metadata
43
47
 
44
48
  > [!NOTE]
45
49
  > \* Tools with known limitations. See [Known Limitations](#known-limitations) for details.
46
- >
50
+
47
51
  > [!TIP]
48
52
  > **Help us expand tool support!** We're actively collecting sample images from experimental tools (Easy Diffusion, Fooocus) and unsupported tools. If you have sample images generated by these or other AI tools, please consider contributing them! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
49
53
 
@@ -218,10 +222,10 @@ Writes metadata to an image file.
218
222
 
219
223
  > [!WARNING]
220
224
  > **ComfyUI JPEG/WebP**: While reading supports major custom node formats (e.g., `save-image-extended`), writing always uses the `comfyui-saveimage-plus` format. This format provides the best information preservation and is compatible with ComfyUI's native drag-and-drop workflow loading.
221
- >
225
+
222
226
  > [!WARNING]
223
227
  > **NovelAI WebP**: Auto-corrects corrupted UTF-8 in the Description field, which means WebP → PNG → WebP round-trip is not content-equivalent (but provides valid, readable metadata).
224
- >
228
+
225
229
  > [!WARNING]
226
230
  > **SwarmUI PNG→JPEG/WebP**: PNG files contain both ComfyUI workflow and SwarmUI parameters. When converting to JPEG/WebP, only parameters are preserved to match the native format. Metadata is fully preserved, but the ComfyUI workflow in the `prompt` chunk is lost.
227
231
 
package/dist/index.js CHANGED
@@ -548,6 +548,10 @@ function parseA1111(entries) {
548
548
  return Result.error({ type: "unsupportedFormat" });
549
549
  }
550
550
  const text = parametersEntry.text;
551
+ const hasAIMarkers = text.includes("Steps:") || text.includes("Sampler:") || text.includes("Negative prompt:");
552
+ if (!hasAIMarkers) {
553
+ return Result.error({ type: "unsupportedFormat" });
554
+ }
551
555
  const { prompt, negativePrompt, settings } = parseParametersText(text);
552
556
  const settingsMap = parseSettings(settings);
553
557
  const size = settingsMap.get("Size") ?? "0x0";
@@ -778,7 +782,7 @@ function parseComfyUI(entries) {
778
782
  }
779
783
  function findPromptJson(entryRecord) {
780
784
  if (entryRecord.prompt) {
781
- return entryRecord.prompt;
785
+ return entryRecord.prompt.replace(/:\s*NaN\b/g, ": null");
782
786
  }
783
787
  const candidates = [
784
788
  entryRecord.Comment,
@@ -792,7 +796,7 @@ function findPromptJson(entryRecord) {
792
796
  for (const candidate of candidates) {
793
797
  if (!candidate) continue;
794
798
  if (candidate.startsWith("{")) {
795
- const cleaned = candidate.replace(/\0+$/, "");
799
+ const cleaned = candidate.replace(/\0+$/, "").replace(/:\s*NaN\b/g, ": null");
796
800
  const parsed = parseJson(cleaned);
797
801
  if (!parsed.ok) continue;
798
802
  if (parsed.value.prompt && typeof parsed.value.prompt === "object") {
@@ -800,7 +804,7 @@ function findPromptJson(entryRecord) {
800
804
  }
801
805
  const values = Object.values(parsed.value);
802
806
  if (values.some((v) => v && typeof v === "object" && "class_type" in v)) {
803
- return candidate;
807
+ return cleaned;
804
808
  }
805
809
  }
806
810
  }
@@ -822,11 +826,17 @@ function extractExtraMetadata(prompt) {
822
826
  // src/parsers/detect.ts
823
827
  function detectSoftware(entries) {
824
828
  const entryRecord = buildEntryRecord(entries);
825
- const keywordResult = detectFromKeywords(entryRecord);
826
- if (keywordResult) return keywordResult;
827
- return detectFromContent(entryRecord);
829
+ const uniqueResult = detectUniqueKeywords(entryRecord);
830
+ if (uniqueResult) return uniqueResult;
831
+ const comfyResult = detectComfyUIEntries(entryRecord);
832
+ if (comfyResult) return comfyResult;
833
+ const text = entryRecord.parameters ?? entryRecord.Comment ?? "";
834
+ if (text) {
835
+ return detectFromTextContent(text);
836
+ }
837
+ return null;
828
838
  }
829
- function detectFromKeywords(entryRecord) {
839
+ function detectUniqueKeywords(entryRecord) {
830
840
  if (entryRecord.Software === "NovelAI") {
831
841
  return "novelai";
832
842
  }
@@ -842,56 +852,80 @@ function detectFromKeywords(entryRecord) {
842
852
  if ("negative_prompt" in entryRecord || "Negative Prompt" in entryRecord) {
843
853
  return "easydiffusion";
844
854
  }
855
+ const parameters = entryRecord.parameters;
856
+ if (parameters?.includes("sui_image_params")) {
857
+ return "swarmui";
858
+ }
845
859
  const comment = entryRecord.Comment;
846
860
  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
- }
861
+ return detectFromCommentJson(comment);
862
+ }
863
+ return null;
864
+ }
865
+ function detectFromCommentJson(comment) {
866
+ try {
867
+ const parsed = JSON.parse(comment);
868
+ if ("invokeai_metadata" in parsed) {
869
+ return "invokeai";
870
+ }
871
+ if ("prompt" in parsed && "workflow" in parsed) {
872
+ const workflow = parsed.workflow;
873
+ const prompt = parsed.prompt;
874
+ const isObject = typeof workflow === "object" || typeof prompt === "object";
875
+ const isJsonString = typeof workflow === "string" && workflow.startsWith("{") || typeof prompt === "string" && prompt.startsWith("{");
876
+ if (isObject || isJsonString) {
877
+ return "comfyui";
860
878
  }
861
- if ("sui_image_params" in parsed) {
879
+ }
880
+ if ("sui_image_params" in parsed) {
881
+ return "swarmui";
882
+ }
883
+ if ("prompt" in parsed && "parameters" in parsed) {
884
+ const params = String(parsed.parameters || "");
885
+ if (params.includes("sui_image_params") || params.includes("swarm_version")) {
862
886
  return "swarmui";
863
887
  }
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
888
  }
889
+ } catch {
872
890
  }
873
891
  return null;
874
892
  }
875
- function detectFromContent(entryRecord) {
893
+ function detectComfyUIEntries(entryRecord) {
876
894
  if ("prompt" in entryRecord && "workflow" in entryRecord) {
877
895
  return "comfyui";
878
896
  }
879
- const text = entryRecord.parameters ?? entryRecord.Comment ?? "";
880
- if (!text) {
881
- if ("workflow" in entryRecord) {
882
- return "comfyui";
897
+ if ("workflow" in entryRecord) {
898
+ return "comfyui";
899
+ }
900
+ if ("prompt" in entryRecord) {
901
+ const promptText = entryRecord.prompt;
902
+ if (promptText?.startsWith("{")) {
903
+ if (promptText.includes("sui_image_params")) {
904
+ return "swarmui";
905
+ }
906
+ if (promptText.includes("class_type")) {
907
+ return "comfyui";
908
+ }
883
909
  }
884
- return null;
885
910
  }
911
+ return null;
912
+ }
913
+ function detectFromTextContent(text) {
886
914
  if (text.startsWith("{")) {
887
- return detectFromJson(text);
915
+ return detectFromJsonFormat(text);
888
916
  }
889
- return detectFromA1111Text(text);
917
+ return detectFromA1111Format(text);
890
918
  }
891
- function detectFromJson(json) {
919
+ function detectFromJsonFormat(json) {
892
920
  if (json.includes("sui_image_params")) {
893
921
  return "swarmui";
894
922
  }
923
+ if (json.includes('"software":"RuinedFooocus"') || json.includes('"software": "RuinedFooocus"')) {
924
+ return "ruined-fooocus";
925
+ }
926
+ if (json.includes('"use_stable_diffusion_model"')) {
927
+ return "easydiffusion";
928
+ }
895
929
  if (json.includes("civitai:") || json.includes('"resource-stack"')) {
896
930
  return "civitai";
897
931
  }
@@ -901,12 +935,6 @@ function detectFromJson(json) {
901
935
  if (json.includes('"Model"') && json.includes('"resolution"')) {
902
936
  return "hf-space";
903
937
  }
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
938
  if (json.includes('"prompt"') && json.includes('"base_model"')) {
911
939
  return "fooocus";
912
940
  }
@@ -915,11 +943,8 @@ function detectFromJson(json) {
915
943
  }
916
944
  return null;
917
945
  }
918
- function detectFromA1111Text(text) {
919
- if (text.includes("sui_image_params")) {
920
- return "swarmui";
921
- }
922
- if (text.includes("swarm_version")) {
946
+ function detectFromA1111Format(text) {
947
+ if (text.includes("sui_image_params") || text.includes("swarm_version")) {
923
948
  return "swarmui";
924
949
  }
925
950
  const versionMatch = text.match(/Version:\s*([^\s,]+)/);