@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.
- package/CHANGELOG.md +73 -0
- package/README.md +138 -113
- package/bin/sdlab.js +126 -0
- package/lib/args.js +106 -0
- package/lib/card.js +145 -0
- package/lib/comfyui.js +109 -0
- package/lib/config.js +248 -0
- package/lib/deprecation.js +14 -0
- package/lib/eligibility.js +110 -0
- package/lib/eval-pack.js +283 -0
- package/lib/export.js +259 -0
- package/lib/paths.js +38 -0
- package/lib/records.js +69 -0
- package/lib/snapshot.js +211 -0
- package/lib/split.js +409 -0
- package/package.json +20 -9
- package/scripts/bulk-curate-wave2-5.js +1 -1
- package/scripts/bulk-curate-waves11-18.js +1 -1
- package/scripts/canon-bind.js +188 -338
- package/scripts/card.js +73 -0
- package/scripts/compare.js +40 -30
- package/scripts/curate-wave25.js +1 -1
- package/scripts/curate.js +34 -28
- package/scripts/doctor.js +324 -0
- package/scripts/eligibility.js +91 -0
- package/scripts/eval-pack.js +92 -0
- package/scripts/export.js +85 -0
- package/scripts/generate-controlnet.js +36 -121
- package/scripts/generate-identity.js +38 -106
- package/scripts/generate-ipadapter.js +42 -119
- package/scripts/generate.js +22 -81
- package/scripts/init.js +229 -0
- package/scripts/migrate-records.js +15 -21
- package/scripts/painterly-test.js +20 -47
- package/scripts/painterly.js +27 -106
- package/scripts/snapshot.js +132 -0
- package/scripts/split.js +154 -0
- package/templates/domains/architecture/constitution.json +97 -0
- package/templates/domains/architecture/lanes.json +41 -0
- package/templates/domains/architecture/project.json +20 -0
- package/templates/domains/architecture/rubric.json +40 -0
- package/templates/domains/architecture/terminology.json +19 -0
- package/templates/domains/character-design/constitution.json +88 -0
- package/templates/domains/character-design/lanes.json +35 -0
- package/templates/domains/character-design/project.json +17 -0
- package/templates/domains/character-design/rubric.json +38 -0
- package/templates/domains/character-design/terminology.json +21 -0
- package/templates/domains/creature-design/constitution.json +87 -0
- package/templates/domains/creature-design/lanes.json +41 -0
- package/templates/domains/creature-design/project.json +18 -0
- package/templates/domains/creature-design/rubric.json +39 -0
- package/templates/domains/creature-design/terminology.json +20 -0
- package/templates/domains/game-art/constitution.json +113 -0
- package/templates/domains/game-art/lanes.json +47 -0
- package/templates/domains/game-art/project.json +17 -0
- package/templates/domains/game-art/rubric.json +36 -0
- package/templates/domains/game-art/terminology.json +30 -0
- package/templates/domains/vehicle-mech/constitution.json +87 -0
- package/templates/domains/vehicle-mech/lanes.json +41 -0
- package/templates/domains/vehicle-mech/project.json +17 -0
- package/templates/domains/vehicle-mech/rubric.json +40 -0
- package/templates/domains/vehicle-mech/terminology.json +21 -0
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
11
|
* node scripts/generate-identity.js inputs/identity-packets/wave27a-identity-spine.json
|
|
12
|
-
*
|
|
13
|
-
* node scripts/generate-identity.js inputs/identity-packets/wave27a-identity-spine.json --subject kael_maren
|
|
14
|
-
* node scripts/generate-identity.js inputs/identity-packets/wave27a-identity-spine.json --seeds 3
|
|
12
|
+
* sdlab generate:identity inputs/identity-packets/wave27a.json --project star-freight
|
|
15
13
|
*
|
|
16
14
|
* Options:
|
|
17
15
|
* --dry-run Print what would be generated without touching ComfyUI
|
|
@@ -20,20 +18,13 @@
|
|
|
20
18
|
* --phase MODE Generation phase: discovery | follow_on (default: discovery)
|
|
21
19
|
* --anchor FILE Anchor source image path (required for --phase follow_on)
|
|
22
20
|
* --denoise N Denoise strength for follow_on phase (default: 0.38)
|
|
23
|
-
*
|
|
24
|
-
* Phases:
|
|
25
|
-
* discovery — txt2img from prompt, 3 seeds per shot. No prior image input.
|
|
26
|
-
* follow_on — img2img from anchor image. Requires --anchor and --denoise.
|
|
27
|
-
* Anchor curation happens between phases (manual step).
|
|
28
21
|
*/
|
|
29
22
|
|
|
30
|
-
import { readFile, writeFile, mkdir
|
|
23
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
31
24
|
import { join } from "node:path";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
|
|
36
|
-
const GAME_ROOT = join(REPO_ROOT, 'games', GAME);
|
|
25
|
+
import { getProjectName } from "../lib/args.js";
|
|
26
|
+
import { REPO_ROOT } from "../lib/paths.js";
|
|
27
|
+
import { comfyHealth, submitAndWait, downloadImage } from "../lib/comfyui.js";
|
|
37
28
|
|
|
38
29
|
// ── Defaults (match existing lab pipeline) ──
|
|
39
30
|
|
|
@@ -129,7 +120,6 @@ function validatePacket(pack) {
|
|
|
129
120
|
errors.push(`Subject ${sn}, shot ${shot.id}: invalid view_type '${shot.view_type}'`);
|
|
130
121
|
}
|
|
131
122
|
|
|
132
|
-
// Variant intent enforcement
|
|
133
123
|
if (shot.view_type === "expression_variant") {
|
|
134
124
|
if (!shot.variant_intent) {
|
|
135
125
|
errors.push(`Subject ${sn}, shot ${shot.id}: expression_variant missing variant_intent`);
|
|
@@ -149,16 +139,7 @@ function validatePacket(pack) {
|
|
|
149
139
|
return errors;
|
|
150
140
|
}
|
|
151
141
|
|
|
152
|
-
// ── ComfyUI
|
|
153
|
-
|
|
154
|
-
async function comfyHealth() {
|
|
155
|
-
try {
|
|
156
|
-
const res = await fetch(`${COMFY_URL}/system_stats`);
|
|
157
|
-
return res.ok;
|
|
158
|
-
} catch {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
142
|
+
// ── ComfyUI Workflows ──
|
|
162
143
|
|
|
163
144
|
function buildTxt2ImgWorkflow(prompt, negativePrompt, seed) {
|
|
164
145
|
const nodes = {};
|
|
@@ -268,14 +249,12 @@ function buildImg2ImgWorkflow(prompt, negativePrompt, seed, imagePath, denoise)
|
|
|
268
249
|
clipOut = [loraId, 1];
|
|
269
250
|
}
|
|
270
251
|
|
|
271
|
-
// Load source image
|
|
272
252
|
const loadImgId = String(nextId++);
|
|
273
253
|
nodes[loadImgId] = {
|
|
274
254
|
class_type: "LoadImage",
|
|
275
255
|
inputs: { image: imagePath },
|
|
276
256
|
};
|
|
277
257
|
|
|
278
|
-
// Encode image to latent via VAE
|
|
279
258
|
const encodeId = String(nextId++);
|
|
280
259
|
nodes[encodeId] = {
|
|
281
260
|
class_type: "VAEEncode",
|
|
@@ -326,46 +305,6 @@ function buildImg2ImgWorkflow(prompt, negativePrompt, seed, imagePath, denoise)
|
|
|
326
305
|
return nodes;
|
|
327
306
|
}
|
|
328
307
|
|
|
329
|
-
const MAX_POLL_MS = 600_000; // 10 minutes
|
|
330
|
-
|
|
331
|
-
async function submitAndWait(workflow) {
|
|
332
|
-
const clientId = `idp-${Date.now()}`;
|
|
333
|
-
const res = await fetch(`${COMFY_URL}/prompt`, {
|
|
334
|
-
method: "POST",
|
|
335
|
-
headers: { "Content-Type": "application/json" },
|
|
336
|
-
body: JSON.stringify({ prompt: workflow, client_id: clientId }),
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
if (!res.ok) {
|
|
340
|
-
const text = await res.text();
|
|
341
|
-
throw new Error(`ComfyUI submit failed: ${res.status} ${text}`);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const { prompt_id: promptId } = await res.json();
|
|
345
|
-
|
|
346
|
-
const start = Date.now();
|
|
347
|
-
while (true) {
|
|
348
|
-
if (Date.now() - start > MAX_POLL_MS) throw new Error(`ComfyUI poll timeout after ${MAX_POLL_MS/1000}s for prompt ${promptId}`);
|
|
349
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
350
|
-
const histRes = await fetch(`${COMFY_URL}/history/${promptId}`);
|
|
351
|
-
if (!histRes.ok) continue;
|
|
352
|
-
const history = await histRes.json();
|
|
353
|
-
const entry = history[promptId];
|
|
354
|
-
if (!entry) continue;
|
|
355
|
-
if (entry.status?.completed) return entry;
|
|
356
|
-
if (entry.status?.status_str === "error") {
|
|
357
|
-
throw new Error(`ComfyUI generation failed: ${JSON.stringify(entry.status)}`);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
async function downloadImage(filename, subfolder, type = "output") {
|
|
363
|
-
const url = `${COMFY_URL}/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder || "")}&type=${type}`;
|
|
364
|
-
const res = await fetch(url);
|
|
365
|
-
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
366
|
-
return Buffer.from(await res.arrayBuffer());
|
|
367
|
-
}
|
|
368
|
-
|
|
369
308
|
// ── Record Builder ──
|
|
370
309
|
|
|
371
310
|
function buildRecord(pack, subject, shot, seed, seedIndex, imgData, destPath, elapsedMs, phase, anchorInfo) {
|
|
@@ -434,58 +373,53 @@ function buildRecord(pack, subject, shot, seed, seedIndex, imgData, destPath, el
|
|
|
434
373
|
|
|
435
374
|
// ── Main ──
|
|
436
375
|
|
|
437
|
-
async function
|
|
438
|
-
const
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
376
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
377
|
+
const projectName = getProjectName(argv);
|
|
378
|
+
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
379
|
+
const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
|
|
380
|
+
|
|
381
|
+
const packPath = argv.find((a) => !a.startsWith("--"));
|
|
382
|
+
const dryRun = argv.includes("--dry-run");
|
|
383
|
+
const subjectFilter = argv.includes("--subject")
|
|
384
|
+
? argv[argv.indexOf("--subject") + 1]
|
|
443
385
|
: null;
|
|
444
|
-
const seedCount =
|
|
445
|
-
? parseInt(
|
|
386
|
+
const seedCount = argv.includes("--seeds")
|
|
387
|
+
? parseInt(argv[argv.indexOf("--seeds") + 1], 10)
|
|
446
388
|
: 3;
|
|
447
|
-
const phase =
|
|
448
|
-
?
|
|
389
|
+
const phase = argv.includes("--phase")
|
|
390
|
+
? argv[argv.indexOf("--phase") + 1]
|
|
449
391
|
: "discovery";
|
|
450
|
-
const anchorPath =
|
|
451
|
-
?
|
|
392
|
+
const anchorPath = argv.includes("--anchor")
|
|
393
|
+
? argv[argv.indexOf("--anchor") + 1]
|
|
452
394
|
: null;
|
|
453
|
-
const denoise =
|
|
454
|
-
? parseFloat(
|
|
395
|
+
const denoise = argv.includes("--denoise")
|
|
396
|
+
? parseFloat(argv[argv.indexOf("--denoise") + 1])
|
|
455
397
|
: 0.38;
|
|
456
398
|
|
|
457
399
|
if (!packPath) {
|
|
458
|
-
|
|
459
|
-
process.exit(1);
|
|
400
|
+
throw new Error("Usage: sdlab generate:identity <packet-file> [options]");
|
|
460
401
|
}
|
|
461
402
|
|
|
462
403
|
if (phase === "follow_on" && !anchorPath) {
|
|
463
|
-
|
|
464
|
-
process.exit(1);
|
|
404
|
+
throw new Error("--phase follow_on requires --anchor <image-path>");
|
|
465
405
|
}
|
|
466
406
|
|
|
467
407
|
const fullPackPath = join(GAME_ROOT, packPath);
|
|
468
408
|
const pack = JSON.parse(await readFile(fullPackPath, "utf-8"));
|
|
469
409
|
|
|
470
|
-
// Validate
|
|
471
410
|
const errors = validatePacket(pack);
|
|
472
411
|
if (errors.length > 0) {
|
|
473
|
-
|
|
474
|
-
for (const e of errors) console.error(` - ${e}`);
|
|
475
|
-
process.exit(1);
|
|
412
|
+
throw new Error("Packet validation failed:\n " + errors.join("\n "));
|
|
476
413
|
}
|
|
477
414
|
|
|
478
|
-
// Filter subjects if requested
|
|
479
415
|
let subjects = pack.subjects;
|
|
480
416
|
if (subjectFilter) {
|
|
481
417
|
subjects = subjects.filter((s) => s.subject_name === subjectFilter);
|
|
482
418
|
if (subjects.length === 0) {
|
|
483
|
-
|
|
484
|
-
process.exit(1);
|
|
419
|
+
throw new Error(`No subject found with name '${subjectFilter}'`);
|
|
485
420
|
}
|
|
486
421
|
}
|
|
487
422
|
|
|
488
|
-
// Count shots
|
|
489
423
|
const totalShots = subjects.reduce((sum, s) => sum + s.shots.length, 0);
|
|
490
424
|
const totalImages = totalShots * seedCount;
|
|
491
425
|
|
|
@@ -503,13 +437,9 @@ async function main() {
|
|
|
503
437
|
console.log("");
|
|
504
438
|
|
|
505
439
|
if (!dryRun) {
|
|
506
|
-
const online = await comfyHealth();
|
|
440
|
+
const online = await comfyHealth(COMFY_URL);
|
|
507
441
|
if (!online) {
|
|
508
|
-
|
|
509
|
-
console.error(
|
|
510
|
-
"Start ComfyUI: cd F:/AI-Models/ComfyUI-runtime && python main.py --listen 127.0.0.1 --port 8188"
|
|
511
|
-
);
|
|
512
|
-
process.exit(1);
|
|
442
|
+
throw new Error("ComfyUI not reachable at " + COMFY_URL);
|
|
513
443
|
}
|
|
514
444
|
console.log("\x1b[32m+\x1b[0m ComfyUI online");
|
|
515
445
|
}
|
|
@@ -555,7 +485,7 @@ async function main() {
|
|
|
555
485
|
}
|
|
556
486
|
|
|
557
487
|
try {
|
|
558
|
-
const result = await submitAndWait(workflow);
|
|
488
|
+
const result = await submitAndWait(workflow, COMFY_URL, { clientPrefix: 'idp' });
|
|
559
489
|
const elapsed = Date.now() - startMs;
|
|
560
490
|
|
|
561
491
|
const outputs = result.outputs || {};
|
|
@@ -576,7 +506,7 @@ async function main() {
|
|
|
576
506
|
continue;
|
|
577
507
|
}
|
|
578
508
|
|
|
579
|
-
const imgData = await downloadImage(imageFile, imageSubfolder);
|
|
509
|
+
const imgData = await downloadImage(imageFile, imageSubfolder, COMFY_URL);
|
|
580
510
|
await writeFile(join(GAME_ROOT, destPath), imgData);
|
|
581
511
|
|
|
582
512
|
const anchorInfo =
|
|
@@ -613,7 +543,6 @@ async function main() {
|
|
|
613
543
|
console.log(`\n\x1b[32m+\x1b[0m Generated ${generated} images`);
|
|
614
544
|
if (dryRun) console.log(" (dry run — no images produced)");
|
|
615
545
|
|
|
616
|
-
// Summary
|
|
617
546
|
console.log("\nNext steps:");
|
|
618
547
|
if (phase === "discovery") {
|
|
619
548
|
console.log(" 1. Review candidates in outputs/candidates/");
|
|
@@ -627,7 +556,10 @@ async function main() {
|
|
|
627
556
|
}
|
|
628
557
|
}
|
|
629
558
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
559
|
+
// Direct execution guard
|
|
560
|
+
if (process.argv[1] && (process.argv[1].endsWith('generate-identity.js') || process.argv[1].endsWith('generate-identity'))) {
|
|
561
|
+
run().catch((err) => {
|
|
562
|
+
console.error(err.message || err);
|
|
563
|
+
process.exit(1);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
@@ -4,31 +4,18 @@
|
|
|
4
4
|
* generate-ipadapter.js — IP-Adapter guided generation for structural subjects.
|
|
5
5
|
*
|
|
6
6
|
* Uses a reference image to guide the shape/structure of the output
|
|
7
|
-
* while the text prompt adds identity-specific details
|
|
7
|
+
* while the text prompt adds identity-specific details.
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
10
|
* node scripts/generate-ipadapter.js --subject corrigan --ref <image> --seeds 4
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Options:
|
|
14
|
-
* --subject NAME Subject name for output file naming
|
|
15
|
-
* --ref PATH Reference image path (the structural truth source)
|
|
16
|
-
* --prompt TEXT Generation prompt
|
|
17
|
-
* --negative TEXT Negative prompt
|
|
18
|
-
* --seeds N Number of seeds (default: 4)
|
|
19
|
-
* --weight W IP-Adapter weight (default: 0.55)
|
|
20
|
-
* --start S IP-Adapter start (default: 0.0)
|
|
21
|
-
* --end E IP-Adapter end (default: 0.8)
|
|
22
|
-
* --dry-run Print workflow without running
|
|
11
|
+
* sdlab generate:ipadapter --subject corrigan --ref <image> --seeds 4 --project star-freight
|
|
23
12
|
*/
|
|
24
13
|
|
|
25
14
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
26
15
|
import { join } from "node:path";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
|
|
31
|
-
const GAME_ROOT = join(REPO_ROOT, 'games', GAME);
|
|
16
|
+
import { getProjectName } from "../lib/args.js";
|
|
17
|
+
import { REPO_ROOT } from "../lib/paths.js";
|
|
18
|
+
import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
|
|
32
19
|
|
|
33
20
|
const DEFAULTS = {
|
|
34
21
|
checkpoint: "dreamshaperXL_v21TurboDPMSDE.safetensors",
|
|
@@ -44,11 +31,10 @@ const DEFAULTS = {
|
|
|
44
31
|
base_seed: 27200,
|
|
45
32
|
};
|
|
46
33
|
|
|
47
|
-
function
|
|
48
|
-
const args = process.argv.slice(2);
|
|
34
|
+
function parseLocalArgs(argv) {
|
|
49
35
|
const get = (flag, def) => {
|
|
50
|
-
const i =
|
|
51
|
-
return i >= 0 && i + 1 <
|
|
36
|
+
const i = argv.indexOf(flag);
|
|
37
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def;
|
|
52
38
|
};
|
|
53
39
|
return {
|
|
54
40
|
subject: get("--subject", "unknown"),
|
|
@@ -59,7 +45,7 @@ function parseArgs() {
|
|
|
59
45
|
weight: parseFloat(get("--weight", "0.55")),
|
|
60
46
|
start: parseFloat(get("--start", "0.0")),
|
|
61
47
|
end: parseFloat(get("--end", "0.8")),
|
|
62
|
-
dryRun:
|
|
48
|
+
dryRun: argv.includes("--dry-run"),
|
|
63
49
|
};
|
|
64
50
|
}
|
|
65
51
|
|
|
@@ -67,7 +53,6 @@ function buildIPAdapterWorkflow(prompt, negativePrompt, seed, refImageName, weig
|
|
|
67
53
|
const nodes = {};
|
|
68
54
|
let nextId = 1;
|
|
69
55
|
|
|
70
|
-
// Checkpoint
|
|
71
56
|
const ckptId = String(nextId++);
|
|
72
57
|
nodes[ckptId] = {
|
|
73
58
|
class_type: "CheckpointLoaderSimple",
|
|
@@ -77,33 +62,30 @@ function buildIPAdapterWorkflow(prompt, negativePrompt, seed, refImageName, weig
|
|
|
77
62
|
let modelOut = [ckptId, 0];
|
|
78
63
|
let clipOut = [ckptId, 1];
|
|
79
64
|
|
|
80
|
-
// LoRA
|
|
81
65
|
for (const lora of DEFAULTS.loras) {
|
|
82
66
|
const loraId = String(nextId++);
|
|
83
67
|
nodes[loraId] = {
|
|
84
68
|
class_type: "LoraLoader",
|
|
85
69
|
inputs: {
|
|
86
|
-
model: modelOut,
|
|
87
|
-
clip: clipOut,
|
|
70
|
+
model: modelOut, clip: clipOut,
|
|
88
71
|
lora_name: lora.name,
|
|
89
|
-
strength_model: lora.weight,
|
|
90
|
-
strength_clip: lora.weight,
|
|
72
|
+
strength_model: lora.weight, strength_clip: lora.weight,
|
|
91
73
|
},
|
|
92
74
|
};
|
|
93
75
|
modelOut = [loraId, 0];
|
|
94
76
|
clipOut = [loraId, 1];
|
|
95
77
|
}
|
|
96
78
|
|
|
97
|
-
//
|
|
79
|
+
// CLIP Vision
|
|
98
80
|
const clipVisionId = String(nextId++);
|
|
99
81
|
nodes[clipVisionId] = {
|
|
100
82
|
class_type: "CLIPVisionLoader",
|
|
101
83
|
inputs: { clip_name: DEFAULTS.clip_vision_model },
|
|
102
84
|
};
|
|
103
85
|
|
|
104
|
-
//
|
|
105
|
-
const
|
|
106
|
-
nodes[
|
|
86
|
+
// IP-Adapter model
|
|
87
|
+
const ipaLoaderId = String(nextId++);
|
|
88
|
+
nodes[ipaLoaderId] = {
|
|
107
89
|
class_type: "IPAdapterModelLoader",
|
|
108
90
|
inputs: { ipadapter_file: DEFAULTS.ipadapter_model },
|
|
109
91
|
};
|
|
@@ -115,47 +97,43 @@ function buildIPAdapterWorkflow(prompt, negativePrompt, seed, refImageName, weig
|
|
|
115
97
|
inputs: { image: refImageName },
|
|
116
98
|
};
|
|
117
99
|
|
|
118
|
-
// Apply IP-Adapter
|
|
100
|
+
// Apply IP-Adapter
|
|
119
101
|
const ipaApplyId = String(nextId++);
|
|
120
102
|
nodes[ipaApplyId] = {
|
|
121
103
|
class_type: "IPAdapterAdvanced",
|
|
122
104
|
inputs: {
|
|
123
105
|
model: modelOut,
|
|
124
|
-
ipadapter: [
|
|
125
|
-
image: [refImgId, 0],
|
|
106
|
+
ipadapter: [ipaLoaderId, 0],
|
|
126
107
|
clip_vision: [clipVisionId, 0],
|
|
127
|
-
|
|
108
|
+
image: [refImgId, 0],
|
|
109
|
+
weight,
|
|
128
110
|
weight_type: "linear",
|
|
129
111
|
combine_embeds: "concat",
|
|
112
|
+
embeds_scaling: "V only",
|
|
130
113
|
start_at: start,
|
|
131
114
|
end_at: end,
|
|
132
|
-
embeds_scaling: "V only",
|
|
133
115
|
},
|
|
134
116
|
};
|
|
135
117
|
modelOut = [ipaApplyId, 0];
|
|
136
118
|
|
|
137
|
-
// Positive prompt
|
|
138
119
|
const posId = String(nextId++);
|
|
139
120
|
nodes[posId] = {
|
|
140
121
|
class_type: "CLIPTextEncode",
|
|
141
122
|
inputs: { text: prompt, clip: clipOut },
|
|
142
123
|
};
|
|
143
124
|
|
|
144
|
-
// Negative prompt
|
|
145
125
|
const negId = String(nextId++);
|
|
146
126
|
nodes[negId] = {
|
|
147
127
|
class_type: "CLIPTextEncode",
|
|
148
128
|
inputs: { text: negativePrompt, clip: clipOut },
|
|
149
129
|
};
|
|
150
130
|
|
|
151
|
-
// Empty latent
|
|
152
131
|
const latentId = String(nextId++);
|
|
153
132
|
nodes[latentId] = {
|
|
154
133
|
class_type: "EmptyLatentImage",
|
|
155
134
|
inputs: { width: DEFAULTS.width, height: DEFAULTS.height, batch_size: 1 },
|
|
156
135
|
};
|
|
157
136
|
|
|
158
|
-
// KSampler
|
|
159
137
|
const samplerId = String(nextId++);
|
|
160
138
|
nodes[samplerId] = {
|
|
161
139
|
class_type: "KSampler",
|
|
@@ -173,14 +151,12 @@ function buildIPAdapterWorkflow(prompt, negativePrompt, seed, refImageName, weig
|
|
|
173
151
|
},
|
|
174
152
|
};
|
|
175
153
|
|
|
176
|
-
// VAE Decode
|
|
177
154
|
const vaeId = String(nextId++);
|
|
178
155
|
nodes[vaeId] = {
|
|
179
156
|
class_type: "VAEDecode",
|
|
180
157
|
inputs: { samples: [samplerId, 0], vae: [ckptId, 2] },
|
|
181
158
|
};
|
|
182
159
|
|
|
183
|
-
// Save
|
|
184
160
|
const saveId = String(nextId++);
|
|
185
161
|
nodes[saveId] = {
|
|
186
162
|
class_type: "SaveImage",
|
|
@@ -190,62 +166,18 @@ function buildIPAdapterWorkflow(prompt, negativePrompt, seed, refImageName, weig
|
|
|
190
166
|
return nodes;
|
|
191
167
|
}
|
|
192
168
|
|
|
193
|
-
async function
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
} catch {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const MAX_POLL_MS = 600_000; // 10 minutes
|
|
169
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
170
|
+
const projectName = getProjectName(argv);
|
|
171
|
+
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
172
|
+
const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
|
|
203
173
|
|
|
204
|
-
|
|
205
|
-
const clientId = `ipa-${Date.now()}`;
|
|
206
|
-
const res = await fetch(`${COMFY_URL}/prompt`, {
|
|
207
|
-
method: "POST",
|
|
208
|
-
headers: { "Content-Type": "application/json" },
|
|
209
|
-
body: JSON.stringify({ prompt: workflow, client_id: clientId }),
|
|
210
|
-
});
|
|
211
|
-
if (!res.ok) {
|
|
212
|
-
const text = await res.text();
|
|
213
|
-
throw new Error(`ComfyUI submit failed: ${res.status} ${text}`);
|
|
214
|
-
}
|
|
215
|
-
const { prompt_id: promptId } = await res.json();
|
|
216
|
-
const start = Date.now();
|
|
217
|
-
while (true) {
|
|
218
|
-
if (Date.now() - start > MAX_POLL_MS) throw new Error(`ComfyUI poll timeout after ${MAX_POLL_MS/1000}s for prompt ${promptId}`);
|
|
219
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
220
|
-
const histRes = await fetch(`${COMFY_URL}/history/${promptId}`);
|
|
221
|
-
if (!histRes.ok) continue;
|
|
222
|
-
const history = await histRes.json();
|
|
223
|
-
const entry = history[promptId];
|
|
224
|
-
if (!entry) continue;
|
|
225
|
-
if (entry.status?.completed) return entry;
|
|
226
|
-
if (entry.status?.status_str === "error") {
|
|
227
|
-
throw new Error(`ComfyUI error: ${JSON.stringify(entry.status)}`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function downloadImage(filename, subfolder, type = "output") {
|
|
233
|
-
const url = `${COMFY_URL}/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder || "")}&type=${type}`;
|
|
234
|
-
const res = await fetch(url);
|
|
235
|
-
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
236
|
-
return Buffer.from(await res.arrayBuffer());
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async function main() {
|
|
240
|
-
const opts = parseArgs();
|
|
174
|
+
const opts = parseLocalArgs(argv);
|
|
241
175
|
|
|
242
176
|
if (!opts.refPath) {
|
|
243
|
-
|
|
244
|
-
process.exit(1);
|
|
177
|
+
throw new Error("--ref <path> is required");
|
|
245
178
|
}
|
|
246
179
|
if (!opts.prompt) {
|
|
247
|
-
|
|
248
|
-
process.exit(1);
|
|
180
|
+
throw new Error("--prompt is required");
|
|
249
181
|
}
|
|
250
182
|
|
|
251
183
|
const negative = opts.negative ||
|
|
@@ -259,33 +191,21 @@ async function main() {
|
|
|
259
191
|
console.log(` Seeds: ${opts.seeds}`);
|
|
260
192
|
console.log("");
|
|
261
193
|
|
|
194
|
+
const refFilename = `ipa_ref_${opts.subject}.png`;
|
|
195
|
+
|
|
262
196
|
if (!opts.dryRun) {
|
|
263
|
-
const online = await comfyHealth();
|
|
197
|
+
const online = await comfyHealth(COMFY_URL);
|
|
264
198
|
if (!online) {
|
|
265
|
-
|
|
266
|
-
process.exit(1);
|
|
199
|
+
throw new Error("ComfyUI not reachable");
|
|
267
200
|
}
|
|
268
201
|
console.log("\x1b[32m+\x1b[0m ComfyUI online");
|
|
269
202
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const refFilename = `ipa_ref_${opts.subject}.png`;
|
|
273
|
-
const formData = new FormData();
|
|
274
|
-
formData.append("image", new Blob([refData], { type: "image/png" }), refFilename);
|
|
275
|
-
formData.append("overwrite", "true");
|
|
276
|
-
const uploadRes = await fetch(`${COMFY_URL}/upload/image`, { method: "POST", body: formData });
|
|
277
|
-
if (!uploadRes.ok) {
|
|
278
|
-
console.error(`Reference upload failed: ${uploadRes.status}`);
|
|
279
|
-
process.exit(1);
|
|
280
|
-
}
|
|
281
|
-
const uploadResult = await uploadRes.json();
|
|
282
|
-
console.log(`\x1b[32m+\x1b[0m Reference uploaded: ${uploadResult.name}`);
|
|
203
|
+
await uploadImage(join(GAME_ROOT, opts.refPath), refFilename, COMFY_URL);
|
|
204
|
+
console.log(`\x1b[32m+\x1b[0m Reference uploaded: ${refFilename}`);
|
|
283
205
|
}
|
|
284
206
|
|
|
285
207
|
await mkdir(join(GAME_ROOT, "outputs/candidates"), { recursive: true });
|
|
286
208
|
|
|
287
|
-
const refFilename = `ipa_ref_${opts.subject}.png`;
|
|
288
|
-
|
|
289
209
|
for (let si = 0; si < opts.seeds; si++) {
|
|
290
210
|
const seed = DEFAULTS.base_seed + si;
|
|
291
211
|
const assetId = `${opts.subject}_ipa_s${si}`;
|
|
@@ -302,7 +222,7 @@ async function main() {
|
|
|
302
222
|
);
|
|
303
223
|
|
|
304
224
|
try {
|
|
305
|
-
const result = await submitAndWait(workflow);
|
|
225
|
+
const result = await submitAndWait(workflow, COMFY_URL, { clientPrefix: 'ipa' });
|
|
306
226
|
const elapsed = Date.now() - startMs;
|
|
307
227
|
|
|
308
228
|
const outputs = result.outputs || {};
|
|
@@ -321,7 +241,7 @@ async function main() {
|
|
|
321
241
|
continue;
|
|
322
242
|
}
|
|
323
243
|
|
|
324
|
-
const imgData = await downloadImage(imageFile, imageSubfolder);
|
|
244
|
+
const imgData = await downloadImage(imageFile, imageSubfolder, COMFY_URL);
|
|
325
245
|
await writeFile(join(GAME_ROOT, destPath), imgData);
|
|
326
246
|
console.log(` \x1b[32m+\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
|
|
327
247
|
} catch (err) {
|
|
@@ -333,7 +253,10 @@ async function main() {
|
|
|
333
253
|
if (opts.dryRun) console.log(" (dry run)");
|
|
334
254
|
}
|
|
335
255
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
256
|
+
// Direct execution guard
|
|
257
|
+
if (process.argv[1] && (process.argv[1].endsWith('generate-ipadapter.js') || process.argv[1].endsWith('generate-ipadapter'))) {
|
|
258
|
+
run().catch((err) => {
|
|
259
|
+
console.error(err.message || err);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
});
|
|
262
|
+
}
|