@mcptoolshop/style-dataset-lab 3.0.0 → 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 +108 -15
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +71 -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 +64 -1
- package/README.pt-BR.md +99 -8
- package/README.zh.md +100 -9
- package/SECURITY.md +6 -5
- package/bin/sdlab.js +289 -62
- package/lib/adapters/comfyui-runner.js +11 -15
- package/lib/adapters/diffusers-lora.js +7 -2
- package/lib/adapters/generic-image-caption.js +6 -1
- package/lib/args.js +200 -13
- package/lib/batch-compiler.js +7 -3
- package/lib/batch-runs.js +162 -31
- package/lib/batch-sheet-render.js +5 -2
- package/lib/brief-compiler.js +38 -11
- package/lib/card.js +11 -4
- package/lib/comfyui-output.js +70 -0
- package/lib/comfyui.js +100 -6
- package/lib/config.js +68 -3
- package/lib/deprecation.js +1 -1
- package/lib/eval-pack.js +94 -4
- package/lib/eval-runs.js +42 -3
- package/lib/export.js +144 -34
- package/lib/implementation-packs.js +15 -1
- package/lib/log.js +41 -4
- package/lib/paths.js +41 -3
- package/lib/refine-briefs.js +16 -9
- package/lib/reingest-selected.js +14 -3
- package/lib/reingest.js +15 -1
- package/lib/run-summary.js +3 -2
- package/lib/runtime-runs.js +59 -9
- package/lib/selections.js +54 -2
- package/lib/snapshot.js +100 -7
- package/lib/split.js +175 -17
- package/lib/training-adapters.js +79 -0
- package/lib/training-manifests.js +17 -3
- package/lib/training-packages.js +29 -28
- package/lib/training-profiles.js +10 -0
- package/package.json +27 -3
- package/scripts/batch-generate.js +72 -20
- package/scripts/batch-sheet.js +3 -3
- package/scripts/batch-show.js +3 -3
- package/scripts/brief-compile.js +8 -8
- package/scripts/brief-show.js +4 -3
- package/scripts/canon-bind.js +7 -9
- package/scripts/card.js +22 -25
- package/scripts/compare.js +38 -33
- package/scripts/critique-show.js +3 -3
- package/scripts/critique.js +3 -3
- package/scripts/curate.js +91 -46
- package/scripts/doctor.js +3 -5
- package/scripts/eligibility.js +17 -14
- package/scripts/eval-pack.js +13 -11
- package/scripts/eval-run.js +30 -29
- package/scripts/export.js +27 -31
- package/scripts/generate-controlnet.js +31 -17
- package/scripts/generate-identity.js +37 -20
- package/scripts/generate-ipadapter.js +22 -16
- package/scripts/generate.js +96 -29
- package/scripts/implementation-pack.js +19 -14
- package/scripts/init.js +115 -17
- package/scripts/painterly.js +8 -10
- package/scripts/refine.js +3 -3
- package/scripts/reingest-selected.js +3 -3
- package/scripts/reingest.js +33 -20
- package/scripts/run-generate.js +4 -4
- package/scripts/run-list.js +3 -3
- package/scripts/run-show.js +3 -3
- package/scripts/select.js +5 -4
- package/scripts/selection-show.js +3 -3
- package/scripts/snapshot.js +22 -24
- package/scripts/split.js +26 -31
- package/scripts/training-manifest.js +29 -24
- package/scripts/training-package.js +24 -20
- package/scripts/training-profile.js +13 -8
- package/scripts/workflow-list.js +2 -2
- package/scripts/workflow-show.js +4 -3
- package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/architecture/terminology.json +10 -8
- package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/character-design/project.json +4 -2
- package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/creature-design/project.json +4 -2
- package/templates/domains/creature-design/terminology.json +1 -0
- package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/game-art/project.json +4 -2
- package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/vehicle-mech/project.json +4 -2
- package/scripts/bulk-curate-wave2-5.js +0 -138
- package/scripts/bulk-curate-waves11-18.js +0 -413
- package/scripts/curate-wave25.js +0 -209
- package/scripts/migrate-records.js +0 -141
- package/templates/inputs/prompts/example-wave.json +0 -27
package/scripts/generate.js
CHANGED
|
@@ -10,11 +10,36 @@
|
|
|
10
10
|
* downloads the output PNG, and writes a provenance record to records/.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
14
15
|
import { join } from "node:path";
|
|
15
|
-
import { getProjectName } from "../lib/args.js";
|
|
16
|
-
import { REPO_ROOT } from "../lib/paths.js";
|
|
16
|
+
import { parseArgs, getProjectName } from "../lib/args.js";
|
|
17
|
+
import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
|
|
18
|
+
import { readJsonFile } from "../lib/config.js";
|
|
19
|
+
import { runtimeError, handleCliError } from "../lib/errors.js";
|
|
17
20
|
import { comfyHealth, submitAndWait, downloadImage } from "../lib/comfyui.js";
|
|
21
|
+
import { pickOutputImage } from "../lib/comfyui-output.js";
|
|
22
|
+
import { info, result } from "../lib/log.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Format milliseconds as a human ETA string like "6m 12s" or "42s".
|
|
26
|
+
*/
|
|
27
|
+
function formatEta(ms) {
|
|
28
|
+
if (!Number.isFinite(ms) || ms < 0) return '—';
|
|
29
|
+
const totalSec = Math.round(ms / 1000);
|
|
30
|
+
if (totalSec < 60) return `${totalSec}s`;
|
|
31
|
+
const m = Math.floor(totalSec / 60);
|
|
32
|
+
const s = totalSec % 60;
|
|
33
|
+
return `${m}m ${s}s`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* COMFY_URL precedence:
|
|
38
|
+
* 1. process.env.COMFY_URL (explicit user env)
|
|
39
|
+
* 2. project defaults.comfy_url (set by `sdlab project doctor`)
|
|
40
|
+
* 3. built-in default http://127.0.0.1:8188
|
|
41
|
+
* The adapter (lib/adapters/comfyui-runner.js) applies the same precedence.
|
|
42
|
+
*/
|
|
18
43
|
|
|
19
44
|
function buildWorkflow(prompt, negativePrompt, checkpoint, loras, seed, steps, cfg, sampler, scheduler, width, height, ipAdapterConfig) {
|
|
20
45
|
const nodes = {};
|
|
@@ -157,11 +182,21 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
157
182
|
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
158
183
|
const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
|
|
159
184
|
|
|
160
|
-
const
|
|
161
|
-
|
|
185
|
+
const parsed = parseArgs(argv, {
|
|
186
|
+
flags: {
|
|
187
|
+
project: { type: 'string' },
|
|
188
|
+
game: { type: 'string' },
|
|
189
|
+
'dry-run': { type: 'boolean' },
|
|
190
|
+
resume: { type: 'boolean' },
|
|
191
|
+
},
|
|
192
|
+
deprecated: { game: 'project' },
|
|
193
|
+
});
|
|
194
|
+
const packPath = parsed.positionals[0] || "inputs/prompts/rpg-icons-lane1.json";
|
|
195
|
+
const dryRun = parsed.flags['dry-run'] === true;
|
|
196
|
+
const resume = parsed.flags['resume'] === true;
|
|
162
197
|
|
|
163
|
-
const fullPackPath =
|
|
164
|
-
const pack =
|
|
198
|
+
const fullPackPath = resolveSafeProjectPath(GAME_ROOT, packPath, { flagName: 'pack-path' });
|
|
199
|
+
const pack = await readJsonFile(fullPackPath, { requiredFields: ['lane', 'subjects', 'variations', 'defaults'] });
|
|
165
200
|
|
|
166
201
|
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m generate`);
|
|
167
202
|
console.log(` Lane: ${pack.lane}`);
|
|
@@ -173,7 +208,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
173
208
|
if (!dryRun) {
|
|
174
209
|
const online = await comfyHealth(COMFY_URL);
|
|
175
210
|
if (!online) {
|
|
176
|
-
throw
|
|
211
|
+
throw runtimeError('RUNTIME_COMFY_UNREACHABLE', "ComfyUI not reachable at " + COMFY_URL, 'Start ComfyUI or set COMFY_URL to the right host.');
|
|
177
212
|
}
|
|
178
213
|
console.log("\x1b[32m✓\x1b[0m ComfyUI online");
|
|
179
214
|
}
|
|
@@ -183,7 +218,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
183
218
|
|
|
184
219
|
const d = pack.defaults;
|
|
185
220
|
let generated = 0;
|
|
221
|
+
let errors = 0;
|
|
222
|
+
let skipped = 0;
|
|
186
223
|
const totalExpected = pack.subjects.length * pack.variations.length;
|
|
224
|
+
const runStartMs = Date.now();
|
|
187
225
|
|
|
188
226
|
for (const subject of pack.subjects) {
|
|
189
227
|
for (const variation of pack.variations) {
|
|
@@ -192,6 +230,20 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
192
230
|
const fullPrompt = `${pack.style_prefix}, ${subject.prompt}`;
|
|
193
231
|
const loras = variation.loras || [];
|
|
194
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
|
+
|
|
195
247
|
console.log(` [${generated + 1}/${totalExpected}] ${assetId} (seed: ${seed}, loras: ${loras.length})`);
|
|
196
248
|
|
|
197
249
|
if (dryRun) {
|
|
@@ -200,7 +252,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
200
252
|
}
|
|
201
253
|
|
|
202
254
|
const startMs = Date.now();
|
|
203
|
-
const { nodes } = buildWorkflow(
|
|
255
|
+
const { nodes, saveNodeId } = buildWorkflow(
|
|
204
256
|
fullPrompt, pack.negative,
|
|
205
257
|
d.checkpoint, loras, seed,
|
|
206
258
|
d.steps, d.cfg, d.sampler, d.scheduler,
|
|
@@ -208,29 +260,20 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
208
260
|
);
|
|
209
261
|
|
|
210
262
|
try {
|
|
211
|
-
const
|
|
263
|
+
const submitResult = await submitAndWait(nodes, COMFY_URL);
|
|
212
264
|
const elapsed = Date.now() - startMs;
|
|
213
265
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
for (const nodeOut of Object.values(outputs)) {
|
|
219
|
-
if (nodeOut.images && nodeOut.images.length > 0) {
|
|
220
|
-
imageFile = nodeOut.images[0].filename;
|
|
221
|
-
imageSubfolder = nodeOut.images[0].subfolder || "";
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
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) {
|
|
227
270
|
console.log(` \x1b[33m⚠\x1b[0m No output image found`);
|
|
228
271
|
generated++;
|
|
229
272
|
continue;
|
|
230
273
|
}
|
|
231
274
|
|
|
232
275
|
// Download and save
|
|
233
|
-
const imgData = await downloadImage(
|
|
276
|
+
const imgData = await downloadImage(picked.filename, picked.subfolder, COMFY_URL);
|
|
234
277
|
const destPath = `outputs/candidates/${assetId}.png`;
|
|
235
278
|
await writeFile(join(GAME_ROOT, destPath), imgData);
|
|
236
279
|
|
|
@@ -272,21 +315,45 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
272
315
|
console.log(` \x1b[32m✓\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
|
|
273
316
|
} catch (err) {
|
|
274
317
|
console.log(` \x1b[31m✗\x1b[0m ${err.message}`);
|
|
318
|
+
errors++;
|
|
275
319
|
}
|
|
276
320
|
|
|
277
321
|
generated++;
|
|
322
|
+
|
|
323
|
+
// ETA every 5 items (not every item — too noisy for long runs).
|
|
324
|
+
if (!dryRun && generated > 0 && generated % 5 === 0 && generated < totalExpected) {
|
|
325
|
+
const elapsedTotal = Date.now() - runStartMs;
|
|
326
|
+
const avgMs = elapsedTotal / generated;
|
|
327
|
+
const remaining = totalExpected - generated;
|
|
328
|
+
const etaMs = avgMs * remaining;
|
|
329
|
+
info('generate', `progress ${generated}/${totalExpected} — avg ${formatEta(avgMs)}/item, ETA ~${formatEta(etaMs)}`);
|
|
330
|
+
}
|
|
278
331
|
}
|
|
279
332
|
}
|
|
280
333
|
|
|
281
334
|
console.log("");
|
|
282
|
-
|
|
335
|
+
const succeeded = generated - errors - skipped;
|
|
336
|
+
const totalElapsedMs = Date.now() - runStartMs;
|
|
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`);
|
|
343
|
+
}
|
|
283
344
|
if (dryRun) console.log(" (dry run — no images produced)");
|
|
345
|
+
// Always print the output directory so scripted pipelines can parse it,
|
|
346
|
+
// even under --quiet.
|
|
347
|
+
if (!dryRun && succeeded > 0) {
|
|
348
|
+
result(`Candidates: ${join(GAME_ROOT, 'outputs/candidates')}`);
|
|
349
|
+
result(`Records: ${join(GAME_ROOT, 'records')}`);
|
|
350
|
+
}
|
|
351
|
+
if (!dryRun && errors > 0 && succeeded === 0) {
|
|
352
|
+
throw runtimeError('RUNTIME_ALL_FAILED', `All ${totalExpected} generation attempts failed.`);
|
|
353
|
+
}
|
|
284
354
|
}
|
|
285
355
|
|
|
286
356
|
// Direct execution guard
|
|
287
357
|
if (process.argv[1] && (process.argv[1].endsWith('generate.js') || process.argv[1].endsWith('generate'))) {
|
|
288
|
-
run().catch(
|
|
289
|
-
console.error(err.message || err);
|
|
290
|
-
process.exit(1);
|
|
291
|
-
});
|
|
358
|
+
run().catch(handleCliError);
|
|
292
359
|
}
|
|
@@ -10,25 +10,30 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
-
import { getProjectName } from '../lib/args.js';
|
|
13
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
14
14
|
import { REPO_ROOT } from '../lib/paths.js';
|
|
15
|
+
import { inputError, handleCliError } from '../lib/errors.js';
|
|
15
16
|
import { listTrainingManifests } from '../lib/training-manifests.js';
|
|
16
17
|
import { buildImplementationPack, listImplementationPacks, loadImplementationPack } from '../lib/implementation-packs.js';
|
|
17
18
|
|
|
18
19
|
export async function run(argv = process.argv.slice(2)) {
|
|
19
|
-
const
|
|
20
|
+
const { flags, positionals } = parseArgs(argv, {
|
|
21
|
+
flags: {
|
|
22
|
+
project: { type: 'string' },
|
|
23
|
+
manifest: { type: 'string' },
|
|
24
|
+
},
|
|
25
|
+
deprecated: { game: 'project' },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const projectName = flags.project || getProjectName(argv);
|
|
20
29
|
const projectRoot = join(REPO_ROOT, 'projects', projectName);
|
|
21
|
-
const subcommand =
|
|
22
|
-
const args = argv.filter(a => a !== subcommand);
|
|
30
|
+
const subcommand = positionals.find(a => !a.startsWith('impl-') && !a.startsWith('tm-')) || 'list';
|
|
23
31
|
|
|
24
32
|
if (subcommand === 'build') {
|
|
25
|
-
let manifestId;
|
|
26
|
-
|
|
27
|
-
if (manifestIdx >= 0) {
|
|
28
|
-
manifestId = args[manifestIdx + 1];
|
|
29
|
-
} else {
|
|
33
|
+
let manifestId = flags.manifest;
|
|
34
|
+
if (!manifestId) {
|
|
30
35
|
const manifests = await listTrainingManifests(projectRoot);
|
|
31
|
-
if (manifests.length === 0) throw
|
|
36
|
+
if (manifests.length === 0) throw inputError('INPUT_NO_MANIFEST', 'No training manifests. Run: sdlab training-manifest create');
|
|
32
37
|
manifestId = manifests[manifests.length - 1].id;
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -44,8 +49,8 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
44
49
|
console.log(` Subject continuity: ${result.subjects}`);
|
|
45
50
|
|
|
46
51
|
} else if (subcommand === 'show') {
|
|
47
|
-
const implId =
|
|
48
|
-
if (!implId) throw
|
|
52
|
+
const implId = positionals.find(a => a.startsWith('impl-'));
|
|
53
|
+
if (!implId) throw inputError('INPUT_MISSING_ARGS', 'Usage: sdlab implementation-pack show <impl-id>');
|
|
49
54
|
|
|
50
55
|
const pack = await loadImplementationPack(projectRoot, implId);
|
|
51
56
|
console.log(`\x1b[1mImplementation pack\x1b[0m — ${implId}\n`);
|
|
@@ -69,10 +74,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
} else {
|
|
72
|
-
throw
|
|
77
|
+
throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: build, show, list`);
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
if (process.argv[1] && (process.argv[1].endsWith('implementation-pack.js') || process.argv[1].endsWith('implementation-pack'))) {
|
|
77
|
-
run().catch(
|
|
82
|
+
run().catch(handleCliError);
|
|
78
83
|
}
|
package/scripts/init.js
CHANGED
|
@@ -13,6 +13,7 @@ import { mkdir, readdir, readFile, writeFile, copyFile } from 'node:fs/promises'
|
|
|
13
13
|
import { join, basename } from 'node:path';
|
|
14
14
|
import { existsSync } from 'node:fs';
|
|
15
15
|
import { REPO_ROOT } from '../lib/paths.js';
|
|
16
|
+
import { inputError, handleCliError } from '../lib/errors.js';
|
|
16
17
|
|
|
17
18
|
const TEMPLATES_DIR = join(REPO_ROOT, 'templates');
|
|
18
19
|
const PROJECTS_DIR = join(REPO_ROOT, 'projects');
|
|
@@ -82,15 +83,28 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
if (!isValidName(projectName)) {
|
|
85
|
-
throw
|
|
86
|
-
|
|
86
|
+
throw inputError(
|
|
87
|
+
'INPUT_BAD_NAME',
|
|
88
|
+
`Invalid project name "${projectName}".`,
|
|
87
89
|
`Use lowercase letters, numbers, and hyphens (e.g. "my-project").`
|
|
88
90
|
);
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
// Validate --domain to prevent path traversal via template lookup.
|
|
94
|
+
if (!isValidName(domain)) {
|
|
95
|
+
throw inputError(
|
|
96
|
+
'INPUT_BAD_DOMAIN',
|
|
97
|
+
`Invalid --domain "${domain}".`,
|
|
98
|
+
`Use lowercase letters, numbers, and hyphens (e.g. "character-design").`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
const projectDir = join(PROJECTS_DIR, projectName);
|
|
92
103
|
if (existsSync(projectDir)) {
|
|
93
|
-
throw
|
|
104
|
+
throw inputError(
|
|
105
|
+
'INPUT_PROJECT_EXISTS',
|
|
106
|
+
`Project "${projectName}" already exists at ${projectDir}`
|
|
107
|
+
);
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
// Resolve domain template source
|
|
@@ -99,8 +113,9 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
99
113
|
domainDir = join(TEMPLATES_DIR, 'domains', domain);
|
|
100
114
|
if (!existsSync(domainDir)) {
|
|
101
115
|
const domains = await listDomains();
|
|
102
|
-
throw
|
|
103
|
-
|
|
116
|
+
throw inputError(
|
|
117
|
+
'INPUT_UNKNOWN_DOMAIN',
|
|
118
|
+
`Domain "${domain}" not found.`,
|
|
104
119
|
`Available: generic, ${domains.join(', ')}`
|
|
105
120
|
);
|
|
106
121
|
}
|
|
@@ -206,18 +221,30 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
206
221
|
|
|
207
222
|
// Copy canon markdown templates
|
|
208
223
|
const canonTemplates = ['constitution.md', 'review-rubric.md'];
|
|
224
|
+
const wroteCanon = {};
|
|
209
225
|
for (const file of canonTemplates) {
|
|
210
226
|
const src = join(TEMPLATES_DIR, 'canon', file);
|
|
211
227
|
if (existsSync(src)) {
|
|
212
228
|
await copyFile(src, join(projectDir, 'canon', file));
|
|
229
|
+
wroteCanon[file] = true;
|
|
213
230
|
console.log(` \x1b[32m✓\x1b[0m canon/${file}`);
|
|
214
231
|
}
|
|
215
232
|
}
|
|
216
233
|
|
|
217
|
-
// Copy example prompt pack
|
|
218
|
-
|
|
219
|
-
|
|
234
|
+
// Copy example prompt pack — prefer per-domain example (subjects match domain lane id_patterns),
|
|
235
|
+
// fall back to legacy global template if it exists (pre-v3.0.1 installs may still ship it).
|
|
236
|
+
let wroteExamplePack = false;
|
|
237
|
+
const domainExamplePack = domainDir
|
|
238
|
+
? join(domainDir, 'inputs', 'prompts', 'example-wave.json')
|
|
239
|
+
: null;
|
|
240
|
+
const globalExamplePack = join(TEMPLATES_DIR, 'inputs', 'prompts', 'example-wave.json');
|
|
241
|
+
const examplePack =
|
|
242
|
+
domainExamplePack && existsSync(domainExamplePack) ? domainExamplePack
|
|
243
|
+
: existsSync(globalExamplePack) ? globalExamplePack
|
|
244
|
+
: null;
|
|
245
|
+
if (examplePack) {
|
|
220
246
|
await copyFile(examplePack, join(projectDir, 'inputs', 'prompts', 'example-wave.json'));
|
|
247
|
+
wroteExamplePack = true;
|
|
221
248
|
console.log(` \x1b[32m✓\x1b[0m inputs/prompts/example-wave.json`);
|
|
222
249
|
}
|
|
223
250
|
|
|
@@ -236,21 +263,92 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
236
263
|
}
|
|
237
264
|
}
|
|
238
265
|
|
|
266
|
+
// Write a starter README.md inside the project so users have a home base
|
|
267
|
+
// with copy-pasteable next commands and a place for their notes.
|
|
268
|
+
const readmeLines = [
|
|
269
|
+
`# ${projectName}`,
|
|
270
|
+
'',
|
|
271
|
+
`A \`style-dataset-lab\` project — domain: **${domain}**.`,
|
|
272
|
+
'',
|
|
273
|
+
'## Quick commands',
|
|
274
|
+
'',
|
|
275
|
+
'```bash',
|
|
276
|
+
`# Validate that the scaffold is healthy`,
|
|
277
|
+
`sdlab project doctor --project ${projectName}`,
|
|
278
|
+
'',
|
|
279
|
+
];
|
|
280
|
+
if (wroteExamplePack) {
|
|
281
|
+
readmeLines.push(
|
|
282
|
+
`# Generate candidates from the example prompt pack`,
|
|
283
|
+
`sdlab generate inputs/prompts/example-wave.json --project ${projectName}`,
|
|
284
|
+
''
|
|
285
|
+
);
|
|
286
|
+
} else {
|
|
287
|
+
readmeLines.push(
|
|
288
|
+
`# Generate candidates (add a prompt pack under inputs/prompts/ first)`,
|
|
289
|
+
`sdlab generate <pack-path> --project ${projectName}`,
|
|
290
|
+
''
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
readmeLines.push(
|
|
294
|
+
`# Curate a candidate (approved | rejected | borderline)`,
|
|
295
|
+
`sdlab curate <asset_id> approved "reads clean" --project ${projectName}`,
|
|
296
|
+
'',
|
|
297
|
+
`# Bind approved records to constitution rules`,
|
|
298
|
+
`sdlab canon-bind --project ${projectName}`,
|
|
299
|
+
'',
|
|
300
|
+
`# Create a frozen dataset snapshot`,
|
|
301
|
+
`sdlab snapshot create --project ${projectName}`,
|
|
302
|
+
'```',
|
|
303
|
+
'',
|
|
304
|
+
'## Project layout',
|
|
305
|
+
'',
|
|
306
|
+
'- `project.json` — defaults (checkpoint, width, height, sampler)',
|
|
307
|
+
'- `constitution.json` — machine-readable style rules',
|
|
308
|
+
'- `canon/` — human-authored style reference (constitution.md, review-rubric.md)',
|
|
309
|
+
'- `inputs/prompts/` — generation prompt packs',
|
|
310
|
+
'- `outputs/candidates/` — raw generated PNGs',
|
|
311
|
+
'- `outputs/approved/` · `outputs/rejected/` · `outputs/borderline/` — curated outputs',
|
|
312
|
+
'- `records/` — provenance + judgment records (one JSON per candidate)',
|
|
313
|
+
'- `snapshots/` · `splits/` · `exports/` — frozen dataset artifacts',
|
|
314
|
+
'',
|
|
315
|
+
'## Docs',
|
|
316
|
+
'',
|
|
317
|
+
'- Handbook: https://mcp-tool-shop-org.github.io/style-dataset-lab/',
|
|
318
|
+
'- CLI reference: `sdlab --help` (or `sdlab <cmd> --help` for any command)',
|
|
319
|
+
'',
|
|
320
|
+
'## TODO',
|
|
321
|
+
'',
|
|
322
|
+
'- [ ] Fill in `canon/constitution.md` with your style rules',
|
|
323
|
+
'- [ ] Encode those rules in `constitution.json` for canon-bind',
|
|
324
|
+
'- [ ] Add at least one prompt pack under `inputs/prompts/`',
|
|
325
|
+
'- [ ] Run `sdlab project doctor` and fix any warnings',
|
|
326
|
+
''
|
|
327
|
+
);
|
|
328
|
+
await writeFile(join(projectDir, 'README.md'), readmeLines.join('\n'));
|
|
329
|
+
console.log(` \x1b[32m✓\x1b[0m README.md`);
|
|
330
|
+
|
|
239
331
|
console.log('');
|
|
240
332
|
console.log(`\x1b[32m✓\x1b[0m Project "${projectName}" initialized`);
|
|
241
333
|
console.log('');
|
|
242
334
|
console.log('Next steps:');
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
console.log(`
|
|
335
|
+
let step = 1;
|
|
336
|
+
if (wroteCanon['constitution.md']) {
|
|
337
|
+
console.log(` ${step++}. Edit ${projectName}/canon/constitution.md — define your visual rules`);
|
|
338
|
+
}
|
|
339
|
+
console.log(` ${step++}. Edit ${projectName}/constitution.json — encode rules for canon-bind`);
|
|
340
|
+
if (wroteExamplePack) {
|
|
341
|
+
console.log(` ${step++}. Run: sdlab generate inputs/prompts/example-wave.json --project ${projectName}`);
|
|
342
|
+
} else {
|
|
343
|
+
console.log(` ${step++}. Create prompt packs under ${projectName}/inputs/prompts/`);
|
|
344
|
+
console.log(` ${step++}. Run: sdlab generate <pack> --project ${projectName}`);
|
|
345
|
+
}
|
|
346
|
+
console.log(` ${step++}. Run: sdlab project doctor --project ${projectName}`);
|
|
347
|
+
console.log('');
|
|
348
|
+
console.log(`See ${projectName}/README.md for the full quick-start.`);
|
|
248
349
|
}
|
|
249
350
|
|
|
250
351
|
// Direct execution guard
|
|
251
352
|
if (process.argv[1] && (process.argv[1].endsWith('init.js') || process.argv[1].endsWith('init'))) {
|
|
252
|
-
run().catch(
|
|
253
|
-
console.error(err.message || err);
|
|
254
|
-
process.exit(1);
|
|
255
|
-
});
|
|
353
|
+
run().catch(handleCliError);
|
|
256
354
|
}
|
package/scripts/painterly.js
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
import { writeFile, mkdir, readdir } from "node:fs/promises";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { existsSync } from "node:fs";
|
|
21
|
-
import { getProjectName } from "../lib/args.js";
|
|
22
|
-
import { REPO_ROOT } from "../lib/paths.js";
|
|
21
|
+
import { getProjectName, parseNumberFlag } from "../lib/args.js";
|
|
22
|
+
import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
|
|
23
|
+
import { runtimeError, handleCliError } from "../lib/errors.js";
|
|
23
24
|
import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
|
|
24
25
|
|
|
25
26
|
// ── Config ──
|
|
@@ -156,11 +157,11 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
156
157
|
|
|
157
158
|
for (let i = 0; i < argv.length; i++) {
|
|
158
159
|
if (argv[i] === "--source" && argv[i + 1]) sourceDir = argv[++i];
|
|
159
|
-
if (argv[i] === "--limit" && argv[i + 1]) limit =
|
|
160
|
-
if (argv[i] === "--offset" && argv[i + 1]) offset =
|
|
160
|
+
else if (argv[i] === "--limit" && argv[i + 1]) limit = parseNumberFlag('limit', argv[++i], { int: true, min: 1 });
|
|
161
|
+
else if (argv[i] === "--offset" && argv[i + 1]) offset = parseNumberFlag('offset', argv[++i], { int: true, min: 0 });
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
const fullSourceDir =
|
|
164
|
+
const fullSourceDir = resolveSafeProjectPath(GAME_ROOT, sourceDir, { flagName: 'source' });
|
|
164
165
|
const outDir = join(GAME_ROOT, "outputs/painterly");
|
|
165
166
|
await mkdir(outDir, { recursive: true });
|
|
166
167
|
|
|
@@ -195,7 +196,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
195
196
|
if (!dryRun) {
|
|
196
197
|
const online = await comfyHealth(COMFY_URL);
|
|
197
198
|
if (!online) {
|
|
198
|
-
throw
|
|
199
|
+
throw runtimeError('RUNTIME_COMFY_UNREACHABLE', "ComfyUI not reachable at " + COMFY_URL);
|
|
199
200
|
}
|
|
200
201
|
console.log("\x1b[32m✓\x1b[0m ComfyUI online");
|
|
201
202
|
}
|
|
@@ -263,8 +264,5 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
263
264
|
|
|
264
265
|
// Direct execution guard
|
|
265
266
|
if (process.argv[1] && (process.argv[1].endsWith('painterly.js') || process.argv[1].endsWith('painterly'))) {
|
|
266
|
-
run().catch(
|
|
267
|
-
console.error(err.message || err);
|
|
268
|
-
process.exit(1);
|
|
269
|
-
});
|
|
267
|
+
run().catch(handleCliError);
|
|
270
268
|
}
|
package/scripts/refine.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { readFileSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
-
import { parseArgs } from '../lib/args.js';
|
|
14
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
15
15
|
import { getProjectRoot, getRunsDir } from '../lib/paths.js';
|
|
16
16
|
import { loadCritique } from '../lib/critique-engine.js';
|
|
17
17
|
import {
|
|
@@ -25,7 +25,7 @@ import { info } from '../lib/log.js';
|
|
|
25
25
|
export async function run(argv = process.argv.slice(2)) {
|
|
26
26
|
const { flags } = parseArgs(argv, {
|
|
27
27
|
flags: {
|
|
28
|
-
project: { type: 'string'
|
|
28
|
+
project: { type: 'string' },
|
|
29
29
|
run: { type: 'string' },
|
|
30
30
|
pick: { type: 'string' },
|
|
31
31
|
preserve: { type: 'string' },
|
|
@@ -49,7 +49,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const projectName = flags.project;
|
|
52
|
+
const projectName = flags.project || getProjectName(argv);
|
|
53
53
|
const projectRoot = getProjectRoot(projectName);
|
|
54
54
|
const runId = flags.run;
|
|
55
55
|
const pickedFilename = flags.pick;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* sdlab reingest selected --selection selection_2026-04-16_001 --dry-run
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { parseArgs } from '../lib/args.js';
|
|
11
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
12
12
|
import { getProjectRoot } from '../lib/paths.js';
|
|
13
13
|
import { reingestSelection } from '../lib/reingest-selected.js';
|
|
14
14
|
import { info } from '../lib/log.js';
|
|
@@ -16,7 +16,7 @@ import { info } from '../lib/log.js';
|
|
|
16
16
|
export async function run(argv = process.argv.slice(2)) {
|
|
17
17
|
const { flags } = parseArgs(argv, {
|
|
18
18
|
flags: {
|
|
19
|
-
project: { type: 'string'
|
|
19
|
+
project: { type: 'string' },
|
|
20
20
|
selection: { type: 'string' },
|
|
21
21
|
'dry-run': { type: 'boolean' },
|
|
22
22
|
json: { type: 'boolean' },
|
|
@@ -34,7 +34,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const projectName = flags.project;
|
|
37
|
+
const projectName = flags.project || getProjectName(argv);
|
|
38
38
|
const projectRoot = getProjectRoot(projectName);
|
|
39
39
|
const dryRun = flags['dry-run'] || false;
|
|
40
40
|
|
package/scripts/reingest.js
CHANGED
|
@@ -5,38 +5,51 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* sdlab reingest generated --source <dir> --manifest <id> [--dry-run] [--project <name>]
|
|
8
|
+
* sdlab reingest selected --selection <id> [--project <name>]
|
|
8
9
|
* sdlab reingest audit [--project <name>]
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { join } from 'node:path';
|
|
12
|
-
import { getProjectName } from '../lib/args.js';
|
|
13
|
-
import { REPO_ROOT } from '../lib/paths.js';
|
|
13
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
14
|
+
import { REPO_ROOT, resolveSafeProjectPath } from '../lib/paths.js';
|
|
15
|
+
import { inputError, handleCliError } from '../lib/errors.js';
|
|
14
16
|
import { reingestGenerated, auditReingest } from '../lib/reingest.js';
|
|
15
17
|
|
|
16
18
|
export async function run(argv = process.argv.slice(2)) {
|
|
17
|
-
const
|
|
19
|
+
const { flags, positionals } = parseArgs(argv, {
|
|
20
|
+
flags: {
|
|
21
|
+
project: { type: 'string' },
|
|
22
|
+
source: { type: 'string' },
|
|
23
|
+
manifest: { type: 'string' },
|
|
24
|
+
selection: { type: 'string' },
|
|
25
|
+
'dry-run': { type: 'boolean', default: false },
|
|
26
|
+
},
|
|
27
|
+
deprecated: { game: 'project' },
|
|
28
|
+
allowUnknown: true, // forward unknowns to delegated 'selected' path
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const projectName = flags.project || getProjectName(argv);
|
|
18
32
|
const projectRoot = join(REPO_ROOT, 'projects', projectName);
|
|
19
|
-
const subcommand =
|
|
20
|
-
const args = argv.filter(a => a !== subcommand);
|
|
33
|
+
const subcommand = positionals[0] || 'audit';
|
|
21
34
|
|
|
22
35
|
if (subcommand === 'generated') {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const manifestId = args[manifestIdx + 1];
|
|
36
|
+
if (!flags.source) {
|
|
37
|
+
throw inputError('INPUT_MISSING_FLAG', '--source <dir> is required (directory of generated images)');
|
|
38
|
+
}
|
|
39
|
+
if (!flags.manifest) {
|
|
40
|
+
throw inputError('INPUT_MISSING_FLAG', '--manifest <id> is required (training manifest that produced the outputs)');
|
|
41
|
+
}
|
|
30
42
|
|
|
31
|
-
const
|
|
43
|
+
const safeSource = resolveSafeProjectPath(projectRoot, flags.source, { baseRoot: REPO_ROOT, flagName: 'source' });
|
|
44
|
+
const dryRun = flags['dry-run'] || flags.dryRun;
|
|
32
45
|
|
|
33
46
|
console.log(`\x1b[1msdlab reingest generated\x1b[0m — ${projectName}`);
|
|
34
|
-
console.log(` Source: ${
|
|
35
|
-
console.log(` Manifest: ${
|
|
47
|
+
console.log(` Source: ${flags.source}`);
|
|
48
|
+
console.log(` Manifest: ${flags.manifest}`);
|
|
36
49
|
if (dryRun) console.log(` Mode: DRY RUN (no records written)`);
|
|
37
50
|
console.log('');
|
|
38
51
|
|
|
39
|
-
const result = await reingestGenerated(projectRoot,
|
|
52
|
+
const result = await reingestGenerated(projectRoot, safeSource, flags.manifest, { dryRun });
|
|
40
53
|
|
|
41
54
|
console.log(` \x1b[32m✓\x1b[0m Created: ${result.created} records`);
|
|
42
55
|
if (result.skipped > 0) {
|
|
@@ -51,9 +64,9 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
} else if (subcommand === 'selected') {
|
|
54
|
-
// Delegate to reingest-selected.js
|
|
67
|
+
// Delegate to reingest-selected.js — pass the full argv so it can re-parse
|
|
55
68
|
const { run: runSelected } = await import('./reingest-selected.js');
|
|
56
|
-
await runSelected(
|
|
69
|
+
await runSelected(argv.slice(argv.indexOf(subcommand) + 1));
|
|
57
70
|
return;
|
|
58
71
|
|
|
59
72
|
} else if (subcommand === 'audit') {
|
|
@@ -83,10 +96,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
} else {
|
|
86
|
-
throw
|
|
99
|
+
throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: generated, selected, audit`);
|
|
87
100
|
}
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
if (process.argv[1] && (process.argv[1].endsWith('reingest.js') || process.argv[1].endsWith('reingest'))) {
|
|
91
|
-
run().catch(
|
|
104
|
+
run().catch(handleCliError);
|
|
92
105
|
}
|