@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
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * eligibility.js — Audit training/eval eligibility for all project records.
5
+ *
6
+ * Usage:
7
+ * sdlab eligibility audit [--project <name>]
8
+ */
9
+
10
+ import { readdir, readFile } from 'node:fs/promises';
11
+ import { join } from 'node:path';
12
+ import { getProjectName } from '../lib/args.js';
13
+ import { REPO_ROOT } from '../lib/paths.js';
14
+ import { loadProjectConfig, loadSelectionProfile } from '../lib/config.js';
15
+ import { evaluateEligibility, categorizeExclusions } from '../lib/eligibility.js';
16
+
17
+ export async function run(argv = process.argv.slice(2)) {
18
+ const projectName = getProjectName(argv);
19
+ const projectRoot = join(REPO_ROOT, 'projects', projectName);
20
+
21
+ const profileIdx = argv.indexOf('--profile');
22
+ const profileId = profileIdx >= 0 ? argv[profileIdx + 1] : null;
23
+ const profile = loadSelectionProfile(projectRoot, profileId);
24
+ const config = loadProjectConfig(projectRoot);
25
+
26
+ const recordsDir = join(projectRoot, 'records');
27
+ const files = (await readdir(recordsDir)).filter(f => f.endsWith('.json')).sort();
28
+
29
+ console.log(`\x1b[1msdlab eligibility audit\x1b[0m — ${projectName}`);
30
+ console.log(` Profile: ${profileId || 'training-default (built-in)'}`);
31
+ console.log(` Records: ${files.length}`);
32
+ console.log('');
33
+
34
+ const eligible = [];
35
+ const excluded = [];
36
+ const nearMiss = []; // Records with exactly 1 failing reason
37
+
38
+ for (const file of files) {
39
+ const raw = await readFile(join(recordsDir, file), 'utf-8');
40
+ const record = JSON.parse(raw);
41
+
42
+ const result = evaluateEligibility(record, profile, config.lanes, config.terminology);
43
+
44
+ if (result.eligible) {
45
+ eligible.push(record.id);
46
+ } else {
47
+ excluded.push({ record_id: record.id, reasons: result.reasons });
48
+ if (result.reasons.length === 1) {
49
+ nearMiss.push({ record_id: record.id, reason: result.reasons[0] });
50
+ }
51
+ }
52
+ }
53
+
54
+ console.log(` \x1b[32m✓\x1b[0m Eligible: ${eligible.length}`);
55
+ console.log(` \x1b[31m✗\x1b[0m Excluded: ${excluded.length}`);
56
+ console.log('');
57
+
58
+ // Categorize exclusions
59
+ const categories = categorizeExclusions(excluded);
60
+ console.log(' Exclusion breakdown:');
61
+ for (const [cat, count] of Object.entries(categories)) {
62
+ if (count > 0) {
63
+ const bar = '█'.repeat(Math.min(Math.round(count / files.length * 50), 50));
64
+ console.log(` ${cat.padEnd(20)} ${String(count).padStart(4)} ${bar}`);
65
+ }
66
+ }
67
+
68
+ // Near-miss improvement opportunities
69
+ if (nearMiss.length > 0) {
70
+ console.log(`\n Improvement opportunities (${nearMiss.length} records with 1 failing check):`);
71
+ const byReason = {};
72
+ for (const nm of nearMiss) {
73
+ byReason[nm.reason] = (byReason[nm.reason] || 0) + 1;
74
+ }
75
+ for (const [reason, count] of Object.entries(byReason).sort((a, b) => b[1] - a[1]).slice(0, 5)) {
76
+ console.log(` ${count} records: ${reason}`);
77
+ }
78
+ }
79
+
80
+ console.log('');
81
+ const pct = ((eligible.length / files.length) * 100).toFixed(1);
82
+ console.log(` Eligibility rate: ${pct}% (${eligible.length}/${files.length})`);
83
+ }
84
+
85
+ // Direct execution guard
86
+ if (process.argv[1] && (process.argv[1].endsWith('eligibility.js') || process.argv[1].endsWith('eligibility'))) {
87
+ run().catch((err) => {
88
+ console.error(err.message || err);
89
+ process.exit(1);
90
+ });
91
+ }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * eval-pack.js — Build and inspect canon-aware eval packs.
5
+ *
6
+ * Usage:
7
+ * sdlab eval-pack build [--project <name>]
8
+ * sdlab eval-pack list [--project <name>]
9
+ * sdlab eval-pack show <eval-id> [--project <name>]
10
+ */
11
+
12
+ import { readFile } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { getProjectName } from '../lib/args.js';
15
+ import { REPO_ROOT } from '../lib/paths.js';
16
+ import { buildEvalPack, listEvalPacks, loadEvalPack } from '../lib/eval-pack.js';
17
+
18
+ export async function run(argv = process.argv.slice(2)) {
19
+ const projectName = getProjectName(argv);
20
+ const projectRoot = join(REPO_ROOT, 'projects', projectName);
21
+ const subcommand = argv.find(a => !a.startsWith('--')) || 'list';
22
+ const args = argv.filter(a => a !== subcommand);
23
+
24
+ if (subcommand === 'build') {
25
+ console.log(`\x1b[1msdlab eval-pack build\x1b[0m — ${projectName}\n`);
26
+
27
+ const result = await buildEvalPack(projectRoot);
28
+
29
+ console.log(` \x1b[32m✓\x1b[0m Eval pack: ${result.evalId}`);
30
+ console.log('');
31
+ console.log(' Tasks:');
32
+ for (const [task, count] of Object.entries(result.tasks)) {
33
+ console.log(` ${task.padEnd(24)} ${count} records`);
34
+ }
35
+ const total = Object.values(result.tasks).reduce((s, n) => s + n, 0);
36
+ console.log(` ${'─'.repeat(36)}`);
37
+ console.log(` ${'total'.padEnd(24)} ${total} records`);
38
+
39
+ } else if (subcommand === 'list') {
40
+ const packs = await listEvalPacks(projectRoot);
41
+ if (packs.length === 0) {
42
+ console.log('No eval packs found. Run: sdlab eval-pack build');
43
+ return;
44
+ }
45
+ console.log(`\x1b[1mEval packs\x1b[0m — ${projectName}\n`);
46
+ for (const p of packs) {
47
+ console.log(` ${p.id} total=${p.total} lane=${p.tasks.lane_coverage} drift=${p.tasks.forbidden_drift} gold=${p.tasks.anchor_gold} cont=${p.tasks.subject_continuity} ${p.created_at}`);
48
+ }
49
+
50
+ } else if (subcommand === 'show') {
51
+ const evalId = args.find(a => a.startsWith('eval-'));
52
+ if (!evalId) throw new Error('Usage: sdlab eval-pack show <eval-id>');
53
+
54
+ const manifest = await loadEvalPack(projectRoot, evalId);
55
+ console.log(`\x1b[1mEval pack\x1b[0m — ${evalId}\n`);
56
+ console.log(` Created: ${manifest.created_at}`);
57
+ console.log(` Project: ${manifest.project}`);
58
+ console.log('');
59
+ console.log(' Tasks:');
60
+ for (const [task, count] of Object.entries(manifest.task_counts)) {
61
+ console.log(` ${task.padEnd(24)} ${count} records`);
62
+ }
63
+ console.log(`\n Total: ${manifest.total_records} records`);
64
+
65
+ // Show sample from each task file
66
+ const evalDir = join(projectRoot, 'eval-packs', evalId);
67
+ for (const taskFile of ['lane-coverage', 'forbidden-drift', 'anchor-gold', 'subject-continuity']) {
68
+ const raw = await readFile(join(evalDir, `${taskFile}.jsonl`), 'utf-8');
69
+ const entries = raw.trim().split('\n').filter(Boolean).map(l => JSON.parse(l));
70
+ if (entries.length > 0) {
71
+ console.log(`\n ${taskFile} (first 3):`);
72
+ for (const entry of entries.slice(0, 3)) {
73
+ const summary = entry.record_id || entry.subject_name;
74
+ const detail = entry.pass_ratio != null ? `ratio=${entry.pass_ratio}` :
75
+ entry.violations ? `violations=${entry.violations.length}` :
76
+ `records=${entry.record_count}`;
77
+ console.log(` ${summary} ${detail}`);
78
+ }
79
+ }
80
+ }
81
+
82
+ } else {
83
+ throw new Error(`Unknown subcommand: ${subcommand}. Use: build, list, show`);
84
+ }
85
+ }
86
+
87
+ if (process.argv[1] && (process.argv[1].endsWith('eval-pack.js') || process.argv[1].endsWith('eval-pack'))) {
88
+ run().catch((err) => {
89
+ console.error(err.message || err);
90
+ process.exit(1);
91
+ });
92
+ }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * export.js — Build and list versioned export packages.
5
+ *
6
+ * Usage:
7
+ * sdlab export build [--snapshot <id>] [--split <id>] [--profile <name>] [--copy] [--project <name>]
8
+ * sdlab export list [--project <name>]
9
+ */
10
+
11
+ import { join } from 'node:path';
12
+ import { getProjectName } from '../lib/args.js';
13
+ import { REPO_ROOT } from '../lib/paths.js';
14
+ import { listSnapshots } from '../lib/snapshot.js';
15
+ import { listSplits } from '../lib/split.js';
16
+ import { buildExport, listExports, loadExport } from '../lib/export.js';
17
+
18
+ export async function run(argv = process.argv.slice(2)) {
19
+ const projectName = getProjectName(argv);
20
+ const projectRoot = join(REPO_ROOT, 'projects', projectName);
21
+ const subcommand = argv.find(a => !a.startsWith('--') && a !== 'true') || 'list';
22
+ const args = argv.filter(a => a !== subcommand);
23
+
24
+ if (subcommand === 'build') {
25
+ // Find snapshot
26
+ let snapshotId;
27
+ const snapIdx = args.indexOf('--snapshot');
28
+ if (snapIdx >= 0) {
29
+ snapshotId = args[snapIdx + 1];
30
+ } else {
31
+ const snapshots = await listSnapshots(projectRoot);
32
+ if (snapshots.length === 0) throw new Error('No snapshots found. Run: sdlab snapshot create');
33
+ snapshotId = snapshots[snapshots.length - 1].id;
34
+ }
35
+
36
+ // Find split
37
+ let splitId;
38
+ const splitIdx = args.indexOf('--split');
39
+ if (splitIdx >= 0) {
40
+ splitId = args[splitIdx + 1];
41
+ } else {
42
+ const splits = await listSplits(projectRoot);
43
+ if (splits.length === 0) throw new Error('No splits found. Run: sdlab split build');
44
+ splitId = splits[splits.length - 1].id;
45
+ }
46
+
47
+ const profileIdx = args.indexOf('--profile');
48
+ const profileId = profileIdx >= 0 ? args[profileIdx + 1] : null;
49
+ const copy = args.includes('--copy');
50
+
51
+ console.log(`\x1b[1msdlab export build\x1b[0m — ${projectName}`);
52
+ console.log(` Snapshot: ${snapshotId}`);
53
+ console.log(` Split: ${splitId}`);
54
+ console.log(` Profile: ${profileId || 'canonical-package (built-in)'}`);
55
+ console.log(` Images: ${copy ? 'copy' : 'symlink'}`);
56
+ console.log('');
57
+
58
+ const result = await buildExport(projectRoot, snapshotId, splitId, { profileId, copy });
59
+
60
+ console.log(` \x1b[32m✓\x1b[0m Export: ${result.exportId}`);
61
+ console.log(` Records: ${result.recordCount}`);
62
+ console.log(` Images: ${result.imageCount}`);
63
+
64
+ } else if (subcommand === 'list') {
65
+ const exports = await listExports(projectRoot);
66
+ if (exports.length === 0) {
67
+ console.log('No exports found. Run: sdlab export build');
68
+ return;
69
+ }
70
+ console.log(`\x1b[1mExports\x1b[0m — ${projectName}\n`);
71
+ for (const e of exports) {
72
+ console.log(` ${e.id} snap=${e.snapshot_id} split=${e.split_id} records=${e.records} images=${e.images} ${e.created_at}`);
73
+ }
74
+
75
+ } else {
76
+ throw new Error(`Unknown subcommand: ${subcommand}. Use: build, list`);
77
+ }
78
+ }
79
+
80
+ if (process.argv[1] && (process.argv[1].endsWith('export.js') || process.argv[1].endsWith('export'))) {
81
+ run().catch((err) => {
82
+ console.error(err.message || err);
83
+ process.exit(1);
84
+ });
85
+ }
@@ -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
+ }