@enslo/sd-metadata 2.0.0 → 2.0.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/README.ja.md CHANGED
@@ -11,7 +11,7 @@ AI生成画像に埋め込まれたメタデータを読み書きするための
11
11
  ## 特徴
12
12
 
13
13
  - **マルチフォーマット対応**: PNG (tEXt / iTXt)、JPEG (COM / Exif)、WebP (Exif)
14
- - **統一API**: シンプルな `read()` と `write()` 関数で全フォーマットに対応
14
+ - **シンプルAPI**: `read()`、`write()`、`embed()`、`stringify()` — 4つの関数で全ユースケースをカバー
15
15
  - **TypeScriptネイティブ**: TypeScriptで書かれており、型定義を完全同梱
16
16
  - **ゼロ依存**: Node.jsとブラウザで外部依存なしで動作
17
17
  - **フォーマット変換**: PNG、JPEG、WebP間でメタデータをシームレスに変換
@@ -95,10 +95,9 @@ const { read } = require('@enslo/sd-metadata');
95
95
  ### Node.jsでの使用
96
96
 
97
97
  ```typescript
98
- import { read, write } from '@enslo/sd-metadata';
99
- import { readFileSync, writeFileSync } from 'fs';
98
+ import { read, stringify } from '@enslo/sd-metadata';
99
+ import { readFileSync } from 'fs';
100
100
 
101
- // サポートされている任意のフォーマットからメタデータを読み込み
102
101
  const imageData = readFileSync('image.png');
103
102
  const result = read(imageData);
104
103
 
@@ -108,24 +107,30 @@ if (result.status === 'success') {
108
107
  console.log('Model:', result.metadata.model?.name);
109
108
  console.log('Size:', result.metadata.width, 'x', result.metadata.height);
110
109
  }
110
+
111
+ // 読みやすいテキストにフォーマット(任意のstatusで動作)
112
+ const text = stringify(result);
113
+ if (text) {
114
+ console.log(text);
115
+ }
111
116
  ```
112
117
 
113
118
  ### ブラウザでの使用
114
119
 
115
120
  ```typescript
116
- import { read } from '@enslo/sd-metadata';
121
+ import { read, softwareLabels } from '@enslo/sd-metadata';
117
122
 
118
123
  // ファイル入力を処理
119
124
  const fileInput = document.querySelector('input[type="file"]');
120
125
  fileInput.addEventListener('change', async (e) => {
121
126
  const file = e.target.files[0];
122
127
  if (!file) return;
123
-
128
+
124
129
  const arrayBuffer = await file.arrayBuffer();
125
130
  const result = read(arrayBuffer);
126
-
131
+
127
132
  if (result.status === 'success') {
128
- document.getElementById('tool').textContent = result.metadata.software;
133
+ document.getElementById('tool').textContent = softwareLabels[result.metadata.software];
129
134
  document.getElementById('prompt').textContent = result.metadata.prompt;
130
135
  document.getElementById('model').textContent = result.metadata.model?.name || 'N/A';
131
136
  }
@@ -154,7 +159,7 @@ if (result.status === 'success') {
154
159
  > 本番環境では `@latest` の代わりに特定のバージョンを指定してください:
155
160
  >
156
161
  > ```text
157
- > https://cdn.jsdelivr.net/npm/@enslo/sd-metadata@2.0.0/dist/index.js
162
+ > https://cdn.jsdelivr.net/npm/@enslo/sd-metadata@2.0.1/dist/index.js
158
163
  > ```
159
164
 
160
165
  ### 応用例
@@ -185,8 +190,7 @@ if (parseResult.status === 'success') {
185
190
  }
186
191
  ```
187
192
 
