@mcptoolshop/style-dataset-lab 2.3.0 → 3.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.
Files changed (106) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/CODE_OF_CONDUCT.md +9 -0
  3. package/CONTRIBUTING.md +71 -0
  4. package/README.md +82 -9
  5. package/SECURITY.md +6 -5
  6. package/bin/sdlab.js +359 -11
  7. package/lib/adapters/comfyui-runner.js +170 -0
  8. package/lib/adapters/comfyui-workflows.js +163 -0
  9. package/lib/adapters/diffusers-lora.js +7 -2
  10. package/lib/adapters/generic-image-caption.js +6 -1
  11. package/lib/args.js +200 -13
  12. package/lib/batch-compiler.js +122 -0
  13. package/lib/batch-modes.js +111 -0
  14. package/lib/batch-runs.js +237 -0
  15. package/lib/batch-sheet-render.js +209 -0
  16. package/lib/brief-compiler.js +625 -0
  17. package/lib/brief-render.js +205 -0
  18. package/lib/card.js +11 -4
  19. package/lib/comfyui.js +100 -6
  20. package/lib/config.js +68 -3
  21. package/lib/critique-engine.js +413 -0
  22. package/lib/critique-render.js +171 -0
  23. package/lib/deprecation.js +1 -1
  24. package/lib/eval-pack.js +94 -4
  25. package/lib/eval-runs.js +42 -3
  26. package/lib/export.js +144 -34
  27. package/lib/generated-provenance.js +87 -0
  28. package/lib/implementation-packs.js +15 -1
  29. package/lib/log.js +41 -4
  30. package/lib/paths.js +72 -3
  31. package/lib/refine-briefs.js +336 -0
  32. package/lib/reingest-selected.js +184 -0
  33. package/lib/reingest.js +15 -1
  34. package/lib/run-summary.js +132 -0
  35. package/lib/runtime-runs.js +218 -0
  36. package/lib/selections.js +375 -0
  37. package/lib/snapshot.js +100 -7
  38. package/lib/split.js +175 -17
  39. package/lib/training-manifests.js +17 -3
  40. package/lib/training-packages.js +28 -14
  41. package/lib/workflow-profiles.js +161 -0
  42. package/package.json +13 -4
  43. package/scripts/batch-generate.js +176 -0
  44. package/scripts/batch-sheet.js +46 -0
  45. package/scripts/batch-show.js +86 -0
  46. package/scripts/brief-compile.js +134 -0
  47. package/scripts/brief-show.js +53 -0
  48. package/scripts/canon-bind.js +7 -9
  49. package/scripts/card.js +22 -25
  50. package/scripts/compare.js +38 -33
  51. package/scripts/critique-show.js +50 -0
  52. package/scripts/critique.js +70 -0
  53. package/scripts/curate.js +91 -46
  54. package/scripts/doctor.js +172 -6
  55. package/scripts/eligibility.js +17 -14
  56. package/scripts/eval-pack.js +13 -11
  57. package/scripts/eval-run.js +30 -29
  58. package/scripts/export.js +27 -31
  59. package/scripts/generate-controlnet.js +31 -17
  60. package/scripts/generate-identity.js +37 -20
  61. package/scripts/generate-ipadapter.js +22 -16
  62. package/scripts/generate.js +68 -13
  63. package/scripts/implementation-pack.js +19 -14
  64. package/scripts/init.js +137 -17
  65. package/scripts/painterly.js +8 -10
  66. package/scripts/refine.js +119 -0
  67. package/scripts/reingest-selected.js +78 -0
  68. package/scripts/reingest.js +37 -18
  69. package/scripts/run-generate.js +76 -0
  70. package/scripts/run-list.js +58 -0
  71. package/scripts/run-show.js +63 -0
  72. package/scripts/select.js +119 -0
  73. package/scripts/selection-show.js +88 -0
  74. package/scripts/snapshot.js +22 -24
  75. package/scripts/split.js +26 -31
  76. package/scripts/training-manifest.js +29 -24
  77. package/scripts/training-package.js +24 -20
  78. package/scripts/training-profile.js +13 -8
  79. package/scripts/workflow-list.js +61 -0
  80. package/scripts/workflow-show.js +97 -0
  81. package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
  82. package/templates/domains/architecture/terminology.json +10 -8
  83. package/templates/domains/architecture/workflows/profiles/environment-moodboard.json +62 -0
  84. package/templates/domains/architecture/workflows/profiles/interior-variant-pack.json +60 -0
  85. package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
  86. package/templates/domains/character-design/project.json +4 -2
  87. package/templates/domains/character-design/workflows/profiles/character-portrait-set.json +61 -0
  88. package/templates/domains/character-design/workflows/profiles/expression-sheet.json +59 -0
  89. package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
  90. package/templates/domains/creature-design/project.json +4 -2
  91. package/templates/domains/creature-design/terminology.json +1 -0
  92. package/templates/domains/creature-design/workflows/profiles/creature-portrait-set.json +61 -0
  93. package/templates/domains/creature-design/workflows/profiles/silhouette-exploration.json +58 -0
  94. package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
  95. package/templates/domains/game-art/project.json +4 -2
  96. package/templates/domains/game-art/workflows/profiles/character-portrait-set.json +62 -0
  97. package/templates/domains/game-art/workflows/profiles/environment-moodboard.json +61 -0
  98. package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
  99. package/templates/domains/vehicle-mech/project.json +4 -2
  100. package/templates/domains/vehicle-mech/workflows/profiles/variant-pack.json +60 -0
  101. package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
  102. package/scripts/bulk-curate-wave2-5.js +0 -138
  103. package/scripts/bulk-curate-waves11-18.js +0 -413
  104. package/scripts/curate-wave25.js +0 -209
  105. package/scripts/migrate-records.js +0 -141
  106. package/templates/inputs/prompts/example-wave.json +0 -27
