@mcptoolshop/style-dataset-lab 3.1.0 → 3.3.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 +61 -0
- package/README.es.md +96 -63
- package/README.fr.md +97 -64
- package/README.hi.md +108 -77
- package/README.it.md +97 -64
- package/README.ja.md +105 -72
- package/README.md +33 -0
- package/README.pt-BR.md +94 -61
- package/README.zh.md +98 -65
- package/bin/sdlab.js +71 -0
- package/lib/adapters/ai-toolkit.js +209 -0
- package/lib/adapters/comfyui-runner.js +36 -2
- package/lib/adapters/comfyui-workflows.js +133 -2
- package/lib/adapters/diffusers-lora.js +46 -66
- package/lib/adapters/generic-image-caption.js +22 -50
- package/lib/canon-build/build.js +450 -0
- package/lib/canon-build/emitters/base.js +122 -0
- package/lib/canon-build/emitters/character.js +167 -0
- package/lib/canon-build/emitters/deity.js +165 -0
- package/lib/canon-build/emitters/faction.js +161 -0
- package/lib/canon-build/emitters/index.js +38 -0
- package/lib/canon-build/emitters/location.js +153 -0
- package/lib/canon-build/emitters/monster.js +131 -0
- package/lib/canon-build/emitters/relic.js +142 -0
- package/lib/canon-build/emitters/ship.js +154 -0
- package/lib/canon-build/emitters/species.js +165 -0
- package/lib/canon-build/frontmatter.js +76 -0
- package/lib/canon-build/lane-resolve.js +64 -0
- package/lib/canon-build/load-config.js +117 -0
- package/lib/canon-build/load-entry.js +62 -0
- package/lib/canon-build/load-schema.js +46 -0
- package/lib/captions.js +197 -0
- package/lib/config.js +19 -0
- package/lib/critique-engine.js +46 -1
- package/lib/critique-render.js +45 -0
- package/lib/freeze-events.js +109 -0
- package/lib/freeze-gate.js +208 -0
- package/lib/freeze-stamp.js +161 -0
- package/lib/rows.js +228 -0
- package/lib/run-manifest.js +308 -0
- package/lib/snapshot.js +7 -1
- package/lib/training-adapters.js +5 -0
- package/lib/training-packages.js +24 -18
- package/lib/training-profiles.js +33 -0
- package/lib/verifier.js +67 -0
- package/package.json +12 -3
- package/scripts/_batch_cut.py +71 -0
- package/scripts/_seam_cutout.py +74 -0
- package/scripts/canon-build.js +88 -0
- package/scripts/canon-drift.js +193 -0
- package/scripts/canon-freeze-status.js +85 -0
- package/scripts/canon-freeze.js +211 -0
- package/scripts/canon-unfreeze.js +137 -0
- package/scripts/critique.js +32 -7
- package/scripts/curate.js +33 -0
- package/scripts/generate-controlnet.js +12 -0
- package/scripts/generate-identity.js +20 -4
- package/scripts/generate-ipadapter.js +11 -0
- package/scripts/generate.js +262 -102
- package/scripts/qwen_generate.py +327 -0
- package/scripts/run-generate.js +3 -0
package/scripts/generate.js
CHANGED
|
@@ -15,10 +15,12 @@ import { existsSync } from "node:fs";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { parseArgs, getProjectName } from "../lib/args.js";
|
|
17
17
|
import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
|
|
18
|
-
import { readJsonFile } from "../lib/config.js";
|
|
19
|
-
import { runtimeError, handleCliError } from "../lib/errors.js";
|
|
18
|
+
import { readJsonFile, loadProjectMeta, resolveGpuModel } from "../lib/config.js";
|
|
19
|
+
import { runtimeError, inputError, handleCliError } from "../lib/errors.js";
|
|
20
20
|
import { comfyHealth, submitAndWait, downloadImage } from "../lib/comfyui.js";
|
|
21
21
|
import { pickOutputImage } from "../lib/comfyui-output.js";
|
|
22
|
+
import { buildQwenGraph, QWEN_DEFAULTS } from "../lib/adapters/comfyui-workflows.js";
|
|
23
|
+
import { buildPinning, SEED_POLICIES, createHashCache, flushHashCache } from "../lib/run-manifest.js";
|
|
22
24
|
import { info, result } from "../lib/log.js";
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -177,6 +179,89 @@ function buildWorkflow(prompt, negativePrompt, checkpoint, loras, seed, steps, c
|
|
|
177
179
|
return { nodes, saveNodeId: saveId };
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Detect the input schema and normalize to a unified shape:
|
|
184
|
+
* { lane, style_prefix, negative, defaults, base, jobs:[{assetId, subjectId, variationId, prompt, seed, loras}] }
|
|
185
|
+
*
|
|
186
|
+
* Two accepted schemas (both produce jobs in subject-major order, so the legacy
|
|
187
|
+
* seed sequence stays byte-identical):
|
|
188
|
+
*
|
|
189
|
+
* • Legacy pack — top-level `lane` + `variations` (array) applied to every
|
|
190
|
+
* subject. seed = base_seed + jobIndex + (variation.seed_offset||0);
|
|
191
|
+
* assetId = `${subject.id}_${variation.id}`; `negative` is top-level.
|
|
192
|
+
*
|
|
193
|
+
* • Wave (what `sdlab init` scaffolds, e.g. example-wave.json) — each subject
|
|
194
|
+
* carries `variations` as an integer count; `negative` lives in `defaults`.
|
|
195
|
+
* seed = base_seed + jobIndex; assetId = `${subject.id}_v${v}`. This matches
|
|
196
|
+
* scripts/qwen_generate.py's seed/filename scheme.
|
|
197
|
+
*
|
|
198
|
+
* `base` resolves: input.base → input.defaults.base → project.json defaults.base
|
|
199
|
+
* → "sdxl". `"qwen-image"` routes to the non-anime Qwen graph.
|
|
200
|
+
*/
|
|
201
|
+
function normalizeGenerationInput(raw, projectMeta) {
|
|
202
|
+
if (!Array.isArray(raw.subjects) || raw.subjects.length === 0) {
|
|
203
|
+
throw inputError(
|
|
204
|
+
'INPUT_MISSING_FIELDS',
|
|
205
|
+
'Prompt pack/wave must have a non-empty "subjects" array.',
|
|
206
|
+
'See inputs/prompts/example-wave.json for the expected shape.'
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const defaults = raw.defaults || {};
|
|
210
|
+
const base = String(
|
|
211
|
+
raw.base || defaults.base || projectMeta?.defaults?.base || 'sdxl'
|
|
212
|
+
).toLowerCase();
|
|
213
|
+
const baseSeed = Number.isFinite(defaults.base_seed) ? defaults.base_seed : 0;
|
|
214
|
+
const stylePrefix = raw.style_prefix || '';
|
|
215
|
+
const compose = (p) => (stylePrefix ? `${stylePrefix}, ${p}` : p);
|
|
216
|
+
|
|
217
|
+
const jobs = [];
|
|
218
|
+
let k = 0;
|
|
219
|
+
|
|
220
|
+
if (Array.isArray(raw.variations)) {
|
|
221
|
+
// Legacy pack schema
|
|
222
|
+
if (!raw.lane) {
|
|
223
|
+
throw inputError(
|
|
224
|
+
'INPUT_MISSING_FIELDS',
|
|
225
|
+
'Legacy pack (top-level "variations" array) requires a "lane".',
|
|
226
|
+
'Add a "lane" string, or use the wave schema (per-subject integer "variations").'
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const negative = raw.negative ?? '';
|
|
230
|
+
for (const subject of raw.subjects) {
|
|
231
|
+
for (const variation of raw.variations) {
|
|
232
|
+
jobs.push({
|
|
233
|
+
assetId: `${subject.id}_${variation.id}`,
|
|
234
|
+
subjectId: subject.id,
|
|
235
|
+
variationId: variation.id,
|
|
236
|
+
prompt: compose(subject.prompt),
|
|
237
|
+
seed: baseSeed + k + (variation.seed_offset || 0),
|
|
238
|
+
loras: variation.loras || [],
|
|
239
|
+
});
|
|
240
|
+
k++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { lane: raw.lane, style_prefix: stylePrefix, negative, defaults, base, jobs, schema: 'pack' };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Wave schema
|
|
247
|
+
const negative = raw.negative ?? defaults.negative ?? '';
|
|
248
|
+
for (const subject of raw.subjects) {
|
|
249
|
+
const nvar = Number.isFinite(subject.variations) ? subject.variations : 1;
|
|
250
|
+
for (let v = 0; v < nvar; v++) {
|
|
251
|
+
jobs.push({
|
|
252
|
+
assetId: `${subject.id}_v${v}`,
|
|
253
|
+
subjectId: subject.id,
|
|
254
|
+
variationId: `v${v}`,
|
|
255
|
+
prompt: compose(subject.prompt),
|
|
256
|
+
seed: baseSeed + k,
|
|
257
|
+
loras: [],
|
|
258
|
+
});
|
|
259
|
+
k++;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { lane: raw.wave || raw.lane || 'default', style_prefix: stylePrefix, negative, defaults, base, jobs, schema: 'wave' };
|
|
263
|
+
}
|
|
264
|
+
|
|
180
265
|
export async function run(argv = process.argv.slice(2)) {
|
|
181
266
|
const projectName = getProjectName(argv);
|
|
182
267
|
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
@@ -188,21 +273,54 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
188
273
|
game: { type: 'string' },
|
|
189
274
|
'dry-run': { type: 'boolean' },
|
|
190
275
|
resume: { type: 'boolean' },
|
|
276
|
+
'hash-models': { type: 'boolean' },
|
|
191
277
|
},
|
|
192
278
|
deprecated: { game: 'project' },
|
|
193
279
|
});
|
|
194
280
|
const packPath = parsed.positionals[0] || "inputs/prompts/rpg-icons-lane1.json";
|
|
195
281
|
const dryRun = parsed.flags['dry-run'] === true;
|
|
196
282
|
const resume = parsed.flags['resume'] === true;
|
|
283
|
+
// PIN_PER_STEP: --hash-models opts into content hashing of the checkpoint/
|
|
284
|
+
// unet/clip/vae/LoRA files (best-effort, cached). Without it the pinning block
|
|
285
|
+
// still records comfy_workflow_sha + model name/size + seed_policy.
|
|
286
|
+
const hashModels = parsed.flags['hash-models'] === true;
|
|
287
|
+
const hashCache = hashModels ? createHashCache() : undefined;
|
|
197
288
|
|
|
198
289
|
const fullPackPath = resolveSafeProjectPath(GAME_ROOT, packPath, { flagName: 'pack-path' });
|
|
199
|
-
const
|
|
290
|
+
const raw = await readJsonFile(fullPackPath);
|
|
291
|
+
const projectMeta = loadProjectMeta(GAME_ROOT);
|
|
292
|
+
const gpuModel = resolveGpuModel(projectMeta?.defaults);
|
|
293
|
+
const pack = normalizeGenerationInput(raw, projectMeta);
|
|
294
|
+
|
|
295
|
+
const d = pack.defaults;
|
|
296
|
+
const base = pack.base;
|
|
297
|
+
const isQwen = base === 'qwen-image';
|
|
298
|
+
|
|
299
|
+
// Resolve effective sampler params once. Qwen applies its own defaults when a
|
|
300
|
+
// wave omits them; SDXL uses the pack defaults exactly as authored (unchanged).
|
|
301
|
+
const resolved = isQwen
|
|
302
|
+
? {
|
|
303
|
+
width: d.width || QWEN_DEFAULTS.width,
|
|
304
|
+
height: d.height || QWEN_DEFAULTS.height,
|
|
305
|
+
steps: d.steps || QWEN_DEFAULTS.steps,
|
|
306
|
+
cfg: d.cfg || QWEN_DEFAULTS.cfg,
|
|
307
|
+
sampler: d.sampler || QWEN_DEFAULTS.sampler,
|
|
308
|
+
scheduler: d.scheduler || QWEN_DEFAULTS.scheduler,
|
|
309
|
+
}
|
|
310
|
+
: {
|
|
311
|
+
width: d.width,
|
|
312
|
+
height: d.height,
|
|
313
|
+
steps: d.steps,
|
|
314
|
+
cfg: d.cfg,
|
|
315
|
+
sampler: d.sampler,
|
|
316
|
+
scheduler: d.scheduler,
|
|
317
|
+
};
|
|
200
318
|
|
|
201
319
|
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m generate`);
|
|
202
320
|
console.log(` Lane: ${pack.lane}`);
|
|
203
|
-
console.log(`
|
|
204
|
-
console.log(`
|
|
205
|
-
console.log(` Total candidates: ${pack.
|
|
321
|
+
console.log(` Base: ${base}${isQwen ? ' \x1b[2m(non-anime)\x1b[0m' : ''}`);
|
|
322
|
+
console.log(` Subjects: ${raw.subjects.length}`);
|
|
323
|
+
console.log(` Total candidates: ${pack.jobs.length}`);
|
|
206
324
|
console.log("");
|
|
207
325
|
|
|
208
326
|
if (!dryRun) {
|
|
@@ -216,121 +334,163 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
216
334
|
await mkdir(join(GAME_ROOT, "outputs/candidates"), { recursive: true });
|
|
217
335
|
await mkdir(join(GAME_ROOT, "records"), { recursive: true });
|
|
218
336
|
|
|
219
|
-
const d = pack.defaults;
|
|
220
337
|
let generated = 0;
|
|
221
338
|
let errors = 0;
|
|
222
339
|
let skipped = 0;
|
|
223
|
-
const totalExpected = pack.
|
|
340
|
+
const totalExpected = pack.jobs.length;
|
|
224
341
|
const runStartMs = Date.now();
|
|
225
342
|
|
|
226
|
-
for (const
|
|
227
|
-
|
|
228
|
-
const assetId = `${subject.id}_${variation.id}`;
|
|
229
|
-
const seed = (d.base_seed + generated) + (variation.seed_offset || 0);
|
|
230
|
-
const fullPrompt = `${pack.style_prefix}, ${subject.prompt}`;
|
|
231
|
-
const loras = variation.loras || [];
|
|
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
|
-
|
|
247
|
-
console.log(` [${generated + 1}/${totalExpected}] ${assetId} (seed: ${seed}, loras: ${loras.length})`);
|
|
343
|
+
for (const job of pack.jobs) {
|
|
344
|
+
const { assetId, prompt: fullPrompt, seed, loras } = job;
|
|
248
345
|
|
|
249
|
-
|
|
346
|
+
// --resume: skip jobs whose record + image are both already on disk.
|
|
347
|
+
// Seeds are precomputed per job index, so skipping never shifts later seeds.
|
|
348
|
+
if (resume && !dryRun) {
|
|
349
|
+
const recordPath = join(GAME_ROOT, `records/${assetId}.json`);
|
|
350
|
+
const imagePath = join(GAME_ROOT, `outputs/candidates/${assetId}.png`);
|
|
351
|
+
if (existsSync(recordPath) && existsSync(imagePath)) {
|
|
352
|
+
console.log(` [${generated + 1}/${totalExpected}] ${assetId} \x1b[2m(resumed — skipped)\x1b[0m`);
|
|
250
353
|
generated++;
|
|
354
|
+
skipped++;
|
|
251
355
|
continue;
|
|
252
356
|
}
|
|
357
|
+
}
|
|
253
358
|
|
|
254
|
-
|
|
255
|
-
const { nodes, saveNodeId } = buildWorkflow(
|
|
256
|
-
fullPrompt, pack.negative,
|
|
257
|
-
d.checkpoint, loras, seed,
|
|
258
|
-
d.steps, d.cfg, d.sampler, d.scheduler,
|
|
259
|
-
d.width, d.height,
|
|
260
|
-
);
|
|
359
|
+
console.log(` [${generated + 1}/${totalExpected}] ${assetId} (seed: ${seed}, loras: ${isQwen ? (d.loras || []).length : loras.length})`);
|
|
261
360
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
negative_prompt: pack.negative,
|
|
294
|
-
seed,
|
|
295
|
-
steps: d.steps,
|
|
296
|
-
cfg: d.cfg,
|
|
297
|
-
sampler: d.sampler,
|
|
298
|
-
scheduler: d.scheduler,
|
|
299
|
-
width: d.width,
|
|
300
|
-
height: d.height,
|
|
301
|
-
generation_time_ms: elapsed,
|
|
302
|
-
gpu_model: "RTX 5080",
|
|
303
|
-
batch_index: generated,
|
|
304
|
-
},
|
|
305
|
-
judgment: null,
|
|
306
|
-
canon: null,
|
|
307
|
-
iteration: null,
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
await writeFile(
|
|
311
|
-
join(GAME_ROOT, `records/${assetId}.json`),
|
|
312
|
-
JSON.stringify(record, null, 2),
|
|
361
|
+
if (dryRun) {
|
|
362
|
+
generated++;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const startMs = Date.now();
|
|
367
|
+
const { nodes, saveNodeId } = isQwen
|
|
368
|
+
? buildQwenGraph({
|
|
369
|
+
prompt: fullPrompt,
|
|
370
|
+
negativePrompt: pack.negative,
|
|
371
|
+
seed,
|
|
372
|
+
width: resolved.width,
|
|
373
|
+
height: resolved.height,
|
|
374
|
+
steps: resolved.steps,
|
|
375
|
+
cfg: resolved.cfg,
|
|
376
|
+
sampler: resolved.sampler,
|
|
377
|
+
scheduler: resolved.scheduler,
|
|
378
|
+
shift: d.shift ?? QWEN_DEFAULTS.shift,
|
|
379
|
+
unet: d.unet || QWEN_DEFAULTS.unet,
|
|
380
|
+
clip: d.clip || QWEN_DEFAULTS.clip,
|
|
381
|
+
vae: d.vae || QWEN_DEFAULTS.vae,
|
|
382
|
+
clipType: d.clip_type || QWEN_DEFAULTS.clip_type,
|
|
383
|
+
weightDtype: d.weight_dtype || QWEN_DEFAULTS.weight_dtype,
|
|
384
|
+
loras: d.loras || [],
|
|
385
|
+
filenamePrefix: 'sdl',
|
|
386
|
+
})
|
|
387
|
+
: buildWorkflow(
|
|
388
|
+
fullPrompt, pack.negative,
|
|
389
|
+
d.checkpoint, loras, seed,
|
|
390
|
+
resolved.steps, resolved.cfg, resolved.sampler, resolved.scheduler,
|
|
391
|
+
resolved.width, resolved.height,
|
|
313
392
|
);
|
|
314
393
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
errors++;
|
|
319
|
-
}
|
|
394
|
+
try {
|
|
395
|
+
const submitResult = await submitAndWait(nodes, COMFY_URL);
|
|
396
|
+
const elapsed = Date.now() - startMs;
|
|
320
397
|
|
|
321
|
-
|
|
398
|
+
// Prefer the explicit SaveImage node from the builder; fall back
|
|
399
|
+
// to highest numeric node id (see lib/comfyui-output.js).
|
|
400
|
+
const picked = pickOutputImage(submitResult.outputs, { preferNodeId: saveNodeId });
|
|
401
|
+
if (!picked) {
|
|
402
|
+
console.log(` \x1b[33m⚠\x1b[0m No output image found`);
|
|
403
|
+
generated++;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
322
406
|
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
407
|
+
// Download and save
|
|
408
|
+
const imgData = await downloadImage(picked.filename, picked.subfolder, COMFY_URL);
|
|
409
|
+
const destPath = `outputs/candidates/${assetId}.png`;
|
|
410
|
+
await writeFile(join(GAME_ROOT, destPath), imgData);
|
|
411
|
+
|
|
412
|
+
// Write provenance record (base-aware: Qwen records unet/clip/vae/shift,
|
|
413
|
+
// SDXL records checkpoint/loras).
|
|
414
|
+
const provenance = {
|
|
415
|
+
workflow_id: pack.lane,
|
|
416
|
+
workflow_version: "1.0.0",
|
|
417
|
+
base,
|
|
418
|
+
prompt: fullPrompt,
|
|
419
|
+
negative_prompt: pack.negative,
|
|
420
|
+
seed,
|
|
421
|
+
steps: resolved.steps,
|
|
422
|
+
cfg: resolved.cfg,
|
|
423
|
+
sampler: resolved.sampler,
|
|
424
|
+
scheduler: resolved.scheduler,
|
|
425
|
+
width: resolved.width,
|
|
426
|
+
height: resolved.height,
|
|
427
|
+
generation_time_ms: elapsed,
|
|
428
|
+
gpu_model: gpuModel,
|
|
429
|
+
batch_index: generated,
|
|
430
|
+
};
|
|
431
|
+
if (isQwen) {
|
|
432
|
+
provenance.unet = d.unet || QWEN_DEFAULTS.unet;
|
|
433
|
+
provenance.clip = d.clip || QWEN_DEFAULTS.clip;
|
|
434
|
+
provenance.vae = d.vae || QWEN_DEFAULTS.vae;
|
|
435
|
+
provenance.shift = d.shift ?? QWEN_DEFAULTS.shift;
|
|
436
|
+
provenance.loras = d.loras || [];
|
|
437
|
+
} else {
|
|
438
|
+
provenance.checkpoint = d.checkpoint;
|
|
439
|
+
provenance.loras = loras;
|
|
330
440
|
}
|
|
441
|
+
|
|
442
|
+
// PIN_PER_STEP: pin the exact graph + model/LoRA content identity + seed
|
|
443
|
+
// policy via the shared contract (lib/run-manifest.js). Model hashing is
|
|
444
|
+
// wave-constant; the shared cache means it runs at most once per wave.
|
|
445
|
+
provenance.pinning = buildPinning({
|
|
446
|
+
graph: nodes,
|
|
447
|
+
models: isQwen
|
|
448
|
+
? { unet: provenance.unet, clip: provenance.clip, vae: provenance.vae }
|
|
449
|
+
: { checkpoint: d.checkpoint },
|
|
450
|
+
loras: isQwen ? (d.loras || []) : loras,
|
|
451
|
+
seedPolicy: SEED_POLICIES.BASE_INCREMENT,
|
|
452
|
+
hashModels,
|
|
453
|
+
cache: hashCache,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const record = {
|
|
457
|
+
id: assetId,
|
|
458
|
+
schema_version: "1.0.0",
|
|
459
|
+
created_at: new Date().toISOString(),
|
|
460
|
+
asset_path: destPath,
|
|
461
|
+
image: { format: "png", width: resolved.width, height: resolved.height, bytes: imgData.length },
|
|
462
|
+
provenance,
|
|
463
|
+
judgment: null,
|
|
464
|
+
canon: null,
|
|
465
|
+
iteration: null,
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
await writeFile(
|
|
469
|
+
join(GAME_ROOT, `records/${assetId}.json`),
|
|
470
|
+
JSON.stringify(record, null, 2),
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
console.log(` \x1b[32m✓\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
console.log(` \x1b[31m✗\x1b[0m ${err.message}`);
|
|
476
|
+
errors++;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
generated++;
|
|
480
|
+
|
|
481
|
+
// ETA every 5 items (not every item — too noisy for long runs).
|
|
482
|
+
if (!dryRun && generated > 0 && generated % 5 === 0 && generated < totalExpected) {
|
|
483
|
+
const elapsedTotal = Date.now() - runStartMs;
|
|
484
|
+
const avgMs = elapsedTotal / generated;
|
|
485
|
+
const remaining = totalExpected - generated;
|
|
486
|
+
const etaMs = avgMs * remaining;
|
|
487
|
+
info('generate', `progress ${generated}/${totalExpected} — avg ${formatEta(avgMs)}/item, ETA ~${formatEta(etaMs)}`);
|
|
331
488
|
}
|
|
332
489
|
}
|
|
333
490
|
|
|
491
|
+
// Persist any newly computed model hashes so the next wave reuses them.
|
|
492
|
+
flushHashCache(hashCache);
|
|
493
|
+
|
|
334
494
|
console.log("");
|
|
335
495
|
const succeeded = generated - errors - skipped;
|
|
336
496
|
const totalElapsedMs = Date.now() - runStartMs;
|