@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 +7 -3
- package/dist/index.js +73 -48
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# sd-metadata
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@enslo/sd-metadata)
|
|
4
|
+
[](https://www.npmjs.com/package/@enslo/sd-metadata)
|
|
5
|
+
[](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
|
|
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
|
|
826
|
-
if (
|
|
827
|
-
|
|
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
|
|
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
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
|
|
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
|
|
893
|
+
function detectComfyUIEntries(entryRecord) {
|
|
876
894
|
if ("prompt" in entryRecord && "workflow" in entryRecord) {
|
|
877
895
|
return "comfyui";
|
|
878
896
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
|
915
|
+
return detectFromJsonFormat(text);
|
|
888
916
|
}
|
|
889
|
-
return
|
|
917
|
+
return detectFromA1111Format(text);
|
|
890
918
|
}
|
|
891
|
-
function
|
|
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
|
|
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,]+)/);
|