@mcptoolshop/style-dataset-lab 1.2.1 → 2.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +138 -113
  3. package/bin/sdlab.js +126 -0
  4. package/lib/args.js +106 -0
  5. package/lib/card.js +145 -0
  6. package/lib/comfyui.js +109 -0
  7. package/lib/config.js +248 -0
  8. package/lib/deprecation.js +14 -0
  9. package/lib/eligibility.js +110 -0
  10. package/lib/eval-pack.js +283 -0
  11. package/lib/export.js +259 -0
  12. package/lib/paths.js +38 -0
  13. package/lib/records.js +69 -0
  14. package/lib/snapshot.js +211 -0
  15. package/lib/split.js +409 -0
  16. package/package.json +20 -9
  17. package/scripts/bulk-curate-wave2-5.js +1 -1
  18. package/scripts/bulk-curate-waves11-18.js +1 -1
  19. package/scripts/canon-bind.js +188 -338
  20. package/scripts/card.js +73 -0
  21. package/scripts/compare.js +40 -30
  22. package/scripts/curate-wave25.js +1 -1
  23. package/scripts/curate.js +34 -28
  24. package/scripts/doctor.js +324 -0
  25. package/scripts/eligibility.js +91 -0
  26. package/scripts/eval-pack.js +92 -0
  27. package/scripts/export.js +85 -0
  28. package/scripts/generate-controlnet.js +36 -121
  29. package/scripts/generate-identity.js +38 -106
  30. package/scripts/generate-ipadapter.js +42 -119
  31. package/scripts/generate.js +22 -81
  32. package/scripts/init.js +229 -0
  33. package/scripts/migrate-records.js +15 -21
  34. package/scripts/painterly-test.js +20 -47
  35. package/scripts/painterly.js +27 -106
  36. package/scripts/snapshot.js +132 -0
  37. package/scripts/split.js +154 -0
  38. package/templates/domains/architecture/constitution.json +97 -0
  39. package/templates/domains/architecture/lanes.json +41 -0
  40. package/templates/domains/architecture/project.json +20 -0
  41. package/templates/domains/architecture/rubric.json +40 -0
  42. package/templates/domains/architecture/terminology.json +19 -0
  43. package/templates/domains/character-design/constitution.json +88 -0
  44. package/templates/domains/character-design/lanes.json +35 -0
  45. package/templates/domains/character-design/project.json +17 -0
  46. package/templates/domains/character-design/rubric.json +38 -0
  47. package/templates/domains/character-design/terminology.json +21 -0
  48. package/templates/domains/creature-design/constitution.json +87 -0
  49. package/templates/domains/creature-design/lanes.json +41 -0
  50. package/templates/domains/creature-design/project.json +18 -0
  51. package/templates/domains/creature-design/rubric.json +39 -0
  52. package/templates/domains/creature-design/terminology.json +20 -0
  53. package/templates/domains/game-art/constitution.json +113 -0
  54. package/templates/domains/game-art/lanes.json +47 -0
  55. package/templates/domains/game-art/project.json +17 -0
  56. package/templates/domains/game-art/rubric.json +36 -0
  57. package/templates/domains/game-art/terminology.json +30 -0
  58. package/templates/domains/vehicle-mech/constitution.json +87 -0
  59. package/templates/domains/vehicle-mech/lanes.json +41 -0
  60. package/templates/domains/vehicle-mech/project.json +17 -0
  61. package/templates/domains/vehicle-mech/rubric.json +40 -0
  62. package/templates/domains/vehicle-mech/terminology.json +21 -0
@@ -9,9 +9,7 @@
9
9
  *
10
10
  * Usage:
11
11
  * node scripts/generate-identity.js inputs/identity-packets/wave27a-identity-spine.json
12
- * node scripts/generate-identity.js inputs/identity-packets/wave27a-identity-spine.json --dry-run
13
- * node scripts/generate-identity.js inputs/identity-packets/wave27a-identity-spine.json --subject kael_maren
14
- * node scripts/generate-identity.js inputs/identity-packets/wave27a-identity-spine.json --seeds 3
12
+ * sdlab generate:identity inputs/identity-packets/wave27a.json --project star-freight
15
13
  *
16
14
  * Options:
17
15
  * --dry-run Print what would be generated without touching ComfyUI
