@mcptoolshop/style-dataset-lab 1.2.1 → 2.0.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 (50) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +95 -116
  3. package/bin/sdlab.js +103 -0
  4. package/lib/args.js +106 -0
  5. package/lib/comfyui.js +109 -0
  6. package/lib/config.js +104 -0
  7. package/lib/deprecation.js +14 -0
  8. package/lib/paths.js +38 -0
  9. package/lib/records.js +69 -0
  10. package/package.json +13 -4
  11. package/scripts/bulk-curate-wave2-5.js +1 -1
  12. package/scripts/bulk-curate-waves11-18.js +1 -1
  13. package/scripts/canon-bind.js +268 -332
  14. package/scripts/compare.js +40 -30
  15. package/scripts/curate-wave25.js +1 -1
  16. package/scripts/curate.js +34 -28
  17. package/scripts/doctor.js +321 -0
  18. package/scripts/generate-controlnet.js +36 -121
  19. package/scripts/generate-identity.js +38 -106
  20. package/scripts/generate-ipadapter.js +42 -119
  21. package/scripts/generate.js +22 -81
  22. package/scripts/init.js +226 -0
  23. package/scripts/migrate-records.js +15 -21
  24. package/scripts/painterly-test.js +20 -47
  25. package/scripts/painterly.js +27 -106
  26. package/templates/domains/architecture/constitution.json +97 -0
  27. package/templates/domains/architecture/lanes.json +41 -0
  28. package/templates/domains/architecture/project.json +20 -0
  29. package/templates/domains/architecture/rubric.json +40 -0
  30. package/templates/domains/architecture/terminology.json +19 -0
  31. package/templates/domains/character-design/constitution.json +88 -0
  32. package/templates/domains/character-design/lanes.json +35 -0
  33. package/templates/domains/character-design/project.json +17 -0
  34. package/templates/domains/character-design/rubric.json +38 -0
  35. package/templates/domains/character-design/terminology.json +21 -0
  36. package/templates/domains/creature-design/constitution.json +87 -0
  37. package/templates/domains/creature-design/lanes.json +41 -0
  38. package/templates/domains/creature-design/project.json +18 -0
  39. package/templates/domains/creature-design/rubric.json +39 -0
  40. package/templates/domains/creature-design/terminology.json +20 -0
  41. package/templates/domains/game-art/constitution.json +113 -0
  42. package/templates/domains/game-art/lanes.json +47 -0
  43. package/templates/domains/game-art/project.json +17 -0
  44. package/templates/domains/game-art/rubric.json +36 -0
  45. package/templates/domains/game-art/terminology.json +30 -0
  46. package/templates/domains/vehicle-mech/constitution.json +87 -0
  47. package/templates/domains/vehicle-mech/lanes.json +41 -0
  48. package/templates/domains/vehicle-mech/project.json +17 -0
  49. package/templates/domains/vehicle-mech/rubric.json +40 -0
  50. package/templates/domains/vehicle-mech/terminology.json +21 -0
@@ -9,29 +9,14 @@
9
9
  *
10
10
  * Usage:
11
11
  * node scripts/generate-controlnet.js --subject naia --guide inputs/control-guides/naia_structural_guide.png --seeds 4
12
- * node scripts/generate-controlnet.js --subject corrigan --guide inputs/control-guides/corrigan_structural_guide.png --seeds 4
13
- * node scripts/generate-controlnet.js --subject naia --guide ... --dry-run
14
- *
15
- * Options:
16
- * --subject NAME Subject name (used for output file naming)
17
- * --guide PATH Path to structural guide image (white-on-black silhouette)
18
- * --prompt TEXT Generation prompt (or reads from --prompt-file)
19
- * --prompt-file F JSON file with {prompt, negative} fields
20
- * --negative TEXT Negative prompt override
21
- * --seeds N Number of seeds (default: 4)
22
- * --weight W ControlNet weight (default: 0.70)
23
- * --guidance-end E ControlNet guidance end (default: 0.65)
24
- * --model M ControlNet model filename (default: controlnet-canny-sdxl-1.0.safetensors)
25
- * --dry-run Print workflow without running
12
+ * sdlab generate:controlnet --subject naia --guide <path> --seeds 4 --project star-freight
26
13
  */
