@mcptoolshop/style-dataset-lab 1.2.0 → 2.0.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 +45 -0
- package/README.es.md +94 -54
- package/README.fr.md +86 -46
- package/README.hi.md +86 -46
- package/README.it.md +93 -53
- package/README.ja.md +86 -46
- package/README.md +95 -116
- package/README.pt-BR.md +86 -46
- package/README.zh.md +90 -50
- package/bin/sdlab.js +103 -0
- package/lib/args.js +106 -0
- package/lib/comfyui.js +109 -0
- package/lib/config.js +104 -0
- package/lib/deprecation.js +14 -0
- package/lib/paths.js +38 -0
- package/lib/records.js +69 -0
- package/package.json +13 -4
- package/scripts/bulk-curate-wave2-5.js +1 -1
- package/scripts/bulk-curate-waves11-18.js +1 -1
- package/scripts/canon-bind.js +268 -332
- package/scripts/compare.js +40 -30
- package/scripts/curate-wave25.js +1 -1
- package/scripts/curate.js +34 -28
- package/scripts/doctor.js +321 -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 +226 -0
- package/scripts/migrate-records.js +15 -21
- package/scripts/painterly-test.js +20 -47
- package/scripts/painterly.js +27 -106
- 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,29 +9,14 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
11
|
* node scripts/generate-controlnet.js --subject naia --guide inputs/control-guides/naia_structural_guide.png --seeds 4
|
|
12
|
-
*
|
|
13
|
-
* node scripts/generate-controlnet.js --subject naia --guide ... --dry-run
|
|
14
|
-
*
|
|
15
|
-
* Options:
|
|
16
|
-
* --subject NAME Subject name (used for output file naming)
|
|
17
|
-
* --guide PATH Path to structural guide image (white-on-black silhouette)
|
|
18
|
-
* --prompt TEXT Generation prompt (or reads from --prompt-file)
|
|
19
|
-
* --prompt-file F JSON file with {prompt, negative} fields
|
|
20
|
-
* --negative TEXT Negative prompt override
|
|
21
|
-
* --seeds N Number of seeds (default: 4)
|
|
22
|
-
* --weight W ControlNet weight (default: 0.70)
|
|
23
|
-
* --guidance-end E ControlNet guidance end (default: 0.65)
|
|
24
|
-
* --model M ControlNet model filename (default: controlnet-canny-sdxl-1.0.safetensors)
|
|
25
|
-
* --dry-run Print workflow without running
|
|
12
|
+
* sdlab generate:controlnet --subject naia --guide <path> --seeds 4 --project star-freight
|
|
26
13
|
*/
|
|
27
14
|
|
|
28
15
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
29
16
|
import { join } from "node:path";
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
|
|
34
|
-
const GAME_ROOT = join(REPO_ROOT, 'games', GAME);
|
|
17
|
+
import { getProjectName } from "../lib/args.js";
|
|
18
|
+
import { REPO_ROOT } from "../lib/paths.js";
|
|
19
|
+
import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
|
|
35
20
|
|
|
36
21
|
const DEFAULTS = {
|
|
37
22
|
checkpoint: "dreamshaperXL_v21TurboDPMSDE.safetensors",
|
|
@@ -45,11 +30,10 @@ const DEFAULTS = {
|
|
|
45
30
|
base_seed: 27100,
|
|
46
31
|
};
|
|
47
32
|
|
|
48
|
-
function
|
|
49
|
-
const args = process.argv.slice(2);
|
|
33
|
+
function parseLocalArgs(argv) {
|
|
50
34
|
const get = (flag, def) => {
|
|
51
|
-
const i =
|
|
52
|
-
return i >= 0 && i + 1 <
|
|
35
|
+
const i = argv.indexOf(flag);
|
|
36
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def;
|
|
53
37
|
};
|
|
54
38
|
return {
|
|
55
39
|
subject: get("--subject", "unknown"),
|
|
@@ -61,7 +45,7 @@ function parseArgs() {
|
|
|
61
45
|
weight: parseFloat(get("--weight", "0.70")),
|
|
62
46
|
guidanceEnd: parseFloat(get("--guidance-end", "0.65")),
|
|
63
47
|
controlModel: get("--model", "controlnet-canny-sdxl-1.0.safetensors"),
|
|
64
|
-
dryRun:
|
|
48
|
+
dryRun: argv.includes("--dry-run"),
|
|
65
49
|
};
|
|
66
50
|
}
|
|
67
51
|
|
|
@@ -69,7 +53,6 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
|
|
|
69
53
|
const nodes = {};
|
|
70
54
|
let nextId = 1;
|
|
71
55
|
|
|
72
|
-
// Checkpoint
|
|
73
56
|
const ckptId = String(nextId++);
|
|
74
57
|
nodes[ckptId] = {
|
|
75
58
|
class_type: "CheckpointLoaderSimple",
|
|
@@ -79,17 +62,14 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
|
|
|
79
62
|
let modelOut = [ckptId, 0];
|
|
80
63
|
let clipOut = [ckptId, 1];
|
|
81
64
|
|
|
82
|
-
// LoRA
|
|
83
65
|
for (const lora of DEFAULTS.loras) {
|
|
84
66
|
const loraId = String(nextId++);
|
|
85
67
|
nodes[loraId] = {
|
|
86
68
|
class_type: "LoraLoader",
|
|
87
69
|
inputs: {
|
|
88
|
-
model: modelOut,
|
|
89
|
-
clip: clipOut,
|
|
70
|
+
model: modelOut, clip: clipOut,
|
|
90
71
|
lora_name: lora.name,
|
|
91
|
-
strength_model: lora.weight,
|
|
92
|
-
strength_clip: lora.weight,
|
|
72
|
+
strength_model: lora.weight, strength_clip: lora.weight,
|
|
93
73
|
},
|
|
94
74
|
};
|
|
95
75
|
modelOut = [loraId, 0];
|
|
@@ -97,34 +77,33 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
|
|
|
97
77
|
}
|
|
98
78
|
|
|
99
79
|
// Load guide image
|
|
100
|
-
const
|
|
101
|
-
nodes[
|
|
80
|
+
const guideId = String(nextId++);
|
|
81
|
+
nodes[guideId] = {
|
|
102
82
|
class_type: "LoadImage",
|
|
103
83
|
inputs: { image: guideImagePath },
|
|
104
84
|
};
|
|
105
85
|
|
|
106
|
-
//
|
|
86
|
+
// ControlNet loader
|
|
107
87
|
const cnLoadId = String(nextId++);
|
|
108
88
|
nodes[cnLoadId] = {
|
|
109
89
|
class_type: "ControlNetLoader",
|
|
110
90
|
inputs: { control_net_name: controlModel },
|
|
111
91
|
};
|
|
112
92
|
|
|
113
|
-
//
|
|
93
|
+
// Text encoders
|
|
114
94
|
const posId = String(nextId++);
|
|
115
95
|
nodes[posId] = {
|
|
116
96
|
class_type: "CLIPTextEncode",
|
|
117
97
|
inputs: { text: prompt, clip: clipOut },
|
|
118
98
|
};
|
|
119
99
|
|
|
120
|
-
// Negative prompt
|
|
121
100
|
const negId = String(nextId++);
|
|
122
101
|
nodes[negId] = {
|
|
123
102
|
class_type: "CLIPTextEncode",
|
|
124
103
|
inputs: { text: negativePrompt, clip: clipOut },
|
|
125
104
|
};
|
|
126
105
|
|
|
127
|
-
// Apply ControlNet
|
|
106
|
+
// Apply ControlNet
|
|
128
107
|
const cnApplyId = String(nextId++);
|
|
129
108
|
nodes[cnApplyId] = {
|
|
130
109
|
class_type: "ControlNetApplyAdvanced",
|
|
@@ -132,7 +111,7 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
|
|
|
132
111
|
positive: [posId, 0],
|
|
133
112
|
negative: [negId, 0],
|
|
134
113
|
control_net: [cnLoadId, 0],
|
|
135
|
-
image: [
|
|
114
|
+
image: [guideId, 0],
|
|
136
115
|
strength: weight,
|
|
137
116
|
start_percent: 0.0,
|
|
138
117
|
end_percent: guidanceEnd,
|
|
@@ -171,7 +150,6 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
|
|
|
171
150
|
inputs: { samples: [samplerId, 0], vae: [ckptId, 2] },
|
|
172
151
|
};
|
|
173
152
|
|
|
174
|
-
// Save
|
|
175
153
|
const saveId = String(nextId++);
|
|
176
154
|
nodes[saveId] = {
|
|
177
155
|
class_type: "SaveImage",
|
|
@@ -181,61 +159,15 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
|
|
|
181
159
|
return nodes;
|
|
182
160
|
}
|
|
183
161
|
|
|
184
|
-
async function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
} catch {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const MAX_POLL_MS = 600_000; // 10 minutes
|
|
162
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
163
|
+
const projectName = getProjectName(argv);
|
|
164
|
+
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
165
|
+
const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
|
|
194
166
|
|
|
195
|
-
|
|
196
|
-
const clientId = `cn-${Date.now()}`;
|
|
197
|
-
const res = await fetch(`${COMFY_URL}/prompt`, {
|
|
198
|
-
method: "POST",
|
|
199
|
-
headers: { "Content-Type": "application/json" },
|
|
200
|
-
body: JSON.stringify({ prompt: workflow, client_id: clientId }),
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
if (!res.ok) {
|
|
204
|
-
const text = await res.text();
|
|
205
|
-
throw new Error(`ComfyUI submit failed: ${res.status} ${text}`);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const { prompt_id: promptId } = await res.json();
|
|
209
|
-
|
|
210
|
-
const start = Date.now();
|
|
211
|
-
while (true) {
|
|
212
|
-
if (Date.now() - start > MAX_POLL_MS) throw new Error(`ComfyUI poll timeout after ${MAX_POLL_MS/1000}s for prompt ${promptId}`);
|
|
213
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
214
|
-
const histRes = await fetch(`${COMFY_URL}/history/${promptId}`);
|
|
215
|
-
if (!histRes.ok) continue;
|
|
216
|
-
const history = await histRes.json();
|
|
217
|
-
const entry = history[promptId];
|
|
218
|
-
if (!entry) continue;
|
|
219
|
-
if (entry.status?.completed) return entry;
|
|
220
|
-
if (entry.status?.status_str === "error") {
|
|
221
|
-
throw new Error(`ComfyUI error: ${JSON.stringify(entry.status)}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function downloadImage(filename, subfolder, type = "output") {
|
|
227
|
-
const url = `${COMFY_URL}/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder || "")}&type=${type}`;
|
|
228
|
-
const res = await fetch(url);
|
|
229
|
-
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
230
|
-
return Buffer.from(await res.arrayBuffer());
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function main() {
|
|
234
|
-
const opts = parseArgs();
|
|
167
|
+
const opts = parseLocalArgs(argv);
|
|
235
168
|
|
|
236
169
|
if (!opts.guidePath) {
|
|
237
|
-
|
|
238
|
-
process.exit(1);
|
|
170
|
+
throw new Error("--guide <path> is required");
|
|
239
171
|
}
|
|
240
172
|
|
|
241
173
|
// Resolve prompt
|
|
@@ -249,8 +181,7 @@ async function main() {
|
|
|
249
181
|
}
|
|
250
182
|
|
|
251
183
|
if (!prompt) {
|
|
252
|
-
|
|
253
|
-
process.exit(1);
|
|
184
|
+
throw new Error("--prompt or --prompt-file required");
|
|
254
185
|
}
|
|
255
186
|
|
|
256
187
|
if (!negative) {
|
|
@@ -267,38 +198,19 @@ async function main() {
|
|
|
267
198
|
console.log("");
|
|
268
199
|
|
|
269
200
|
if (!opts.dryRun) {
|
|
270
|
-
const online = await comfyHealth();
|
|
201
|
+
const online = await comfyHealth(COMFY_URL);
|
|
271
202
|
if (!online) {
|
|
272
|
-
|
|
273
|
-
process.exit(1);
|
|
203
|
+
throw new Error("ComfyUI not reachable at " + COMFY_URL);
|
|
274
204
|
}
|
|
275
205
|
console.log("\x1b[32m+\x1b[0m ComfyUI online");
|
|
276
206
|
}
|
|
277
207
|
|
|
278
|
-
// The guide image needs to be in ComfyUI's input directory
|
|
279
|
-
// Copy it there
|
|
280
208
|
const guideFullPath = join(GAME_ROOT, opts.guidePath);
|
|
281
|
-
const guideData = await readFile(guideFullPath);
|
|
282
209
|
const guideFilename = `cn_guide_${opts.subject}.png`;
|
|
283
210
|
|
|
284
211
|
if (!opts.dryRun) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
formData.append("image", new Blob([guideData], { type: "image/png" }), guideFilename);
|
|
288
|
-
formData.append("overwrite", "true");
|
|
289
|
-
|
|
290
|
-
const uploadRes = await fetch(`${COMFY_URL}/upload/image`, {
|
|
291
|
-
method: "POST",
|
|
292
|
-
body: formData,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
if (!uploadRes.ok) {
|
|
296
|
-
const text = await uploadRes.text();
|
|
297
|
-
console.error(`Guide upload failed: ${uploadRes.status} ${text}`);
|
|
298
|
-
process.exit(1);
|
|
299
|
-
}
|
|
300
|
-
const uploadResult = await uploadRes.json();
|
|
301
|
-
console.log(`\x1b[32m+\x1b[0m Guide uploaded: ${uploadResult.name}`);
|
|
212
|
+
await uploadImage(guideFullPath, guideFilename, COMFY_URL);
|
|
213
|
+
console.log(`\x1b[32m+\x1b[0m Guide uploaded: ${guideFilename}`);
|
|
302
214
|
}
|
|
303
215
|
|
|
304
216
|
await mkdir(join(GAME_ROOT, "outputs/candidates"), { recursive: true });
|
|
@@ -320,7 +232,7 @@ async function main() {
|
|
|
320
232
|
);
|
|
321
233
|
|
|
322
234
|
try {
|
|
323
|
-
const result = await submitAndWait(workflow);
|
|
235
|
+
const result = await submitAndWait(workflow, COMFY_URL, { clientPrefix: 'cn' });
|
|
324
236
|
const elapsed = Date.now() - startMs;
|
|
325
237
|
|
|
326
238
|
const outputs = result.outputs || {};
|
|
@@ -339,7 +251,7 @@ async function main() {
|
|
|
339
251
|
continue;
|
|
340
252
|
}
|
|
341
253
|
|
|
342
|
-
const imgData = await downloadImage(imageFile, imageSubfolder);
|
|
254
|
+
const imgData = await downloadImage(imageFile, imageSubfolder, COMFY_URL);
|
|
343
255
|
await writeFile(join(GAME_ROOT, destPath), imgData);
|
|
344
256
|
console.log(` \x1b[32m+\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
|
|
345
257
|
} catch (err) {
|
|
@@ -351,7 +263,10 @@ async function main() {
|
|
|
351
263
|
if (opts.dryRun) console.log(" (dry run)");
|
|
352
264
|
}
|
|
353
265
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
266
|
+
// Direct execution guard
|
|
267
|
+
if (process.argv[1] && (process.argv[1].endsWith('generate-controlnet.js') || process.argv[1].endsWith('generate-controlnet'))) {
|
|
268
|
+
run().catch((err) => {
|
|
269
|
+
console.error(err.message || err);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
@@ -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
|
+
}
|