@mcptoolshop/style-dataset-lab 3.0.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.
- package/CHANGELOG.md +68 -15
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +71 -0
- package/README.md +46 -1
- package/SECURITY.md +6 -5
- package/bin/sdlab.js +281 -62
- 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 +100 -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.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-manifests.js +17 -3
- package/lib/training-packages.js +28 -14
- package/package.json +8 -2
- package/scripts/batch-generate.js +16 -7
- 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 +68 -13
- 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
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* sdlab generate:controlnet --subject naia --guide <path> --seeds 4 --project star-freight
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
-
import { getProjectName } from "../lib/args.js";
|
|
18
|
-
import { REPO_ROOT } from "../lib/paths.js";
|
|
17
|
+
import { getProjectName, parseNumberFlag } from "../lib/args.js";
|
|
18
|
+
import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
|
|
19
|
+
import { readJsonFile } from "../lib/config.js";
|
|
20
|
+
import { inputError, runtimeError, handleCliError } from "../lib/errors.js";
|
|
19
21
|
import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
|
|
20
22
|
|
|
21
23
|
const DEFAULTS = {
|
|
@@ -33,7 +35,12 @@ const DEFAULTS = {
|
|
|
33
35
|
function parseLocalArgs(argv) {
|
|
34
36
|
const get = (flag, def) => {
|
|
35
37
|
const i = argv.indexOf(flag);
|
|
36
|
-
|
|
38
|
+
if (i < 0 || i + 1 >= argv.length) return def;
|
|
39
|
+
const v = argv[i + 1];
|
|
40
|
+
if (typeof v === 'string' && v.startsWith('--')) {
|
|
41
|
+
throw inputError('INPUT_MISSING_VALUE', `Flag ${flag} is missing its value (got another flag: ${v}).`);
|
|
42
|
+
}
|
|
43
|
+
return v;
|
|
37
44
|
};
|
|
38
45
|
return {
|
|
39
46
|
subject: get("--subject", "unknown"),
|
|
@@ -41,9 +48,9 @@ function parseLocalArgs(argv) {
|
|
|
41
48
|
prompt: get("--prompt", null),
|
|
42
49
|
promptFile: get("--prompt-file", null),
|
|
43
50
|
negative: get("--negative", null),
|
|
44
|
-
seeds:
|
|
45
|
-
weight:
|
|
46
|
-
guidanceEnd:
|
|
51
|
+
seeds: parseNumberFlag('seeds', get("--seeds", "4"), { int: true, min: 1 }),
|
|
52
|
+
weight: parseNumberFlag('weight', get("--weight", "0.70"), { min: 0, max: 2 }),
|
|
53
|
+
guidanceEnd: parseNumberFlag('guidance-end', get("--guidance-end", "0.65"), { min: 0, max: 1 }),
|
|
47
54
|
controlModel: get("--model", "controlnet-canny-sdxl-1.0.safetensors"),
|
|
48
55
|
dryRun: argv.includes("--dry-run"),
|
|
49
56
|
};
|
|
@@ -167,21 +174,25 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
167
174
|
const opts = parseLocalArgs(argv);
|
|
168
175
|
|
|
169
176
|
if (!opts.guidePath) {
|
|
170
|
-
throw
|
|
177
|
+
throw inputError('INPUT_MISSING_FLAG', "--guide <path> is required");
|
|
171
178
|
}
|
|
172
179
|
|
|
180
|
+
// Validate guide stays inside the project tree.
|
|
181
|
+
const safeGuide = resolveSafeProjectPath(GAME_ROOT, opts.guidePath, { flagName: 'guide' });
|
|
182
|
+
|
|
173
183
|
// Resolve prompt
|
|
174
184
|
let prompt = opts.prompt;
|
|
175
185
|
let negative = opts.negative;
|
|
176
186
|
|
|
177
187
|
if (opts.promptFile && !prompt) {
|
|
178
|
-
const
|
|
188
|
+
const safePromptFile = resolveSafeProjectPath(GAME_ROOT, opts.promptFile, { flagName: 'prompt-file' });
|
|
189
|
+
const pf = await readJsonFile(safePromptFile, { requiredFields: ['prompt'] });
|
|
179
190
|
prompt = pf.prompt;
|
|
180
191
|
negative = negative || pf.negative;
|
|
181
192
|
}
|
|
182
193
|
|
|
183
194
|
if (!prompt) {
|
|
184
|
-
throw
|
|
195
|
+
throw inputError('INPUT_MISSING_FLAG', "--prompt or --prompt-file required");
|
|
185
196
|
}
|
|
186
197
|
|
|
187
198
|
if (!negative) {
|
|
@@ -200,12 +211,12 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
200
211
|
if (!opts.dryRun) {
|
|
201
212
|
const online = await comfyHealth(COMFY_URL);
|
|
202
213
|
if (!online) {
|
|
203
|
-
throw
|
|
214
|
+
throw runtimeError('RUNTIME_COMFY_UNREACHABLE', "ComfyUI not reachable at " + COMFY_URL);
|
|
204
215
|
}
|
|
205
216
|
console.log("\x1b[32m+\x1b[0m ComfyUI online");
|
|
206
217
|
}
|
|
207
218
|
|
|
208
|
-
const guideFullPath =
|
|
219
|
+
const guideFullPath = safeGuide;
|
|
209
220
|
const guideFilename = `cn_guide_${opts.subject}.png`;
|
|
210
221
|
|
|
211
222
|
if (!opts.dryRun) {
|
|
@@ -215,6 +226,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
215
226
|
|
|
216
227
|
await mkdir(join(GAME_ROOT, "outputs/candidates"), { recursive: true });
|
|
217
228
|
|
|
229
|
+
let successes = 0, failures = 0;
|
|
218
230
|
for (let si = 0; si < opts.seeds; si++) {
|
|
219
231
|
const seed = DEFAULTS.base_seed + si;
|
|
220
232
|
const assetId = `${opts.subject}_cn_s${si}`;
|
|
@@ -254,19 +266,21 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
254
266
|
const imgData = await downloadImage(imageFile, imageSubfolder, COMFY_URL);
|
|
255
267
|
await writeFile(join(GAME_ROOT, destPath), imgData);
|
|
256
268
|
console.log(` \x1b[32m+\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
|
|
269
|
+
successes++;
|
|
257
270
|
} catch (err) {
|
|
258
271
|
console.log(` \x1b[31mx\x1b[0m ${err.message}`);
|
|
272
|
+
failures++;
|
|
259
273
|
}
|
|
260
274
|
}
|
|
261
275
|
|
|
262
|
-
console.log(
|
|
276
|
+
console.log(`\n\x1b[32m+\x1b[0m ControlNet discovery complete (${successes} success, ${failures} failed)`);
|
|
263
277
|
if (opts.dryRun) console.log(" (dry run)");
|
|
278
|
+
if (!opts.dryRun && failures > 0 && successes === 0) {
|
|
279
|
+
throw runtimeError('RUNTIME_ALL_FAILED', `All ${opts.seeds} ControlNet generation attempts failed.`);
|
|
280
|
+
}
|
|
264
281
|
}
|
|
265
282
|
|
|
266
283
|
// Direct execution guard
|
|
267
284
|
if (process.argv[1] && (process.argv[1].endsWith('generate-controlnet.js') || process.argv[1].endsWith('generate-controlnet'))) {
|
|
268
|
-
run().catch(
|
|
269
|
-
console.error(err.message || err);
|
|
270
|
-
process.exit(1);
|
|
271
|
-
});
|
|
285
|
+
run().catch(handleCliError);
|
|
272
286
|
}
|
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
* --denoise N Denoise strength for follow_on phase (default: 0.38)
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
24
24
|
import { join } from "node:path";
|
|
25
|
-
import { getProjectName } from "../lib/args.js";
|
|
26
|
-
import { REPO_ROOT } from "../lib/paths.js";
|
|
25
|
+
import { getProjectName, parseNumberFlag } from "../lib/args.js";
|
|
26
|
+
import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
|
|
27
|
+
import { readJsonFile } from "../lib/config.js";
|
|
28
|
+
import { inputError, runtimeError, handleCliError } from "../lib/errors.js";
|
|
27
29
|
import { comfyHealth, submitAndWait, downloadImage } from "../lib/comfyui.js";
|
|
28
30
|
|
|
29
31
|
// ── Defaults (match existing lab pipeline) ──
|
|
@@ -378,13 +380,24 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
378
380
|
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
379
381
|
const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
|
|
380
382
|
|
|
381
|
-
|
|
383
|
+
// Collect positionals, skipping values that belong to known flags
|
|
384
|
+
const knownFlagsWithValue = new Set(['--subject', '--seeds', '--phase', '--anchor', '--denoise', '--project', '--game']);
|
|
385
|
+
const positionals = [];
|
|
386
|
+
for (let k = 0; k < argv.length; k++) {
|
|
387
|
+
const a = argv[k];
|
|
388
|
+
if (a.startsWith('--')) {
|
|
389
|
+
if (!a.includes('=') && knownFlagsWithValue.has(a)) k++;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
positionals.push(a);
|
|
393
|
+
}
|
|
394
|
+
const packPath = positionals[0];
|
|
382
395
|
const dryRun = argv.includes("--dry-run");
|
|
383
396
|
const subjectFilter = argv.includes("--subject")
|
|
384
397
|
? argv[argv.indexOf("--subject") + 1]
|
|
385
398
|
: null;
|
|
386
399
|
const seedCount = argv.includes("--seeds")
|
|
387
|
-
?
|
|
400
|
+
? parseNumberFlag('seeds', argv[argv.indexOf("--seeds") + 1], { int: true, min: 1 })
|
|
388
401
|
: 3;
|
|
389
402
|
const phase = argv.includes("--phase")
|
|
390
403
|
? argv[argv.indexOf("--phase") + 1]
|
|
@@ -393,30 +406,31 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
393
406
|
? argv[argv.indexOf("--anchor") + 1]
|
|
394
407
|
: null;
|
|
395
408
|
const denoise = argv.includes("--denoise")
|
|
396
|
-
?
|
|
409
|
+
? parseNumberFlag('denoise', argv[argv.indexOf("--denoise") + 1], { min: 0, max: 1 })
|
|
397
410
|
: 0.38;
|
|
398
411
|
|
|
399
412
|
if (!packPath) {
|
|
400
|
-
throw
|
|
413
|
+
throw inputError('INPUT_MISSING_ARGS', "Usage: sdlab generate:identity <packet-file> [options]");
|
|
401
414
|
}
|
|
402
415
|
|
|
403
416
|
if (phase === "follow_on" && !anchorPath) {
|
|
404
|
-
throw
|
|
417
|
+
throw inputError('INPUT_MISSING_FLAG', "--phase follow_on requires --anchor <image-path>");
|
|
405
418
|
}
|
|
406
419
|
|
|
407
|
-
const fullPackPath =
|
|
408
|
-
|
|
420
|
+
const fullPackPath = resolveSafeProjectPath(GAME_ROOT, packPath, { flagName: 'packet-file' });
|
|
421
|
+
if (anchorPath) resolveSafeProjectPath(GAME_ROOT, anchorPath, { flagName: 'anchor' });
|
|
422
|
+
const pack = await readJsonFile(fullPackPath, { requiredFields: ['subjects'] });
|
|
409
423
|
|
|
410
|
-
const
|
|
411
|
-
if (
|
|
412
|
-
throw
|
|
424
|
+
const vErrors = validatePacket(pack);
|
|
425
|
+
if (vErrors.length > 0) {
|
|
426
|
+
throw inputError('INPUT_BAD_PACKET', "Packet validation failed:\n " + vErrors.join("\n "));
|
|
413
427
|
}
|
|
414
428
|
|
|
415
429
|
let subjects = pack.subjects;
|
|
416
430
|
if (subjectFilter) {
|
|
417
431
|
subjects = subjects.filter((s) => s.subject_name === subjectFilter);
|
|
418
432
|
if (subjects.length === 0) {
|
|
419
|
-
throw
|
|
433
|
+
throw inputError('INPUT_UNKNOWN_SUBJECT', `No subject found with name '${subjectFilter}'`);
|
|
420
434
|
}
|
|
421
435
|
}
|
|
422
436
|
|
|
@@ -439,7 +453,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
439
453
|
if (!dryRun) {
|
|
440
454
|
const online = await comfyHealth(COMFY_URL);
|
|
441
455
|
if (!online) {
|
|
442
|
-
throw
|
|
456
|
+
throw runtimeError('RUNTIME_COMFY_UNREACHABLE', "ComfyUI not reachable at " + COMFY_URL);
|
|
443
457
|
}
|
|
444
458
|
console.log("\x1b[32m+\x1b[0m ComfyUI online");
|
|
445
459
|
}
|
|
@@ -448,6 +462,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
448
462
|
await mkdir(join(GAME_ROOT, "records"), { recursive: true });
|
|
449
463
|
|
|
450
464
|
let generated = 0;
|
|
465
|
+
let errors = 0;
|
|
451
466
|
let imageIndex = 0;
|
|
452
467
|
|
|
453
468
|
for (const subject of subjects) {
|
|
@@ -532,6 +547,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
532
547
|
console.log(` \x1b[32m+\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
|
|
533
548
|
} catch (err) {
|
|
534
549
|
console.log(` \x1b[31mx\x1b[0m ${err.message}`);
|
|
550
|
+
errors++;
|
|
535
551
|
}
|
|
536
552
|
|
|
537
553
|
generated++;
|
|
@@ -540,8 +556,12 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
540
556
|
}
|
|
541
557
|
}
|
|
542
558
|
|
|
543
|
-
|
|
559
|
+
const succeeded = generated - errors;
|
|
560
|
+
console.log(`\n\x1b[32m+\x1b[0m Generated ${succeeded} images (${errors} errors)`);
|
|
544
561
|
if (dryRun) console.log(" (dry run — no images produced)");
|
|
562
|
+
if (!dryRun && errors > 0 && succeeded === 0) {
|
|
563
|
+
throw runtimeError('RUNTIME_ALL_FAILED', `All ${generated} generation attempts failed.`);
|
|
564
|
+
}
|
|
545
565
|
|
|
546
566
|
console.log("\nNext steps:");
|
|
547
567
|
if (phase === "discovery") {
|
|
@@ -558,8 +578,5 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
558
578
|
|
|
559
579
|
// Direct execution guard
|
|
560
580
|
if (process.argv[1] && (process.argv[1].endsWith('generate-identity.js') || process.argv[1].endsWith('generate-identity'))) {
|
|
561
|
-
run().catch(
|
|
562
|
-
console.error(err.message || err);
|
|
563
|
-
process.exit(1);
|
|
564
|
-
});
|
|
581
|
+
run().catch(handleCliError);
|
|
565
582
|
}
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* sdlab generate:ipadapter --subject corrigan --ref <image> --seeds 4 --project star-freight
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
-
import { getProjectName } from "../lib/args.js";
|
|
17
|
-
import { REPO_ROOT } from "../lib/paths.js";
|
|
16
|
+
import { getProjectName, parseNumberFlag } from "../lib/args.js";
|
|
17
|
+
import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
|
|
18
|
+
import { inputError, runtimeError, handleCliError } from "../lib/errors.js";
|
|
18
19
|
import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
|
|
19
20
|
|
|
20
21
|
const DEFAULTS = {
|
|
@@ -34,17 +35,22 @@ const DEFAULTS = {
|
|
|
34
35
|
function parseLocalArgs(argv) {
|
|
35
36
|
const get = (flag, def) => {
|
|
36
37
|
const i = argv.indexOf(flag);
|
|
37
|
-
|
|
38
|
+
if (i < 0 || i + 1 >= argv.length) return def;
|
|
39
|
+
const v = argv[i + 1];
|
|
40
|
+
if (typeof v === 'string' && v.startsWith('--')) {
|
|
41
|
+
throw inputError('INPUT_MISSING_VALUE', `Flag ${flag} is missing its value (got another flag: ${v}).`);
|
|
42
|
+
}
|
|
43
|
+
return v;
|
|
38
44
|
};
|
|
39
45
|
return {
|
|
40
46
|
subject: get("--subject", "unknown"),
|
|
41
47
|
refPath: get("--ref", null),
|
|
42
48
|
prompt: get("--prompt", null),
|
|
43
49
|
negative: get("--negative", null),
|
|
44
|
-
seeds:
|
|
45
|
-
weight:
|
|
46
|
-
start:
|
|
47
|
-
end:
|
|
50
|
+
seeds: parseNumberFlag('seeds', get("--seeds", "4"), { int: true, min: 1 }),
|
|
51
|
+
weight: parseNumberFlag('weight', get("--weight", "0.55"), { min: 0, max: 2 }),
|
|
52
|
+
start: parseNumberFlag('start', get("--start", "0.0"), { min: 0, max: 1 }),
|
|
53
|
+
end: parseNumberFlag('end', get("--end", "0.8"), { min: 0, max: 1 }),
|
|
48
54
|
dryRun: argv.includes("--dry-run"),
|
|
49
55
|
};
|
|
50
56
|
}
|
|
@@ -174,12 +180,15 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
174
180
|
const opts = parseLocalArgs(argv);
|
|
175
181
|
|
|
176
182
|
if (!opts.refPath) {
|
|
177
|
-
throw
|
|
183
|
+
throw inputError('INPUT_MISSING_FLAG', "--ref <path> is required");
|
|
178
184
|
}
|
|
179
185
|
if (!opts.prompt) {
|
|
180
|
-
throw
|
|
186
|
+
throw inputError('INPUT_MISSING_FLAG', "--prompt is required");
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
// Validate --ref path stays inside project tree.
|
|
190
|
+
const safeRefPath = resolveSafeProjectPath(GAME_ROOT, opts.refPath, { flagName: 'ref' });
|
|
191
|
+
|
|
183
192
|
const negative = opts.negative ||
|
|
184
193
|
"photorealistic, photograph, 3d render, anime, cartoon, text, watermark, blurry, low quality";
|
|
185
194
|
|
|
@@ -196,11 +205,11 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
196
205
|
if (!opts.dryRun) {
|
|
197
206
|
const online = await comfyHealth(COMFY_URL);
|
|
198
207
|
if (!online) {
|
|
199
|
-
throw
|
|
208
|
+
throw runtimeError('RUNTIME_COMFY_UNREACHABLE', "ComfyUI not reachable at " + COMFY_URL);
|
|
200
209
|
}
|
|
201
210
|
console.log("\x1b[32m+\x1b[0m ComfyUI online");
|
|
202
211
|
|
|
203
|
-
await uploadImage(
|
|
212
|
+
await uploadImage(safeRefPath, refFilename, COMFY_URL);
|
|
204
213
|
console.log(`\x1b[32m+\x1b[0m Reference uploaded: ${refFilename}`);
|
|
205
214
|
}
|
|
206
215
|
|
|
@@ -255,8 +264,5 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
255
264
|
|
|
256
265
|
// Direct execution guard
|
|
257
266
|
if (process.argv[1] && (process.argv[1].endsWith('generate-ipadapter.js') || process.argv[1].endsWith('generate-ipadapter'))) {
|
|
258
|
-
run().catch(
|
|
259
|
-
console.error(err.message || err);
|
|
260
|
-
process.exit(1);
|
|
261
|
-
});
|
|
267
|
+
run().catch(handleCliError);
|
|
262
268
|
}
|
package/scripts/generate.js
CHANGED
|
@@ -10,11 +10,34 @@
|
|
|
10
10
|
* downloads the output PNG, and writes a provenance record to records/.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
-
import { getProjectName } from "../lib/args.js";
|
|
16
|
-
import { REPO_ROOT } from "../lib/paths.js";
|
|
15
|
+
import { parseArgs, getProjectName } from "../lib/args.js";
|
|
16
|
+
import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
|
|
17
|
+
import { readJsonFile } from "../lib/config.js";
|
|
18
|
+
import { runtimeError, handleCliError } from "../lib/errors.js";
|
|
17
19
|
import { comfyHealth, submitAndWait, downloadImage } from "../lib/comfyui.js";
|
|
20
|
+
import { info, result } from "../lib/log.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format milliseconds as a human ETA string like "6m 12s" or "42s".
|
|
24
|
+
*/
|
|
25
|
+
function formatEta(ms) {
|
|
26
|
+
if (!Number.isFinite(ms) || ms < 0) return '—';
|
|
27
|
+
const totalSec = Math.round(ms / 1000);
|
|
28
|
+
if (totalSec < 60) return `${totalSec}s`;
|
|
29
|
+
const m = Math.floor(totalSec / 60);
|
|
30
|
+
const s = totalSec % 60;
|
|
31
|
+
return `${m}m ${s}s`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* COMFY_URL precedence:
|
|
36
|
+
* 1. process.env.COMFY_URL (explicit user env)
|
|
37
|
+
* 2. project defaults.comfy_url (set by `sdlab project doctor`)
|
|
38
|
+
* 3. built-in default http://127.0.0.1:8188
|
|
39
|
+
* The adapter (lib/adapters/comfyui-runner.js) applies the same precedence.
|
|
40
|
+
*/
|
|
18
41
|
|
|
19
42
|
function buildWorkflow(prompt, negativePrompt, checkpoint, loras, seed, steps, cfg, sampler, scheduler, width, height, ipAdapterConfig) {
|
|
20
43
|
const nodes = {};
|
|
@@ -157,11 +180,19 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
157
180
|
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
158
181
|
const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
|
|
159
182
|
|
|
160
|
-
const
|
|
161
|
-
|
|
183
|
+
const parsed = parseArgs(argv, {
|
|
184
|
+
flags: {
|
|
185
|
+
project: { type: 'string' },
|
|
186
|
+
game: { type: 'string' },
|
|
187
|
+
'dry-run': { type: 'boolean' },
|
|
188
|
+
},
|
|
189
|
+
deprecated: { game: 'project' },
|
|
190
|
+
});
|
|
191
|
+
const packPath = parsed.positionals[0] || "inputs/prompts/rpg-icons-lane1.json";
|
|
192
|
+
const dryRun = parsed.flags['dry-run'] === true;
|
|
162
193
|
|
|
163
|
-
const fullPackPath =
|
|
164
|
-
const pack =
|
|
194
|
+
const fullPackPath = resolveSafeProjectPath(GAME_ROOT, packPath, { flagName: 'pack-path' });
|
|
195
|
+
const pack = await readJsonFile(fullPackPath, { requiredFields: ['lane', 'subjects', 'variations', 'defaults'] });
|
|
165
196
|
|
|
166
197
|
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m generate`);
|
|
167
198
|
console.log(` Lane: ${pack.lane}`);
|
|
@@ -173,7 +204,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
173
204
|
if (!dryRun) {
|
|
174
205
|
const online = await comfyHealth(COMFY_URL);
|
|
175
206
|
if (!online) {
|
|
176
|
-
throw
|
|
207
|
+
throw runtimeError('RUNTIME_COMFY_UNREACHABLE', "ComfyUI not reachable at " + COMFY_URL, 'Start ComfyUI or set COMFY_URL to the right host.');
|
|
177
208
|
}
|
|
178
209
|
console.log("\x1b[32m✓\x1b[0m ComfyUI online");
|
|
179
210
|
}
|
|
@@ -183,7 +214,9 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
183
214
|
|
|
184
215
|
const d = pack.defaults;
|
|
185
216
|
let generated = 0;
|
|
217
|
+
let errors = 0;
|
|
186
218
|
const totalExpected = pack.subjects.length * pack.variations.length;
|
|
219
|
+
const runStartMs = Date.now();
|
|
187
220
|
|
|
188
221
|
for (const subject of pack.subjects) {
|
|
189
222
|
for (const variation of pack.variations) {
|
|
@@ -272,21 +305,43 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
272
305
|
console.log(` \x1b[32m✓\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
|
|
273
306
|
} catch (err) {
|
|
274
307
|
console.log(` \x1b[31m✗\x1b[0m ${err.message}`);
|
|
308
|
+
errors++;
|
|
275
309
|
}
|
|
276
310
|
|
|
277
311
|
generated++;
|
|
312
|
+
|
|
313
|
+
// ETA every 5 items (not every item — too noisy for long runs).
|
|
314
|
+
if (!dryRun && generated > 0 && generated % 5 === 0 && generated < totalExpected) {
|
|
315
|
+
const elapsedTotal = Date.now() - runStartMs;
|
|
316
|
+
const avgMs = elapsedTotal / generated;
|
|
317
|
+
const remaining = totalExpected - generated;
|
|
318
|
+
const etaMs = avgMs * remaining;
|
|
319
|
+
info('generate', `progress ${generated}/${totalExpected} — avg ${formatEta(avgMs)}/item, ETA ~${formatEta(etaMs)}`);
|
|
320
|
+
}
|
|
278
321
|
}
|
|
279
322
|
}
|
|
280
323
|
|
|
281
324
|
console.log("");
|
|
282
|
-
|
|
325
|
+
const succeeded = generated - errors;
|
|
326
|
+
const totalElapsedMs = Date.now() - runStartMs;
|
|
327
|
+
console.log(`\x1b[32m✓\x1b[0m Generated ${succeeded} candidates (${errors} errors)`);
|
|
328
|
+
if (!dryRun && generated > 0) {
|
|
329
|
+
const avgMs = totalElapsedMs / generated;
|
|
330
|
+
console.log(` Total: ${formatEta(totalElapsedMs)} — avg ${formatEta(avgMs)}/item`);
|
|
331
|
+
}
|
|
283
332
|
if (dryRun) console.log(" (dry run — no images produced)");
|
|
333
|
+
// Always print the output directory so scripted pipelines can parse it,
|
|
334
|
+
// even under --quiet.
|
|
335
|
+
if (!dryRun && succeeded > 0) {
|
|
336
|
+
result(`Candidates: ${join(GAME_ROOT, 'outputs/candidates')}`);
|
|
337
|
+
result(`Records: ${join(GAME_ROOT, 'records')}`);
|
|
338
|
+
}
|
|
339
|
+
if (!dryRun && errors > 0 && succeeded === 0) {
|
|
340
|
+
throw runtimeError('RUNTIME_ALL_FAILED', `All ${totalExpected} generation attempts failed.`);
|
|
341
|
+
}
|
|
284
342
|
}
|
|
285
343
|
|
|
286
344
|
// Direct execution guard
|
|
287
345
|
if (process.argv[1] && (process.argv[1].endsWith('generate.js') || process.argv[1].endsWith('generate'))) {
|
|
288
|
-
run().catch(
|
|
289
|
-
console.error(err.message || err);
|
|
290
|
-
process.exit(1);
|
|
291
|
-
});
|
|
346
|
+
run().catch(handleCliError);
|
|
292
347
|
}
|
|
@@ -10,25 +10,30 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
-
import { getProjectName } from '../lib/args.js';
|
|
13
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
14
14
|
import { REPO_ROOT } from '../lib/paths.js';
|
|
15
|
+
import { inputError, handleCliError } from '../lib/errors.js';
|
|
15
16
|
import { listTrainingManifests } from '../lib/training-manifests.js';
|
|
16
17
|
import { buildImplementationPack, listImplementationPacks, loadImplementationPack } from '../lib/implementation-packs.js';
|
|
17
18
|
|
|
18
19
|
export async function run(argv = process.argv.slice(2)) {
|
|
19
|
-
const
|
|
20
|
+
const { flags, positionals } = parseArgs(argv, {
|
|
21
|
+
flags: {
|
|
22
|
+
project: { type: 'string' },
|
|
23
|
+
manifest: { type: 'string' },
|
|
24
|
+
},
|
|
25
|
+
deprecated: { game: 'project' },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const projectName = flags.project || getProjectName(argv);
|
|
20
29
|
const projectRoot = join(REPO_ROOT, 'projects', projectName);
|
|
21
|
-
const subcommand =
|
|
22
|
-
const args = argv.filter(a => a !== subcommand);
|
|
30
|
+
const subcommand = positionals.find(a => !a.startsWith('impl-') && !a.startsWith('tm-')) || 'list';
|
|
23
31
|
|
|
24
32
|
if (subcommand === 'build') {
|
|
25
|
-
let manifestId;
|
|
26
|
-
|
|
27
|
-
if (manifestIdx >= 0) {
|
|
28
|
-
manifestId = args[manifestIdx + 1];
|
|
29
|
-
} else {
|
|
33
|
+
let manifestId = flags.manifest;
|
|
34
|
+
if (!manifestId) {
|
|
30
35
|
const manifests = await listTrainingManifests(projectRoot);
|
|
31
|
-
if (manifests.length === 0) throw
|
|
36
|
+
if (manifests.length === 0) throw inputError('INPUT_NO_MANIFEST', 'No training manifests. Run: sdlab training-manifest create');
|
|
32
37
|
manifestId = manifests[manifests.length - 1].id;
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -44,8 +49,8 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
44
49
|
console.log(` Subject continuity: ${result.subjects}`);
|
|
45
50
|
|
|
46
51
|
} else if (subcommand === 'show') {
|
|
47
|
-
const implId =
|
|
48
|
-
if (!implId) throw
|
|
52
|
+
const implId = positionals.find(a => a.startsWith('impl-'));
|
|
53
|
+
if (!implId) throw inputError('INPUT_MISSING_ARGS', 'Usage: sdlab implementation-pack show <impl-id>');
|
|
49
54
|
|
|
50
55
|
const pack = await loadImplementationPack(projectRoot, implId);
|
|
51
56
|
console.log(`\x1b[1mImplementation pack\x1b[0m — ${implId}\n`);
|
|
@@ -69,10 +74,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
} else {
|
|
72
|
-
throw
|
|
77
|
+
throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: build, show, list`);
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
if (process.argv[1] && (process.argv[1].endsWith('implementation-pack.js') || process.argv[1].endsWith('implementation-pack'))) {
|
|
77
|
-
run().catch(
|
|
82
|
+
run().catch(handleCliError);
|
|
78
83
|
}
|