27
14
 
28
15
  import { readFile, writeFile, mkdir } from "node:fs/promises";
29
16
  import { join } from "node:path";
30
-
31
- const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
32
- const REPO_ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
33
- const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
34
- const GAME_ROOT = join(REPO_ROOT, 'games', GAME);
17
+ import { getProjectName } from "../lib/args.js";
18
+ import { REPO_ROOT } from "../lib/paths.js";
19
+ import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
35
20
 
36
21
  const DEFAULTS = {
37
22
  checkpoint: "dreamshaperXL_v21TurboDPMSDE.safetensors",
@@ -45,11 +30,10 @@ const DEFAULTS = {
45
30
  base_seed: 27100,
46
31
  };
47
32
 
48
- function parseArgs() {
49
- const args = process.argv.slice(2);
33
+ function parseLocalArgs(argv) {
50
34
  const get = (flag, def) => {
51
- const i = args.indexOf(flag);
52
- return i >= 0 && i + 1 < args.length ? args[i + 1] : def;
35
+ const i = argv.indexOf(flag);
36
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def;
53
37
  };
54
38
  return {
55
39
  subject: get("--subject", "unknown"),
@@ -61,7 +45,7 @@ function parseArgs() {
61
45
  weight: parseFloat(get("--weight", "0.70")),
62
46
  guidanceEnd: parseFloat(get("--guidance-end", "0.65")),
63
47
  controlModel: get("--model", "controlnet-canny-sdxl-1.0.safetensors"),
64
- dryRun: args.includes("--dry-run"),
48
+ dryRun: argv.includes("--dry-run"),
65
49
  };
66
50
  }
67
51
 
@@ -69,7 +53,6 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
69
53
  const nodes = {};
70
54
  let nextId = 1;
71
55
 
72
- // Checkpoint
73
56
  const ckptId = String(nextId++);
74
57
  nodes[ckptId] = {
75
58
  class_type: "CheckpointLoaderSimple",
@@ -79,17 +62,14 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
79
62
  let modelOut = [ckptId, 0];
80
63
  let clipOut = [ckptId, 1];
81
64
 
82
- // LoRA
83
65
  for (const lora of DEFAULTS.loras) {
84
66
  const loraId = String(nextId++);
85
67
  nodes[loraId] = {
86
68
  class_type: "LoraLoader",
87
69
  inputs: {
88
- model: modelOut,
89
- clip: clipOut,
70
+ model: modelOut, clip: clipOut,
90
71
  lora_name: lora.name,
91
- strength_model: lora.weight,
92
- strength_clip: lora.weight,
72
+ strength_model: lora.weight, strength_clip: lora.weight,
93
73
  },
94
74
  };
95
75
  modelOut = [loraId, 0];
@@ -97,34 +77,33 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
97
77
  }
98
78
 
99
79
  // Load guide image
100
- const loadGuideId = String(nextId++);
101
- nodes[loadGuideId] = {
80
+ const guideId = String(nextId++);
81
+ nodes[guideId] = {
102
82
  class_type: "LoadImage",
103
83
  inputs: { image: guideImagePath },
104
84
  };
105
85
 
106
- // Load ControlNet model
86
+ // ControlNet loader
107
87
  const cnLoadId = String(nextId++);
108
88
  nodes[cnLoadId] = {
109
89
  class_type: "ControlNetLoader",
110
90
  inputs: { control_net_name: controlModel },
111
91
  };
112
92
 
113
- // Positive prompt
93
+ // Text encoders
114
94
  const posId = String(nextId++);
115
95
  nodes[posId] = {
116
96
  class_type: "CLIPTextEncode",
117
97
  inputs: { text: prompt, clip: clipOut },
118
98
  };
119
99
 
120
- // Negative prompt
121
100
  const negId = String(nextId++);
122
101
  nodes[negId] = {
123
102
  class_type: "CLIPTextEncode",
124
103
  inputs: { text: negativePrompt, clip: clipOut },
125
104
  };
126
105
 
127
- // Apply ControlNet (Advanced for start/end control)
106
+ // Apply ControlNet
128
107
  const cnApplyId = String(nextId++);
129
108
  nodes[cnApplyId] = {
130
109
  class_type: "ControlNetApplyAdvanced",
@@ -132,7 +111,7 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
132
111
  positive: [posId, 0],
133
112
  negative: [negId, 0],
134
113
  control_net: [cnLoadId, 0],
135
- image: [loadGuideId, 0],
114
+ image: [guideId, 0],
136
115
  strength: weight,
137
116
  start_percent: 0.0,
138
117
  end_percent: guidanceEnd,
@@ -171,7 +150,6 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
171
150
  inputs: { samples: [samplerId, 0], vae: [ckptId, 2] },
172
151
  };
173
152
 
174
- // Save
175
153
  const saveId = String(nextId++);
176
154
  nodes[saveId] = {
177
155
  class_type: "SaveImage",
@@ -181,61 +159,15 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
181
159
  return nodes;
182
160
  }
183
161
 
184
- async function comfyHealth() {
185
- try {
186
- const res = await fetch(`${COMFY_URL}/system_stats`);
187
- return res.ok;
188
- } catch {
189
- return false;
190
- }
191
- }
192
-
193
- const MAX_POLL_MS = 600_000; // 10 minutes
162
+ export async function run(argv = process.argv.slice(2)) {
163
+ const projectName = getProjectName(argv);
164
+ const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
165
+ const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
194
166
 
195
- async function submitAndWait(workflow) {
196
- const clientId = `cn-${Date.now()}`;
197
- const res = await fetch(`${COMFY_URL}/prompt`, {
198
- method: "POST",
199
- headers: { "Content-Type": "application/json" },
200
- body: JSON.stringify({ prompt: workflow, client_id: clientId }),
201
- });
202
-
203
- if (!res.ok) {
204
- const text = await res.text();
205
- throw new Error(`ComfyUI submit failed: ${res.status} ${text}`);
206
- }
207
-
208
- const { prompt_id: promptId } = await res.json();
209
-
210
- const start = Date.now();
211
- while (true) {
212
- if (Date.now() - start > MAX_POLL_MS) throw new Error(`ComfyUI poll timeout after ${MAX_POLL_MS/1000}s for prompt ${promptId}`);
213
- await new Promise((r) => setTimeout(r, 1000));
214
- const histRes = await fetch(`${COMFY_URL}/history/${promptId}`);
215
- if (!histRes.ok) continue;
216
- const history = await histRes.json();
217
- const entry = history[promptId];
218
- if (!entry) continue;
219
- if (entry.status?.completed) return entry;
220
- if (entry.status?.status_str === "error") {
221
- throw new Error(`ComfyUI error: ${JSON.stringify(entry.status)}`);
222
- }
223
- }
224
- }
225
-
226
- async function downloadImage(filename, subfolder, type = "output") {
227
- const url = `${COMFY_URL}/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder || "")}&type=${type}`;
228
- const res = await fetch(url);
229
- if (!res.ok) throw new Error(`Download failed: ${res.status}`);
230
- return Buffer.from(await res.arrayBuffer());
231
- }
232
-
233
- async function main() {
234
- const opts = parseArgs();
167
+ const opts = parseLocalArgs(argv);
235
168
 
236
169
  if (!opts.guidePath) {
237
- console.error("Error: --guide <path> is required");
238
- process.exit(1);
170
+ throw new Error("--guide <path> is required");
239
171
  }
240
172
 
241
173
  // Resolve prompt
@@ -249,8 +181,7 @@ async function main() {
249
181
  }
250
182
 
251
183
  if (!prompt) {
252
- console.error("Error: --prompt or --prompt-file required");
253
- process.exit(1);
184
+ throw new Error("--prompt or --prompt-file required");
254
185
  }
255
186
 
256
187
  if (!negative) {
@@ -267,38 +198,19 @@ async function main() {
267
198
  console.log("");
268
199
 
269
200
  if (!opts.dryRun) {
270
- const online = await comfyHealth();
201
+ const online = await comfyHealth(COMFY_URL);
271
202
  if (!online) {
272
- console.error("\x1b[31mError:\x1b[0m ComfyUI not reachable at " + COMFY_URL);
273
- process.exit(1);
203
+ throw new Error("ComfyUI not reachable at " + COMFY_URL);
274
204
  }
275
205
  console.log("\x1b[32m+\x1b[0m ComfyUI online");
276
206
  }
277
207
 
278
- // The guide image needs to be in ComfyUI's input directory
279
- // Copy it there
280
208
  const guideFullPath = join(GAME_ROOT, opts.guidePath);
281
- const guideData = await readFile(guideFullPath);
282
209
  const guideFilename = `cn_guide_${opts.subject}.png`;
283
210
 
284
211
  if (!opts.dryRun) {
285
- // Upload via ComfyUI upload endpoint
286
- const formData = new FormData();
287
- formData.append("image", new Blob([guideData], { type: "image/png" }), guideFilename);
288
- formData.append("overwrite", "true");
289
-
290
- const uploadRes = await fetch(`${COMFY_URL}/upload/image`, {
291
- method: "POST",
292
- body: formData,
293
- });
294
-
295
- if (!uploadRes.ok) {
296
- const text = await uploadRes.text();
297
- console.error(`Guide upload failed: ${uploadRes.status} ${text}`);
298
- process.exit(1);
299
- }
300
- const uploadResult = await uploadRes.json();
301
- console.log(`\x1b[32m+\x1b[0m Guide uploaded: ${uploadResult.name}`);
212
+ await uploadImage(guideFullPath, guideFilename, COMFY_URL);
213
+ console.log(`\x1b[32m+\x1b[0m Guide uploaded: ${guideFilename}`);
302
214
  }
303
215
 
304
216
  await mkdir(join(GAME_ROOT, "outputs/candidates"), { recursive: true });
@@ -320,7 +232,7 @@ async function main() {
320
232
  );
321
233
 
322
234
  try {
323
- const result = await submitAndWait(workflow);
235
+ const result = await submitAndWait(workflow, COMFY_URL, { clientPrefix: 'cn' });
324
236
  const elapsed = Date.now() - startMs;
325
237
 
326
238
  const outputs = result.outputs || {};
@@ -339,7 +251,7 @@ async function main() {
339
251
  continue;
340
252
  }
341
253
 
342
- const imgData = await downloadImage(imageFile, imageSubfolder);
254
+ const imgData = await downloadImage(imageFile, imageSubfolder, COMFY_URL);
343
255
  await writeFile(join(GAME_ROOT, destPath), imgData);
344
256
  console.log(` \x1b[32m+\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
345
257
  } catch (err) {
@@ -351,7 +263,10 @@ async function main() {
351
263
  if (opts.dryRun) console.log(" (dry run)");
352
264
  }
353
265
 
354
- main().catch((err) => {
355
- console.error(err);
356
- process.exit(1);
357
- });
266
+ // Direct execution guard
267
+ if (process.argv[1] && (process.argv[1].endsWith('generate-controlnet.js') || process.argv[1].endsWith('generate-controlnet'))) {
268
+ run().catch((err) => {
269
+ console.error(err.message || err);
270
+ process.exit(1);
271
+ });
272
+ }
@@ -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
+ }