@@ -20,20 +18,13 @@
20
18
  * --phase MODE Generation phase: discovery | follow_on (default: discovery)
21
19
  * --anchor FILE Anchor source image path (required for --phase follow_on)
22
20
  * --denoise N Denoise strength for follow_on phase (default: 0.38)
23
- *
24
- * Phases:
25
- * discovery — txt2img from prompt, 3 seeds per shot. No prior image input.
26
- * follow_on — img2img from anchor image. Requires --anchor and --denoise.
27
- * Anchor curation happens between phases (manual step).
28
21
  */
29
22
 
30
- import { readFile, writeFile, mkdir, stat } from "node:fs/promises";
23
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
31
24
  import { join } from "node:path";
32
-
33
- const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
34
- const REPO_ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
35
- const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
36
- const GAME_ROOT = join(REPO_ROOT, 'games', GAME);
25
+ import { getProjectName } from "../lib/args.js";
26
+ import { REPO_ROOT } from "../lib/paths.js";
27
+ import { comfyHealth, submitAndWait, downloadImage } from "../lib/comfyui.js";
37
28
 
38
29
  // ── Defaults (match existing lab pipeline) ──
39
30
 
@@ -129,7 +120,6 @@ function validatePacket(pack) {
129
120
  errors.push(`Subject ${sn}, shot ${shot.id}: invalid view_type '${shot.view_type}'`);
130
121
  }
131
122
 
