@mcptoolshop/style-dataset-lab 2.3.0 → 3.0.1
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 +178 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +71 -0
- package/README.md +82 -9
- package/SECURITY.md +6 -5
- package/bin/sdlab.js +359 -11
- package/lib/adapters/comfyui-runner.js +170 -0
- package/lib/adapters/comfyui-workflows.js +163 -0
- 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 +122 -0
- package/lib/batch-modes.js +111 -0
- package/lib/batch-runs.js +237 -0
- package/lib/batch-sheet-render.js +209 -0
- package/lib/brief-compiler.js +625 -0
- package/lib/brief-render.js +205 -0
- package/lib/card.js +11 -4
- package/lib/comfyui.js +100 -6
- package/lib/config.js +68 -3
- package/lib/critique-engine.js +413 -0
- package/lib/critique-render.js +171 -0
- 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/generated-provenance.js +87 -0
- package/lib/implementation-packs.js +15 -1
- package/lib/log.js +41 -4
- package/lib/paths.js +72 -3
- package/lib/refine-briefs.js +336 -0
- package/lib/reingest-selected.js +184 -0
- package/lib/reingest.js +15 -1
- package/lib/run-summary.js +132 -0
- package/lib/runtime-runs.js +218 -0
- package/lib/selections.js +375 -0
- package/lib/snapshot.js +100 -7
- package/lib/split.js +175 -17
- package/lib/training-manifests.js +17 -3
- package/lib/training-packages.js +28 -14
- package/lib/workflow-profiles.js +161 -0
- package/package.json +13 -4
- package/scripts/batch-generate.js +176 -0
- package/scripts/batch-sheet.js +46 -0
- package/scripts/batch-show.js +86 -0
- package/scripts/brief-compile.js +134 -0
- package/scripts/brief-show.js +53 -0
- package/scripts/canon-bind.js +7 -9
- package/scripts/card.js +22 -25
- package/scripts/compare.js +38 -33
- package/scripts/critique-show.js +50 -0
- package/scripts/critique.js +70 -0
- package/scripts/curate.js +91 -46
- package/scripts/doctor.js +172 -6
- 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 +68 -13
- package/scripts/implementation-pack.js +19 -14
- package/scripts/init.js +137 -17
- package/scripts/painterly.js +8 -10
- package/scripts/refine.js +119 -0
- package/scripts/reingest-selected.js +78 -0
- package/scripts/reingest.js +37 -18
- package/scripts/run-generate.js +76 -0
- package/scripts/run-list.js +58 -0
- package/scripts/run-show.js +63 -0
- package/scripts/select.js +119 -0
- package/scripts/selection-show.js +88 -0
- 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 +61 -0
- package/scripts/workflow-show.js +97 -0
- package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/architecture/terminology.json +10 -8
- package/templates/domains/architecture/workflows/profiles/environment-moodboard.json +62 -0
- package/templates/domains/architecture/workflows/profiles/interior-variant-pack.json +60 -0
- package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/character-design/project.json +4 -2
- package/templates/domains/character-design/workflows/profiles/character-portrait-set.json +61 -0
- package/templates/domains/character-design/workflows/profiles/expression-sheet.json +59 -0
- 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/creature-design/workflows/profiles/creature-portrait-set.json +61 -0
- package/templates/domains/creature-design/workflows/profiles/silhouette-exploration.json +58 -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/game-art/workflows/profiles/character-portrait-set.json +62 -0
- package/templates/domains/game-art/workflows/profiles/environment-moodboard.json +61 -0
- package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/vehicle-mech/project.json +4 -2
- package/templates/domains/vehicle-mech/workflows/profiles/variant-pack.json +60 -0
- package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
- 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
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* sdlab generate:ipadapter --subject corrigan --ref <image> --seeds 4 --project star-freight
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
-
import { getProjectName } from "../lib/args.js";
|
|
17
|
-
import { REPO_ROOT } from "../lib/paths.js";
|
|
16
|
+
import { getProjectName, parseNumberFlag } from "../lib/args.js";
|
|
17
|
+
import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
|
|
18
|
+
import { inputError, runtimeError, handleCliError } from "../lib/errors.js";
|
|
18
19
|
import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
|
|
19
20
|
|
|
20
21
|
const DEFAULTS = {
|
|
@@ -34,17 +35,22 @@ const DEFAULTS = {
|
|
|
34
35
|
function parseLocalArgs(argv) {
|
|
35
36
|
const get = (flag, def) => {
|
|
36
37
|
const i = argv.indexOf(flag);
|
|
37
|
-
|
|
38
|
+
if (i < 0 || i + 1 >= argv.length) return def;
|
|
39
|
+
const v = argv[i + 1];
|
|
40
|
+
if (typeof v === 'string' && v.startsWith('--')) {
|
|
41
|
+
throw inputError('INPUT_MISSING_VALUE', `Flag ${flag} is missing its value (got another flag: ${v}).`);
|
|
42
|
+
}
|
|
43
|
+
return v;
|
|
38
44
|
};
|
|
39
45
|
return {
|
|
40
46
|
subject: get("--subject", "unknown"),
|
|
41
47
|
refPath: get("--ref", null),
|
|
42
48
|
prompt: get("--prompt", null),
|
|
43
49
|
negative: get("--negative", null),
|
|
44
|
-
seeds:
|
|
45
|
-
weight:
|
|
46
|
-
start:
|
|
47
|
-
end:
|
|
50
|
+
seeds: parseNumberFlag('seeds', get("--seeds", "4"), { int: true, min: 1 }),
|
|
51
|
+
weight: parseNumberFlag('weight', get("--weight", "0.55"), { min: 0, max: 2 }),
|
|
52
|
+
start: parseNumberFlag('start', get("--start", "0.0"), { min: 0, max: 1 }),
|
|
53
|
+
end: parseNumberFlag('end', get("--end", "0.8"), { min: 0, max: 1 }),
|
|
48
54
|
dryRun: argv.includes("--dry-run"),
|
|
49
55
|
};
|
|
50
56
|
}
|
|
@@ -174,12 +180,15 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
174
180
|
const opts = parseLocalArgs(argv);
|
|
175
181
|
|
|
176
182
|
if (!opts.refPath) {
|
|
177
|
-
throw
|
|
183
|
+
throw inputError('INPUT_MISSING_FLAG', "--ref <path> is required");
|
|
178
184
|
}
|
|
179
185
|
if (!opts.prompt) {
|
|
180
|
-
throw
|
|
186
|
+
throw inputError('INPUT_MISSING_FLAG', "--prompt is required");
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
// Validate --ref path stays inside project tree.
|
|
190
|
+
const safeRefPath = resolveSafeProjectPath(GAME_ROOT, opts.refPath, { flagName: 'ref' });
|
|
191
|
+
|
|
183
192
|
const negative = opts.negative ||
|
|
184
193
|
"photorealistic, photograph, 3d render, anime, cartoon, text, watermark, blurry, low quality";
|
|
185
194
|
|
|
@@ -196,11 +205,11 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
196
205
|
if (!opts.dryRun) {
|
|
197
206
|
const online = await comfyHealth(COMFY_URL);
|
|
198
207
|
if (!online) {
|
|
199
|
-
throw
|
|
208
|
+
throw runtimeError('RUNTIME_COMFY_UNREACHABLE', "ComfyUI not reachable at " + COMFY_URL);
|
|
200
209
|
}
|
|
201
210
|
console.log("\x1b[32m+\x1b[0m ComfyUI online");
|
|
202
211
|
|
|
203
|
-
await uploadImage(
|
|
212
|
+
await uploadImage(safeRefPath, refFilename, COMFY_URL);
|
|
204
213
|
console.log(`\x1b[32m+\x1b[0m Reference uploaded: ${refFilename}`);
|
|
205
214
|
}
|
|
206
215
|
|
|
@@ -255,8 +264,5 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
255
264
|
|
|
256
265
|
// Direct execution guard
|
|
257
266
|
if (process.argv[1] && (process.argv[1].endsWith('generate-ipadapter.js') || process.argv[1].endsWith('generate-ipadapter'))) {
|
|
258
|
-
run().catch(
|
|
259
|
-
console.error(err.message || err);
|
|
260
|
-
process.exit(1);
|
|
261
|
-
});
|
|
267
|
+
run().catch(handleCliError);
|
|
262
268
|
}
|
package/scripts/generate.js
CHANGED
|
@@ -10,11 +10,34 @@
|
|
|
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
14
|
import { join } from "node:path";
|
|
15
|
-
import { getProjectName } from "../lib/args.js";
|
|
16
|
-
import { REPO_ROOT } from "../lib/paths.js";
|
|
15
|
+
import { parseArgs, getProjectName } from "../lib/args.js";
|
|
16
|
+
import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
|
|
17
|
+
import { readJsonFile } from "../lib/config.js";
|
|
18
|
+
import { runtimeError, handleCliError } from "../lib/errors.js";
|
|
17
19
|
import { comfyHealth, submitAndWait, downloadImage } from "../lib/comfyui.js";
|
|
20
|
+
import { info, result } from "../lib/log.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format milliseconds as a human ETA string like "6m 12s" or "42s".
|
|
24
|
+
*/
|
|
25
|
+
function formatEta(ms) {
|
|
26
|
+
if (!Number.isFinite(ms) || ms < 0) return '—';
|
|
27
|
+
const totalSec = Math.round(ms / 1000);
|
|
28
|
+
if (totalSec < 60) return `${totalSec}s`;
|
|
29
|
+
const m = Math.floor(totalSec / 60);
|
|
30
|
+
const s = totalSec % 60;
|
|
31
|
+
return `${m}m ${s}s`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* COMFY_URL precedence:
|
|
36
|
+
* 1. process.env.COMFY_URL (explicit user env)
|
|
37
|
+
* 2. project defaults.comfy_url (set by `sdlab project doctor`)
|
|
38
|
+
* 3. built-in default http://127.0.0.1:8188
|
|
39
|
+
* The adapter (lib/adapters/comfyui-runner.js) applies the same precedence.
|
|
40
|
+
*/
|
|
18
41
|
|
|
19
42
|
function buildWorkflow(prompt, negativePrompt, checkpoint, loras, seed, steps, cfg, sampler, scheduler, width, height, ipAdapterConfig) {
|
|
20
43
|
const nodes = {};
|
|
@@ -157,11 +180,19 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
157
180
|
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
158
181
|
const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
|
|
159
182
|
|
|
160
|
-
const
|
|
161
|
-
|
|
183
|
+
const parsed = parseArgs(argv, {
|
|
184
|
+
flags: {
|
|
185
|
+
project: { type: 'string' },
|
|
186
|
+
game: { type: 'string' },
|
|
187
|
+
'dry-run': { type: 'boolean' },
|
|
188
|
+
},
|
|
189
|
+
deprecated: { game: 'project' },
|
|
190
|
+
});
|
|
191
|
+
const packPath = parsed.positionals[0] || "inputs/prompts/rpg-icons-lane1.json";
|
|
192
|
+
const dryRun = parsed.flags['dry-run'] === true;
|
|
162
193
|
|
|
163
|
-
const fullPackPath =
|
|
164
|
-
const pack =
|
|
194
|
+
const fullPackPath = resolveSafeProjectPath(GAME_ROOT, packPath, { flagName: 'pack-path' });
|
|
195
|
+
const pack = await readJsonFile(fullPackPath, { requiredFields: ['lane', 'subjects', 'variations', 'defaults'] });
|
|
165
196
|
|
|
166
197
|
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m generate`);
|
|
167
198
|
console.log(` Lane: ${pack.lane}`);
|
|
@@ -173,7 +204,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
173
204
|
if (!dryRun) {
|
|
174
205
|
const online = await comfyHealth(COMFY_URL);
|
|
175
206
|
if (!online) {
|
|
176
|
-
throw
|
|
207
|
+
throw runtimeError('RUNTIME_COMFY_UNREACHABLE', "ComfyUI not reachable at " + COMFY_URL, 'Start ComfyUI or set COMFY_URL to the right host.');
|
|
177
208
|
}
|
|
178
209
|
console.log("\x1b[32m✓\x1b[0m ComfyUI online");
|
|
179
210
|
}
|
|
@@ -183,7 +214,9 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
183
214
|
|
|
184
215
|
const d = pack.defaults;
|
|
185
216
|
let generated = 0;
|
|
217
|
+
let errors = 0;
|
|
186
218
|
const totalExpected = pack.subjects.length * pack.variations.length;
|
|
219
|
+
const runStartMs = Date.now();
|
|
187
220
|
|
|
188
221
|
for (const subject of pack.subjects) {
|
|
189
222
|
for (const variation of pack.variations) {
|
|
@@ -272,21 +305,43 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
272
305
|
console.log(` \x1b[32m✓\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
|
|
273
306
|
} catch (err) {
|
|
274
307
|
console.log(` \x1b[31m✗\x1b[0m ${err.message}`);
|
|
308
|
+
errors++;
|
|
275
309
|
}
|
|
276
310
|
|
|
277
311
|
generated++;
|
|
312
|
+
|
|
313
|
+
// ETA every 5 items (not every item — too noisy for long runs).
|
|
314
|
+
if (!dryRun && generated > 0 && generated % 5 === 0 && generated < totalExpected) {
|
|
315
|
+
const elapsedTotal = Date.now() - runStartMs;
|
|
316
|
+
const avgMs = elapsedTotal / generated;
|
|
317
|
+
const remaining = totalExpected - generated;
|
|
318
|
+
const etaMs = avgMs * remaining;
|
|
319
|
+
info('generate', `progress ${generated}/${totalExpected} — avg ${formatEta(avgMs)}/item, ETA ~${formatEta(etaMs)}`);
|
|
320
|
+
}
|
|
278
321
|
}
|
|
279
322
|
}
|
|
280
323
|
|
|
281
324
|
console.log("");
|
|
282
|
-
|
|
325
|
+
const succeeded = generated - errors;
|
|
326
|
+
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`);
|
|
331
|
+
}
|
|
283
332
|
if (dryRun) console.log(" (dry run — no images produced)");
|
|
333
|
+
// Always print the output directory so scripted pipelines can parse it,
|
|
334
|
+
// even under --quiet.
|
|
335
|
+
if (!dryRun && succeeded > 0) {
|
|
336
|
+
result(`Candidates: ${join(GAME_ROOT, 'outputs/candidates')}`);
|
|
337
|
+
result(`Records: ${join(GAME_ROOT, 'records')}`);
|
|
338
|
+
}
|
|
339
|
+
if (!dryRun && errors > 0 && succeeded === 0) {
|
|
340
|
+
throw runtimeError('RUNTIME_ALL_FAILED', `All ${totalExpected} generation attempts failed.`);
|
|
341
|
+
}
|
|
284
342
|
}
|
|
285
343
|
|
|
286
344
|
// Direct execution guard
|
|
287
345
|
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
|
-
});
|
|
346
|
+
run().catch(handleCliError);
|
|
292
347
|
}
|
|
@@ -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');
|
|
@@ -40,6 +41,13 @@ const PROJECT_DIRS = [
|
|
|
40
41
|
'training/packages',
|
|
41
42
|
'training/eval-runs',
|
|
42
43
|
'training/implementations',
|
|
44
|
+
'workflows/profiles',
|
|
45
|
+
'workflows/batch-modes',
|
|
46
|
+
'briefs',
|
|
47
|
+
'runs',
|
|
48
|
+
'batches',
|
|
49
|
+
'selections',
|
|
50
|
+
'inbox/generated',
|
|
43
51
|
];
|
|
44
52
|
|
|
45
53
|
function isValidName(name) {
|
|
@@ -75,15 +83,28 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
if (!isValidName(projectName)) {
|
|
78
|
-
throw
|
|
79
|
-
|
|
86
|
+
throw inputError(
|
|
87
|
+
'INPUT_BAD_NAME',
|
|
88
|
+
`Invalid project name "${projectName}".`,
|
|
80
89
|
`Use lowercase letters, numbers, and hyphens (e.g. "my-project").`
|
|
81
90
|
);
|
|
82
91
|
}
|
|
83
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
|
+
|
|
84
102
|
const projectDir = join(PROJECTS_DIR, projectName);
|
|
85
103
|
if (existsSync(projectDir)) {
|
|
86
|
-
throw
|
|
104
|
+
throw inputError(
|
|
105
|
+
'INPUT_PROJECT_EXISTS',
|
|
106
|
+
`Project "${projectName}" already exists at ${projectDir}`
|
|
107
|
+
);
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
// Resolve domain template source
|
|
@@ -92,8 +113,9 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
92
113
|
domainDir = join(TEMPLATES_DIR, 'domains', domain);
|
|
93
114
|
if (!existsSync(domainDir)) {
|
|
94
115
|
const domains = await listDomains();
|
|
95
|
-
throw
|
|
96
|
-
|
|
116
|
+
throw inputError(
|
|
117
|
+
'INPUT_UNKNOWN_DOMAIN',
|
|
118
|
+
`Domain "${domain}" not found.`,
|
|
97
119
|
`Available: generic, ${domains.join(', ')}`
|
|
98
120
|
);
|
|
99
121
|
}
|
|
@@ -199,36 +221,134 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
199
221
|
|
|
200
222
|
// Copy canon markdown templates
|
|
201
223
|
const canonTemplates = ['constitution.md', 'review-rubric.md'];
|
|
224
|
+
const wroteCanon = {};
|
|
202
225
|
for (const file of canonTemplates) {
|
|
203
226
|
const src = join(TEMPLATES_DIR, 'canon', file);
|
|
204
227
|
if (existsSync(src)) {
|
|
205
228
|
await copyFile(src, join(projectDir, 'canon', file));
|
|
229
|
+
wroteCanon[file] = true;
|
|
206
230
|
console.log(` \x1b[32m✓\x1b[0m canon/${file}`);
|
|
207
231
|
}
|
|
208
232
|
}
|
|
209
233
|
|
|
210
|
-
// Copy example prompt pack
|
|
211
|
-
|
|
212
|
-
|
|
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) {
|
|
213
246
|
await copyFile(examplePack, join(projectDir, 'inputs', 'prompts', 'example-wave.json'));
|
|
247
|
+
wroteExamplePack = true;
|
|
214
248
|
console.log(` \x1b[32m✓\x1b[0m inputs/prompts/example-wave.json`);
|
|
215
249
|
}
|
|
216
250
|
|
|
251
|
+
// Copy domain workflow profiles if available
|
|
252
|
+
if (domainDir) {
|
|
253
|
+
const domainWorkflowsDir = join(domainDir, 'workflows', 'profiles');
|
|
254
|
+
if (existsSync(domainWorkflowsDir)) {
|
|
255
|
+
const wfFiles = (await readdir(domainWorkflowsDir)).filter(f => f.endsWith('.json'));
|
|
256
|
+
for (const file of wfFiles) {
|
|
257
|
+
await copyFile(
|
|
258
|
+
join(domainWorkflowsDir, file),
|
|
259
|
+
join(projectDir, 'workflows', 'profiles', file)
|
|
260
|
+
);
|
|
261
|
+
console.log(` \x1b[32m✓\x1b[0m workflows/profiles/${file}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
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
|
+
|
|
217
331
|
console.log('');
|
|
218
332
|
console.log(`\x1b[32m✓\x1b[0m Project "${projectName}" initialized`);
|
|
219
333
|
console.log('');
|
|
220
334
|
console.log('Next steps:');
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
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.`);
|
|
226
349
|
}
|
|
227
350
|
|
|
228
351
|
// Direct execution guard
|
|
229
352
|
if (process.argv[1] && (process.argv[1].endsWith('init.js') || process.argv[1].endsWith('init'))) {
|
|
230
|
-
run().catch(
|
|
231
|
-
console.error(err.message || err);
|
|
232
|
-
process.exit(1);
|
|
233
|
-
});
|
|
353
|
+
run().catch(handleCliError);
|
|
234
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
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* refine.js — Generate a refined next-pass brief from critique.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab refine --project star-freight --run run_2026-04-15_001 --pick 003.png
|
|
8
|
+
* sdlab refine --run run_2026-04-15_001 --pick 003.png --push "stronger faction read" --suppress "extra ornament"
|
|
9
|
+
* sdlab refine --run run_2026-04-15_001 --pick 003.png --json
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
15
|
+
import { getProjectRoot, getRunsDir } from '../lib/paths.js';
|
|
16
|
+
import { loadCritique } from '../lib/critique-engine.js';
|
|
17
|
+
import {
|
|
18
|
+
extractDelta,
|
|
19
|
+
buildRefinedBrief,
|
|
20
|
+
saveRefinedBrief,
|
|
21
|
+
renderRefinedBriefMarkdown,
|
|
22
|
+
} from '../lib/refine-briefs.js';
|
|
23
|
+
import { info } from '../lib/log.js';
|
|
24
|
+
|
|
25
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
26
|
+
const { flags } = parseArgs(argv, {
|
|
27
|
+
flags: {
|
|
28
|
+
project: { type: 'string' },
|
|
29
|
+
run: { type: 'string' },
|
|
30
|
+
pick: { type: 'string' },
|
|
31
|
+
preserve: { type: 'string' },
|
|
32
|
+
push: { type: 'string' },
|
|
33
|
+
suppress: { type: 'string' },
|
|
34
|
+
json: { type: 'boolean' },
|
|
35
|
+
},
|
|
36
|
+
deprecated: { game: 'project' },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!flags.run || !flags.pick) {
|
|
40
|
+
console.log('Usage: sdlab refine --run <id> --pick <filename> [--project <name>]');
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log('Options:');
|
|
43
|
+
console.log(' --run <id> Run to refine from (required)');
|
|
44
|
+
console.log(' --pick <filename> Candidate filename to refine from (required)');
|
|
45
|
+
console.log(' --preserve <notes> Comma-separated preserve instructions');
|
|
46
|
+
console.log(' --push <notes> Comma-separated push instructions');
|
|
47
|
+
console.log(' --suppress <notes> Comma-separated suppress instructions');
|
|
48
|
+
console.log(' --json Output raw JSON');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const projectName = flags.project || getProjectName(argv);
|
|
53
|
+
const projectRoot = getProjectRoot(projectName);
|
|
54
|
+
const runId = flags.run;
|
|
55
|
+
const pickedFilename = flags.pick;
|
|
56
|
+
|
|
57
|
+
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m refine`);
|
|
58
|
+
console.log(` Project: ${projectName}`);
|
|
59
|
+
console.log(` Run: ${runId}`);
|
|
60
|
+
console.log(` Picked: ${pickedFilename}`);
|
|
61
|
+
console.log('');
|
|
62
|
+
|
|
63
|
+
// Load critique
|
|
64
|
+
const critique = loadCritique(projectRoot, runId);
|
|
65
|
+
|
|
66
|
+
// Load parent brief from the run
|
|
67
|
+
const runDir = join(getRunsDir(projectRoot), runId);
|
|
68
|
+
const parentBrief = JSON.parse(readFileSync(join(runDir, 'brief.json'), 'utf-8'));
|
|
69
|
+
|
|
70
|
+
// Parse CLI delta overrides
|
|
71
|
+
const cliOverrides = {
|
|
72
|
+
preserve: flags.preserve ? flags.preserve.split(',').map(s => s.trim()).filter(Boolean) : [],
|
|
73
|
+
push: flags.push ? flags.push.split(',').map(s => s.trim()).filter(Boolean) : [],
|
|
74
|
+
suppress: flags.suppress ? flags.suppress.split(',').map(s => s.trim()).filter(Boolean) : [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Extract delta from critique + overrides
|
|
78
|
+
const delta = extractDelta(critique, pickedFilename, cliOverrides);
|
|
79
|
+
|
|
80
|
+
// Build refined brief
|
|
81
|
+
const refineDir = join(runDir, 'refine');
|
|
82
|
+
const refinedBrief = buildRefinedBrief({
|
|
83
|
+
parentBrief,
|
|
84
|
+
critique,
|
|
85
|
+
pickedCandidate: pickedFilename,
|
|
86
|
+
delta,
|
|
87
|
+
runId,
|
|
88
|
+
refineDir,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Save
|
|
92
|
+
const { jsonPath, mdPath } = await saveRefinedBrief(projectRoot, runId, refinedBrief);
|
|
93
|
+
|
|
94
|
+
if (flags.json) {
|
|
95
|
+
console.log(JSON.stringify(refinedBrief, null, 2));
|
|
96
|
+
} else {
|
|
97
|
+
console.log(`Refined brief: ${refinedBrief.brief_id}`);
|
|
98
|
+
console.log(`Parent: ${refinedBrief.parent_brief_id}`);
|
|
99
|
+
console.log(`Source: ${runId} → ${pickedFilename}`);
|
|
100
|
+
console.log('');
|
|
101
|
+
|
|
102
|
+
if (delta.preserve.length > 0) {
|
|
103
|
+
console.log('Preserve:');
|
|
104
|
+
for (const p of delta.preserve) console.log(` ✓ ${p}`);
|
|
105
|
+
}
|
|
106
|
+
if (delta.push.length > 0) {
|
|
107
|
+
console.log('Push:');
|
|
108
|
+
for (const p of delta.push) console.log(` → ${p}`);
|
|
109
|
+
}
|
|
110
|
+
if (delta.suppress.length > 0) {
|
|
111
|
+
console.log('Suppress:');
|
|
112
|
+
for (const s of delta.suppress) console.log(` ✗ ${s}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log('');
|
|
116
|
+
info(`Saved: runs/${runId}/refine/${refinedBrief.brief_id}.json`);
|
|
117
|
+
info(`Next: sdlab run generate --brief ${refinedBrief.brief_id} (from refine dir)`);
|
|
118
|
+
}
|
|
119
|
+
}
|