@mcptoolshop/style-dataset-lab 2.2.1 → 3.0.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.es.md +76 -144
  3. package/README.fr.md +75 -143
  4. package/README.hi.md +75 -143
  5. package/README.it.md +76 -144
  6. package/README.ja.md +75 -143
  7. package/README.md +36 -8
  8. package/README.pt-BR.md +75 -143
  9. package/README.zh.md +76 -144
  10. package/SECURITY.md +3 -1
  11. package/bin/sdlab.js +146 -17
  12. package/lib/adapters/comfyui-runner.js +170 -0
  13. package/lib/adapters/comfyui-workflows.js +163 -0
  14. package/lib/batch-compiler.js +118 -0
  15. package/lib/batch-modes.js +111 -0
  16. package/lib/batch-runs.js +168 -0
  17. package/lib/batch-sheet-render.js +206 -0
  18. package/lib/brief-compiler.js +598 -0
  19. package/lib/brief-render.js +205 -0
  20. package/lib/critique-engine.js +413 -0
  21. package/lib/critique-render.js +171 -0
  22. package/lib/errors.js +82 -0
  23. package/lib/generated-provenance.js +87 -0
  24. package/lib/log.js +36 -0
  25. package/lib/paths.js +31 -0
  26. package/lib/refine-briefs.js +329 -0
  27. package/lib/reingest-selected.js +173 -0
  28. package/lib/run-summary.js +131 -0
  29. package/lib/runtime-runs.js +168 -0
  30. package/lib/selections.js +323 -0
  31. package/lib/snapshot.js +5 -2
  32. package/lib/split.js +5 -2
  33. package/lib/workflow-profiles.js +161 -0
  34. package/package.json +8 -6
  35. package/scripts/batch-generate.js +167 -0
  36. package/scripts/batch-sheet.js +46 -0
  37. package/scripts/batch-show.js +86 -0
  38. package/scripts/brief-compile.js +134 -0
  39. package/scripts/brief-show.js +52 -0
  40. package/scripts/critique-show.js +50 -0
  41. package/scripts/critique.js +70 -0
  42. package/scripts/doctor.js +169 -1
  43. package/scripts/init.js +22 -0
  44. package/scripts/refine.js +119 -0
  45. package/scripts/reingest-selected.js +78 -0
  46. package/scripts/reingest.js +7 -1
  47. package/scripts/run-generate.js +76 -0
  48. package/scripts/run-list.js +58 -0
  49. package/scripts/run-show.js +63 -0
  50. package/scripts/select.js +118 -0
  51. package/scripts/selection-show.js +88 -0
  52. package/scripts/snapshot.js +27 -23
  53. package/scripts/split.js +3 -1
  54. package/scripts/workflow-list.js +61 -0
  55. package/scripts/workflow-show.js +96 -0
  56. package/templates/domains/architecture/workflows/profiles/environment-moodboard.json +62 -0
  57. package/templates/domains/architecture/workflows/profiles/interior-variant-pack.json +60 -0
  58. package/templates/domains/character-design/workflows/profiles/character-portrait-set.json +61 -0
  59. package/templates/domains/character-design/workflows/profiles/expression-sheet.json +59 -0
  60. package/templates/domains/creature-design/workflows/profiles/creature-portrait-set.json +61 -0
  61. package/templates/domains/creature-design/workflows/profiles/silhouette-exploration.json +58 -0
  62. package/templates/domains/game-art/workflows/profiles/character-portrait-set.json +62 -0
  63. package/templates/domains/game-art/workflows/profiles/environment-moodboard.json +61 -0
  64. package/templates/domains/vehicle-mech/workflows/profiles/variant-pack.json +60 -0
  65. package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Batch runs — create batch directories, execute coordinated runs, save manifests.