132
- // Variant intent enforcement
133
123
  if (shot.view_type === "expression_variant") {
134
124
  if (!shot.variant_intent) {
135
125
  errors.push(`Subject ${sn}, shot ${shot.id}: expression_variant missing variant_intent`);
@@ -149,16 +139,7 @@ function validatePacket(pack) {
149
139
  return errors;
150
140
  }
151
141
 
152
- // ── ComfyUI API ──
153
-
154
- async function comfyHealth() {
155
- try {
156
- const res = await fetch(`${COMFY_URL}/system_stats`);
157
- return res.ok;
158
- } catch {
159
- return false;
160
- }
161
- }
142
+ // ── ComfyUI Workflows ──
162
143
 
163
144
  function buildTxt2ImgWorkflow(prompt, negativePrompt, seed) {
164
145
  const nodes = {};
@@ -268,14 +249,12 @@ function buildImg2ImgWorkflow(prompt, negativePrompt, seed, imagePath, denoise)
268
249
  clipOut = [loraId, 1];
269
250
  }
270
251
 
271
- // Load source image
272
252
  const loadImgId = String(nextId++);
273
253
  nodes[loadImgId] = {
274
254
  class_type: "LoadImage",
275
255
  inputs: { image: imagePath },
276
256
  };
277
257
 
278
- // Encode image to latent via VAE
279
258
  const encodeId = String(nextId++);
280
259
  nodes[encodeId] = {
281
260
  class_type: "VAEEncode",
@@ -326,46 +305,6 @@ function buildImg2ImgWorkflow(prompt, negativePrompt, seed, imagePath, denoise)
326
305
  return nodes;
327
306
  }
328
307
 
329
- const MAX_POLL_MS = 600_000; // 10 minutes
330
-
331
- async function submitAndWait(workflow) {
332
- const clientId = `idp-${Date.now()}`;
333
- const res = await fetch(`${COMFY_URL}/prompt`, {
334
- method: "POST",
335
- headers: { "Content-Type": "application/json" },
336
- body: JSON.stringify({ prompt: workflow, client_id: clientId }),
337
- });
338
-
339
- if (!res.ok) {
340
- const text = await res.text();
341
- throw new Error(`ComfyUI submit failed: ${res.status} ${text}`);
342
- }
343
-
344
- const { prompt_id: promptId } = await res.json();
345
-
346
- const start = Date.now();
347
- while (true) {
348
- if (Date.now() - start > MAX_POLL_MS) throw new Error(`ComfyUI poll timeout after ${MAX_POLL_MS/1000}s for prompt ${promptId}`);
349
- await new Promise((r) => setTimeout(r, 1000));
350
- const histRes = await fetch(`${COMFY_URL}/history/${promptId}`);
351
- if (!histRes.ok) continue;
352
- const history = await histRes.json();
353
- const entry = history[promptId];
354
- if (!entry) continue;
355
- if (entry.status?.completed) return entry;
356
- if (entry.status?.status_str === "error") {
357
- throw new Error(`ComfyUI generation failed: ${JSON.stringify(entry.status)}`);
358
- }
359
- }
360
- }
361
-
362
- async function downloadImage(filename, subfolder, type = "output") {
363
- const url = `${COMFY_URL}/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder || "")}&type=${type}`;
364
- const res = await fetch(url);
365
- if (!res.ok) throw new Error(`Download failed: ${res.status}`);
366
- return Buffer.from(await res.arrayBuffer());
367
- }
368
-
369
308
  // ── Record Builder ──
370
309
 
371
310
  function buildRecord(pack, subject, shot, seed, seedIndex, imgData, destPath, elapsedMs, phase, anchorInfo) {
@@ -434,58 +373,53 @@ function buildRecord(pack, subject, shot, seed, seedIndex, imgData, destPath, el
434
373
 
435
374
  // ── Main ──
436
375
 
437
- async function main() {
438
- const args = process.argv.slice(2);
439
- const packPath = args.find((a) => !a.startsWith("--"));
440
- const dryRun = args.includes("--dry-run");
441
- const subjectFilter = args.includes("--subject")
442
- ? args[args.indexOf("--subject") + 1]
376
+ export async function run(argv = process.argv.slice(2)) {
377
+ const projectName = getProjectName(argv);
378
+ const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
379
+ const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
380
+
381
+ const packPath = argv.find((a) => !a.startsWith("--"));
382
+ const dryRun = argv.includes("--dry-run");
383
+ const subjectFilter = argv.includes("--subject")
384
+ ? argv[argv.indexOf("--subject") + 1]
443
385
  : null;
444
- const seedCount = args.includes("--seeds")
445
- ? parseInt(args[args.indexOf("--seeds") + 1], 10)
386
+ const seedCount = argv.includes("--seeds")
387
+ ? parseInt(argv[argv.indexOf("--seeds") + 1], 10)
446
388
  : 3;
447
- const phase = args.includes("--phase")
448
- ? args[args.indexOf("--phase") + 1]
389
+ const phase = argv.includes("--phase")
390
+ ? argv[argv.indexOf("--phase") + 1]
449
391
  : "discovery";
450
- const anchorPath = args.includes("--anchor")
451
- ? args[args.indexOf("--anchor") + 1]
392
+ const anchorPath = argv.includes("--anchor")
393
+ ? argv[argv.indexOf("--anchor") + 1]
452
394
  : null;
453
- const denoise = args.includes("--denoise")
454
- ? parseFloat(args[args.indexOf("--denoise") + 1])
395
+ const denoise = argv.includes("--denoise")
396
+ ? parseFloat(argv[argv.indexOf("--denoise") + 1])
455
397
  : 0.38;
456
398
 
457
399
  if (!packPath) {
458
- console.error("Usage: node scripts/generate-identity.js <packet-file> [options]");
459
- process.exit(1);
400
+ throw new Error("Usage: sdlab generate:identity <packet-file> [options]");
460
401
  }
461
402
 
462
403
  if (phase === "follow_on" && !anchorPath) {
463
- console.error("Error: --phase follow_on requires --anchor <image-path>");
464
- process.exit(1);
404
+ throw new Error("--phase follow_on requires --anchor <image-path>");
465
405
  }
466
406
 
467
407
  const fullPackPath = join(GAME_ROOT, packPath);
468
408
  const pack = JSON.parse(await readFile(fullPackPath, "utf-8"));
469
409
 
470
- // Validate
471
410
  const errors = validatePacket(pack);
472
411
  if (errors.length > 0) {
473
- console.error("\x1b[31mPacket validation failed:\x1b[0m");
474
- for (const e of errors) console.error(` - ${e}`);
475
- process.exit(1);
412
+ throw new Error("Packet validation failed:\n " + errors.join("\n "));
476
413
  }
477
414
 
478
- // Filter subjects if requested
479
415
  let subjects = pack.subjects;
480
416
  if (subjectFilter) {
481
417
  subjects = subjects.filter((s) => s.subject_name === subjectFilter);
482
418
  if (subjects.length === 0) {
483
- console.error(`No subject found with name '${subjectFilter}'`);
484
- process.exit(1);
419
+ throw new Error(`No subject found with name '${subjectFilter}'`);
485
420
  }
486
421
  }
487
422
 
488
- // Count shots
489
423
  const totalShots = subjects.reduce((sum, s) => sum + s.shots.length, 0);
490
424
  const totalImages = totalShots * seedCount;
491
425
 
@@ -503,13 +437,9 @@ async function main() {
503
437
  console.log("");
504
438
 
505
439
  if (!dryRun) {
506
- const online = await comfyHealth();
440
+ const online = await comfyHealth(COMFY_URL);
507
441
  if (!online) {
508
- console.error("\x1b[31mError:\x1b[0m ComfyUI not reachable at " + COMFY_URL);
509
- console.error(
510
- "Start ComfyUI: cd F:/AI-Models/ComfyUI-runtime && python main.py --listen 127.0.0.1 --port 8188"
511
- );
512
- process.exit(1);
442
+ throw new Error("ComfyUI not reachable at " + COMFY_URL);
513
443
  }
514
444
  console.log("\x1b[32m+\x1b[0m ComfyUI online");
515
445
  }
@@ -555,7 +485,7 @@ async function main() {
555
485
  }
556
486
 
557
487
  try {
558
- const result = await submitAndWait(workflow);
488
+ const result = await submitAndWait(workflow, COMFY_URL, { clientPrefix: 'idp' });
559
489
  const elapsed = Date.now() - startMs;
560
490
 
561
491
  const outputs = result.outputs || {};
@@ -576,7 +506,7 @@ async function main() {
576
506
  continue;
577
507
  }
578
508
 
579
- const imgData = await downloadImage(imageFile, imageSubfolder);
509
+ const imgData = await downloadImage(imageFile, imageSubfolder, COMFY_URL);
580
510
  await writeFile(join(GAME_ROOT, destPath), imgData);
581
511
 
582
512
  const anchorInfo =
@@ -613,7 +543,6 @@ async function main() {
613
543
  console.log(`\n\x1b[32m+\x1b[0m Generated ${generated} images`);
614
544
  if (dryRun) console.log(" (dry run — no images produced)");
615
545
 
616
- // Summary
617
546
  console.log("\nNext steps:");
618
547
  if (phase === "discovery") {
619
548
  console.log(" 1. Review candidates in outputs/candidates/");
@@ -627,7 +556,10 @@ async function main() {
627
556
  }
628
557
  }
629
558
 
630
- main().catch((err) => {
631
- console.error(err);
632
- process.exit(1);
633
- });
559
+ // Direct execution guard
560
+ if (process.argv[1] && (process.argv[1].endsWith('generate-identity.js') || process.argv[1].endsWith('generate-identity'))) {
561
+ run().catch((err) => {
562
+ console.error(err.message || err);
563
+ process.exit(1);
564
+ });
565
+ }
@@ -4,31 +4,18 @@
4
4
  * generate-ipadapter.js — IP-Adapter guided generation for structural subjects.
5
5
  *
6
6
  * Uses a reference image to guide the shape/structure of the output
7
- * while the text prompt adds identity-specific details (wear, patching, name).
7
+ * while the text prompt adds identity-specific details.
8
8
  *
9
9
  * Usage:
10
10
  * node scripts/generate-ipadapter.js --subject corrigan --ref <image> --seeds 4
11
- * node scripts/generate-ipadapter.js --subject corrigan --ref <image> --seeds 4 --dry-run
12
- *
13
- * Options:
14
- * --subject NAME Subject name for output file naming
15
- * --ref PATH Reference image path (the structural truth source)
16
- * --prompt TEXT Generation prompt
17
- * --negative TEXT Negative prompt
18
- * --seeds N Number of seeds (default: 4)
19
- * --weight W IP-Adapter weight (default: 0.55)
20
- * --start S IP-Adapter start (default: 0.0)
21
- * --end E IP-Adapter end (default: 0.8)
22
- * --dry-run Print workflow without running
11
+ * sdlab generate:ipadapter --subject corrigan --ref <image> --seeds 4 --project star-freight
23
12
  */
24
13
 
25
14
  import { readFile, writeFile, mkdir } from "node:fs/promises";
26
15
  import { join } from "node:path";
27
-
28
- const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
29
- const REPO_ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
30
- const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
31
- const GAME_ROOT = join(REPO_ROOT, 'games', GAME);
16
+ import { getProjectName } from "../lib/args.js";
17
+ import { REPO_ROOT } from "../lib/paths.js";
18
+ import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
32
19
 
33
20
  const DEFAULTS = {
34
21
  checkpoint: "dreamshaperXL_v21TurboDPMSDE.safetensors",
@@ -44,11 +31,10 @@ const DEFAULTS = {
44
31
  base_seed: 27200,
45
32
  };
46
33
 
47
- function parseArgs() {
48
- const args = process.argv.slice(2);
34
+ function parseLocalArgs(argv) {
49
35
  const get = (flag, def) => {
50
- const i = args.indexOf(flag);
51
- return i >= 0 && i + 1 < args.length ? args[i + 1] : def;
36
+ const i = argv.indexOf(flag);
37
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def;
52
38
  };
53
39
  return {
54
40
  subject: get("--subject", "unknown"),
@@ -59,7 +45,7 @@ function parseArgs() {
59
45
  weight: parseFloat(get("--weight", "0.55")),
60
46
  start: parseFloat(get("--start", "0.0")),
61
47
  end: parseFloat(get("--end", "0.8")),
62
- dryRun: args.includes("--dry-run"),
48
+ dryRun: argv.includes("--dry-run"),
63
49
  };
64
50
  }
65
51
 
@@ -67,7 +53,6 @@ function buildIPAdapterWorkflow(prompt, negativePrompt, seed, refImageName, weig
67
53
  const nodes = {};
68
54
  let nextId = 1;
69
55
 
70
- // Checkpoint
71
56
  const ckptId = String(nextId++);
72
57
  nodes[ckptId] = {
73
58
  class_type: "CheckpointLoaderSimple",
@@ -77,33 +62,30 @@ function buildIPAdapterWorkflow(prompt, negativePrompt, seed, refImageName, weig
77
62
  let modelOut = [ckptId, 0];
78
63
  let clipOut = [ckptId, 1];
79
64
 
80
- // LoRA
81
65
  for (const lora of DEFAULTS.loras) {
82
66
  const loraId = String(nextId++);
83
67
  nodes[loraId] = {
84
68
  class_type: "LoraLoader",
85
69
  inputs: {
86
- model: modelOut,
87
- clip: clipOut,
70
+ model: modelOut, clip: clipOut,
88
71
  lora_name: lora.name,
89
- strength_model: lora.weight,
90
- strength_clip: lora.weight,
72
+ strength_model: lora.weight, strength_clip: lora.weight,
91
73
  },
92
74
  };
93
75
  modelOut = [loraId, 0];
94
76
  clipOut = [loraId, 1];
95
77
  }
96
78
 
97
- // Load CLIP Vision
79
+ // CLIP Vision
98
80
  const clipVisionId = String(nextId++);
99
81
  nodes[clipVisionId] = {
100
82
  class_type: "CLIPVisionLoader",
101
83
  inputs: { clip_name: DEFAULTS.clip_vision_model },
102
84
  };
103
85
 
104
- // Load IP-Adapter model
105
- const ipaLoadId = String(nextId++);
106
- nodes[ipaLoadId] = {
86
+ // IP-Adapter model
87
+ const ipaLoaderId = String(nextId++);
88
+ nodes[ipaLoaderId] = {
107
89
  class_type: "IPAdapterModelLoader",
108
90
  inputs: { ipadapter_file: DEFAULTS.ipadapter_model },
109
91
  };
@@ -115,47 +97,43 @@ function buildIPAdapterWorkflow(prompt, negativePrompt, seed, refImageName, weig
115
97
  inputs: { image: refImageName },
116
98
  };
117
99
 
118
- // Apply IP-Adapter (using IPAdapterAdvanced from ComfyUI_IPAdapter_plus)
100
+ // Apply IP-Adapter
119
101
  const ipaApplyId = String(nextId++);
120
102
  nodes[ipaApplyId] = {
121
103
  class_type: "IPAdapterAdvanced",
122
104
  inputs: {
123
105
  model: modelOut,
124
- ipadapter: [ipaLoadId, 0],
125
- image: [refImgId, 0],
106
+ ipadapter: [ipaLoaderId, 0],
126
107
  clip_vision: [clipVisionId, 0],
127
- weight: weight,
108
+ image: [refImgId, 0],
109
+ weight,
128
110
  weight_type: "linear",
129
111
  combine_embeds: "concat",
112
+ embeds_scaling: "V only",
130
113
  start_at: start,
131
114
  end_at: end,
132
- embeds_scaling: "V only",
133
115
  },
134
116
  };
135
117
  modelOut = [ipaApplyId, 0];
136
118
 
137
- // Positive prompt
138
119
  const posId = String(nextId++);
139
120
  nodes[posId] = {
140
121
  class_type: "CLIPTextEncode",
141
122
  inputs: { text: prompt, clip: clipOut },
142
123
  };
143
124
 
144
- // Negative prompt
145
125
  const negId = String(nextId++);
146
126
  nodes[negId] = {
147
127
  class_type: "CLIPTextEncode",
148
128
  inputs: { text: negativePrompt, clip: clipOut },
149
129
  };
150
130
 
151
- // Empty latent
152
131
  const latentId = String(nextId++);
153
132
  nodes[latentId] = {
154
133
  class_type: "EmptyLatentImage",
155
134
  inputs: { width: DEFAULTS.width, height: DEFAULTS.height, batch_size: 1 },
156
135
  };
157
136
 
158
- // KSampler
159
137
  const samplerId = String(nextId++);
160
138
  nodes[samplerId] = {
161
139
  class_type: "KSampler",
@@ -173,14 +151,12 @@ function buildIPAdapterWorkflow(prompt, negativePrompt, seed, refImageName, weig
173
151
  },
174
152
  };
175
153
 
176
- // VAE Decode
177
154
  const vaeId = String(nextId++);
178
155
  nodes[vaeId] = {
179
156
  class_type: "VAEDecode",
180
157
  inputs: { samples: [samplerId, 0], vae: [ckptId, 2] },
181
158
  };
182
159
 
183
- // Save
184
160
  const saveId = String(nextId++);
185
161
  nodes[saveId] = {
186
162
  class_type: "SaveImage",
@@ -190,62 +166,18 @@ function buildIPAdapterWorkflow(prompt, negativePrompt, seed, refImageName, weig
190
166
  return nodes;
191
167
  }
192
168
 
193
- async function comfyHealth() {
194
- try {
195
- const res = await fetch(`${COMFY_URL}/system_stats`);
196
- return res.ok;
197
- } catch {
198
- return false;
199
- }
200
- }
201
-
202
- const MAX_POLL_MS = 600_000; // 10 minutes
169
+ export async function run(argv = process.argv.slice(2)) {
170
+ const projectName = getProjectName(argv);
171
+ const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
172
+ const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
203
173
 
204
- async function submitAndWait(workflow) {
205
- const clientId = `ipa-${Date.now()}`;
206
- const res = await fetch(`${COMFY_URL}/prompt`, {
207
- method: "POST",
208
- headers: { "Content-Type": "application/json" },
209
- body: JSON.stringify({ prompt: workflow, client_id: clientId }),
210
- });
211
- if (!res.ok) {
212
- const text = await res.text();
213
- throw new Error(`ComfyUI submit failed: ${res.status} ${text}`);
214
- }
215
- const { prompt_id: promptId } = await res.json();
216
- const start = Date.now();
217
- while (true) {
218
- if (Date.now() - start > MAX_POLL_MS) throw new Error(`ComfyUI poll timeout after ${MAX_POLL_MS/1000}s for prompt ${promptId}`);
219
- await new Promise((r) => setTimeout(r, 1000));
220
- const histRes = await fetch(`${COMFY_URL}/history/${promptId}`);
221
- if (!histRes.ok) continue;
222
- const history = await histRes.json();
223
- const entry = history[promptId];
224
- if (!entry) continue;
225
- if (entry.status?.completed) return entry;
226
- if (entry.status?.status_str === "error") {
227
- throw new Error(`ComfyUI error: ${JSON.stringify(entry.status)}`);
228
- }
229
- }
230
- }
231
-
232
- async function downloadImage(filename, subfolder, type = "output") {
233
- const url = `${COMFY_URL}/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder || "")}&type=${type}`;
234
- const res = await fetch(url);
235
- if (!res.ok) throw new Error(`Download failed: ${res.status}`);
236
- return Buffer.from(await res.arrayBuffer());
237
- }
238
-
239
- async function main() {
240
- const opts = parseArgs();
174
+ const opts = parseLocalArgs(argv);
241
175
 
242
176
  if (!opts.refPath) {
243
- console.error("Error: --ref <path> is required");
244
- process.exit(1);
177
+ throw new Error("--ref <path> is required");
245
178
  }
246
179
  if (!opts.prompt) {
247
- console.error("Error: --prompt is required");
248
- process.exit(1);
180
+ throw new Error("--prompt is required");
249
181
  }
250
182
 
251
183
  const negative = opts.negative ||
@@ -259,33 +191,21 @@ async function main() {
259
191
  console.log(` Seeds: ${opts.seeds}`);
260
192
  console.log("");
261
193
 
194
+ const refFilename = `ipa_ref_${opts.subject}.png`;
195
+
262
196
  if (!opts.dryRun) {
263
- const online = await comfyHealth();
197
+ const online = await comfyHealth(COMFY_URL);
264
198
  if (!online) {
265
- console.error("\x1b[31mError:\x1b[0m ComfyUI not reachable");
266
- process.exit(1);
199
+ throw new Error("ComfyUI not reachable");
267
200
  }
268
201
  console.log("\x1b[32m+\x1b[0m ComfyUI online");
269
202
 
270
- // Upload reference image
271
- const refData = await readFile(join(GAME_ROOT, opts.refPath));
272
- const refFilename = `ipa_ref_${opts.subject}.png`;
273
- const formData = new FormData();
274
- formData.append("image", new Blob([refData], { type: "image/png" }), refFilename);
275
- formData.append("overwrite", "true");
276
- const uploadRes = await fetch(`${COMFY_URL}/upload/image`, { method: "POST", body: formData });
277
- if (!uploadRes.ok) {
278
- console.error(`Reference upload failed: ${uploadRes.status}`);
279
- process.exit(1);
280
- }
281
- const uploadResult = await uploadRes.json();
282
- console.log(`\x1b[32m+\x1b[0m Reference uploaded: ${uploadResult.name}`);
203
+ await uploadImage(join(GAME_ROOT, opts.refPath), refFilename, COMFY_URL);
204
+ console.log(`\x1b[32m+\x1b[0m Reference uploaded: ${refFilename}`);
283
205
  }
284
206
 
285
207
  await mkdir(join(GAME_ROOT, "outputs/candidates"), { recursive: true });
286
208
 
287
- const refFilename = `ipa_ref_${opts.subject}.png`;
288
-
289
209
  for (let si = 0; si < opts.seeds; si++) {
290
210
  const seed = DEFAULTS.base_seed + si;
291
211
  const assetId = `${opts.subject}_ipa_s${si}`;
@@ -302,7 +222,7 @@ async function main() {
302
222
  );
303
223
 
304
224
  try {
305
- const result = await submitAndWait(workflow);
225
+ const result = await submitAndWait(workflow, COMFY_URL, { clientPrefix: 'ipa' });
306
226
  const elapsed = Date.now() - startMs;
307
227
 
308
228
  const outputs = result.outputs || {};
@@ -321,7 +241,7 @@ async function main() {
321
241
  continue;
322
242
  }
323
243
 
324
- const imgData = await downloadImage(imageFile, imageSubfolder);
244
+ const imgData = await downloadImage(imageFile, imageSubfolder, COMFY_URL);
325
245
  await writeFile(join(GAME_ROOT, destPath), imgData);
326
246
  console.log(` \x1b[32m+\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
327
247
  } catch (err) {
@@ -333,7 +253,10 @@ async function main() {
333
253
  if (opts.dryRun) console.log(" (dry run)");
334
254
  }
335
255
 
336
- main().catch((err) => {
337
- console.error(err);
338
- process.exit(1);
339
- });
256
+ // Direct execution guard
257
+ if (process.argv[1] && (process.argv[1].endsWith('generate-ipadapter.js') || process.argv[1].endsWith('generate-ipadapter'))) {
258
+ run().catch((err) => {
259
+ console.error(err.message || err);
260
+ process.exit(1);
261
+ });
262
+ }