@mcptoolshop/style-dataset-lab 3.1.0 → 3.2.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 +37 -0
- package/README.es.md +84 -62
- package/README.fr.md +85 -63
- package/README.hi.md +97 -77
- package/README.it.md +86 -64
- package/README.ja.md +94 -72
- package/README.md +22 -0
- package/README.pt-BR.md +83 -61
- package/README.zh.md +88 -66
- package/bin/sdlab.js +71 -0
- package/lib/adapters/ai-toolkit.js +209 -0
- 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/freeze-events.js +109 -0
- package/lib/freeze-gate.js +165 -0
- package/lib/freeze-stamp.js +161 -0
- package/lib/rows.js +228 -0
- package/lib/training-adapters.js +5 -0
- package/lib/training-packages.js +24 -18
- package/lib/training-profiles.js +33 -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/curate.js +12 -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 +238 -102
- package/scripts/qwen_generate.py +124 -0
package/scripts/generate.js
CHANGED
|
@@ -15,10 +15,11 @@ 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";
|
|
22
23
|
import { info, result } from "../lib/log.js";
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -177,6 +178,89 @@ function buildWorkflow(prompt, negativePrompt, checkpoint, loras, seed, steps, c
|
|
|
177
178
|
return { nodes, saveNodeId: saveId };
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Detect the input schema and normalize to a unified shape:
|
|
183
|
+
* { lane, style_prefix, negative, defaults, base, jobs:[{assetId, subjectId, variationId, prompt, seed, loras}] }
|
|
184
|
+
*
|
|
185
|
+
* Two accepted schemas (both produce jobs in subject-major order, so the legacy
|
|
186
|
+
* seed sequence stays byte-identical):
|
|
187
|
+
*
|
|
188
|
+
* • Legacy pack — top-level `lane` + `variations` (array) applied to every
|
|
189
|
+
* subject. seed = base_seed + jobIndex + (variation.seed_offset||0);
|
|
190
|
+
* assetId = `${subject.id}_${variation.id}`; `negative` is top-level.
|
|
191
|
+
*
|
|
192
|
+
* • Wave (what `sdlab init` scaffolds, e.g. example-wave.json) — each subject
|
|
193
|
+
* carries `variations` as an integer count; `negative` lives in `defaults`.
|
|
194
|
+
* seed = base_seed + jobIndex; assetId = `${subject.id}_v${v}`. This matches
|
|
195
|
+
* scripts/qwen_generate.py's seed/filename scheme.
|
|
196
|
+
*
|
|
197
|
+
* `base` resolves: input.base → input.defaults.base → project.json defaults.base
|
|
198
|
+
* → "sdxl". `"qwen-image"` routes to the non-anime Qwen graph.
|
|
199
|
+
*/
|
|
200
|
+
function normalizeGenerationInput(raw, projectMeta) {
|
|
201
|
+
if (!Array.isArray(raw.subjects) || raw.subjects.length === 0) {
|
|
202
|
+
throw inputError(
|
|
203
|
+
'INPUT_MISSING_FIELDS',
|
|
204
|
+
'Prompt pack/wave must have a non-empty "subjects" array.',
|
|
205
|
+
'See inputs/prompts/example-wave.json for the expected shape.'
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const defaults = raw.defaults || {};
|
|
209
|
+
const base = String(
|
|
210
|
+
raw.base || defaults.base || projectMeta?.defaults?.base || 'sdxl'
|
|
211
|
+
).toLowerCase();
|
|
212
|
+
const baseSeed = Number.isFinite(defaults.base_seed) ? defaults.base_seed : 0;
|
|
213
|
+
const stylePrefix = raw.style_prefix || '';
|
|
214
|
+
const compose = (p) => (stylePrefix ? `${stylePrefix}, ${p}` : p);
|
|
215
|
+
|
|
216
|
+
const jobs = [];
|
|
217
|
+
let k = 0;
|
|
218
|
+
|
|
219
|
+
if (Array.isArray(raw.variations)) {
|
|
220
|
+
// Legacy pack schema
|
|
221
|
+
if (!raw.lane) {
|
|
222
|
+
throw inputError(
|
|
223
|
+
'INPUT_MISSING_FIELDS',
|
|
224
|
+
'Legacy pack (top-level "variations" array) requires a "lane".',
|
|
225
|
+
'Add a "lane" string, or use the wave schema (per-subject integer "variations").'
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const negative = raw.negative ?? '';
|
|
229
|
+
for (const subject of raw.subjects) {
|
|
230
|
+
for (const variation of raw.variations) {
|
|
231
|
+
jobs.push({
|
|
232
|
+
assetId: `${subject.id}_${variation.id}`,
|
|
233
|
+
subjectId: subject.id,
|
|
234
|
+
variationId: variation.id,
|
|
235
|
+
prompt: compose(subject.prompt),
|
|
236
|
+
seed: baseSeed + k + (variation.seed_offset || 0),
|
|
237
|
+
loras: variation.loras || [],
|
|
238
|
+
});
|
|
239
|
+
k++;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { lane: raw.lane, style_prefix: stylePrefix, negative, defaults, base, jobs, schema: 'pack' };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Wave schema
|
|
246
|
+
const negative = raw.negative ?? defaults.negative ?? '';
|
|
247
|
+
for (const subject of raw.subjects) {
|
|
248
|
+
const nvar = Number.isFinite(subject.variations) ? subject.variations : 1;
|
|
249
|
+
for (let v = 0; v < nvar; v++) {
|
|
250
|
+
jobs.push({
|
|
251
|
+
assetId: `${subject.id}_v${v}`,
|
|
252
|
+
subjectId: subject.id,
|
|
253
|
+
variationId: `v${v}`,
|
|
254
|
+
prompt: compose(subject.prompt),
|
|
255
|
+
seed: baseSeed + k,
|
|
256
|
+
loras: [],
|
|
257
|
+
});
|
|
258
|
+
k++;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { lane: raw.wave || raw.lane || 'default', style_prefix: stylePrefix, negative, defaults, base, jobs, schema: 'wave' };
|
|
262
|
+
}
|
|
263
|
+
|
|
180
264
|
export async function run(argv = process.argv.slice(2)) {
|
|
181
265
|
const projectName = getProjectName(argv);
|
|
182
266
|
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
@@ -196,13 +280,40 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
196
280
|
const resume = parsed.flags['resume'] === true;
|
|
197
281
|
|
|
198
282
|
const fullPackPath = resolveSafeProjectPath(GAME_ROOT, packPath, { flagName: 'pack-path' });
|
|
199
|
-
const
|
|
283
|
+
const raw = await readJsonFile(fullPackPath);
|
|
284
|
+
const projectMeta = loadProjectMeta(GAME_ROOT);
|
|
285
|
+
const gpuModel = resolveGpuModel(projectMeta?.defaults);
|
|
286
|
+
const pack = normalizeGenerationInput(raw, projectMeta);
|
|
287
|
+
|
|
288
|
+
const d = pack.defaults;
|
|
289
|
+
const base = pack.base;
|
|
290
|
+
const isQwen = base === 'qwen-image';
|
|
291
|
+
|
|
292
|
+
// Resolve effective sampler params once. Qwen applies its own defaults when a
|
|
293
|
+
// wave omits them; SDXL uses the pack defaults exactly as authored (unchanged).
|
|
294
|
+
const resolved = isQwen
|
|
295
|
+
? {
|
|
296
|
+
width: d.width || QWEN_DEFAULTS.width,
|
|
297
|
+
height: d.height || QWEN_DEFAULTS.height,
|
|
298
|
+
steps: d.steps || QWEN_DEFAULTS.steps,
|
|
299
|
+
cfg: d.cfg || QWEN_DEFAULTS.cfg,
|
|
300
|
+
sampler: d.sampler || QWEN_DEFAULTS.sampler,
|
|
301
|
+
scheduler: d.scheduler || QWEN_DEFAULTS.scheduler,
|
|
302
|
+
}
|
|
303
|
+
: {
|
|
304
|
+
width: d.width,
|
|
305
|
+
height: d.height,
|
|
306
|
+
steps: d.steps,
|
|
307
|
+
cfg: d.cfg,
|
|
308
|
+
sampler: d.sampler,
|
|
309
|
+
scheduler: d.scheduler,
|
|
310
|
+
};
|
|
200
311
|
|
|
201
312
|
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m generate`);
|
|
202
313
|
console.log(` Lane: ${pack.lane}`);
|
|
203
|
-
console.log(`
|
|
204
|
-
console.log(`
|
|
205
|
-
console.log(` Total candidates: ${pack.
|
|
314
|
+
console.log(` Base: ${base}${isQwen ? ' \x1b[2m(non-anime)\x1b[0m' : ''}`);
|
|
315
|
+
console.log(` Subjects: ${raw.subjects.length}`);
|
|
316
|
+
console.log(` Total candidates: ${pack.jobs.length}`);
|
|
206
317
|
console.log("");
|
|
207
318
|
|
|
208
319
|
if (!dryRun) {
|
|
@@ -216,118 +327,143 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
216
327
|
await mkdir(join(GAME_ROOT, "outputs/candidates"), { recursive: true });
|
|
217
328
|
await mkdir(join(GAME_ROOT, "records"), { recursive: true });
|
|
218
329
|
|
|
219
|
-
const d = pack.defaults;
|
|
220
330
|
let generated = 0;
|
|
221
331
|
let errors = 0;
|
|
222
332
|
let skipped = 0;
|
|
223
|
-
const totalExpected = pack.
|
|
333
|
+
const totalExpected = pack.jobs.length;
|
|
224
334
|
const runStartMs = Date.now();
|
|
225
335
|
|
|
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})`);
|
|
336
|
+
for (const job of pack.jobs) {
|
|
337
|
+
const { assetId, prompt: fullPrompt, seed, loras } = job;
|
|
248
338
|
|
|
249
|
-
|
|
339
|
+
// --resume: skip jobs whose record + image are both already on disk.
|
|
340
|
+
// Seeds are precomputed per job index, so skipping never shifts later seeds.
|
|
341
|
+
if (resume && !dryRun) {
|
|
342
|
+
const recordPath = join(GAME_ROOT, `records/${assetId}.json`);
|
|
343
|
+
const imagePath = join(GAME_ROOT, `outputs/candidates/${assetId}.png`);
|
|
344
|
+
if (existsSync(recordPath) && existsSync(imagePath)) {
|
|
345
|
+
console.log(` [${generated + 1}/${totalExpected}] ${assetId} \x1b[2m(resumed — skipped)\x1b[0m`);
|
|
250
346
|
generated++;
|
|
347
|
+
skipped++;
|
|
251
348
|
continue;
|
|
252
349
|
}
|
|
350
|
+
}
|
|
253
351
|
|
|
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
|
-
);
|
|
352
|
+
console.log(` [${generated + 1}/${totalExpected}] ${assetId} (seed: ${seed}, loras: ${isQwen ? (d.loras || []).length : loras.length})`);
|
|
261
353
|
|
|
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),
|
|
354
|
+
if (dryRun) {
|
|
355
|
+
generated++;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const startMs = Date.now();
|
|
360
|
+
const { nodes, saveNodeId } = isQwen
|
|
361
|
+
? buildQwenGraph({
|
|
362
|
+
prompt: fullPrompt,
|
|
363
|
+
negativePrompt: pack.negative,
|
|
364
|
+
seed,
|
|
365
|
+
width: resolved.width,
|
|
366
|
+
height: resolved.height,
|
|
367
|
+
steps: resolved.steps,
|
|
368
|
+
cfg: resolved.cfg,
|
|
369
|
+
sampler: resolved.sampler,
|
|
370
|
+
scheduler: resolved.scheduler,
|
|
371
|
+
shift: d.shift ?? QWEN_DEFAULTS.shift,
|
|
372
|
+
unet: d.unet || QWEN_DEFAULTS.unet,
|
|
373
|
+
clip: d.clip || QWEN_DEFAULTS.clip,
|
|
374
|
+
vae: d.vae || QWEN_DEFAULTS.vae,
|
|
375
|
+
clipType: d.clip_type || QWEN_DEFAULTS.clip_type,
|
|
376
|
+
weightDtype: d.weight_dtype || QWEN_DEFAULTS.weight_dtype,
|
|
377
|
+
loras: d.loras || [],
|
|
378
|
+
filenamePrefix: 'sdl',
|
|
379
|
+
})
|
|
380
|
+
: buildWorkflow(
|
|
381
|
+
fullPrompt, pack.negative,
|
|
382
|
+
d.checkpoint, loras, seed,
|
|
383
|
+
resolved.steps, resolved.cfg, resolved.sampler, resolved.scheduler,
|
|
384
|
+
resolved.width, resolved.height,
|
|
313
385
|
);
|
|
314
386
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
errors++;
|
|
319
|
-
}
|
|
387
|
+
try {
|
|
388
|
+
const submitResult = await submitAndWait(nodes, COMFY_URL);
|
|
389
|
+
const elapsed = Date.now() - startMs;
|
|
320
390
|
|
|
321
|
-
|
|
391
|
+
// Prefer the explicit SaveImage node from the builder; fall back
|
|
392
|
+
// to highest numeric node id (see lib/comfyui-output.js).
|
|
393
|
+
const picked = pickOutputImage(submitResult.outputs, { preferNodeId: saveNodeId });
|
|
394
|
+
if (!picked) {
|
|
395
|
+
console.log(` \x1b[33m⚠\x1b[0m No output image found`);
|
|
396
|
+
generated++;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
322
399
|
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
400
|
+
// Download and save
|
|
401
|
+
const imgData = await downloadImage(picked.filename, picked.subfolder, COMFY_URL);
|
|
402
|
+
const destPath = `outputs/candidates/${assetId}.png`;
|
|
403
|
+
await writeFile(join(GAME_ROOT, destPath), imgData);
|
|
404
|
+
|
|
405
|
+
// Write provenance record (base-aware: Qwen records unet/clip/vae/shift,
|
|
406
|
+
// SDXL records checkpoint/loras).
|
|
407
|
+
const provenance = {
|
|
408
|
+
workflow_id: pack.lane,
|
|
409
|
+
workflow_version: "1.0.0",
|
|
410
|
+
base,
|
|
411
|
+
prompt: fullPrompt,
|
|
412
|
+
negative_prompt: pack.negative,
|
|
413
|
+
seed,
|
|
414
|
+
steps: resolved.steps,
|
|
415
|
+
cfg: resolved.cfg,
|
|
416
|
+
sampler: resolved.sampler,
|
|
417
|
+
scheduler: resolved.scheduler,
|
|
418
|
+
width: resolved.width,
|
|
419
|
+
height: resolved.height,
|
|
420
|
+
generation_time_ms: elapsed,
|
|
421
|
+
gpu_model: gpuModel,
|
|
422
|
+
batch_index: generated,
|
|
423
|
+
};
|
|
424
|
+
if (isQwen) {
|
|
425
|
+
provenance.unet = d.unet || QWEN_DEFAULTS.unet;
|
|
426
|
+
provenance.clip = d.clip || QWEN_DEFAULTS.clip;
|
|
427
|
+
provenance.vae = d.vae || QWEN_DEFAULTS.vae;
|
|
428
|
+
provenance.shift = d.shift ?? QWEN_DEFAULTS.shift;
|
|
429
|
+
provenance.loras = d.loras || [];
|
|
430
|
+
} else {
|
|
431
|
+
provenance.checkpoint = d.checkpoint;
|
|
432
|
+
provenance.loras = loras;
|
|
330
433
|
}
|
|
434
|
+
|
|
435
|
+
const record = {
|
|
436
|
+
id: assetId,
|
|
437
|
+
schema_version: "1.0.0",
|
|
438
|
+
created_at: new Date().toISOString(),
|
|
439
|
+
asset_path: destPath,
|
|
440
|
+
image: { format: "png", width: resolved.width, height: resolved.height, bytes: imgData.length },
|
|
441
|
+
provenance,
|
|
442
|
+
judgment: null,
|
|
443
|
+
canon: null,
|
|
444
|
+
iteration: null,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
await writeFile(
|
|
448
|
+
join(GAME_ROOT, `records/${assetId}.json`),
|
|
449
|
+
JSON.stringify(record, null, 2),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
console.log(` \x1b[32m✓\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
|
|
453
|
+
} catch (err) {
|
|
454
|
+
console.log(` \x1b[31m✗\x1b[0m ${err.message}`);
|
|
455
|
+
errors++;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
generated++;
|
|
459
|
+
|
|
460
|
+
// ETA every 5 items (not every item — too noisy for long runs).
|
|
461
|
+
if (!dryRun && generated > 0 && generated % 5 === 0 && generated < totalExpected) {
|
|
462
|
+
const elapsedTotal = Date.now() - runStartMs;
|
|
463
|
+
const avgMs = elapsedTotal / generated;
|
|
464
|
+
const remaining = totalExpected - generated;
|
|
465
|
+
const etaMs = avgMs * remaining;
|
|
466
|
+
info('generate', `progress ${generated}/${totalExpected} — avg ${formatEta(avgMs)}/item, ETA ~${formatEta(etaMs)}`);
|
|
331
467
|
}
|
|
332
468
|
}
|
|
333
469
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Studio NON-ANIME generation on Qwen-Image (the verified base) via ComfyUI.
|
|
3
|
+
|
|
4
|
+
Reads a canon-derived WAVE json ({style_prefix, defaults, subjects:[{id,prompt,variations}]}) and
|
|
5
|
+
generates each subject on Qwen-Image, saving images + a provenance receipt (PIN_PER_STEP) to an
|
|
6
|
+
output dir. This is the bridge generator until sdlab's buildWorkflowGraph() gains native Qwen
|
|
7
|
+
support (tracked follow-on). It is the studio's NON-ANIME path — see memory/feedback_no_anime.md.
|
|
8
|
+
|
|
9
|
+
All Qwen-Image components are on the rig (diffusion_models/text_encoders/vae). Run with ComfyUI's
|
|
10
|
+
embedded python (has PIL): start the watchdog (E:/AI/training/_watchdog.ps1) before GPU runs.
|
|
11
|
+
|
|
12
|
+
python qwen_generate.py --wave <wave.json> --out <dir> [--variations N]
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import time
|
|
20
|
+
import urllib.request
|
|
21
|
+
import uuid
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from PIL import Image
|
|
25
|
+
|
|
26
|
+
COMFY = "http://127.0.0.1:8188"
|
|
27
|
+
COMFY_OUT = Path(r"E:/AI-Models/ComfyUI_windows_portable/ComfyUI/output")
|
|
28
|
+
|
|
29
|
+
# Qwen-Image native components (on rig). NON-ANIME base — never an anime checkpoint.
|
|
30
|
+
QWEN_UNET = "qwen_image_fp8_e4m3fn.safetensors"
|
|
31
|
+
QWEN_CLIP = "qwen_2.5_vl_7b_fp8_scaled.safetensors"
|
|
32
|
+
QWEN_VAE = "qwen_image_vae.safetensors"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def qwen_graph(pos: str, neg: str, seed: int, w: int, h: int, steps: int, cfg: float, prefix: str,
|
|
36
|
+
loras: list | None = None) -> dict:
|
|
37
|
+
"""loras: [{name, weight}] chained as DiT-only LoraLoaderModelOnly nodes (ids from
|
|
38
|
+
"40", between UNETLoader and ModelSamplingAuraFlow) — mirrors sdlab buildQwenGraph.
|
|
39
|
+
Model-only because studio Qwen style LoRAs train the transformer with the text
|
|
40
|
+
encoder frozen."""
|
|
41
|
+
nodes = {
|
|
42
|
+
"37": {"class_type": "UNETLoader", "inputs": {"unet_name": QWEN_UNET, "weight_dtype": "default"}},
|
|
43
|
+
"38": {"class_type": "CLIPLoader", "inputs": {"clip_name": QWEN_CLIP, "type": "qwen_image"}},
|
|
44
|
+
"39": {"class_type": "VAELoader", "inputs": {"vae_name": QWEN_VAE}},
|
|
45
|
+
"6": {"class_type": "CLIPTextEncode", "inputs": {"text": pos, "clip": ["38", 0]}},
|
|
46
|
+
"7": {"class_type": "CLIPTextEncode", "inputs": {"text": neg, "clip": ["38", 0]}},
|
|
47
|
+
}
|
|
48
|
+
model_out = ["37", 0]
|
|
49
|
+
for i, lora in enumerate(loras or []):
|
|
50
|
+
lid = str(40 + i)
|
|
51
|
+
nodes[lid] = {"class_type": "LoraLoaderModelOnly", "inputs": {
|
|
52
|
+
"lora_name": lora["name"], "strength_model": lora.get("weight", 1.0), "model": model_out}}
|
|
53
|
+
model_out = [lid, 0]
|
|
54
|
+
nodes.update({
|
|
55
|
+
"66": {"class_type": "ModelSamplingAuraFlow", "inputs": {"shift": 3.1, "model": model_out}},
|
|
56
|
+
"5": {"class_type": "EmptySD3LatentImage", "inputs": {"width": w, "height": h, "batch_size": 1}},
|
|
57
|
+
"3": {"class_type": "KSampler", "inputs": {"seed": seed, "steps": steps, "cfg": cfg, "sampler_name": "euler",
|
|
58
|
+
"scheduler": "simple", "denoise": 1.0, "model": ["66", 0], "positive": ["6", 0], "negative": ["7", 0], "latent_image": ["5", 0]}},
|
|
59
|
+
"8": {"class_type": "VAEDecode", "inputs": {"samples": ["3", 0], "vae": ["39", 0]}},
|
|
60
|
+
"9": {"class_type": "SaveImage", "inputs": {"filename_prefix": prefix, "images": ["8", 0]}},
|
|
61
|
+
})
|
|
62
|
+
return nodes
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _post(path: str, payload: dict):
|
|
66
|
+
return json.loads(urllib.request.urlopen(urllib.request.Request(
|
|
67
|
+
f"{COMFY}{path}", json.dumps(payload).encode(), {"Content-Type": "application/json"}), timeout=90).read())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def generate(pos: str, neg: str, seed: int, dst: Path, **kw) -> Path:
|
|
71
|
+
g = qwen_graph(pos, neg, seed, kw["w"], kw["h"], kw["steps"], kw["cfg"], dst.stem, loras=kw.get("loras"))
|
|
72
|
+
pid = _post("/prompt", {"prompt": g, "client_id": uuid.uuid4().hex})["prompt_id"]
|
|
73
|
+
for _ in range(1200):
|
|
74
|
+
hist = json.loads(urllib.request.urlopen(f"{COMFY}/history/{pid}", timeout=30).read())
|
|
75
|
+
if pid in hist and hist[pid].get("outputs"):
|
|
76
|
+
img = hist[pid]["outputs"]["9"]["images"][0]
|
|
77
|
+
src = COMFY_OUT / img["subfolder"] / img["filename"]
|
|
78
|
+
for _w in range(120):
|
|
79
|
+
if src.exists():
|
|
80
|
+
Image.open(src).convert("RGB").save(dst)
|
|
81
|
+
return dst
|
|
82
|
+
time.sleep(0.5)
|
|
83
|
+
time.sleep(0.5)
|
|
84
|
+
raise RuntimeError("ComfyUI generation timed out")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main() -> None:
|
|
88
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
89
|
+
ap.add_argument("--wave", required=True, type=Path)
|
|
90
|
+
ap.add_argument("--out", required=True, type=Path)
|
|
91
|
+
ap.add_argument("--variations", type=int, default=None, help="override per-subject variation count")
|
|
92
|
+
args = ap.parse_args()
|
|
93
|
+
|
|
94
|
+
wave = json.loads(args.wave.read_text(encoding="utf-8"))
|
|
95
|
+
prefix = wave.get("style_prefix", "")
|
|
96
|
+
d = wave.get("defaults", {})
|
|
97
|
+
neg = d.get("negative", "")
|
|
98
|
+
w, h = d.get("width", 1024), d.get("height", 1024)
|
|
99
|
+
steps, cfg = d.get("steps", 22), d.get("cfg", 3.5)
|
|
100
|
+
base_seed = d.get("base_seed", 1000)
|
|
101
|
+
loras = d.get("loras", [])
|
|
102
|
+
args.out.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
|
|
104
|
+
receipt = {"wave": wave.get("wave"), "base": "qwen-image", "unet": QWEN_UNET, "clip": QWEN_CLIP,
|
|
105
|
+
"vae": QWEN_VAE, "sampler": "euler", "scheduler": "simple", "shift": 3.1,
|
|
106
|
+
"steps": steps, "cfg": cfg, "size": [w, h], "style_prefix": prefix, "negative": neg,
|
|
107
|
+
"loras": loras, "items": []}
|
|
108
|
+
seed = base_seed
|
|
109
|
+
for subj in wave["subjects"]:
|
|
110
|
+
nvar = args.variations if args.variations is not None else subj.get("variations", 1)
|
|
111
|
+
pos = f"{prefix}, {subj['prompt']}" if prefix else subj["prompt"]
|
|
112
|
+
for v in range(nvar):
|
|
113
|
+
dst = args.out / f"{subj['id']}_v{v}.png"
|
|
114
|
+
generate(pos, neg, seed, dst, w=w, h=h, steps=steps, cfg=cfg, loras=loras)
|
|
115
|
+
print(f" {subj['id']} v{v} (seed {seed}) -> {dst.name}", flush=True)
|
|
116
|
+
receipt["items"].append({"id": subj["id"], "variation": v, "seed": seed, "file": dst.name, "prompt": pos})
|
|
117
|
+
seed += 1
|
|
118
|
+
|
|
119
|
+
(args.out / "generation.json").write_text(json.dumps(receipt, indent=2), encoding="utf-8")
|
|
120
|
+
print(f"\n{len(receipt['items'])} images -> {args.out} (+ generation.json provenance)")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
main()
|