@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.
Files changed (106) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/CODE_OF_CONDUCT.md +9 -0
  3. package/CONTRIBUTING.md +71 -0
  4. package/README.md +82 -9
  5. package/SECURITY.md +6 -5
  6. package/bin/sdlab.js +359 -11
  7. package/lib/adapters/comfyui-runner.js +170 -0
  8. package/lib/adapters/comfyui-workflows.js +163 -0
  9. package/lib/adapters/diffusers-lora.js +7 -2
  10. package/lib/adapters/generic-image-caption.js +6 -1
  11. package/lib/args.js +200 -13
  12. package/lib/batch-compiler.js +122 -0
  13. package/lib/batch-modes.js +111 -0
  14. package/lib/batch-runs.js +237 -0
  15. package/lib/batch-sheet-render.js +209 -0
  16. package/lib/brief-compiler.js +625 -0
  17. package/lib/brief-render.js +205 -0
  18. package/lib/card.js +11 -4
  19. package/lib/comfyui.js +100 -6
  20. package/lib/config.js +68 -3
  21. package/lib/critique-engine.js +413 -0
  22. package/lib/critique-render.js +171 -0
  23. package/lib/deprecation.js +1 -1
  24. package/lib/eval-pack.js +94 -4
  25. package/lib/eval-runs.js +42 -3
  26. package/lib/export.js +144 -34
  27. package/lib/generated-provenance.js +87 -0
  28. package/lib/implementation-packs.js +15 -1
  29. package/lib/log.js +41 -4
  30. package/lib/paths.js +72 -3
  31. package/lib/refine-briefs.js +336 -0
  32. package/lib/reingest-selected.js +184 -0
  33. package/lib/reingest.js +15 -1
  34. package/lib/run-summary.js +132 -0
  35. package/lib/runtime-runs.js +218 -0
  36. package/lib/selections.js +375 -0
  37. package/lib/snapshot.js +100 -7
  38. package/lib/split.js +175 -17
  39. package/lib/training-manifests.js +17 -3
  40. package/lib/training-packages.js +28 -14
  41. package/lib/workflow-profiles.js +161 -0
  42. package/package.json +13 -4
  43. package/scripts/batch-generate.js +176 -0
  44. package/scripts/batch-sheet.js +46 -0
  45. package/scripts/batch-show.js +86 -0
  46. package/scripts/brief-compile.js +134 -0
  47. package/scripts/brief-show.js +53 -0
  48. package/scripts/canon-bind.js +7 -9
  49. package/scripts/card.js +22 -25
  50. package/scripts/compare.js +38 -33
  51. package/scripts/critique-show.js +50 -0
  52. package/scripts/critique.js +70 -0
  53. package/scripts/curate.js +91 -46
  54. package/scripts/doctor.js +172 -6
  55. package/scripts/eligibility.js +17 -14
  56. package/scripts/eval-pack.js +13 -11
  57. package/scripts/eval-run.js +30 -29
  58. package/scripts/export.js +27 -31
  59. package/scripts/generate-controlnet.js +31 -17
  60. package/scripts/generate-identity.js +37 -20
  61. package/scripts/generate-ipadapter.js +22 -16
  62. package/scripts/generate.js +68 -13
  63. package/scripts/implementation-pack.js +19 -14
  64. package/scripts/init.js +137 -17
  65. package/scripts/painterly.js +8 -10
  66. package/scripts/refine.js +119 -0
  67. package/scripts/reingest-selected.js +78 -0
  68. package/scripts/reingest.js +37 -18
  69. package/scripts/run-generate.js +76 -0
  70. package/scripts/run-list.js +58 -0
  71. package/scripts/run-show.js +63 -0
  72. package/scripts/select.js +119 -0
  73. package/scripts/selection-show.js +88 -0
  74. package/scripts/snapshot.js +22 -24
  75. package/scripts/split.js +26 -31
  76. package/scripts/training-manifest.js +29 -24
  77. package/scripts/training-package.js +24 -20
  78. package/scripts/training-profile.js +13 -8
  79. package/scripts/workflow-list.js +61 -0
  80. package/scripts/workflow-show.js +97 -0
  81. package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
  82. package/templates/domains/architecture/terminology.json +10 -8
  83. package/templates/domains/architecture/workflows/profiles/environment-moodboard.json +62 -0
  84. package/templates/domains/architecture/workflows/profiles/interior-variant-pack.json +60 -0
  85. package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
  86. package/templates/domains/character-design/project.json +4 -2
  87. package/templates/domains/character-design/workflows/profiles/character-portrait-set.json +61 -0
  88. package/templates/domains/character-design/workflows/profiles/expression-sheet.json +59 -0
  89. package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
  90. package/templates/domains/creature-design/project.json +4 -2
  91. package/templates/domains/creature-design/terminology.json +1 -0
  92. package/templates/domains/creature-design/workflows/profiles/creature-portrait-set.json +61 -0
  93. package/templates/domains/creature-design/workflows/profiles/silhouette-exploration.json +58 -0
  94. package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
  95. package/templates/domains/game-art/project.json +4 -2
  96. package/templates/domains/game-art/workflows/profiles/character-portrait-set.json +62 -0
  97. package/templates/domains/game-art/workflows/profiles/environment-moodboard.json +61 -0
  98. package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
  99. package/templates/domains/vehicle-mech/project.json +4 -2
  100. package/templates/domains/vehicle-mech/workflows/profiles/variant-pack.json +60 -0
  101. package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
  102. package/scripts/bulk-curate-wave2-5.js +0 -138
  103. package/scripts/bulk-curate-waves11-18.js +0 -413
  104. package/scripts/curate-wave25.js +0 -209
  105. package/scripts/migrate-records.js +0 -141
  106. package/templates/inputs/prompts/example-wave.json +0 -27