188
- > [!TIP]
189
- > このライブラリはメタデータの読み書きのみを扱います。実際の画像フォーマット変換(ピクセルのデコード/エンコード)には、[sharp](https://www.npmjs.com/package/sharp)、[jimp](https://www.npmjs.com/package/jimp)、ブラウザCanvas APIなどの画像処理ライブラリを使用してください。
193
+ > **Tip:** このライブラリはメタデータの読み書きのみを扱います。実際の画像フォーマット変換(ピクセルのデコード/エンコード)には、[sharp](https://www.npmjs.com/package/sharp)、[jimp](https://www.npmjs.com/package/jimp)、ブラウザCanvas APIなどの画像処理ライブラリを使用してください。
190
194
 
191
195
  </details>
192
196
 
@@ -309,8 +313,7 @@ const result = embed(imageData, {
309
313
  });
310
314
  ```
311
315
 
312
- > [!TIP]
313
- > extras のキーが構造化フィールド(例:`Steps`)と一致する場合、extras の値が元の位置で構造化フィールドを上書きします。新しいキーは末尾に追加されます。
316
+ > **Tip:** extras のキーが構造化フィールド(例:`Steps`)と一致する場合、extras の値が元の位置で構造化フィールドを上書きします。新しいキーは末尾に追加されます。
314
317
 
315
318
  `EmbedMetadata` はすべての `GenerationMetadata` バリアントのサブセットなので、パース結果のメタデータをそのまま渡せます — `characterPrompts` を持つ NovelAI も含めて:
316
319
 
@@ -353,10 +356,15 @@ if (text) {
353
356
 
354
357
  ## APIリファレンス
355
358
 
356
- ### `read(input: Uint8Array | ArrayBuffer): ParseResult`
359
+ ### `read(input: Uint8Array | ArrayBuffer, options?: ReadOptions): ParseResult`
357
360
 
358
361
  画像ファイルからメタデータを読み込み、パースします。
359
362
 
363
+ **パラメータ:**
364
+
365
+ - `input` - 画像ファイルデータ(PNG、JPEG、またはWebP)
366
+ - `options` - オプションの読み込み設定(詳細は[型ドキュメント](./docs/types.ja.md)を参照)
367
+
360
368
  **戻り値:**
361
369
 
362
370
  - `{ status: 'success', metadata, raw }` - パース成功
@@ -575,45 +583,28 @@ type RawMetadata =
575
583
  | { format: 'webp'; segments: MetadataSegment[] };
576
584
  ```
577
585
 
578
- > [!TIP]
579
- > TypeScriptユーザー向け:全ての型はエクスポートされており、インポートして使用できます。
580
- >
581
- > ```typescript
582
- > import type {
583
- > BaseMetadata,
584
- > EmbedMetadata,
585
- > ParseResult,
586
- > GenerationMetadata,
587
- > GenerationSoftware,
588
- > ModelSettings,
589
- > SamplingSettings
590
- > } from '@enslo/sd-metadata';
591
- > ```
592
- >
593
- > IDEのIntelliSenseを使用して自動補完とインラインドキュメントを活用してください。
594
-
595
586
  `ModelSettings`、`SamplingSettings`、フォーマット固有の型を含む全てのエクスポート型の詳細なドキュメントについては、[型ドキュメント](./docs/types.ja.md)を参照してください。
596
587
 
597
588
  ## 開発
598
589
 
599
590
  ```bash
600
591
  # 依存関係をインストール
601
- npm install
592
+ pnpm install
602
593
 
603
594
  # テストを実行
604
- npm test
605
-
606
- # ウォッチモード
607
- npm run test:watch
608
-
609
- # テストカバレッジ
610
- npm run test:coverage
595
+ pnpm test
611
596
 
612
597
  # ビルド
613
- npm run build
598
+ pnpm build
614
599
 
615
600
  # リント
616
- npm run lint
601
+ pnpm lint
602
+
603
+ # 型チェック
604
+ pnpm typecheck
605
+
606
+ # デモサイト起動
607
+ pnpm demo
617
608
  ```
618
609
 
619
610
  ## ライセンス
package/README.md CHANGED
@@ -11,7 +11,7 @@ A TypeScript library to read and write metadata embedded in AI-generated images.
11
11
  ## Features
12
12
 
13
13
  - **Multi-format Support**: PNG (tEXt / iTXt), JPEG (COM / Exif), WebP (Exif)
14
- - **Unified API**: Simple `read()` and `write()` functions work across all formats
14
+ - **Simple API**: `read()`, `write()`, `embed()`, `stringify()` — four functions cover all use cases
15
15
  - **TypeScript Native**: Written in TypeScript with full type definitions included
16
16
  - **Zero Dependencies**: Works in Node.js and browsers without any external dependencies
17
17
  - **Format Conversion**: Seamlessly convert metadata between PNG, JPEG, and WebP
@@ -95,8 +95,8 @@ const { read } = require('@enslo/sd-metadata');
95
95
  ### Node.js Usage
96
96
 
97
97
  ```typescript
98
- import { read, write } from '@enslo/sd-metadata';
99
- import { readFileSync, writeFileSync } from 'fs';
98
+ import { read, stringify } from '@enslo/sd-metadata';
99
+ import { readFileSync } from 'fs';
100
100
 
101
101
  const imageData = readFileSync('image.png');
102
102
  const result = read(imageData);
@@ -107,24 +107,30 @@ if (result.status === 'success') {
107
107
  console.log('Model:', result.metadata.model?.name);
108
108
  console.log('Size:', result.metadata.width, 'x', result.metadata.height);
109
109
  }
110
+
111
+ // Format as human-readable text (works with any status)
112
+ const text = stringify(result);
113
+ if (text) {
114
+ console.log(text);
115
+ }
110
116
  ```
111
117
 
112
118
  ### Browser Usage
113
119
 
114
120
  ```typescript
115
- import { read } from '@enslo/sd-metadata';
121
+ import { read, softwareLabels } from '@enslo/sd-metadata';
116
122
 
117
123
  // Handle file input
118
124
  const fileInput = document.querySelector('input[type="file"]');
119
125
  fileInput.addEventListener('change', async (e) => {
120
126
  const file = e.target.files[0];
121
127
  if (!file) return;
122
-
128
+
123
129
  const arrayBuffer = await file.arrayBuffer();
124
130
  const result = read(arrayBuffer);
125
-
131
+
126
132
  if (result.status === 'success') {
127
- document.getElementById('tool').textContent = result.metadata.software;
133
+ document.getElementById('tool').textContent = softwareLabels[result.metadata.software];
128
134
  document.getElementById('prompt').textContent = result.metadata.prompt;
129
135
  document.getElementById('model').textContent = result.metadata.model?.name || 'N/A';
130
136
  }
@@ -153,7 +159,7 @@ if (result.status === 'success') {
153
159
  > For production use, pin to a specific version instead of `@latest`:
154
160
  >
155
161
  > ```text
156
- > https://cdn.jsdelivr.net/npm/@enslo/sd-metadata@2.0.0/dist/index.js
162
+ > https://cdn.jsdelivr.net/npm/@enslo/sd-metadata@2.0.1/dist/index.js
157
163
  > ```
158
164
 
159
165
  ### Advanced Examples
@@ -184,8 +190,7 @@ if (parseResult.status === 'success') {
184
190
  }
185
191
  ```
186
192
 
187
- > [!TIP]
188
- > This library handles metadata read/write only. For actual image format conversion (decoding/encoding pixels), use image processing libraries like [sharp](https://www.npmjs.com/package/sharp), [jimp](https://www.npmjs.com/package/jimp), or browser Canvas API.
193
+ > **Tip:** This library handles metadata read/write only. For actual image format conversion (decoding/encoding pixels), use image processing libraries like [sharp](https://www.npmjs.com/package/sharp), [jimp](https://www.npmjs.com/package/jimp), or browser Canvas API.
189
194
 
190
195
  </details>
191
196
 
@@ -305,8 +310,7 @@ const result = embed(imageData, {
305
310
  });
306
311
  ```
307
312
 
308
- > [!TIP]
309
- > If an extras key matches a structured field (e.g., `Steps`), the extras value overrides the structured value at its original position. New keys are appended at the end.
313
+ > **Tip:** If an extras key matches a structured field (e.g., `Steps`), the extras value overrides the structured value at its original position. New keys are appended at the end.
310
314
 
311
315
  Since `EmbedMetadata` is a subset of all `GenerationMetadata` variants, you can pass parsed metadata directly — including NovelAI with its `characterPrompts`:
312
316
 
@@ -349,10 +353,15 @@ if (text) {
349
353
 
350
354
  ## API Reference
351
355
 
352
- ### `read(input: Uint8Array | ArrayBuffer): ParseResult`
356
+ ### `read(input: Uint8Array | ArrayBuffer, options?: ReadOptions): ParseResult`
353
357
 
354
358
  Reads and parses metadata from an image file.
355
359
 
360
+ **Parameters:**
361
+
362
+ - `input` - Image file data (PNG, JPEG, or WebP)
363
+ - `options` - Optional read options (see [Type Documentation](./docs/types.md) for details)
364
+
356
365
  **Returns:**
357
366
 
358
367
  - `{ status: 'success', metadata, raw }` - Successfully parsed
@@ -574,45 +583,28 @@ type RawMetadata =
574
583
  | { format: 'webp'; segments: MetadataSegment[] };
575
584
  ```
576
585
 
577
- > [!TIP]
578
- > For TypeScript users: All types are exported and available for import.
579
- >
580
- > ```typescript
581
- > import type {
582
- > BaseMetadata,
583
- > EmbedMetadata,
584
- > ParseResult,
585
- > GenerationMetadata,
586
- > GenerationSoftware,
587
- > ModelSettings,
588
- > SamplingSettings
589
- > } from '@enslo/sd-metadata';
590
- > ```
591
- >
592
- > Use your IDE's IntelliSense for auto-completion and inline documentation.
593
-
594
586
  For detailed documentation of all exported types including `ModelSettings`, `SamplingSettings`, and format-specific types, see the [Type Documentation](./docs/types.md).
595
587
 
596
588
  ## Development
597
589
 
598
590
  ```bash
599
591
  # Install dependencies
600
- npm install
592
+ pnpm install
601
593
 
602
594
  # Run tests
603
- npm test
604
-
605
- # Watch mode
606
- npm run test:watch
607
-
608
- # Test coverage
609
- npm run test:coverage
595
+ pnpm test
610
596
 
611
597
  # Build
612
- npm run build
598
+ pnpm build
613
599
 
614
600
  # Lint
615
- npm run lint
601
+ pnpm lint
602
+
603
+ # Type check
604
+ pnpm typecheck
605
+
606
+ # Start demo site
607
+ pnpm demo
616
608
  ```
617
609
 
618
610
  ## License
package/dist/index.js CHANGED
@@ -936,7 +936,13 @@ function toJsonResult(value) {
936
936
  }
937
937
 
938
938
  // src/parsers/comfyui-nodes.ts
939
- var SAMPLER_TYPES = ["KSampler", "KSamplerAdvanced", "SamplerCustomAdvanced"];
939
+ var SAMPLER_TYPES = [
940
+ "KSampler",
941
+ "KSamplerAdvanced",
942
+ "SamplerCustomAdvanced",
943
+ "SamplerCustom",
944
+ "DetailerForEach"
945
+ ];
940
946
  var LATENT_IMAGE_TYPES = ["EmptyLatentImage"];
941
947
  var LATENT_IMAGE_RGTHREE_TYPES = ["SDXL Empty Latent Image (rgthree)"];
942
948
  var CHECKPOINT_TYPES = ["CheckpointLoaderSimple", "CheckpointLoader"];
@@ -1004,13 +1010,29 @@ function extractPromptTexts(nodes, sampler) {
1004
1010
  return { promptText: "", negativeText: "" };
1005
1011
  }
1006
1012
  const conditioningSource = resolveConditioningSource(nodes, sampler);
1007
- const positiveRef = conditioningSource.inputs.positive;
1008
- const negativeRef = conditioningSource.inputs.negative;
1009
1013
  return {
1010
- promptText: isNodeReference(positiveRef) ? extractText(nodes, String(positiveRef[0])) : "",
1011
- negativeText: isNodeReference(negativeRef) ? extractText(nodes, String(negativeRef[0])) : ""
1014
+ promptText: extractTextFromConditioning(
1015
+ nodes,
1016
+ conditioningSource.inputs.positive,
1017
+ "positive"
1018
+ ),
1019
+ negativeText: extractTextFromConditioning(
1020
+ nodes,
1021
+ conditioningSource.inputs.negative,
1022
+ "negative"
1023
+ )
1012
1024
  };
1013
1025
  }
1026
+ function extractTextFromConditioning(nodes, ref, condKey, maxDepth = 10) {
1027
+ if (maxDepth <= 0 || !isNodeReference(ref)) return "";
1028
+ const nodeId = String(ref[0]);
1029
+ const text = extractText(nodes, nodeId);
1030
+ if (text) return text;
1031
+ const node = nodes[nodeId];
1032
+ if (!node) return "";
1033
+ const next = node.inputs[condKey];
1034
+ return extractTextFromConditioning(nodes, next, condKey, maxDepth - 1);
1035
+ }
1014
1036
  function extractDimensions(latentImage, latentImageRgthree) {
1015
1037
  if (latentImage) {
1016
1038
  const width = Number(latentImage.inputs.width) || 0;
@@ -1033,20 +1055,21 @@ function extractSampling(nodes, sampler) {
1033
1055
  if (sampler.class_type === "SamplerCustomAdvanced") {
1034
1056
  return extractAdvancedSampling(nodes, sampler);
1035
1057
  }
1058
+ if (sampler.class_type === "SamplerCustom") {
1059
+ return extractCustomSampling(nodes, sampler);
1060
+ }
1036
1061
  let seed = sampler.inputs.seed;
1037
1062
  if (isNodeReference(seed)) {
1038
1063
  const seedNode = nodes[String(seed[0])];
1039
1064
  seed = seedNode?.inputs.seed;
1040
1065
  }
1041
- const rawDenoise = sampler.inputs.denoise;
1042
- const denoise = typeof rawDenoise === "number" && rawDenoise < 1 ? rawDenoise : void 0;
1043
1066
  return {
1044
1067
  seed,
1045
1068
  steps: sampler.inputs.steps,
1046
1069
  cfg: sampler.inputs.cfg,
1047
1070
  sampler: sampler.inputs.sampler_name,
1048
1071
  scheduler: sampler.inputs.scheduler,
1049
- denoise
1072
+ denoise: sampler.inputs.denoise
1050
1073
  };
1051
1074
  }
1052
1075
  function extractAdvancedSampling(nodes, sampler) {
@@ -1054,15 +1077,30 @@ function extractAdvancedSampling(nodes, sampler) {
1054
1077
  const guiderNode = resolveNode(nodes, sampler.inputs.guider);
1055
1078
  const samplerSelectNode = resolveNode(nodes, sampler.inputs.sampler);
1056
1079
  const schedulerNode = resolveNode(nodes, sampler.inputs.sigmas);
1057
- const rawDenoise = schedulerNode?.inputs.denoise;
1058
- const denoise = typeof rawDenoise === "number" && rawDenoise < 1 ? rawDenoise : void 0;
1059
1080
  return {
1060
1081
  seed: noiseNode?.inputs.noise_seed,
1061
1082
  steps: schedulerNode?.inputs.steps,
1062
1083
  cfg: guiderNode?.inputs.cfg,
1063
1084
  sampler: samplerSelectNode?.inputs.sampler_name,
1064
1085
  scheduler: schedulerNode?.inputs.scheduler,
1065
- denoise
1086
+ denoise: schedulerNode?.inputs.denoise
1087
+ };
1088
+ }
1089
+ function extractCustomSampling(nodes, sampler) {
1090
+ const samplerSelectNode = resolveNode(nodes, sampler.inputs.sampler);
1091
+ const schedulerNode = resolveNode(nodes, sampler.inputs.sigmas);
1092
+ let seed = sampler.inputs.noise_seed;
1093
+ if (isNodeReference(seed)) {
1094
+ const seedNode = nodes[String(seed[0])];
1095
+ seed = seedNode?.inputs.seed;
1096
+ }
1097
+ return {
1098
+ seed,
1099
+ steps: schedulerNode?.inputs.steps,
1100
+ cfg: sampler.inputs.cfg,
1101
+ sampler: samplerSelectNode?.inputs.sampler_name,
1102
+ scheduler: schedulerNode?.inputs.scheduler,
1103
+ denoise: schedulerNode?.inputs.denoise
1066
1104
  };
1067
1105
  }
1068
1106
  function extractModel(checkpoint, unetLoader) {
@@ -1192,6 +1230,14 @@ function parseComfyUI(entries) {
1192
1230
  ...merged
1193
1231
  });
1194
1232
  }
1233
+ function normalizeDenoise(sampling) {
1234
+ if (!sampling || typeof sampling.denoise !== "number") return sampling;
1235
+ if (sampling.denoise >= 1) {
1236
+ const { denoise: _, ...rest } = sampling;
1237
+ return rest;
1238
+ }
1239
+ return sampling;
1240
+ }
1195
1241
  function cleanJsonString(json) {
1196
1242
  return json.replace(/\0+$/, "").replace(/:\s*NaN\b/g, ": null");
1197
1243
  }
@@ -1233,10 +1279,12 @@ function extractComfyUIMetadata(nodes) {
1233
1279
  c.latentImageRgthree
1234
1280
  );
1235
1281
  const hiresSamplerNode = findHiresSampler(nodes);
1236
- const hiresSampling = hiresSamplerNode ? extractSampling(nodes, hiresSamplerNode) : void 0;
1282
+ const hiresSampling = normalizeDenoise(
1283
+ hiresSamplerNode ? extractSampling(nodes, hiresSamplerNode) : void 0
1284
+ );
1237
1285
  const hiresScale = resolveHiresScale(nodes, c, width);
1238
1286
  const upscalerName = c.hiresModelUpscale?.inputs.model_name;
1239
- const rawSampling = extractSampling(nodes, c.sampler);
1287
+ const rawSampling = normalizeDenoise(extractSampling(nodes, c.sampler));
1240
1288
  const clipSkip = extractClipSkip(c.clipSetLastLayer);
1241
1289
  return trimObject({
1242
1290
  prompt: promptText || void 0,