@mcptoolshop/style-dataset-lab 3.0.1 → 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 +40 -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 +18 -0
- package/README.pt-BR.md +99 -8
- package/README.zh.md +100 -9
- package/bin/sdlab.js +10 -2
- package/lib/adapters/comfyui-runner.js +11 -15
- package/lib/batch-runs.js +62 -0
- package/lib/comfyui-output.js +70 -0
- package/lib/training-adapters.js +79 -0
- package/lib/training-packages.js +1 -14
- package/lib/training-profiles.js +10 -0
- package/package.json +20 -2
- package/scripts/batch-generate.js +56 -13
- package/scripts/generate.js +33 -21
|
@@ -15,6 +15,7 @@ import { compileBatch } from '../lib/batch-compiler.js';
|
|
|
15
15
|
import { getBatchMode } from '../lib/batch-modes.js';
|
|
16
16
|
import {
|
|
17
17
|
createBatchDir,
|
|
18
|
+
openBatchDirForResume,
|
|
18
19
|
executeBatchRuns,
|
|
19
20
|
saveBatchManifest,
|
|
20
21
|
} from '../lib/batch-runs.js';
|
|
@@ -31,20 +32,23 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
31
32
|
subject: { type: 'string' },
|
|
32
33
|
theme: { type: 'string' },
|
|
33
34
|
asset: { type: 'string' },
|
|
35
|
+
resume: { type: 'string' },
|
|
34
36
|
'dry-run': { type: 'boolean' },
|
|
35
37
|
json: { type: 'boolean' },
|
|
36
38
|
},
|
|
37
39
|
deprecated: { game: 'project' },
|
|
38
40
|
});
|
|
39
41
|
|
|
40
|
-
if (!flags.mode) {
|
|
42
|
+
if (!flags.mode && !flags.resume) {
|
|
41
43
|
console.log('Usage: sdlab batch generate --mode <id> [--subject <id>] [--theme <label>] [--project <name>]');
|
|
44
|
+
console.log(' sdlab batch generate --resume <batch_id> [--project <name>]');
|
|
42
45
|
console.log('');
|
|
43
46
|
console.log('Options:');
|
|
44
|
-
console.log(' --mode <id> Batch mode (required)');
|
|
47
|
+
console.log(' --mode <id> Batch mode (required unless --resume is given)');
|
|
45
48
|
console.log(' --subject <id> Subject for subject-driven modes');
|
|
46
49
|
console.log(' --theme <label> Theme label for environment modes');
|
|
47
50
|
console.log(' --asset <id> Training asset reference');
|
|
51
|
+
console.log(' --resume <id> Resume an existing batch — re-runs only failed/missing slots');
|
|
48
52
|
console.log(' --dry-run Prepare batch without submitting to ComfyUI');
|
|
49
53
|
console.log(' --json Output manifest as JSON');
|
|
50
54
|
return;
|
|
@@ -53,17 +57,47 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
53
57
|
const projectName = flags.project || getProjectName(argv);
|
|
54
58
|
const projectRoot = getProjectRoot(projectName);
|
|
55
59
|
const dryRun = flags['dry-run'] || false;
|
|
60
|
+
const resumeId = flags.resume || null;
|
|
61
|
+
|
|
62
|
+
// Resume short-circuit: load prior manifest and reuse its mode_id + subject/theme
|
|
63
|
+
// so the user only needs to pass --resume <batch_id>.
|
|
64
|
+
let batchId;
|
|
65
|
+
let batchDir;
|
|
66
|
+
let priorResults = [];
|
|
67
|
+
let resumedMode = null;
|
|
68
|
+
let resumedSubject = null;
|
|
69
|
+
let resumedTheme = null;
|
|
70
|
+
let resumedAsset = null;
|
|
71
|
+
if (resumeId) {
|
|
72
|
+
const opened = openBatchDirForResume(projectRoot, resumeId);
|
|
73
|
+
batchId = opened.batchId;
|
|
74
|
+
batchDir = opened.batchDir;
|
|
75
|
+
priorResults = opened.priorResults;
|
|
76
|
+
resumedMode = opened.priorManifest.mode_id || null;
|
|
77
|
+
resumedSubject = opened.priorManifest.subject_id || null;
|
|
78
|
+
resumedTheme = opened.priorManifest.theme || null;
|
|
79
|
+
resumedAsset = opened.priorManifest.asset_ref || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const modeId = flags.mode || resumedMode;
|
|
83
|
+
const subjectId = flags.subject || resumedSubject;
|
|
84
|
+
const theme = flags.theme || resumedTheme;
|
|
85
|
+
const assetRef = flags.asset || resumedAsset;
|
|
56
86
|
|
|
57
87
|
// Load mode for display
|
|
58
|
-
const mode = getBatchMode(projectRoot,
|
|
88
|
+
const mode = getBatchMode(projectRoot, modeId);
|
|
59
89
|
|
|
60
90
|
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m batch generate`);
|
|
61
91
|
console.log(` Project: ${projectName}`);
|
|
62
92
|
console.log(` Mode: ${mode.label} (${mode.mode_id})`);
|
|
63
93
|
console.log(` Type: ${mode.batch_type}`);
|
|
64
94
|
console.log(` Slots: ${mode.variant_plan.length}`);
|
|
65
|
-
if (
|
|
66
|
-
if (
|
|
95
|
+
if (subjectId) console.log(` Subject: ${subjectId}`);
|
|
96
|
+
if (theme) console.log(` Theme: ${theme}`);
|
|
97
|
+
if (resumeId) {
|
|
98
|
+
const completed = priorResults.filter(r => r && r.status === 'ok').length;
|
|
99
|
+
console.log(` Resume: ${resumeId} (${completed}/${priorResults.length} slots already complete)`);
|
|
100
|
+
}
|
|
67
101
|
if (dryRun) console.log(' ⚠ DRY RUN');
|
|
68
102
|
console.log('');
|
|
69
103
|
|
|
@@ -71,14 +105,18 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
71
105
|
const { baseBrief, slotBriefs } = await compileBatch({
|
|
72
106
|
projectRoot,
|
|
73
107
|
projectId: projectName,
|
|
74
|
-
modeId
|
|
75
|
-
subjectId
|
|
76
|
-
theme
|
|
77
|
-
assetRef
|
|
108
|
+
modeId,
|
|
109
|
+
subjectId,
|
|
110
|
+
theme,
|
|
111
|
+
assetRef,
|
|
78
112
|
});
|
|
79
113
|
|
|
80
|
-
// 2. Create batch directory
|
|
81
|
-
|
|
114
|
+
// 2. Create or reuse batch directory
|
|
115
|
+
if (!resumeId) {
|
|
116
|
+
const created = await createBatchDir(projectRoot);
|
|
117
|
+
batchId = created.batchId;
|
|
118
|
+
batchDir = created.batchDir;
|
|
119
|
+
}
|
|
82
120
|
info('batch-generate', `Batch: ${batchId}`);
|
|
83
121
|
|
|
84
122
|
// 3. Execute runs
|
|
@@ -90,6 +128,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
90
128
|
mode,
|
|
91
129
|
slotBriefs,
|
|
92
130
|
dryRun,
|
|
131
|
+
priorResults,
|
|
93
132
|
});
|
|
94
133
|
const batchElapsedMs = Date.now() - batchStartMs;
|
|
95
134
|
if (!dryRun && results.length > 0) {
|
|
@@ -111,7 +150,9 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
111
150
|
label: r.label,
|
|
112
151
|
brief_id: r.brief_id,
|
|
113
152
|
run_id: r.run_id,
|
|
153
|
+
status: r.status,
|
|
114
154
|
selected_output: r.selected_output,
|
|
155
|
+
...(r.error ? { error: r.error } : {}),
|
|
115
156
|
})),
|
|
116
157
|
sheet_paths: [],
|
|
117
158
|
summary: {
|
|
@@ -121,8 +162,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
121
162
|
},
|
|
122
163
|
};
|
|
123
164
|
|
|
124
|
-
if (
|
|
125
|
-
if (
|
|
165
|
+
if (subjectId) manifest.subject_id = subjectId;
|
|
166
|
+
if (theme) manifest.theme = theme;
|
|
167
|
+
if (assetRef) manifest.asset_ref = assetRef;
|
|
168
|
+
if (resumeId) manifest.resumed_from = resumeId;
|
|
126
169
|
|
|
127
170
|
// 5. Render sheet
|
|
128
171
|
const sheetHtml = renderSheetHTML({ manifest, mode, projectRoot, batchDir });
|
package/scripts/generate.js
CHANGED
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
14
15
|
import { join } from "node:path";
|
|
15
16
|
import { parseArgs, getProjectName } from "../lib/args.js";
|
|
16
17
|
import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
|
|
17
18
|
import { readJsonFile } from "../lib/config.js";
|
|
18
19
|
import { runtimeError, handleCliError } from "../lib/errors.js";
|
|
19
20
|
import { comfyHealth, submitAndWait, downloadImage } from "../lib/comfyui.js";
|
|
21
|
+
import { pickOutputImage } from "../lib/comfyui-output.js";
|
|
20
22
|
import { info, result } from "../lib/log.js";
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -185,11 +187,13 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
185
187
|
project: { type: 'string' },
|
|
186
188
|
game: { type: 'string' },
|
|
187
189
|
'dry-run': { type: 'boolean' },
|
|
190
|
+
resume: { type: 'boolean' },
|
|
188
191
|
},
|
|
189
192
|
deprecated: { game: 'project' },
|
|
190
193
|
});
|
|
191
194
|
const packPath = parsed.positionals[0] || "inputs/prompts/rpg-icons-lane1.json";
|
|
192
195
|
const dryRun = parsed.flags['dry-run'] === true;
|
|
196
|
+
const resume = parsed.flags['resume'] === true;
|
|
193
197
|
|
|
194
198
|
const fullPackPath = resolveSafeProjectPath(GAME_ROOT, packPath, { flagName: 'pack-path' });
|
|
195
199
|
const pack = await readJsonFile(fullPackPath, { requiredFields: ['lane', 'subjects', 'variations', 'defaults'] });
|
|
@@ -215,6 +219,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
215
219
|
const d = pack.defaults;
|
|
216
220
|
let generated = 0;
|
|
217
221
|
let errors = 0;
|
|
222
|
+
let skipped = 0;
|
|
218
223
|
const totalExpected = pack.subjects.length * pack.variations.length;
|
|
219
224
|
const runStartMs = Date.now();
|
|
220
225
|
|
|
@@ -225,6 +230,20 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
225
230
|
const fullPrompt = `${pack.style_prefix}, ${subject.prompt}`;
|
|
226
231
|
const loras = variation.loras || [];
|
|
227
232
|
|
|
233
|
+
// --resume: skip subjects whose record + image are both already on disk.
|
|
234
|
+
// Seeds are deterministic per (base_seed, generated, variation), so
|
|
235
|
+
// increment `generated` for skipped slots to keep downstream seeds stable.
|
|
236
|
+
if (resume && !dryRun) {
|
|
237
|
+
const recordPath = join(GAME_ROOT, `records/${assetId}.json`);
|
|
238
|
+
const imagePath = join(GAME_ROOT, `outputs/candidates/${assetId}.png`);
|
|
239
|
+
if (existsSync(recordPath) && existsSync(imagePath)) {
|
|
240
|
+
console.log(` [${generated + 1}/${totalExpected}] ${assetId} \x1b[2m(resumed — skipped)\x1b[0m`);
|
|
241
|
+
generated++;
|
|
242
|
+
skipped++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
228
247
|
console.log(` [${generated + 1}/${totalExpected}] ${assetId} (seed: ${seed}, loras: ${loras.length})`);
|
|
229
248
|
|
|
230
249
|
if (dryRun) {
|
|
@@ -233,7 +252,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
233
252
|
}
|
|
234
253
|
|
|
235
254
|
const startMs = Date.now();
|
|
236
|
-
const { nodes } = buildWorkflow(
|
|
255
|
+
const { nodes, saveNodeId } = buildWorkflow(
|
|
237
256
|
fullPrompt, pack.negative,
|
|
238
257
|
d.checkpoint, loras, seed,
|
|
239
258
|
d.steps, d.cfg, d.sampler, d.scheduler,
|
|
@@ -241,29 +260,20 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
241
260
|
);
|
|
242
261
|
|
|
243
262
|
try {
|
|
244
|
-
const
|
|
263
|
+
const submitResult = await submitAndWait(nodes, COMFY_URL);
|
|
245
264
|
const elapsed = Date.now() - startMs;
|
|
246
265
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
for (const nodeOut of Object.values(outputs)) {
|
|
252
|
-
if (nodeOut.images && nodeOut.images.length > 0) {
|
|
253
|
-
imageFile = nodeOut.images[0].filename;
|
|
254
|
-
imageSubfolder = nodeOut.images[0].subfolder || "";
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (!imageFile) {
|
|
266
|
+
// Prefer the explicit SaveImage node from buildWorkflow; fall back
|
|
267
|
+
// to highest numeric node id (see lib/comfyui-output.js).
|
|
268
|
+
const picked = pickOutputImage(submitResult.outputs, { preferNodeId: saveNodeId });
|
|
269
|
+
if (!picked) {
|
|
260
270
|
console.log(` \x1b[33m⚠\x1b[0m No output image found`);
|
|
261
271
|
generated++;
|
|
262
272
|
continue;
|
|
263
273
|
}
|
|
264
274
|
|
|
265
275
|
// Download and save
|
|
266
|
-
const imgData = await downloadImage(
|
|
276
|
+
const imgData = await downloadImage(picked.filename, picked.subfolder, COMFY_URL);
|
|
267
277
|
const destPath = `outputs/candidates/${assetId}.png`;
|
|
268
278
|
await writeFile(join(GAME_ROOT, destPath), imgData);
|
|
269
279
|
|
|
@@ -322,12 +332,14 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
322
332
|
}
|
|
323
333
|
|
|
324
334
|
console.log("");
|
|
325
|
-
const succeeded = generated - errors;
|
|
335
|
+
const succeeded = generated - errors - skipped;
|
|
326
336
|
const totalElapsedMs = Date.now() - runStartMs;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
337
|
+
const skippedSuffix = skipped > 0 ? `, ${skipped} resumed` : '';
|
|
338
|
+
console.log(`\x1b[32m✓\x1b[0m Generated ${succeeded} candidates (${errors} errors${skippedSuffix})`);
|
|
339
|
+
if (!dryRun && generated > skipped) {
|
|
340
|
+
const ran = generated - skipped;
|
|
341
|
+
const avgMs = totalElapsedMs / ran;
|
|
342
|
+
console.log(` Total: ${formatEta(totalElapsedMs)} — avg ${formatEta(avgMs)}/item over ${ran} new`);
|
|
331
343
|
}
|
|
332
344
|
if (dryRun) console.log(" (dry run — no images produced)");
|
|
333
345
|
// Always print the output directory so scripted pipelines can parse it,
|