package/lib/reingest.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * canon-bound like everything else.
8
8
  */
9
9
 
10
- import { readFile, writeFile, readdir, stat } from 'node:fs/promises';
10
+ import { readFile, writeFile, readdir, stat, mkdir, copyFile } from 'node:fs/promises';
11
11
  import { join, basename, extname } from 'node:path';
12
12
  import { existsSync } from 'node:fs';
13
13
  import { loadTrainingManifest } from './training-manifests.js';
@@ -33,6 +33,12 @@ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
33
33
  export async function reingestGenerated(projectRoot, sourcePath, manifestId, options = {}) {
34
34
  const manifest = await loadTrainingManifest(projectRoot, manifestId);
35
35
  const recordsDir = join(projectRoot, 'records');
36
+ // D-020: ensure outputs/candidates/ exists so the record's asset_path
37
+ // contract ("outputs/candidates/<file>") actually points at the image.
38
+ const candidatesDir = join(projectRoot, 'outputs', 'candidates');
39
+ if (!options.dryRun) {
40
+ await mkdir(candidatesDir, { recursive: true });
41
+ }
36
42
 
37
43
  // Find image files in source directory
38
44
  const entries = await readdir(sourcePath);
@@ -80,6 +86,14 @@ export async function reingestGenerated(projectRoot, sourcePath, manifestId, opt
80
86
  record.schema_version = '2.1.0';
81
87
 
82
88
  if (!options.dryRun) {
89
+ // D-020: copy the image into outputs/candidates/ so the record's
90
+ // project-relative asset_path contract is satisfied. Without this,
91
+ // downstream readers (export, adapters) would silently skip the
92
+ // record because the file does not exist at the declared path.
93
+ const destPath = join(candidatesDir, file);
94
+ if (!existsSync(destPath)) {
95
+ await copyFile(filePath, destPath);
96
+ }
83
97
  await writeFile(recordPath, JSON.stringify(record, null, 2) + '\n');
84
98
  }
85
99
 
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Run summary — render manifest to human-readable summary files.
3
+ *
4
+ * Writes summary.md + summary.json to the run directory.
5
+ * Contact sheet generation is separate (requires image processing).
6
+ */
7
+
8
+ import { writeFile } from 'node:fs/promises';
9
+ import { join } from 'node:path';
10
+
11
+ /**
12
+ * Build a summary object from a manifest.
13
+ */
14
+ export function buildRunSummary(manifest) {
15
+ const ok = manifest.outputs.filter(o => o.status === 'ok');
16
+ const failed = manifest.outputs.filter(o => o.status === 'error');
17
+ const totalElapsed = ok.reduce((sum, o) => sum + (o.elapsed_ms || 0), 0);
18
+
19
+ return {
20
+ run_id: manifest.run_id,
21
+ brief_id: manifest.brief_id,
22
+ project_id: manifest.project_id,
23
+ adapter_target: manifest.adapter_target,
24
+ output_mode: manifest.output_mode,
25
+ created_at: manifest.created_at,
26
+ dry_run: manifest.dry_run,
27
+ success_count: manifest.success_count,
28
+ error_count: manifest.error_count,
29
+ total_outputs: manifest.output_count,
30
+ total_elapsed_ms: totalElapsed,
31
+ avg_elapsed_ms: ok.length > 0 ? Math.round(totalElapsed / ok.length) : 0,
32
+ seed_plan: manifest.seed_plan,
33
+ checkpoint: manifest.checkpoint,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Render a manifest to Markdown summary.
39
+ */
40
+ export function renderRunMarkdown(manifest) {
41
+ const lines = [];
42
+ lines.push(`# Run: ${manifest.run_id}`);
43
+ lines.push('');
44
+ lines.push(`**Brief:** ${manifest.brief_id}`);
45
+ lines.push(`**Project:** ${manifest.project_id}`);
46
+ lines.push(`**Created:** ${manifest.created_at}`);
47
+ lines.push(`**Adapter:** ${manifest.adapter_target}`);
48
+ lines.push(`**Mode:** ${manifest.output_mode}`);
49
+ if (manifest.dry_run) lines.push('**⚠ DRY RUN — no images generated**');
50
+ lines.push('');
51
+
52
+ // Results
53
+ lines.push('## Results');
54
+ lines.push('');
55
+ lines.push(`| # | Seed | Status | File | Time |`);
56
+ lines.push(`|---|------|--------|------|------|`);
57
+ for (const out of manifest.outputs) {
58
+ const file = out.filename || '—';
59
+ const time = out.elapsed_ms ? `${(out.elapsed_ms / 1000).toFixed(1)}s` : '—';
60
+ const status = out.status === 'ok' ? '✓' : out.status === 'dry_run' ? '○' : '✗';
61
+ lines.push(`| ${out.index + 1} | ${out.seed} | ${status} | ${file} | ${time} |`);
62
+ }
63
+ lines.push('');
64
+
65
+ // Errors
66
+ const errors = manifest.errors || [];
67
+ if (errors.length > 0) {
68
+ lines.push('## Errors');
69
+ lines.push('');
70
+ for (const err of errors) {
71
+ lines.push(`- **Image ${err.index + 1}** (seed ${err.seed}): ${err.error}`);
72
+ }
73
+ lines.push('');
74
+ }
75
+
76
+ // Runtime
77
+ lines.push('## Runtime');
78
+ lines.push('');
79
+ lines.push(`| Parameter | Value |`);
80
+ lines.push(`|-----------|-------|`);
81
+ lines.push(`| Checkpoint | ${manifest.checkpoint || '—'} |`);
82
+ lines.push(`| LoRAs | ${(manifest.loras || []).map(l => l.name).join(', ') || '—'} |`);
83
+ lines.push(`| ComfyUI | ${manifest.comfy_url || '—'} |`);
84
+
85
+ const rp = manifest.runtime_plan || {};
86
+ lines.push(`| Size | ${rp.width || '?'}×${rp.height || '?'} |`);
87
+ lines.push(`| Steps | ${rp.steps || '?'} |`);
88
+ lines.push(`| CFG | ${rp.cfg || '?'} |`);
89
+ lines.push(`| Sampler | ${rp.sampler || '?'} |`);
90
+ lines.push(`| Seed mode | ${rp.seed_mode || '?'} |`);
91
+ lines.push(`| Base seed | ${manifest.seed_plan?.base_seed || '?'} |`);
92
+ lines.push('');
93
+
94
+ return lines.join('\n');
95
+ }
96
+
97
+ /**
98
+ * Render a manifest to terminal text.
99
+ */
100
+ export function renderRunText(manifest) {
101
+ const ok = manifest.outputs.filter(o => o.status === 'ok' || o.status === 'dry_run');
102
+ const failed = manifest.outputs.filter(o => o.status === 'error');
103
+ const lines = [];
104
+
105
+ lines.push(`Run: ${manifest.run_id}`);
106
+ lines.push(`Brief: ${manifest.brief_id}`);
107
+ lines.push(`Project: ${manifest.project_id}`);
108
+ lines.push(`Mode: ${manifest.output_mode} (${manifest.adapter_target})`);
109
+ if (manifest.dry_run) lines.push('⚠ DRY RUN');
110
+ lines.push(`Results: ${ok.length}/${manifest.output_count} succeeded`);
111
+ if (failed.length > 0) lines.push(`Errors: ${failed.length}`);
112
+ lines.push('');
113
+
114
+ for (const out of manifest.outputs) {
115
+ const status = out.status === 'ok' ? '✓' : out.status === 'dry_run' ? '○' : '✗';
116
+ const time = out.elapsed_ms ? ` (${(out.elapsed_ms / 1000).toFixed(1)}s)` : '';
117
+ const file = out.filename ? ` → ${out.filename}` : '';
118
+ const err = out.error ? ` — ${out.error}` : '';
119
+ lines.push(` ${status} [${out.index + 1}] seed ${out.seed}${file}${time}${err}`);
120
+ }
121
+
122
+ return lines.join('\n');
123
+ }
124
+
125
+ /**
126
+ * Save summary files into the run directory.
127
+ */
128
+ export async function saveRunSummary(runDir, manifest) {
129
+ const summary = buildRunSummary(manifest);
130
+ await writeFile(join(runDir, 'summary.json'), JSON.stringify(summary, null, 2) + '\n');
131
+ await writeFile(join(runDir, 'summary.md'), renderRunMarkdown(manifest) + '\n');
132
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Runtime run management — create, load, list run artifacts.
3
+ *
4
+ * A run is a frozen execution artifact: brief + seeds + outputs + manifest.
5
+ * The run folder IS the review surface — humans open it directly.
6
+ */
7
+
8
+ import { readFile, writeFile, mkdir, readdir, copyFile, rename, unlink } from 'node:fs/promises';
9
+ import { join } from 'node:path';
10
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
11
+ import { randomInt } from 'node:crypto';
12
+ import { getRunsDir, getBriefsDir } from './paths.js';
13
+ import { inputError } from './errors.js';
14
+ import { verbose } from './log.js';
15
+
16
+ // ─── Run ID generation ───────────────────────────────────────────
17
+
18
+ /**
19
+ * Generate a run ID: run_YYYY-MM-DD_NNN
20
+ */
21
+ // PB-008: tighten regex so stray directories like 'run_<date>_001_wip'
22
+ // don't inflate the sequence via parseInt's lenient parsing.
23
+ const RUN_SEQ_RE = /^(\d{3})$/;
24
+
25
+ export function generateRunId(runsDir) {
26
+ const today = new Date().toISOString().slice(0, 10);
27
+ const prefix = `run_${today}_`;
28
+
29
+ let maxSeq = 0;
30
+ if (existsSync(runsDir)) {
31
+ const existing = readdirSync(runsDir, { withFileTypes: true })
32
+ .filter(e => e.isDirectory() && e.name.startsWith(prefix));
33
+ for (const e of existing) {
34
+ const seqStr = e.name.slice(prefix.length);
35
+ const match = RUN_SEQ_RE.exec(seqStr);
36
+ if (!match) continue;
37
+ const seq = parseInt(match[1], 10);
38
+ if (seq > maxSeq) maxSeq = seq;
39
+ }
40
+ }
41
+
42
+ const seq = String(maxSeq + 1).padStart(3, '0');
43
+ return `${prefix}${seq}`;
44
+ }
45
+
46
+ // ─── Seed planning ───────────────────────────────────────────────
47
+
48
+ /**
49
+ * Build a deterministic seed plan.
50
+ * @param {Object} opts
51
+ * @param {'fixed'|'increment'|'random'} opts.seedMode
52
+ * @param {number} opts.outputCount
53
+ * @param {number} [opts.baseSeed] — explicit base seed (default: random)
54
+ * @returns {{ base_seed: number, seeds: number[] }}
55
+ */
56
+ export function buildSeedPlan({ seedMode, outputCount, baseSeed }) {
57
+ const base = baseSeed ?? randomInt(2_147_483_647);
58
+ const seeds = [];
59
+
60
+ switch (seedMode) {
61
+ case 'fixed':
62
+ for (let i = 0; i < outputCount; i++) seeds.push(base);
63
+ break;
64
+ case 'increment':
65
+ for (let i = 0; i < outputCount; i++) seeds.push(base + i);
66
+ break;
67
+ case 'random':
68
+ seeds.push(base);
69
+ for (let i = 1; i < outputCount; i++) {
70
+ seeds.push(randomInt(2_147_483_647));
71
+ }
72
+ break;
73
+ default:
74
+ // Default to increment
75
+ for (let i = 0; i < outputCount; i++) seeds.push(base + i);
76
+ }
77
+
78
+ return { base_seed: base, seeds };
79
+ }
80
+
81
+ // ─── Run directory setup ─────────────────────────────────────────
82
+
83
+ /**
84
+ * Prepare a run directory and copy brief artifacts into it.
85
+ * @returns {{ runId: string, runDir: string }}
86
+ */
87
+ export async function prepareRunDir(projectRoot, brief) {
88
+ const runsDir = getRunsDir(projectRoot);
89
+ const runId = generateRunId(runsDir);
90
+ const runDir = join(runsDir, runId);
91
+
92
+ await mkdir(join(runDir, 'outputs'), { recursive: true });
93
+
94
+ // Copy brief artifacts
95
+ const briefsDir = getBriefsDir(projectRoot);
96
+ const briefJsonSrc = join(briefsDir, `${brief.brief_id}.json`);
97
+ const briefMdSrc = join(briefsDir, `${brief.brief_id}.md`);
98
+
99
+ await writeFile(join(runDir, 'brief.json'), JSON.stringify(brief, null, 2) + '\n');
100
+
101
+ if (existsSync(briefMdSrc)) {
102
+ await copyFile(briefMdSrc, join(runDir, 'brief.md'));
103
+ }
104
+
105
+ // Write standalone prompt/negative files for easy inspection
106
+ await writeFile(join(runDir, 'prompt.txt'), brief.prompt || '');
107
+ await writeFile(join(runDir, 'negative-prompt.txt'), brief.negative_prompt || '');
108
+
109
+ return { runId, runDir };
110
+ }
111
+
112
+ // ─── Manifest ────────────────────────────────────────────────────
113
+
114
+ /**
115
+ * Atomic write: write to a temp file in the same directory, then rename.
116
+ * Rename is atomic on the same filesystem, so a crash mid-write can never
117
+ * leave a half-written manifest in place of a good one.
118
+ *
119
+ * PB-002 / PB-005: callers may invoke this after each slot/candidate to
120
+ * produce incremental checkpoints.
121
+ */
122
+ async function atomicWriteJson(targetPath, obj) {
123
+ const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
124
+ const body = JSON.stringify(obj, null, 2) + '\n';
125
+ try {
126
+ await writeFile(tmpPath, body);
127
+ await rename(tmpPath, targetPath);
128
+ } catch (err) {
129
+ // Best-effort cleanup of the temp file on failure.
130
+ try { await unlink(tmpPath); } catch { /* ignore */ }
131
+ throw err;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Build and save a run manifest.
137
+ *
138
+ * PB-002: uses atomic write (temp + rename) so a crash mid-write cannot
139
+ * corrupt an existing manifest. Safe to call repeatedly for incremental
140
+ * checkpointing after each output completes.
141
+ */
142
+ export async function saveRunManifest(runDir, manifest) {
143
+ const target = join(runDir, 'manifest.json');
144
+ await atomicWriteJson(target, manifest);
145
+ verbose(`saveRunManifest: wrote ${target}`);
146
+ }
147
+
148
+ /**
149
+ * Incremental checkpoint — same shape as saveRunManifest but stamps
150
+ * `incremental: true` and `last_checkpoint_at` so a human reading a
151
+ * partial manifest knows the run is in progress.
152
+ *
153
+ * PB-002: call after each completed output in long-running loops so a
154
+ * crash/interruption leaves a partial record instead of an orphan dir.
155
+ */
156
+ export async function checkpointRunManifest(runDir, manifest) {
157
+ const snapshot = {
158
+ ...manifest,
159
+ incremental: true,
160
+ last_checkpoint_at: new Date().toISOString(),
161
+ };
162
+ await atomicWriteJson(join(runDir, 'manifest.json'), snapshot);
163
+ verbose(`checkpointRunManifest: ${manifest.run_id} (${manifest.outputs?.length ?? 0} outputs)`);
164
+ }
165
+
166
+ // ─── Load / List ─────────────────────────────────────────────────
167
+
168
+ /**
169
+ * Load a run manifest by ID.
170
+ */
171
+ export async function loadRun(projectRoot, runId) {
172
+ const runDir = join(getRunsDir(projectRoot), runId);
173
+ const manifestPath = join(runDir, 'manifest.json');
174
+
175
+ if (!existsSync(manifestPath)) {
176
+ throw inputError(
177
+ 'RUN_NOT_FOUND',
178
+ `Run "${runId}" not found`,
179
+ `Check the run ID or run: sdlab run generate --brief <id>`
180
+ );
181
+ }
182
+
183
+ return JSON.parse(await readFile(manifestPath, 'utf-8'));
184
+ }
185
+
186
+ /**
187
+ * List all runs in a project.
188
+ */
189
+ export function listRuns(projectRoot) {
190
+ const runsDir = getRunsDir(projectRoot);
191
+ if (!existsSync(runsDir)) return [];
192
+
193
+ const entries = readdirSync(runsDir, { withFileTypes: true })
194
+ .filter(e => e.isDirectory() && e.name.startsWith('run_'))
195
+ .sort((a, b) => a.name.localeCompare(b.name));
196
+
197
+ const runs = [];
198
+ for (const entry of entries) {
199
+ const manifestPath = join(runsDir, entry.name, 'manifest.json');
200
+ if (!existsSync(manifestPath)) continue;
201
+
202
+ try {
203
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
204
+ runs.push({
205
+ run_id: manifest.run_id,
206
+ brief_id: manifest.brief_id,
207
+ workflow_template_id: manifest.workflow_template_id,
208
+ output_count: manifest.output_count,
209
+ created_at: manifest.created_at,
210
+ adapter_target: manifest.adapter_target,
211
+ });
212
+ } catch {
213
+ // Skip malformed
214
+ }
215
+ }
216
+
217
+ return runs;
218
+ }