3
+ *
4
+ * A batch groups multiple runs under one artifact:
5
+ * batch_YYYY-MM-DD_NNN/
6
+ * manifest.json
7
+ * summary.json / summary.md
8
+ * sheets/
9
+ * runs/ (symlinks or copies to project-level runs)
10
+ * briefs/ (slot briefs)
11
+ */
12
+
13
+ import { writeFile, mkdir, copyFile } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
16
+ import { getProjectRoot, getRunsDir } from './paths.js';
17
+ import { executeRun } from './adapters/comfyui-runner.js';
18
+ import { saveRunSummary } from './run-summary.js';
19
+ import { log, info, warn } from './log.js';
20
+ import { inputError } from './errors.js';
21
+
22
+ /**
23
+ * Return the batches directory for a project.
24
+ */
25
+ export function getBatchesDir(projectRoot) {
26
+ return join(projectRoot, 'batches');
27
+ }
28
+
29
+ /**
30
+ * Generate batch ID: batch_YYYY-MM-DD_NNN
31
+ */
32
+ export function generateBatchId(batchesDir) {
33
+ const today = new Date().toISOString().slice(0, 10);
34
+ const prefix = `batch_${today}_`;
35
+
36
+ let maxSeq = 0;
37
+ if (existsSync(batchesDir)) {
38
+ const existing = readdirSync(batchesDir, { withFileTypes: true })
39
+ .filter(e => e.isDirectory() && e.name.startsWith(prefix));
40
+ for (const e of existing) {
41
+ const seqStr = e.name.slice(prefix.length);
42
+ const seq = parseInt(seqStr, 10);
43
+ if (!isNaN(seq) && seq > maxSeq) maxSeq = seq;
44
+ }
45
+ }
46
+
47
+ const seq = String(maxSeq + 1).padStart(3, '0');
48
+ return `${prefix}${seq}`;
49
+ }
50
+
51
+ /**
52
+ * Create a batch directory and prepare its structure.
53
+ */
54
+ export async function createBatchDir(projectRoot) {
55
+ const batchesDir = getBatchesDir(projectRoot);
56
+ const batchId = generateBatchId(batchesDir);
57
+ const batchDir = join(batchesDir, batchId);
58
+
59
+ await mkdir(join(batchDir, 'sheets'), { recursive: true });
60
+ await mkdir(join(batchDir, 'briefs'), { recursive: true });
61
+
62
+ return { batchId, batchDir };
63
+ }
64
+
65
+ /**
66
+ * Execute all slot briefs as coordinated runs.
67
+ *
68
+ * @param {Object} opts
69
+ * @param {string} opts.projectRoot
70
+ * @param {string} opts.projectName
71
+ * @param {string} opts.batchDir
72
+ * @param {Object} opts.mode — batch mode definition
73
+ * @param {Object[]} opts.slotBriefs — one brief per slot
74
+ * @param {boolean} [opts.dryRun]
75
+ * @returns {Promise<Object[]>} — array of { slot, brief, manifest }
76
+ */
77
+ export async function executeBatchRuns({
78
+ projectRoot,
79
+ projectName,
80
+ batchDir,
81
+ mode,
82
+ slotBriefs,
83
+ dryRun = false,
84
+ }) {
85
+ const results = [];
86
+
87
+ for (let i = 0; i < slotBriefs.length; i++) {
88
+ const slot = mode.variant_plan[i];
89
+ const brief = slotBriefs[i];
90
+ const slotLabel = slot.label || slot.slot_id;
91
+
92
+ info(`[${i + 1}/${slotBriefs.length}] Slot: ${slotLabel}`);
93
+
94
+ // Save brief into batch/briefs/
95
+ const briefFilename = `${slot.slot_id}.json`;
96
+ await writeFile(
97
+ join(batchDir, 'briefs', briefFilename),
98
+ JSON.stringify(brief, null, 2) + '\n'
99
+ );
100
+
101
+ // Execute the run through the existing 4B pipeline
102
+ const manifest = await executeRun({
103
+ projectRoot,
104
+ projectName,
105
+ brief,
106
+ dryRun,
107
+ });
108
+
109
+ // Save run summary
110
+ const runDir = join(getRunsDir(projectRoot), manifest.run_id);
111
+ await saveRunSummary(runDir, manifest);
112
+
113
+ results.push({
114
+ slot_id: slot.slot_id,
115
+ label: slotLabel,
116
+ brief_id: brief.brief_id,
117
+ run_id: manifest.run_id,
118
+ manifest,
119
+ // Pick the first successful output as the selected one
120
+ selected_output: manifest.outputs.find(o =>
121
+ o.status === 'ok' || o.status === 'dry_run'
122
+ )?.filename || null,
123
+ });
124
+ }
125
+
126
+ return results;
127
+ }
128
+
129
+ /**
130
+ * Save a batch manifest.
131
+ */
132
+ export async function saveBatchManifest(batchDir, manifest) {
133
+ await writeFile(
134
+ join(batchDir, 'manifest.json'),
135
+ JSON.stringify(manifest, null, 2) + '\n'
136
+ );
137
+ }
138
+
139
+ /**
140
+ * Load a batch manifest by ID.
141
+ */
142
+ export function loadBatchManifest(projectRoot, batchId) {
143
+ const batchDir = join(getBatchesDir(projectRoot), batchId);
144
+ const manifestPath = join(batchDir, 'manifest.json');
145
+
146
+ if (!existsSync(manifestPath)) {
147
+ throw inputError(
148
+ 'BATCH_NOT_FOUND',
149
+ `Batch "${batchId}" not found`,
150
+ 'Check the batch ID or run: sdlab batch generate --mode <id>'
151
+ );
152
+ }
153
+
154
+ return JSON.parse(readFileSync(manifestPath, 'utf-8'));
155
+ }
156
+
157
+ /**
158
+ * List all batches for a project.
159
+ */
160
+ export function listBatches(projectRoot) {
161
+ const batchesDir = getBatchesDir(projectRoot);
162
+ if (!existsSync(batchesDir)) return [];
163
+
164
+ return readdirSync(batchesDir, { withFileTypes: true })
165
+ .filter(e => e.isDirectory() && e.name.startsWith('batch_'))
166
+ .sort((a, b) => a.name.localeCompare(b.name))
167
+ .map(e => e.name);
168
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Batch sheet renderer — assembles run outputs into reviewable HTML surfaces.
3
+ *
4
+ * Produces an HTML file with a CSS grid layout that references output images.
5
+ * Zero dependencies — works in any browser. Slot labels, seeds, and metadata
6
+ * are embedded for human review.
7
+ *
8
+ * Future: add PNG rendering via sharp if needed.
9
+ */
10
+
11
+ import { writeFile } from 'node:fs/promises';
12
+ import { join, relative } from 'node:path';
13
+ import { existsSync } from 'node:fs';
14
+ import { getRunsDir } from './paths.js';
15
+
16
+ /**
17
+ * Resolve the image path for a slot result.
18
+ * Returns a relative path from the sheet directory to the output image.
19
+ */
20
+ function resolveImagePath(projectRoot, result, batchDir) {
21
+ if (!result.selected_output || !result.run_id) return null;
22
+
23
+ const imagePath = join(getRunsDir(projectRoot), result.run_id, 'outputs', result.selected_output);
24
+ if (!existsSync(imagePath)) return null;
25
+
26
+ // Relative from sheets/ dir to the image
27
+ const sheetsDir = join(batchDir, 'sheets');
28
+ return relative(sheetsDir, imagePath).replace(/\\/g, '/');
29
+ }
30
+
31
+ /**
32
+ * Render a batch sheet as HTML.
33
+ *
34
+ * @param {Object} opts
35
+ * @param {Object} opts.manifest — batch manifest
36
+ * @param {Object} opts.mode — batch mode definition
37
+ * @param {string} opts.projectRoot
38
+ * @param {string} opts.batchDir
39
+ * @returns {string} HTML content
40
+ */
41
+ export function renderSheetHTML({ manifest, mode, projectRoot, batchDir }) {
42
+ const assembly = mode.assembly || {};
43
+ const layout = assembly.layout || 'grid';
44
+ const columns = assembly.columns || computeColumns(manifest.slots.length);
45
+ const showSlotLabels = assembly.annotate_slots !== false;
46
+ const showSeed = assembly.show_seed || false;
47
+
48
+ const title = `${manifest.mode_id} — ${manifest.subject_id || manifest.theme || manifest.project_id}`;
49
+ const slots = manifest.slots || [];
50
+
51
+ // Build slot cells
52
+ const cells = slots.map(slot => {
53
+ const imgPath = resolveImagePath(projectRoot, slot, batchDir);
54
+ const hasImage = !!imgPath;
55
+
56
+ return {
57
+ slot_id: slot.slot_id,
58
+ label: slot.label,
59
+ run_id: slot.run_id,
60
+ selected_output: slot.selected_output,
61
+ imgPath,
62
+ hasImage,
63
+ seed: findSlotSeed(manifest, slot),
64
+ };
65
+ });
66
+
67
+ // Generate HTML
68
+ const css = generateCSS(layout, columns, cells.length);
69
+ const grid = cells.map((cell, i) => renderCell(cell, i, showSlotLabels, showSeed)).join('\n');
70
+
71
+ return `<!DOCTYPE html>
72
+ <html lang="en">
73
+ <head>
74
+ <meta charset="utf-8">
75
+ <title>${escapeHtml(title)}</title>
76
+ <style>${css}</style>
77
+ </head>
78
+ <body>
79
+ <header>
80
+ <h1>${escapeHtml(title)}</h1>
81
+ <p class="meta">
82
+ Batch: ${escapeHtml(manifest.batch_id)} |
83
+ Mode: ${escapeHtml(manifest.mode_id)} |
84
+ Type: ${escapeHtml(mode.batch_type)} |
85
+ ${slots.length} slots |
86
+ ${manifest.created_at?.slice(0, 16) || '?'}
87
+ </p>
88
+ </header>
89
+ <div class="grid">${grid}
90
+ </div>
91
+ <footer>
92
+ <p>Generated by style-dataset-lab — ${manifest.created_at || ''}</p>
93
+ </footer>
94
+ </body>
95
+ </html>`;
96
+ }
97
+
98
+ /**
99
+ * Compute optimal column count based on slot count.
100
+ */
101
+ function computeColumns(slotCount) {
102
+ if (slotCount <= 3) return slotCount;
103
+ if (slotCount <= 4) return 2;
104
+ if (slotCount <= 6) return 3;
105
+ if (slotCount <= 9) return 3;
106
+ return 4;
107
+ }
108
+
109
+ /**
110
+ * Generate CSS for the layout.
111
+ */
112
+ function generateCSS(layout, columns, slotCount) {
113
+ return `
114
+ * { margin: 0; padding: 0; box-sizing: border-box; }
115
+ body {
116
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
117
+ background: #1a1a1a; color: #e0e0e0;
118
+ padding: 24px;
119
+ }
120
+ header { margin-bottom: 24px; }
121
+ h1 { font-size: 1.4rem; font-weight: 600; }
122
+ .meta { font-size: 0.85rem; color: #888; margin-top: 4px; }
123
+ .grid {
124
+ display: grid;
125
+ grid-template-columns: repeat(${columns}, 1fr);
126
+ gap: 12px;
127
+ }
128
+ .cell {
129
+ background: #252525;
130
+ border: 1px solid #333;
131
+ border-radius: 6px;
132
+ overflow: hidden;
133
+ position: relative;
134
+ }
135
+ .cell img {
136
+ width: 100%; height: auto; display: block;
137
+ }
138
+ .cell .placeholder {
139
+ width: 100%; aspect-ratio: 1;
140
+ background: #2a2a2a;
141
+ display: flex; align-items: center; justify-content: center;
142
+ color: #555; font-size: 0.9rem;
143
+ }
144
+ .cell .label-bar {
145
+ padding: 6px 10px;
146
+ font-size: 0.8rem;
147
+ background: #2a2a2a;
148
+ border-top: 1px solid #333;
149
+ }
150
+ .cell .label-bar .slot-label { font-weight: 600; color: #ccc; }
151
+ .cell .label-bar .seed { color: #666; margin-left: 8px; }
152
+ footer {
153
+ margin-top: 24px;
154
+ font-size: 0.75rem; color: #555;
155
+ text-align: center;
156
+ }
157
+ `;
158
+ }
159
+
160
+ /**
161
+ * Render a single cell.
162
+ */
163
+ function renderCell(cell, index, showLabel, showSeed) {
164
+ const imgTag = cell.hasImage
165
+ ? `<img src="${escapeHtml(cell.imgPath)}" alt="${escapeHtml(cell.label)}" loading="lazy">`
166
+ : `<div class="placeholder">${escapeHtml(cell.label)}<br><small>${cell.selected_output || 'no output'}</small></div>`;
167
+
168
+ const labelHtml = showLabel
169
+ ? `<div class="label-bar">
170
+ <span class="slot-label">${escapeHtml(cell.label)}</span>
171
+ ${showSeed && cell.seed != null ? `<span class="seed">seed: ${cell.seed}</span>` : ''}
172
+ </div>`
173
+ : '';
174
+
175
+ return ` <div class="cell">${imgTag}${labelHtml}</div>`;
176
+ }
177
+
178
+ /**
179
+ * Find the seed used for a slot's selected output.
180
+ */
181
+ function findSlotSeed(manifest, slot) {
182
+ // Not always available — return null if not found
183
+ return null; // Could be enriched from run manifests later
184
+ }
185
+
186
+ /**
187
+ * Escape HTML entities.
188
+ */
189
+ function escapeHtml(str) {
190
+ if (!str) return '';
191
+ return String(str)
192
+ .replace(/&/g, '&amp;')
193
+ .replace(/</g, '&lt;')
194
+ .replace(/>/g, '&gt;')
195
+ .replace(/"/g, '&quot;');
196
+ }
197
+
198
+ /**
199
+ * Save the sheet HTML to the batch sheets directory.
200
+ */
201
+ export async function saveSheet(batchDir, filename, html) {
202
+ const sheetsDir = join(batchDir, 'sheets');
203
+ const sheetPath = join(sheetsDir, filename);
204
+ await writeFile(sheetPath, html);
205
+ return sheetPath;
206
+ }