@mcptoolshop/style-dataset-lab 3.0.0 → 3.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.
- package/CHANGELOG.md +108 -15
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +71 -0
- package/README.es.md +99 -8
- package/README.fr.md +100 -9
- package/README.hi.md +102 -9
- package/README.it.md +99 -8
- package/README.ja.md +100 -9
- package/README.md +64 -1
- package/README.pt-BR.md +99 -8
- package/README.zh.md +100 -9
- package/SECURITY.md +6 -5
- package/bin/sdlab.js +289 -62
- package/lib/adapters/comfyui-runner.js +11 -15
- package/lib/adapters/diffusers-lora.js +7 -2
- package/lib/adapters/generic-image-caption.js +6 -1
- package/lib/args.js +200 -13
- package/lib/batch-compiler.js +7 -3
- package/lib/batch-runs.js +162 -31
- package/lib/batch-sheet-render.js +5 -2
- package/lib/brief-compiler.js +38 -11
- package/lib/card.js +11 -4
- package/lib/comfyui-output.js +70 -0
- package/lib/comfyui.js +100 -6
- package/lib/config.js +68 -3
- package/lib/deprecation.js +1 -1
- package/lib/eval-pack.js +94 -4
- package/lib/eval-runs.js +42 -3
- package/lib/export.js +144 -34
- package/lib/implementation-packs.js +15 -1
- package/lib/log.js +41 -4
- package/lib/paths.js +41 -3
- package/lib/refine-briefs.js +16 -9
- package/lib/reingest-selected.js +14 -3
- package/lib/reingest.js +15 -1
- package/lib/run-summary.js +3 -2
- package/lib/runtime-runs.js +59 -9
- package/lib/selections.js +54 -2
- package/lib/snapshot.js +100 -7
- package/lib/split.js +175 -17
- package/lib/training-adapters.js +79 -0
- package/lib/training-manifests.js +17 -3
- package/lib/training-packages.js +29 -28
- package/lib/training-profiles.js +10 -0
- package/package.json +27 -3
- package/scripts/batch-generate.js +72 -20
- package/scripts/batch-sheet.js +3 -3
- package/scripts/batch-show.js +3 -3
- package/scripts/brief-compile.js +8 -8
- package/scripts/brief-show.js +4 -3
- package/scripts/canon-bind.js +7 -9
- package/scripts/card.js +22 -25
- package/scripts/compare.js +38 -33
- package/scripts/critique-show.js +3 -3
- package/scripts/critique.js +3 -3
- package/scripts/curate.js +91 -46
- package/scripts/doctor.js +3 -5
- package/scripts/eligibility.js +17 -14
- package/scripts/eval-pack.js +13 -11
- package/scripts/eval-run.js +30 -29
- package/scripts/export.js +27 -31
- package/scripts/generate-controlnet.js +31 -17
- package/scripts/generate-identity.js +37 -20
- package/scripts/generate-ipadapter.js +22 -16
- package/scripts/generate.js +96 -29
- package/scripts/implementation-pack.js +19 -14
- package/scripts/init.js +115 -17
- package/scripts/painterly.js +8 -10
- package/scripts/refine.js +3 -3
- package/scripts/reingest-selected.js +3 -3
- package/scripts/reingest.js +33 -20
- package/scripts/run-generate.js +4 -4
- package/scripts/run-list.js +3 -3
- package/scripts/run-show.js +3 -3
- package/scripts/select.js +5 -4
- package/scripts/selection-show.js +3 -3
- package/scripts/snapshot.js +22 -24
- package/scripts/split.js +26 -31
- package/scripts/training-manifest.js +29 -24
- package/scripts/training-package.js +24 -20
- package/scripts/training-profile.js +13 -8
- package/scripts/workflow-list.js +2 -2
- package/scripts/workflow-show.js +4 -3
- package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/architecture/terminology.json +10 -8
- package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/character-design/project.json +4 -2
- package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/creature-design/project.json +4 -2
- package/templates/domains/creature-design/terminology.json +1 -0
- package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/game-art/project.json +4 -2
- package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/vehicle-mech/project.json +4 -2
- package/scripts/bulk-curate-wave2-5.js +0 -138
- package/scripts/bulk-curate-waves11-18.js +0 -413
- package/scripts/curate-wave25.js +0 -209
- package/scripts/migrate-records.js +0 -141
- 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',
|
|
@@ -79,6 +80,212 @@ const SELECTION_COMMANDS = {
|
|
|
79
80
|
'show': '../scripts/selection-show.js',
|
|
80
81
|
};
|
|
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] [--resume]
|
|
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
|
+
--resume Skip subjects whose record + image are already on disk.
|
|
128
|
+
Seeds remain stable — skipped slots still advance the
|
|
129
|
+
seed counter so downstream outputs are bit-identical to
|
|
130
|
+
a fresh run.
|
|
131
|
+
|
|
132
|
+
Env:
|
|
133
|
+
COMFY_URL ComfyUI HTTP endpoint (default: http://127.0.0.1:8188)
|
|
134
|
+
|
|
135
|
+
Precedence: CLI flag > env COMFY_URL > project defaults > built-in default.`,
|
|
136
|
+
|
|
137
|
+
'batch generate': `sdlab batch generate --mode <id> [--subject <id>] [--theme <label>] --project <name>
|
|
138
|
+
sdlab batch generate --resume <batch_id> --project <name>
|
|
139
|
+
|
|
140
|
+
Execute a batch production mode (expression sheet, silhouette pack,
|
|
141
|
+
environment board, etc).
|
|
142
|
+
|
|
143
|
+
Flags:
|
|
144
|
+
--mode <id> Batch mode id (required unless --resume is given)
|
|
145
|
+
--subject <id> Subject for subject-driven modes
|
|
146
|
+
--theme <label> Theme label for environment modes
|
|
147
|
+
--asset <id> Training asset reference
|
|
148
|
+
--resume <id> Resume an existing batch — re-runs only the failed or
|
|
149
|
+
missing slots, keeps the same batch_id, and inherits
|
|
150
|
+
mode/subject/theme/asset from the prior manifest.
|
|
151
|
+
--project <name> Project to operate on
|
|
152
|
+
--dry-run Prepare batch without submitting to ComfyUI
|
|
153
|
+
--json Output manifest as JSON`,
|
|
154
|
+
|
|
155
|
+
'brief compile': `sdlab brief compile --profile <id> --project <name>
|
|
156
|
+
|
|
157
|
+
Compile a generation brief from project canon (constitution, lanes,
|
|
158
|
+
workflow profile).
|
|
159
|
+
|
|
160
|
+
Flags:
|
|
161
|
+
--profile <id> Workflow profile id to compile against
|
|
162
|
+
--project <name> Project to operate on
|
|
163
|
+
--out <path> Optional explicit output path`,
|
|
164
|
+
|
|
165
|
+
'run generate': `sdlab run generate --brief <id> --project <name> [--dry-run]
|
|
166
|
+
|
|
167
|
+
Execute a compiled brief through ComfyUI.
|
|
168
|
+
|
|
169
|
+
Flags:
|
|
170
|
+
--brief <id> Brief id to execute (required)
|
|
171
|
+
--project <name> Project to operate on
|
|
172
|
+
--dry-run Plan without submitting to ComfyUI`,
|
|
173
|
+
|
|
174
|
+
'critique': `sdlab critique --run <id> --project <name>
|
|
175
|
+
|
|
176
|
+
Critique a completed run. Writes a critique record alongside the run.
|
|
177
|
+
|
|
178
|
+
Flags:
|
|
179
|
+
--run <id> Run id to critique (required)
|
|
180
|
+
--project <name> Project to operate on
|
|
181
|
+
--json Emit critique as JSON on stdout`,
|
|
182
|
+
|
|
183
|
+
'refine': `sdlab refine --run <id> --pick <file> --project <name>
|
|
184
|
+
|
|
185
|
+
Generate a refined next-pass brief based on a specific pick from a
|
|
186
|
+
prior run.
|
|
187
|
+
|
|
188
|
+
Flags:
|
|
189
|
+
--run <id> Source run id (required)
|
|
190
|
+
--pick <file> Picked output filename inside the run (required)
|
|
191
|
+
--project <name> Project to operate on`,
|
|
192
|
+
|
|
193
|
+
'batch sheet': `sdlab batch sheet <batch-id> --project <name>
|
|
194
|
+
|
|
195
|
+
Re-render a batch's contact sheet from its manifest.
|
|
196
|
+
|
|
197
|
+
Positional:
|
|
198
|
+
batch-id Batch id to re-render
|
|
199
|
+
|
|
200
|
+
Flags:
|
|
201
|
+
--project <name> Project to operate on`,
|
|
202
|
+
|
|
203
|
+
'select': `sdlab select [--run <id>|--batch <id>] --approve <files> --project <name>
|
|
204
|
+
|
|
205
|
+
Select approved outputs from a run or batch.
|
|
206
|
+
|
|
207
|
+
Flags:
|
|
208
|
+
--run <id> Source run id
|
|
209
|
+
--batch <id> Source batch id
|
|
210
|
+
--approve <files> Comma-separated output filenames to approve
|
|
211
|
+
--project <name> Project to operate on`,
|
|
212
|
+
|
|
213
|
+
'curate': `sdlab curate <asset_id> <approved|rejected|borderline> <explanation> [--scores k:v,...] [--failures f1,f2] --project <name>
|
|
214
|
+
|
|
215
|
+
Record a judgment on a candidate and move it to the matching outputs
|
|
216
|
+
subdirectory.
|
|
217
|
+
|
|
218
|
+
Positional:
|
|
219
|
+
asset_id Candidate asset id
|
|
220
|
+
status approved | rejected | borderline
|
|
221
|
+
explanation Short reason (quoted)
|
|
222
|
+
|
|
223
|
+
Flags:
|
|
224
|
+
--scores k:v,... Per-dimension scores (e.g. silhouette:0.9,palette:0.8)
|
|
225
|
+
--failures f1,f2 Named failure tags (comma-separated)
|
|
226
|
+
--notes "…" Optional free-form note
|
|
227
|
+
--list Print uncurated candidates and exit
|
|
228
|
+
--dry-run Preview without moving files
|
|
229
|
+
--project <name> Project to operate on`,
|
|
230
|
+
|
|
231
|
+
'canon-bind': `sdlab canon-bind --project <name> [--lane <id>]
|
|
232
|
+
|
|
233
|
+
Bind approved records to constitution rules.
|
|
234
|
+
|
|
235
|
+
Flags:
|
|
236
|
+
--project <name> Project to operate on
|
|
237
|
+
--lane <id> Restrict binding to a single lane`,
|
|
238
|
+
|
|
239
|
+
'snapshot': `sdlab snapshot <create|list|show|diff> [args] --project <name>
|
|
240
|
+
|
|
241
|
+
Manage frozen dataset snapshots.
|
|
242
|
+
|
|
243
|
+
Subcommands:
|
|
244
|
+
create Create a new snapshot from approved records
|
|
245
|
+
list List all snapshots (default subcommand)
|
|
246
|
+
show <id> Show snapshot details
|
|
247
|
+
diff <a> <b> Compare two snapshots
|
|
248
|
+
|
|
249
|
+
Flags:
|
|
250
|
+
--profile <name> Selection profile (create only)
|
|
251
|
+
--project <name> Project to operate on
|
|
252
|
+
--dry-run Preview snapshot contents without writing`,
|
|
253
|
+
|
|
254
|
+
'split': `sdlab split <build|list|show|audit> [args] --project <name>
|
|
255
|
+
|
|
256
|
+
Manage train/val/test splits from a snapshot.
|
|
257
|
+
|
|
258
|
+
Subcommands:
|
|
259
|
+
build Build a split from a snapshot
|
|
260
|
+
list List all splits
|
|
261
|
+
show <id> Show split details
|
|
262
|
+
audit <id> Audit a split for leakage and balance
|
|
263
|
+
|
|
264
|
+
Flags:
|
|
265
|
+
--project <name> Project to operate on`,
|
|
266
|
+
|
|
267
|
+
'export': `sdlab export <build|list> [args] --project <name>
|
|
268
|
+
|
|
269
|
+
Build and list self-contained export packages.
|
|
270
|
+
|
|
271
|
+
Subcommands:
|
|
272
|
+
build Build a new export package
|
|
273
|
+
list List all export packages
|
|
274
|
+
|
|
275
|
+
Flags:
|
|
276
|
+
--project <name> Project to operate on`,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// "bind" is a short alias for "canon-bind" — share help.
|
|
280
|
+
HELP_TEXT['bind'] = HELP_TEXT['canon-bind'];
|
|
281
|
+
|
|
282
|
+
function printCommandHelp(key) {
|
|
283
|
+
const text = HELP_TEXT[key];
|
|
284
|
+
if (!text) return false;
|
|
285
|
+
console.log(text);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
82
289
|
function printHelp() {
|
|
83
290
|
console.log(`\x1b[1msdlab\x1b[0m — Style Dataset Lab CLI\n`);
|
|
84
291
|
console.log('Usage: sdlab <command> [options]\n');
|
|
@@ -94,9 +301,10 @@ function printHelp() {
|
|
|
94
301
|
console.log(' generate:ipadapter IP-Adapter reference-guided generation');
|
|
95
302
|
console.log(' curate Move candidate to approved/rejected/borderline');
|
|
96
303
|
console.log(' compare Record pairwise A-vs-B comparison');
|
|
97
|
-
console.log(' bind
|
|
304
|
+
console.log(' canon-bind Bind approved records to constitution rules');
|
|
305
|
+
console.log(' ("bind" is a short alias for canon-bind)');
|
|
98
306
|
console.log(' painterly Post-processing painterly style pass');
|
|
99
|
-
console.log(' painterly:test
|
|
307
|
+
console.log(' painterly:test Calibrate painterly denoise level on sample images');
|
|
100
308
|
console.log('');
|
|
101
309
|
console.log('Dataset:');
|
|
102
310
|
console.log(' snapshot create Create a frozen dataset snapshot');
|
|
@@ -132,7 +340,8 @@ function printHelp() {
|
|
|
132
340
|
console.log(' implementation-pack build Build implementation example pack');
|
|
133
341
|
console.log(' implementation-pack show <id> Show pack details');
|
|
134
342
|
console.log(' implementation-pack list List all packs');
|
|
135
|
-
console.log(' reingest generated --source <dir> Re-ingest generated outputs');
|
|
343
|
+
console.log(' reingest generated --source <dir> --manifest <id> Re-ingest generated outputs');
|
|
344
|
+
console.log(' reingest selected --selection <id> Re-ingest selected outputs as records');
|
|
136
345
|
console.log(' reingest audit Audit re-ingested record status');
|
|
137
346
|
console.log('');
|
|
138
347
|
console.log('Production:');
|
|
@@ -155,22 +364,59 @@ function printHelp() {
|
|
|
155
364
|
console.log(' reingest selected --selection <id> Re-ingest selected outputs as records');
|
|
156
365
|
console.log('');
|
|
157
366
|
console.log('Options:');
|
|
158
|
-
console.log(' --project <name> Project to operate on (default
|
|
367
|
+
console.log(' --project <name> Project to operate on (no default — fallback to "star-freight" with warning)');
|
|
159
368
|
console.log(' --game <name> Deprecated alias for --project');
|
|
160
369
|
console.log(' --debug Show stack traces on error');
|
|
161
370
|
console.log(' --verbose Verbose output');
|
|
162
371
|
console.log(' --quiet Suppress non-essential output');
|
|
163
372
|
console.log(' --dry-run Preview changes without writing (where supported)');
|
|
164
|
-
console.log(' --help Show this help');
|
|
373
|
+
console.log(' --help Show this help (or per-command help with "sdlab <cmd> --help")');
|
|
165
374
|
console.log('');
|
|
166
375
|
console.log('Examples:');
|
|
167
376
|
console.log(' sdlab init my-project --domain character-design');
|
|
377
|
+
console.log(' sdlab generate --help');
|
|
168
378
|
console.log(' sdlab snapshot create --project star-freight');
|
|
169
379
|
console.log(' sdlab split build --dry-run --project star-freight');
|
|
170
380
|
console.log(' sdlab training-package build --debug --project star-freight');
|
|
171
381
|
}
|
|
172
382
|
|
|
383
|
+
// All top-level command tokens users can type (including two-word heads).
|
|
384
|
+
function allKnownCommands() {
|
|
385
|
+
const oneWord = Object.keys(COMMANDS);
|
|
386
|
+
const heads = ['project', 'workflow', 'brief', 'run', 'critique', 'refine', 'batch', 'select', 'selection'];
|
|
387
|
+
return [...new Set([...oneWord, ...heads])];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function argvHasHelp(argv) {
|
|
391
|
+
return argv.includes('--help') || argv.includes('-h');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ─── Signal + fatal-error handlers ────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
function installProcessHandlers() {
|
|
397
|
+
const onSignal = (sig) => {
|
|
398
|
+
const code = sig === 'SIGINT' ? 130 : 143;
|
|
399
|
+
process.stderr.write(`\n\x1b[31m✗ Interrupted by ${sig}. Partial output may exist.\x1b[0m\n`);
|
|
400
|
+
process.exit(code);
|
|
401
|
+
};
|
|
402
|
+
process.on('SIGINT', () => onSignal('SIGINT'));
|
|
403
|
+
process.on('SIGTERM', () => onSignal('SIGTERM'));
|
|
404
|
+
|
|
405
|
+
process.on('uncaughtException', (err) => {
|
|
406
|
+
handleCliError(err);
|
|
407
|
+
// handleCliError exits, but guard anyway.
|
|
408
|
+
process.exit(2);
|
|
409
|
+
});
|
|
410
|
+
process.on('unhandledRejection', (reason) => {
|
|
411
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
412
|
+
handleCliError(err);
|
|
413
|
+
process.exit(2);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
173
417
|
async function main() {
|
|
418
|
+
installProcessHandlers();
|
|
419
|
+
|
|
174
420
|
const args = process.argv.slice(2);
|
|
175
421
|
|
|
176
422
|
// Global flags
|
|
@@ -186,94 +432,75 @@ async function main() {
|
|
|
186
432
|
let command = args[0];
|
|
187
433
|
let commandArgs = args.slice(1);
|
|
188
434
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const modulePath = PROJECT_COMMANDS[args[1]];
|
|
193
|
-
const mod = await import(modulePath);
|
|
194
|
-
await mod.run(commandArgs);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Handle "workflow <subcommand>" two-word form
|
|
199
|
-
if (command === 'workflow' && args[1] && WORKFLOW_COMMANDS[args[1]]) {
|
|
200
|
-
commandArgs = args.slice(2);
|
|
201
|
-
const modulePath = WORKFLOW_COMMANDS[args[1]];
|
|
202
|
-
const mod = await import(modulePath);
|
|
203
|
-
await mod.run(commandArgs);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
435
|
+
// ── Two-word dispatch tables and --help interception ──
|
|
436
|
+
// For every two-word namespace, we check "cmd sub --help" first,
|
|
437
|
+
// then the single-word "cmd --help", then fall through to dispatch.
|
|
206
438
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
439
|
+
const twoWord = (head, table) => ({ head, table });
|
|
440
|
+
const namespaces = [
|
|
441
|
+
twoWord('project', PROJECT_COMMANDS),
|
|
442
|
+
twoWord('workflow', WORKFLOW_COMMANDS),
|
|
443
|
+
twoWord('brief', BRIEF_COMMANDS),
|
|
444
|
+
twoWord('run', RUN_COMMANDS),
|
|
445
|
+
twoWord('batch', BATCH_COMMANDS),
|
|
446
|
+
twoWord('selection', SELECTION_COMMANDS),
|
|
447
|
+
];
|
|
215
448
|
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
449
|
+
// "project", "workflow", "brief", "run", "batch", "selection" two-word forms
|
|
450
|
+
for (const { head, table } of namespaces) {
|
|
451
|
+
if (command === head && args[1] && table[args[1]]) {
|
|
452
|
+
commandArgs = args.slice(2);
|
|
453
|
+
const twoWordKey = `${head} ${args[1]}`;
|
|
454
|
+
if (argvHasHelp(commandArgs) && printCommandHelp(twoWordKey)) return;
|
|
455
|
+
const mod = await import(table[args[1]]);
|
|
456
|
+
await mod.run(commandArgs);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
223
459
|
}
|
|
224
460
|
|
|
225
|
-
//
|
|
461
|
+
// "critique" — bare or "critique show"
|
|
226
462
|
if (command === 'critique') {
|
|
227
463
|
if (args[1] && CRITIQUE_COMMANDS[args[1]]) {
|
|
228
464
|
commandArgs = args.slice(2);
|
|
229
|
-
|
|
230
|
-
const mod = await import(
|
|
465
|
+
if (argvHasHelp(commandArgs) && printCommandHelp(`critique ${args[1]}`)) return;
|
|
466
|
+
const mod = await import(CRITIQUE_COMMANDS[args[1]]);
|
|
231
467
|
await mod.run(commandArgs);
|
|
232
468
|
return;
|
|
233
469
|
}
|
|
234
|
-
|
|
470
|
+
if (argvHasHelp(commandArgs) && printCommandHelp('critique')) return;
|
|
235
471
|
const mod = await import('../scripts/critique.js');
|
|
236
472
|
await mod.run(commandArgs);
|
|
237
473
|
return;
|
|
238
474
|
}
|
|
239
475
|
|
|
240
|
-
//
|
|
476
|
+
// "refine" — single command
|
|
241
477
|
if (command === 'refine') {
|
|
478
|
+
if (argvHasHelp(commandArgs) && printCommandHelp('refine')) return;
|
|
242
479
|
const mod = await import('../scripts/refine.js');
|
|
243
480
|
await mod.run(commandArgs);
|
|
244
481
|
return;
|
|
245
482
|
}
|
|
246
483
|
|
|
247
|
-
//
|
|
248
|
-
if (command === 'batch' && args[1] && BATCH_COMMANDS[args[1]]) {
|
|
249
|
-
commandArgs = args.slice(2);
|
|
250
|
-
const modulePath = BATCH_COMMANDS[args[1]];
|
|
251
|
-
const mod = await import(modulePath);
|
|
252
|
-
await mod.run(commandArgs);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Handle "select" — single command
|
|
484
|
+
// "select" — single command
|
|
257
485
|
if (command === 'select') {
|
|
486
|
+
if (argvHasHelp(commandArgs) && printCommandHelp('select')) return;
|
|
258
487
|
const mod = await import('../scripts/select.js');
|
|
259
488
|
await mod.run(commandArgs);
|
|
260
489
|
return;
|
|
261
490
|
}
|
|
262
491
|
|
|
263
|
-
// Handle "selection <subcommand>" two-word form
|
|
264
|
-
if (command === 'selection' && args[1] && SELECTION_COMMANDS[args[1]]) {
|
|
265
|
-
commandArgs = args.slice(2);
|
|
266
|
-
const modulePath = SELECTION_COMMANDS[args[1]];
|
|
267
|
-
const mod = await import(modulePath);
|
|
268
|
-
await mod.run(commandArgs);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
492
|
const modulePath = COMMANDS[command];
|
|
273
493
|
if (!modulePath) {
|
|
274
|
-
|
|
494
|
+
const suggestion = findClosest(command, allKnownCommands());
|
|
495
|
+
const hint = suggestion
|
|
496
|
+
? `Did you mean "${suggestion}"? (Run "sdlab --help" for all commands.)`
|
|
497
|
+
: 'Run "sdlab --help" for available commands.';
|
|
498
|
+
throw inputError('INPUT_UNKNOWN_COMMAND', `Unknown command: ${command}`, hint);
|
|
275
499
|
}
|
|
276
500
|
|
|
501
|
+
// Per-command --help for one-word commands
|
|
502
|
+
if (argvHasHelp(commandArgs) && printCommandHelp(command)) return;
|
|
503
|
+
|
|
277
504
|
const mod = await import(modulePath);
|
|
278
505
|
await mod.run(commandArgs);
|
|
279
506
|
}
|
|
@@ -11,6 +11,7 @@ import { comfyHealth, submitAndWait, downloadImage } from '../comfyui.js';
|
|
|
11
11
|
import { buildWorkflowGraph, resolveTemplateId } from './comfyui-workflows.js';
|
|
12
12
|
import { buildSeedPlan, prepareRunDir, saveRunManifest } from '../runtime-runs.js';
|
|
13
13
|
import { loadProjectConfig } from '../config.js';
|
|
14
|
+
import { pickOutputImage } from '../comfyui-output.js';
|
|
14
15
|
import { log, info, warn } from '../log.js';
|
|
15
16
|
import { runtimeError } from '../errors.js';
|
|
16
17
|
|
|
@@ -81,7 +82,7 @@ export async function executeRun({ projectRoot, projectName, brief, baseSeed, dr
|
|
|
81
82
|
|
|
82
83
|
try {
|
|
83
84
|
// Build graph
|
|
84
|
-
const { nodes } = buildWorkflowGraph({
|
|
85
|
+
const { nodes, saveNodeId } = buildWorkflowGraph({
|
|
85
86
|
brief,
|
|
86
87
|
projectDefaults: defaults,
|
|
87
88
|
seed,
|
|
@@ -94,26 +95,20 @@ export async function executeRun({ projectRoot, projectName, brief, baseSeed, dr
|
|
|
94
95
|
|
|
95
96
|
const elapsed = Date.now() - startMs;
|
|
96
97
|
|
|
97
|
-
// Extract output image
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (nodeOut.images && nodeOut.images.length > 0) {
|
|
103
|
-
imageFile = nodeOut.images[0].filename;
|
|
104
|
-
imageSubfolder = nodeOut.images[0].subfolder || '';
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
98
|
+
// Extract output image — prefer the explicit save node from the
|
|
99
|
+
// workflow builder (or `expected_outputs.save_node_id` from the
|
|
100
|
+
// brief/profile), falling back to the highest numeric node id.
|
|
101
|
+
const preferNodeId = brief.expected_outputs?.save_node_id ?? saveNodeId;
|
|
102
|
+
const picked = pickOutputImage(result.outputs, { preferNodeId });
|
|
108
103
|
|
|
109
|
-
if (!
|
|
104
|
+
if (!picked) {
|
|
110
105
|
warn(` No output image for seed ${seed}`);
|
|
111
106
|
outputs.push({ index: i, seed, status: 'no_output', elapsed_ms: elapsed });
|
|
112
107
|
continue;
|
|
113
108
|
}
|
|
114
109
|
|
|
115
110
|
// Download and save
|
|
116
|
-
const imgData = await downloadImage(
|
|
111
|
+
const imgData = await downloadImage(picked.filename, picked.subfolder, comfyUrl);
|
|
117
112
|
const destFilename = `${imageIndex}.png`;
|
|
118
113
|
await writeFile(join(runDir, 'outputs', destFilename), imgData);
|
|
119
114
|
|
|
@@ -124,7 +119,8 @@ export async function executeRun({ projectRoot, projectName, brief, baseSeed, dr
|
|
|
124
119
|
filename: destFilename,
|
|
125
120
|
bytes: imgData.length,
|
|
126
121
|
elapsed_ms: elapsed,
|
|
127
|
-
comfy_filename:
|
|
122
|
+
comfy_filename: picked.filename,
|
|
123
|
+
comfy_node_id: picked.nodeId,
|
|
128
124
|
});
|
|
129
125
|
|
|
130
126
|
log(` ✓ ${destFilename} (${imgData.length} bytes, ${(elapsed / 1000).toFixed(1)}s)`);
|
|
@@ -68,8 +68,13 @@ export async function buildPackage({ packageDir, partitions, profile, manifest,
|
|
|
68
68
|
const sourcePath = join(projectRoot, record.asset_path);
|
|
69
69
|
if (!existsSync(sourcePath)) continue;
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
// D-009: Name output files by record.id (not source basename) to
|
|
72
|
+
// avoid silent collisions when two records share a filename stem.
|
|
73
|
+
const sourceBasename = record.asset_path.split(/[\\/]/).pop();
|
|
74
|
+
const extMatch = sourceBasename.match(/\.[^.]+$/);
|
|
75
|
+
const ext = extMatch ? extMatch[0] : '';
|
|
76
|
+
const filename = `${record.id}${ext}`;
|
|
77
|
+
const stem = record.id;
|
|
73
78
|
const destPath = join(datasetDir, filename);
|
|
74
79
|
|
|
75
80
|
try {
|
|
@@ -57,7 +57,12 @@ export async function buildPackage({ packageDir, partitions, profile, manifest,
|
|
|
57
57
|
const sourcePath = join(projectRoot, record.asset_path);
|
|
58
58
|
if (!existsSync(sourcePath)) continue;
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
// D-009: Name output files by record.id (not source basename) to
|
|
61
|
+
// avoid silent collisions when two records share a filename stem.
|
|
62
|
+
const sourceBasename = record.asset_path.split(/[\\/]/).pop();
|
|
63
|
+
const extMatch = sourceBasename.match(/\.[^.]+$/);
|
|
64
|
+
const ext = extMatch ? extMatch[0] : '';
|
|
65
|
+
const filename = `${record.id}${ext}`;
|
|
61
66
|
const destPath = join(datasetDir, filename);
|
|
62
67
|
|
|
63
68
|
try {
|