package/bin/sdlab.js CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { enableDebug, handleCliError, inputError } from '../lib/errors.js';
11
11
  import { setLogLevel } from '../lib/log.js';
12
+ import { findClosest } from '../lib/args.js';
12
13
 
13
14
  const COMMANDS = {
14
15
  'init': '../scripts/init.js',
@@ -43,6 +44,240 @@ const PROJECT_COMMANDS = {
43
44
  'migrate': '../scripts/migrate-records.js',
44
45
  };
45
46
 
47
+ // Two-word commands under "workflow" namespace
48
+ const WORKFLOW_COMMANDS = {
49
+ 'list': '../scripts/workflow-list.js',
50
+ 'show': '../scripts/workflow-show.js',
51
+ };
52
+
53
+ // Two-word commands under "brief" namespace
54
+ const BRIEF_COMMANDS = {
55
+ 'compile': '../scripts/brief-compile.js',
56
+ 'show': '../scripts/brief-show.js',
57
+ };
58
+
59
+ // Two-word commands under "run" namespace
60
+ const RUN_COMMANDS = {
61
+ 'generate': '../scripts/run-generate.js',
62
+ 'show': '../scripts/run-show.js',
63
+ 'list': '../scripts/run-list.js',
64
+ };
65
+
66
+ // Two-word commands under "critique" namespace
67
+ const CRITIQUE_COMMANDS = {
68
+ 'show': '../scripts/critique-show.js',
69
+ };
70
+
71
+ // Two-word commands under "batch" namespace
72
+ const BATCH_COMMANDS = {
73
+ 'generate': '../scripts/batch-generate.js',
74
+ 'show': '../scripts/batch-show.js',
75
+ 'sheet': '../scripts/batch-sheet.js',
76
+ };
77
+
78
+ // Two-word commands under "selection" namespace
79
+ const SELECTION_COMMANDS = {
80
+ 'show': '../scripts/selection-show.js',
81
+ };
82
+
83
+ // ─── Per-command help ─────────────────────────────────────────────────
84
+ //
85
+ // Keyed by the user-facing command (including two-word forms joined by
86
+ // a space). Help text is plain — no color codes — to stay readable in
87
+ // dumb terminals and when piped to files.
88
+
89
+ const HELP_TEXT = {
90
+ 'init': `sdlab init <project-name> [--domain <domain>]
91
+
92
+ Scaffold a new project from a domain template.
93
+
94
+ Positional:
95
+ project-name Lowercase, hyphen-separated (e.g. "my-project")
96
+
97
+ Flags:
98
+ --domain <id> Domain template (default: generic)
99
+
100
+ Examples:
101
+ sdlab init my-project
102
+ sdlab init heist-crew --domain character-design
103
+
104
+ Run "sdlab init" with no arguments to see the available domains.`,
105
+
106
+ 'project doctor': `sdlab project doctor --project <name>
107
+
108
+ Validate project config and directory structure. Reports missing
109
+ required files, unmigrated record schemas, and orphaned outputs.
110
+
111
+ Flags:
112
+ --project <name> Project to audit (required in practice)
113
+ --verbose Show per-check detail
114
+ --quiet Only print PASS / FAIL summary`,
115
+
116
+ 'generate': `sdlab generate [<pack-path>] --project <name> [--dry-run]
117
+
118
+ Generate candidates from a prompt pack (JSON file under
119
+ projects/<name>/inputs/prompts/).
120
+
121
+ Positional:
122
+ pack-path Path to the prompt pack (default: inputs/prompts/rpg-icons-lane1.json)
123
+
124
+ Flags:
125
+ --project <name> Project to operate on
126
+ --dry-run Preview work without contacting ComfyUI
127
+
128
+ Env:
129
+ COMFY_URL ComfyUI HTTP endpoint (default: http://127.0.0.1:8188)
130
+
131
+ Precedence: CLI flag > env COMFY_URL > project defaults > built-in default.`,
132
+
133
+ 'batch generate': `sdlab batch generate --mode <id> [--subject <id>] [--theme <label>] --project <name>
134
+
135
+ Execute a batch production mode (expression sheet, silhouette pack,
136
+ environment board, etc).
137
+
138
+ Flags:
139
+ --mode <id> Batch mode id (required)
140
+ --subject <id> Subject for subject-driven modes
141
+ --theme <label> Theme label for environment modes
142
+ --asset <id> Training asset reference
143
+ --project <name> Project to operate on
144
+ --dry-run Prepare batch without submitting to ComfyUI
145
+ --json Output manifest as JSON`,
146
+
147
+ 'brief compile': `sdlab brief compile --profile <id> --project <name>
148
+
149
+ Compile a generation brief from project canon (constitution, lanes,
150
+ workflow profile).
151
+
152
+ Flags:
153
+ --profile <id> Workflow profile id to compile against
154
+ --project <name> Project to operate on
155
+ --out <path> Optional explicit output path`,
156
+
157
+ 'run generate': `sdlab run generate --brief <id> --project <name> [--dry-run]
158
+
159
+ Execute a compiled brief through ComfyUI.
160
+
161
+ Flags:
162
+ --brief <id> Brief id to execute (required)
163
+ --project <name> Project to operate on
164
+ --dry-run Plan without submitting to ComfyUI`,
165
+
166
+ 'critique': `sdlab critique --run <id> --project <name>
167
+
168
+ Critique a completed run. Writes a critique record alongside the run.
169
+
170
+ Flags:
171
+ --run <id> Run id to critique (required)
172
+ --project <name> Project to operate on
173
+ --json Emit critique as JSON on stdout`,
174
+
175
+ 'refine': `sdlab refine --run <id> --pick <file> --project <name>
176
+
177
+ Generate a refined next-pass brief based on a specific pick from a
178
+ prior run.
179
+
180
+ Flags:
181
+ --run <id> Source run id (required)
182
+ --pick <file> Picked output filename inside the run (required)
183
+ --project <name> Project to operate on`,
184
+
185
+ 'batch sheet': `sdlab batch sheet <batch-id> --project <name>
186
+
187
+ Re-render a batch's contact sheet from its manifest.
188
+
189
+ Positional:
190
+ batch-id Batch id to re-render
191
+
192
+ Flags:
193
+ --project <name> Project to operate on`,
194
+
195
+ 'select': `sdlab select [--run <id>|--batch <id>] --approve <files> --project <name>
196
+
197
+ Select approved outputs from a run or batch.
198
+
199
+ Flags:
200
+ --run <id> Source run id
201
+ --batch <id> Source batch id
202
+ --approve <files> Comma-separated output filenames to approve
203
+ --project <name> Project to operate on`,
204
+
205
+ 'curate': `sdlab curate <asset_id> <approved|rejected|borderline> <explanation> [--scores k:v,...] [--failures f1,f2] --project <name>
206
+
207
+ Record a judgment on a candidate and move it to the matching outputs
208
+ subdirectory.
209
+
210
+ Positional:
211
+ asset_id Candidate asset id
212
+ status approved | rejected | borderline
213
+ explanation Short reason (quoted)
214
+
215
+ Flags:
216
+ --scores k:v,... Per-dimension scores (e.g. silhouette:0.9,palette:0.8)
217
+ --failures f1,f2 Named failure tags (comma-separated)
218
+ --notes "…" Optional free-form note
219
+ --list Print uncurated candidates and exit
220
+ --dry-run Preview without moving files
221
+ --project <name> Project to operate on`,
222
+
223
+ 'canon-bind': `sdlab canon-bind --project <name> [--lane <id>]
224
+
225
+ Bind approved records to constitution rules.
226
+
227
+ Flags:
228
+ --project <name> Project to operate on
229
+ --lane <id> Restrict binding to a single lane`,
230
+
231
+ 'snapshot': `sdlab snapshot <create|list|show|diff> [args] --project <name>
232
+
233
+ Manage frozen dataset snapshots.
234
+
235
+ Subcommands:
236
+ create Create a new snapshot from approved records
237
+ list List all snapshots (default subcommand)
238
+ show <id> Show snapshot details
239
+ diff <a> <b> Compare two snapshots
240
+
241
+ Flags:
242
+ --profile <name> Selection profile (create only)
243
+ --project <name> Project to operate on
244
+ --dry-run Preview snapshot contents without writing`,
245
+
246
+ 'split': `sdlab split <build|list|show|audit> [args] --project <name>
247
+
248
+ Manage train/val/test splits from a snapshot.
249
+
250
+ Subcommands:
251
+ build Build a split from a snapshot
252
+ list List all splits
253
+ show <id> Show split details
254
+ audit <id> Audit a split for leakage and balance
255
+
256
+ Flags:
257
+ --project <name> Project to operate on`,
258
+
259
+ 'export': `sdlab export <build|list> [args] --project <name>
260
+
261
+ Build and list self-contained export packages.
262
+
263
+ Subcommands:
264
+ build Build a new export package
265
+ list List all export packages
266
+
267
+ Flags:
268
+ --project <name> Project to operate on`,
269
+ };
270
+
271
+ // "bind" is a short alias for "canon-bind" — share help.
272
+ HELP_TEXT['bind'] = HELP_TEXT['canon-bind'];
273
+
274
+ function printCommandHelp(key) {
275
+ const text = HELP_TEXT[key];
276
+ if (!text) return false;
277
+ console.log(text);
278
+ return true;
279
+ }
280
+
46
281
  function printHelp() {
47
282
  console.log(`\x1b[1msdlab\x1b[0m — Style Dataset Lab CLI\n`);
48
283
  console.log('Usage: sdlab <command> [options]\n');
@@ -58,9 +293,10 @@ function printHelp() {
58
293
  console.log(' generate:ipadapter IP-Adapter reference-guided generation');
59
294
  console.log(' curate Move candidate to approved/rejected/borderline');
60
295
  console.log(' compare Record pairwise A-vs-B comparison');
61
- console.log(' bind Bind approved records to constitution rules');
296
+ console.log(' canon-bind Bind approved records to constitution rules');
297
+ console.log(' ("bind" is a short alias for canon-bind)');
62
298
  console.log(' painterly Post-processing painterly style pass');
63
- console.log(' painterly:test Test denoise levels on reference images');
299
+ console.log(' painterly:test Calibrate painterly denoise level on sample images');
64
300
  console.log('');
65
301
  console.log('Dataset:');
66
302
  console.log(' snapshot create Create a frozen dataset snapshot');
@@ -96,26 +332,83 @@ function printHelp() {
96
332
  console.log(' implementation-pack build Build implementation example pack');
97
333
  console.log(' implementation-pack show <id> Show pack details');
98
334
  console.log(' implementation-pack list List all packs');
99
- console.log(' reingest generated --source <dir> Re-ingest generated outputs');
335
+ console.log(' reingest generated --source <dir> --manifest <id> Re-ingest generated outputs');
336
+ console.log(' reingest selected --selection <id> Re-ingest selected outputs as records');
100
337
  console.log(' reingest audit Audit re-ingested record status');
101
338
  console.log('');
339
+ console.log('Production:');
340
+ console.log(' workflow list List workflow profiles');
341
+ console.log(' workflow show <id> Show workflow profile details');
342
+ console.log(' brief compile Compile generation brief from project truth');
343
+ console.log(' brief show <id> Show compiled brief');
344
+ console.log(' run generate --brief <id> Execute brief through ComfyUI');
345
+ console.log(' run show <id> Show run details');
346
+ console.log(' run list List all runs');
347
+ console.log(' critique --run <id> Critique a completed run');
348
+ console.log(' critique show --run <id> Show saved critique');
349
+ console.log(' refine --run <id> --pick <file> Generate refined next-pass brief');
350
+ console.log(' batch generate --mode <id> Execute a batch production mode');
351
+ console.log(' batch show [batch-id] List batches or show details');
352
+ console.log(' batch sheet <batch-id> Re-render batch sheet from manifest');
353
+ console.log(' select --run <id> --approve <files> Select approved outputs from a run');
354
+ console.log(' select --batch <id> --approve ... Select approved outputs from a batch');
355
+ console.log(' selection show [selection-id] List selections or show details');
356
+ console.log(' reingest selected --selection <id> Re-ingest selected outputs as records');
357
+ console.log('');
102
358
  console.log('Options:');
103
- console.log(' --project <name> Project to operate on (default: star-freight)');
359
+ console.log(' --project <name> Project to operate on (no default — fallback to "star-freight" with warning)');
104
360
  console.log(' --game <name> Deprecated alias for --project');
105
361
  console.log(' --debug Show stack traces on error');
106
362
  console.log(' --verbose Verbose output');
107
363
  console.log(' --quiet Suppress non-essential output');
108
364
  console.log(' --dry-run Preview changes without writing (where supported)');
109
- console.log(' --help Show this help');
365
+ console.log(' --help Show this help (or per-command help with "sdlab <cmd> --help")');
110
366
  console.log('');
111
367
  console.log('Examples:');
112
368
  console.log(' sdlab init my-project --domain character-design');
369
+ console.log(' sdlab generate --help');
113
370
  console.log(' sdlab snapshot create --project star-freight');
114
371
  console.log(' sdlab split build --dry-run --project star-freight');
115
372
  console.log(' sdlab training-package build --debug --project star-freight');
116
373
  }
117
374
 
375
+ // All top-level command tokens users can type (including two-word heads).
376
+ function allKnownCommands() {
377
+ const oneWord = Object.keys(COMMANDS);
378
+ const heads = ['project', 'workflow', 'brief', 'run', 'critique', 'refine', 'batch', 'select', 'selection'];
379
+ return [...new Set([...oneWord, ...heads])];
380
+ }
381
+
382
+ function argvHasHelp(argv) {
383
+ return argv.includes('--help') || argv.includes('-h');
384
+ }
385
+
386
+ // ─── Signal + fatal-error handlers ────────────────────────────────────
387
+
388
+ function installProcessHandlers() {
389
+ const onSignal = (sig) => {
390
+ const code = sig === 'SIGINT' ? 130 : 143;
391
+ process.stderr.write(`\n\x1b[31m✗ Interrupted by ${sig}. Partial output may exist.\x1b[0m\n`);
392
+ process.exit(code);
393
+ };
394
+ process.on('SIGINT', () => onSignal('SIGINT'));
395
+ process.on('SIGTERM', () => onSignal('SIGTERM'));
396
+
397
+ process.on('uncaughtException', (err) => {
398
+ handleCliError(err);
399
+ // handleCliError exits, but guard anyway.
400
+ process.exit(2);
401
+ });
402
+ process.on('unhandledRejection', (reason) => {
403
+ const err = reason instanceof Error ? reason : new Error(String(reason));
404
+ handleCliError(err);
405
+ process.exit(2);
406
+ });
407
+ }
408
+
118
409
  async function main() {
410
+ installProcessHandlers();
411
+
119
412
  const args = process.argv.slice(2);
120
413
 
121
414
  // Global flags
@@ -131,20 +424,75 @@ async function main() {
131
424
  let command = args[0];
132
425
  let commandArgs = args.slice(1);
133
426
 
134
- // Handle "project <subcommand>" two-word form
135
- if (command === 'project' && args[1] && PROJECT_COMMANDS[args[1]]) {
136
- commandArgs = args.slice(2);
137
- const modulePath = PROJECT_COMMANDS[args[1]];
138
- const mod = await import(modulePath);
427
+ // ── Two-word dispatch tables and --help interception ──
428
+ // For every two-word namespace, we check "cmd sub --help" first,
429
+ // then the single-word "cmd --help", then fall through to dispatch.
430
+
431
+ const twoWord = (head, table) => ({ head, table });
432
+ const namespaces = [
433
+ twoWord('project', PROJECT_COMMANDS),
434
+ twoWord('workflow', WORKFLOW_COMMANDS),
435
+ twoWord('brief', BRIEF_COMMANDS),
436
+ twoWord('run', RUN_COMMANDS),
437
+ twoWord('batch', BATCH_COMMANDS),
438
+ twoWord('selection', SELECTION_COMMANDS),
439
+ ];
440
+
441
+ // "project", "workflow", "brief", "run", "batch", "selection" two-word forms
442
+ for (const { head, table } of namespaces) {
443
+ if (command === head && args[1] && table[args[1]]) {
444
+ commandArgs = args.slice(2);
445
+ const twoWordKey = `${head} ${args[1]}`;
446
+ if (argvHasHelp(commandArgs) && printCommandHelp(twoWordKey)) return;
447
+ const mod = await import(table[args[1]]);
448
+ await mod.run(commandArgs);
449
+ return;
450
+ }
451
+ }
452
+
453
+ // "critique" — bare or "critique show"
454
+ if (command === 'critique') {
455
+ if (args[1] && CRITIQUE_COMMANDS[args[1]]) {
456
+ commandArgs = args.slice(2);
457
+ if (argvHasHelp(commandArgs) && printCommandHelp(`critique ${args[1]}`)) return;
458
+ const mod = await import(CRITIQUE_COMMANDS[args[1]]);
459
+ await mod.run(commandArgs);
460
+ return;
461
+ }
462
+ if (argvHasHelp(commandArgs) && printCommandHelp('critique')) return;
463
+ const mod = await import('../scripts/critique.js');
464
+ await mod.run(commandArgs);
465
+ return;
466
+ }
467
+
468
+ // "refine" — single command
469
+ if (command === 'refine') {
470
+ if (argvHasHelp(commandArgs) && printCommandHelp('refine')) return;
471
+ const mod = await import('../scripts/refine.js');
472
+ await mod.run(commandArgs);
473
+ return;
474
+ }
475
+
476
+ // "select" — single command
477
+ if (command === 'select') {
478
+ if (argvHasHelp(commandArgs) && printCommandHelp('select')) return;
479
+ const mod = await import('../scripts/select.js');
139
480
  await mod.run(commandArgs);
140
481
  return;
141
482
  }
142
483
 
143
484
  const modulePath = COMMANDS[command];
144
485
  if (!modulePath) {
145
- throw inputError('INPUT_UNKNOWN_COMMAND', `Unknown command: ${command}`, 'Run "sdlab --help" for available commands.');
486
+ const suggestion = findClosest(command, allKnownCommands());
487
+ const hint = suggestion
488
+ ? `Did you mean "${suggestion}"? (Run "sdlab --help" for all commands.)`
489
+ : 'Run "sdlab --help" for available commands.';
490
+ throw inputError('INPUT_UNKNOWN_COMMAND', `Unknown command: ${command}`, hint);
146
491
  }
147
492
 
493
+ // Per-command --help for one-word commands
494
+ if (argvHasHelp(commandArgs) && printCommandHelp(command)) return;
495
+
148
496
  const mod = await import(modulePath);
149
497
  await mod.run(commandArgs);
150
498
  }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * ComfyUI runner — executes a brief through ComfyUI and captures outputs.
3
+ *
4
+ * Orchestrates: seed plan → graph build → submit → poll → download → manifest.
5
+ * Each image in the seed plan becomes one submission to ComfyUI.
6
+ */
7
+
8
+ import { writeFile, mkdir } from 'node:fs/promises';
9
+ import { join } from 'node:path';
10
+ import { comfyHealth, submitAndWait, downloadImage } from '../comfyui.js';
11
+ import { buildWorkflowGraph, resolveTemplateId } from './comfyui-workflows.js';
12
+ import { buildSeedPlan, prepareRunDir, saveRunManifest } from '../runtime-runs.js';
13
+ import { loadProjectConfig } from '../config.js';
14
+ import { log, info, warn } from '../log.js';
15
+ import { runtimeError } from '../errors.js';
16
+
17
+ /**
18
+ * Execute a brief: build graphs, submit to ComfyUI, capture outputs.
19
+ *
20
+ * @param {Object} opts
21
+ * @param {string} opts.projectRoot — absolute project path
22
+ * @param {string} opts.projectName — project name
23
+ * @param {Object} opts.brief — compiled brief object
24
+ * @param {number} [opts.baseSeed] — override base seed
25
+ * @param {boolean} [opts.dryRun] — if true, prepare run dir but do not submit
26
+ * @returns {Promise<Object>} — manifest
27
+ */
28
+ export async function executeRun({ projectRoot, projectName, brief, baseSeed, dryRun = false }) {
29
+ const config = loadProjectConfig(projectRoot);
30
+ const defaults = config.meta?.defaults ?? {};
31
+ const comfyUrl = defaults.comfy_url || process.env.COMFY_URL || 'http://127.0.0.1:8188';
32
+
33
+ // Resolve template
34
+ const outputMode = brief.expected_outputs?.output_mode || 'portrait_set';
35
+ const templateId = resolveTemplateId(outputMode);
36
+ const outputCount = brief.expected_outputs?.output_count || brief.runtime_plan?.output_count || 4;
37
+
38
+ // Build seed plan
39
+ const seedMode = brief.runtime_plan?.seed_mode || 'increment';
40
+ const seedPlan = buildSeedPlan({ seedMode, outputCount, baseSeed });
41
+
42
+ // Prepare run directory
43
+ const { runId, runDir } = await prepareRunDir(projectRoot, brief);
44
+
45
+ info(`Run ${runId} — ${outputCount} images, seed mode: ${seedMode}, base: ${seedPlan.base_seed}`);
46
+ info(`Template: ${templateId}, ComfyUI: ${comfyUrl}`);
47
+
48
+ // Health check
49
+ if (!dryRun) {
50
+ const healthy = await comfyHealth(comfyUrl);
51
+ if (!healthy) {
52
+ throw runtimeError(
53
+ 'COMFYUI_OFFLINE',
54
+ `ComfyUI not reachable at ${comfyUrl}`,
55
+ 'Start ComfyUI or set COMFY_URL / project defaults.comfy_url'
56
+ );
57
+ }
58
+ info('ComfyUI online');
59
+ }
60
+
61
+ const outputs = [];
62
+ const errors = [];
63
+
64
+ for (let i = 0; i < outputCount; i++) {
65
+ const seed = seedPlan.seeds[i];
66
+ const imageIndex = String(i + 1).padStart(3, '0');
67
+
68
+ log(` [${i + 1}/${outputCount}] seed: ${seed}`);
69
+
70
+ if (dryRun) {
71
+ outputs.push({
72
+ index: i,
73
+ seed,
74
+ status: 'dry_run',
75
+ filename: `${imageIndex}.png`,
76
+ });
77
+ continue;
78
+ }
79
+
80
+ const startMs = Date.now();
81
+
82
+ try {
83
+ // Build graph
84
+ const { nodes } = buildWorkflowGraph({
85
+ brief,
86
+ projectDefaults: defaults,
87
+ seed,
88
+ });
89
+
90
+ // Submit and wait
91
+ const result = await submitAndWait(nodes, comfyUrl, {
92
+ clientPrefix: `sdl-run-${runId}`,
93
+ });
94
+
95
+ const elapsed = Date.now() - startMs;
96
+
97
+ // Extract output image
98
+ const nodeOutputs = result.outputs || {};
99
+ let imageFile = null;
100
+ let imageSubfolder = '';
101
+ for (const nodeOut of Object.values(nodeOutputs)) {
102
+ if (nodeOut.images && nodeOut.images.length > 0) {
103
+ imageFile = nodeOut.images[0].filename;
104
+ imageSubfolder = nodeOut.images[0].subfolder || '';
105
+ break;
106
+ }
107
+ }
108
+
109
+ if (!imageFile) {
110
+ warn(` No output image for seed ${seed}`);
111
+ outputs.push({ index: i, seed, status: 'no_output', elapsed_ms: elapsed });
112
+ continue;
113
+ }
114
+
115
+ // Download and save
116
+ const imgData = await downloadImage(imageFile, imageSubfolder, comfyUrl);
117
+ const destFilename = `${imageIndex}.png`;
118
+ await writeFile(join(runDir, 'outputs', destFilename), imgData);
119
+
120
+ outputs.push({
121
+ index: i,
122
+ seed,
123
+ status: 'ok',
124
+ filename: destFilename,
125
+ bytes: imgData.length,
126
+ elapsed_ms: elapsed,
127
+ comfy_filename: imageFile,
128
+ });
129
+
130
+ log(` ✓ ${destFilename} (${imgData.length} bytes, ${(elapsed / 1000).toFixed(1)}s)`);
131
+ } catch (err) {
132
+ const elapsed = Date.now() - startMs;
133
+ warn(` ✗ seed ${seed}: ${err.message}`);
134
+ errors.push({ index: i, seed, error: err.message });
135
+ outputs.push({
136
+ index: i,
137
+ seed,
138
+ status: 'error',
139
+ error: err.message,
140
+ elapsed_ms: elapsed,
141
+ });
142
+ }
143
+ }
144
+
145
+ // Build manifest
146
+ const manifest = {
147
+ run_id: runId,
148
+ brief_id: brief.brief_id,
149
+ project_id: brief.project_id,
150
+ workflow_template_id: templateId,
151
+ adapter_target: 'comfyui',
152
+ output_mode: outputMode,
153
+ output_count: outputCount,
154
+ created_at: new Date().toISOString(),
155
+ seed_plan: seedPlan,
156
+ comfy_url: comfyUrl,
157
+ checkpoint: defaults.checkpoint,
158
+ loras: defaults.loras || [],
159
+ runtime_plan: brief.runtime_plan,
160
+ outputs,
161
+ errors,
162
+ dry_run: dryRun,
163
+ success_count: outputs.filter(o => o.status === 'ok' || o.status === 'dry_run').length,
164
+ error_count: errors.length,
165
+ };
166
+
167
+ await saveRunManifest(runDir, manifest);
168
+
169
+ return manifest;
170
+ }