@enslo/sd-metadata 1.4.0 → 1.4.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.ja.md CHANGED
@@ -153,7 +153,7 @@ if (result.status === 'success') {
153
153
  > 本番環境では `@latest` の代わりに特定のバージョンを指定してください:
154
154
  >
155
155
  > ```text
156
- > https://cdn.jsdelivr.net/npm/@enslo/sd-metadata@1.4.0/dist/index.js
156
+ > https://cdn.jsdelivr.net/npm/@enslo/sd-metadata@1.4.1/dist/index.js
157
157
  > ```
158
158
 
159
159
  ### 応用例
package/README.md CHANGED
@@ -153,7 +153,7 @@ if (result.status === 'success') {
153
153
  > For production use, pin to a specific version instead of `@latest`:
154
154
  >
155
155
  > ```text
156
- > https://cdn.jsdelivr.net/npm/@enslo/sd-metadata@1.4.0/dist/index.js
156
+ > https://cdn.jsdelivr.net/npm/@enslo/sd-metadata@1.4.2/dist/index.js
157
157
  > ```
158
158
 
159
159
  ### Advanced Examples
package/dist/index.js CHANGED
@@ -314,7 +314,7 @@ function detectSoftware(entries) {
314
314
  return null;
315
315
  }
316
316
  function detectUniqueKeywords(entryRecord) {
317
- if (entryRecord.Software === "NovelAI") {
317
+ if (entryRecord.Software?.startsWith("NovelAI")) {
318
318
  return "novelai";
319
319
  }
320
320
  if ("invokeai_metadata" in entryRecord) {
@@ -345,6 +345,12 @@ function detectFromCommentJson(comment) {
345
345
  if ("invokeai_metadata" in parsed) {
346
346
  return "invokeai";
347
347
  }
348
+ if ("generation_data" in parsed) {
349
+ return "tensorart";
350
+ }
351
+ if ("smproj" in parsed) {
352
+ return "stability-matrix";
353
+ }
348
354
  if ("prompt" in parsed && "workflow" in parsed) {
349
355
  const workflow = parsed.workflow;
350
356
  const prompt = parsed.prompt;
@@ -574,7 +580,7 @@ function parseFooocus(entries) {
574
580
  // src/parsers/hf-space.ts
575
581
  function parseHfSpace(entries) {
576
582
  const entryRecord = buildEntryRecord(entries);
577
- const parametersText = entryRecord.parameters;
583
+ const parametersText = entryRecord.parameters ?? entryRecord.Comment;
578
584
  if (!parametersText) {
579
585
  return Result.error({ type: "unsupportedFormat" });
580
586
  }
@@ -671,7 +677,7 @@ function parseInvokeAI(entries) {
671
677
  // src/parsers/novelai.ts
672
678
  function parseNovelAI(entries) {
673
679
  const entryRecord = buildEntryRecord(entries);
674
- if (entryRecord.Software !== "NovelAI") {
680
+ if (!entryRecord.Software?.startsWith("NovelAI")) {
675
681
  return Result.error({ type: "unsupportedFormat" });
676
682
  }
677
683
  const commentText = entryRecord.Comment;
@@ -727,7 +733,7 @@ function parseNovelAI(entries) {
727
733
  // src/parsers/ruined-fooocus.ts
728
734
  function parseRuinedFooocus(entries) {
729
735
  const entryRecord = buildEntryRecord(entries);
730
- const jsonText = entryRecord.parameters;
736
+ const jsonText = entryRecord.parameters ?? entryRecord.Comment;
731
737
  if (!jsonText || !jsonText.startsWith("{")) {
732
738
  return Result.error({ type: "unsupportedFormat" });
733
739
  }
@@ -775,7 +781,20 @@ function parseStabilityMatrix(entries) {
775
781
  ...comfyResult.value,
776
782
  software: "stability-matrix"
777
783
  };
778
- const jsonText = entryRecord["parameters-json"];
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
+ }
779
798
  if (jsonText) {
780
799
  const parsed = parseJson(jsonText);
781
800
  if (parsed.ok) {
@@ -874,7 +893,26 @@ function parseSwarmUI(entries) {
874
893
  // src/parsers/tensorart.ts
875
894
  function parseTensorArt(entries) {
876
895
  const entryRecord = buildEntryRecord(entries);
877
- const dataText = entryRecord.generation_data;
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
+ }
878
916
  if (!dataText) {
879
917
  return Result.error({ type: "unsupportedFormat" });
880
918
  }
@@ -889,7 +927,6 @@ function parseTensorArt(entries) {
889
927
  const data = parsed.value;
890
928
  const width = data.width ?? 0;
891
929
  const height = data.height ?? 0;
892
- const promptChunk = entryRecord.prompt;
893
930
  if (!promptChunk) {
894
931
  return Result.error({ type: "unsupportedFormat" });
895
932
  }
@@ -1580,7 +1617,7 @@ function tryExpandNovelAIWebpFormat(text) {
1580
1617
  return null;
1581
1618
  }
1582
1619
  const outer = outerParsed.value;
1583
- if (typeof outer !== "object" || outer === null || outer.Software !== "NovelAI" || typeof outer.Comment !== "string") {
1620
+ if (typeof outer !== "object" || outer === null || typeof outer.Software === "string" && !outer.Software.startsWith("NovelAI") || typeof outer.Comment !== "string") {
1584
1621
  return null;
1585
1622
  }
1586
1623
  const entries = [{ keyword: "Software", text: "NovelAI" }];
@@ -1758,35 +1795,18 @@ var stringify = (value) => {
1758
1795
  };
1759
1796
 
1760
1797
  // src/converters/chunk-encoding.ts
1761
- var CHUNK_ENCODING_STRATEGIES = {
1762
- // Dynamic selection tools
1763
- a1111: "dynamic",
1764
- forge: "dynamic",
1765
- "forge-neo": "dynamic",
1766
- "sd-webui": "dynamic",
1767
- invokeai: "dynamic",
1768
- novelai: "dynamic",
1769
- "sd-next": "dynamic",
1770
- easydiffusion: "dynamic",
1771
- // Unicode escape tools (spec-compliant)
1772
- comfyui: "text-unicode-escape",
1773
- swarmui: "text-unicode-escape",
1774
- fooocus: "text-unicode-escape",
1775
- "ruined-fooocus": "text-unicode-escape",
1776
- "hf-space": "text-unicode-escape",
1777
- // Raw UTF-8 tools (non-compliant but compatible)
1778
- "stability-matrix": "text-utf8-raw",
1779
- tensorart: "text-utf8-raw"
1780
- };
1781
- function getEncodingStrategy(tool) {
1782
- return CHUNK_ENCODING_STRATEGIES[tool] ?? "text-unicode-escape";
1783
- }
1784
1798
  function escapeUnicode(text) {
1785
1799
  return text.replace(/[\u0100-\uffff]/g, (char) => {
1786
1800
  const code = char.charCodeAt(0).toString(16).padStart(4, "0");
1787
1801
  return `\\u${code}`;
1788
1802
  });
1789
1803
  }
1804
+ function unescapeUnicode(text) {
1805
+ return text.replace(
1806
+ /\\u([0-9a-fA-F]{4})/g,
1807
+ (_, hex) => String.fromCharCode(Number.parseInt(hex, 16))
1808
+ );
1809
+ }
1790
1810
  function hasNonLatin1(text) {
1791
1811
  return /[^\x00-\xFF]/.test(text);
1792
1812
  }
@@ -1825,15 +1845,11 @@ function convertA1111SegmentsToPng(segments) {
1825
1845
  if (!userComment) {
1826
1846
  return [];
1827
1847
  }
1828
- return createEncodedChunk(
1829
- "parameters",
1830
- userComment.data,
1831
- getEncodingStrategy("a1111")
1832
- );
1848
+ return createEncodedChunk("parameters", userComment.data, "dynamic");
1833
1849
  }
1834
1850
 
1835
- // src/converters/comfyui.ts
1836
- function convertComfyUIPngToSegments(chunks) {
1851
+ // src/converters/base-json.ts
1852
+ function convertKvPngToSegments(chunks) {
1837
1853
  const data = {};
1838
1854
  for (const chunk of chunks) {
1839
1855
  const parsed = parseJson(chunk.text);
@@ -1850,6 +1866,29 @@ function convertComfyUIPngToSegments(chunks) {
1850
1866
  }
1851
1867
  ];
1852
1868
  }
1869
+ function convertKvSegmentsToPng(segments, encodingStrategy) {
1870
+ const userComment = findSegment(segments, "exifUserComment");
1871
+ if (!userComment) {
1872
+ return [];
1873
+ }
1874
+ const parsed = parseJson(userComment.data);
1875
+ if (!parsed.ok) {
1876
+ return [];
1877
+ }
1878
+ return Object.entries(parsed.value).flatMap(([keyword, value]) => {
1879
+ const text = stringify(value);
1880
+ return createEncodedChunk(
1881
+ keyword,
1882
+ text !== void 0 ? unescapeUnicode(text) : void 0,
1883
+ encodingStrategy
1884
+ );
1885
+ });
1886
+ }
1887
+
1888
+ // src/converters/comfyui.ts
1889
+ function convertComfyUIPngToSegments(chunks) {
1890
+ return convertKvPngToSegments(chunks);
1891
+ }
1853
1892
  var tryParseExtendedFormat = (segments) => {
1854
1893
  const imageDescription = findSegment(segments, "exifImageDescription");
1855
1894
  const make = findSegment(segments, "exifMake");
@@ -1857,34 +1896,17 @@ var tryParseExtendedFormat = (segments) => {
1857
1896
  return null;
1858
1897
  }
1859
1898
  return [
1860
- ...createEncodedChunk("prompt", make?.data, getEncodingStrategy("comfyui")),
1899
+ ...createEncodedChunk("prompt", make?.data, "text-unicode-escape"),
1861
1900
  ...createEncodedChunk(
1862
1901
  "workflow",
1863
1902
  imageDescription?.data,
1864
- getEncodingStrategy("comfyui")
1903
+ "text-unicode-escape"
1865
1904
  )
1866
1905
  ];
1867
1906
  };
1868
1907
  var tryParseSaveImagePlusFormat = (segments) => {
1869
- const userComment = findSegment(segments, "exifUserComment");
1870
- if (!userComment) {
1871
- return null;
1872
- }
1873
- const parsed = parseJson(userComment.data);
1874
- if (!parsed.ok) {
1875
- return createEncodedChunk(
1876
- "prompt",
1877
- userComment.data,
1878
- getEncodingStrategy("comfyui")
1879
- );
1880
- }
1881
- return Object.entries(parsed.value).flatMap(
1882
- ([keyword, value]) => createEncodedChunk(
1883
- keyword,
1884
- stringify(value),
1885
- getEncodingStrategy("comfyui")
1886
- )
1887
- );
1908
+ const chunks = convertKvSegmentsToPng(segments, "text-utf8-raw");
1909
+ return chunks.length > 0 ? chunks : null;
1888
1910
  };
1889
1911
  function convertComfyUISegmentsToPng(segments) {
1890
1912
  return tryParseExtendedFormat(segments) ?? tryParseSaveImagePlusFormat(segments) ?? [];
@@ -1911,93 +1933,36 @@ function convertEasyDiffusionSegmentsToPng(segments) {
1911
1933
  if (!parsed.ok) {
1912
1934
  return [];
1913
1935
  }
1914
- return Object.entries(parsed.value).flatMap(([keyword, value]) => {
1915
- const text = value != null ? typeof value === "string" ? value : String(value) : void 0;
1916
- if (!text) return [];
1917
- return createEncodedChunk(
1918
- keyword,
1919
- text,
1920
- getEncodingStrategy("easydiffusion")
1921
- );
1922
- });
1936
+ return Object.entries(parsed.value).flatMap(
1937
+ ([keyword, value]) => createEncodedChunk(keyword, stringify(value), "dynamic")
1938
+ );
1923
1939
  }
1924
1940
 
1925
1941
  // src/converters/invokeai.ts
1926
1942
  function convertInvokeAIPngToSegments(chunks) {
1927
- const data = {};
1928
- for (const chunk of chunks) {
1929
- const parsed = parseJson(chunk.text);
1930
- if (parsed.ok) {
1931
- data[chunk.keyword] = parsed.value;
1932
- } else {
1933
- data[chunk.keyword] = chunk.text;
1934
- }
1935
- }
1936
- return [
1937
- {
1938
- source: { type: "exifUserComment" },
1939
- data: JSON.stringify(data)
1940
- }
1941
- ];
1943
+ return convertKvPngToSegments(chunks);
1942
1944
  }
1943
1945
  function convertInvokeAISegmentsToPng(segments) {
1944
- const userComment = findSegment(segments, "exifUserComment");
1945
- if (!userComment) {
1946
- return [];
1947
- }
1948
- const parsed = parseJson(userComment.data);
1949
- if (!parsed.ok) {
1950
- return createEncodedChunk(
1951
- "invokeai_metadata",
1952
- userComment.data,
1953
- getEncodingStrategy("invokeai")
1954
- );
1955
- }
1956
- const metadataText = stringify(parsed.value.invokeai_metadata);
1957
- const graphText = stringify(parsed.value.invokeai_graph);
1958
- const chunks = [
1959
- ...createEncodedChunk(
1960
- "invokeai_metadata",
1961
- metadataText,
1962
- getEncodingStrategy("invokeai")
1963
- ),
1964
- ...createEncodedChunk(
1965
- "invokeai_graph",
1966
- graphText,
1967
- getEncodingStrategy("invokeai")
1968
- )
1969
- ];
1970
- if (chunks.length > 0) {
1971
- return chunks;
1972
- }
1973
- return createEncodedChunk(
1974
- "invokeai_metadata",
1975
- userComment.data,
1976
- getEncodingStrategy("invokeai")
1977
- );
1946
+ return convertKvSegmentsToPng(segments, "dynamic");
1978
1947
  }
1979
1948
 
1980
1949
  // src/converters/novelai.ts
1981
1950
  var NOVELAI_TITLE = "NovelAI generated image";
1982
1951
  var NOVELAI_SOFTWARE = "NovelAI";
1983
1952
  function convertNovelaiPngToSegments(chunks) {
1984
- const comment = chunks.find((c) => c.keyword === "Comment");
1985
- if (!comment) {
1986
- return [];
1987
- }
1988
1953
  const description = chunks.find((c) => c.keyword === "Description");
1954
+ const descriptionSegment = description && {
1955
+ source: { type: "exifImageDescription" },
1956
+ data: `\0\0\0\0${description.text}`
1957
+ };
1989
1958
  const data = buildUserCommentJson(chunks);
1990
- const descriptionSegment = description ? [
1991
- {
1992
- source: { type: "exifImageDescription" },
1993
- data: `\0\0\0\0${description.text}`
1994
- }
1995
- ] : [];
1996
1959
  const userCommentSegment = {
1997
1960
  source: { type: "exifUserComment" },
1998
1961
  data: JSON.stringify(data)
1999
1962
  };
2000
- return [...descriptionSegment, userCommentSegment];
1963
+ return [descriptionSegment, userCommentSegment].filter(
1964
+ (segment) => segment !== void 0
1965
+ );
2001
1966
  }
2002
1967
  function buildUserCommentJson(chunks) {
2003
1968
  return NOVELAI_KEY_ORDER.map((key) => {
@@ -2034,16 +1999,11 @@ function parseSegments(userCommentSeg, descriptionSeg) {
2034
1999
  descriptionSeg,
2035
2000
  stringify(jsonData.Description)
2036
2001
  );
2037
- const descriptionChunks = descriptionText ? createEncodedChunk(
2038
- "Description",
2039
- descriptionText,
2040
- getEncodingStrategy("novelai")
2041
- ) : [];
2042
2002
  return [
2043
2003
  // Title (required, use default if missing)
2044
2004
  createTextChunk("Title", stringify(jsonData.Title) ?? NOVELAI_TITLE),
2045
2005
  // Description (optional, prefer exifImageDescription over JSON)
2046
- ...descriptionChunks,
2006
+ createEncodedChunk("Description", descriptionText, "dynamic"),
2047
2007
  // Software (required, use default if missing)
2048
2008
  createTextChunk(
2049
2009
  "Software",
@@ -2075,17 +2035,13 @@ function createPngToSegments(keyword) {
2075
2035
  return !chunk ? [] : [{ source: { type: "exifUserComment" }, data: chunk.text }];
2076
2036
  };
2077
2037
  }
2078
- function createSegmentsToPng(keyword) {
2038
+ function createSegmentsToPng(keyword, encodingStrategy) {
2079
2039
  return (segments) => {
2080
2040
  const userComment = segments.find(
2081
2041
  (s) => s.source.type === "exifUserComment"
2082
2042
  );
2083
2043
  if (!userComment) return [];
2084
- return createEncodedChunk(
2085
- keyword,
2086
- userComment.data,
2087
- getEncodingStrategy(keyword)
2088
- );
2044
+ return createEncodedChunk(keyword, userComment.data, encodingStrategy);
2089
2045
  };
2090
2046
  }
2091
2047
 
@@ -2096,11 +2052,10 @@ function convertSwarmUIPngToSegments(chunks) {
2096
2052
  return [];
2097
2053
  }
2098
2054
  const parsed = parseJson(parametersChunk.text);
2099
- const data = parsed.ok ? parsed.value : parametersChunk.text;
2100
2055
  const segments = [
2101
2056
  {
2102
2057
  source: { type: "exifUserComment" },
2103
- data: typeof data === "string" ? data : JSON.stringify(data)
2058
+ data: parsed.ok ? JSON.stringify(parsed.value) : parametersChunk.text
2104
2059
  }
2105
2060
  ];
2106
2061
  const promptChunk = chunks.find((c) => c.keyword === "prompt");
@@ -2114,27 +2069,13 @@ function convertSwarmUIPngToSegments(chunks) {
2114
2069
  }
2115
2070
  function convertSwarmUISegmentsToPng(segments) {
2116
2071
  const userComment = findSegment(segments, "exifUserComment");
2117
- if (!userComment) {
2118
- return [];
2119
- }
2120
- const chunks = [];
2121
2072
  const make = findSegment(segments, "exifMake");
2122
- if (make) {
2123
- chunks.push(
2124
- ...createEncodedChunk(
2125
- "prompt",
2126
- make.data,
2127
- getEncodingStrategy("swarmui")
2128
- )
2129
- );
2130
- }
2131
- chunks.push(
2132
- ...createEncodedChunk(
2133
- "parameters",
2134
- userComment.data,
2135
- getEncodingStrategy("swarmui")
2136
- )
2137
- );
2073
+ const chunks = [
2074
+ // Restore node graph first if present (extended format)
2075
+ createEncodedChunk("prompt", make?.data, "text-unicode-escape"),
2076
+ // Add parameters chunk second (always present)
2077
+ createEncodedChunk("parameters", userComment?.data, "text-unicode-escape")
2078
+ ].flat();
2138
2079
  return chunks;
2139
2080
  }
2140
2081
 
@@ -2203,11 +2144,11 @@ var convertEasyDiffusion = createFormatConverter(
2203
2144
  );
2204
2145
  var convertFooocus = createFormatConverter(
2205
2146
  createPngToSegments("Comment"),
2206
- createSegmentsToPng("Comment")
2147
+ createSegmentsToPng("Comment", "text-unicode-escape")
2207
2148
  );
2208
2149
  var convertRuinedFooocus = createFormatConverter(
2209
2150
  createPngToSegments("parameters"),
2210
- createSegmentsToPng("parameters")
2151
+ createSegmentsToPng("parameters", "text-unicode-escape")
2211
2152
  );
2212
2153
  var convertSwarmUI = createFormatConverter(
2213
2154
  convertSwarmUIPngToSegments,
@@ -2219,7 +2160,7 @@ var convertInvokeAI = createFormatConverter(
2219
2160
  );
2220
2161
  var convertHfSpace = createFormatConverter(
2221
2162
  createPngToSegments("parameters"),
2222
- createSegmentsToPng("parameters")
2163
+ createSegmentsToPng("parameters", "text-unicode-escape")
2223
2164
  );
2224
2165
  var softwareConverters = {
2225
2166
  // NovelAI
@@ -3002,8 +2943,7 @@ function writeAsWebUI(data, metadata) {
3002
2943
  return Result.ok(writeResult.value);
3003
2944
  }
3004
2945
  function createPngChunks(text) {
3005
- const strategy = getEncodingStrategy("a1111");
3006
- return createEncodedChunk("parameters", text, strategy);
2946
+ return createEncodedChunk("parameters", text, "dynamic");
3007
2947
  }
3008
2948
  function createExifSegments(text) {
3009
2949
  return [