@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.
@@ -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, flags.mode);
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 (flags.subject) console.log(` Subject: ${flags.subject}`);
66
- if (flags.theme) console.log(` Theme: ${flags.theme}`);
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: flags.mode,
75
- subjectId: flags.subject,
76
- theme: flags.theme,
77
- assetRef: flags.asset,
108
+ modeId,
109
+ subjectId,
110
+ theme,
111
+ assetRef,
78
112
  });
79
113
 
80
- // 2. Create batch directory
81
- const { batchId, batchDir } = await createBatchDir(projectRoot);
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 (flags.subject) manifest.subject_id = flags.subject;
125
- if (flags.theme) manifest.theme = flags.theme;
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 });
@@ -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 result = await submitAndWait(nodes, COMFY_URL);
263
+ const submitResult = await submitAndWait(nodes, COMFY_URL);
245
264
  const elapsed = Date.now() - startMs;
246
265
 
247
- // Find output image
248
- const outputs = result.outputs || {};
249
- let imageFile = null;
250
- let imageSubfolder = "";
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(imageFile, imageSubfolder, COMFY_URL);
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
- 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`);
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,