@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.
- package/CHANGELOG.md +125 -0
- package/README.es.md +76 -144
- package/README.fr.md +75 -143
- package/README.hi.md +75 -143
- package/README.it.md +76 -144
- package/README.ja.md +75 -143
- package/README.md +36 -8
- package/README.pt-BR.md +75 -143
- package/README.zh.md +76 -144
- package/SECURITY.md +3 -1
- package/bin/sdlab.js +146 -17
- package/lib/adapters/comfyui-runner.js +170 -0
- package/lib/adapters/comfyui-workflows.js +163 -0
- package/lib/batch-compiler.js +118 -0
- package/lib/batch-modes.js +111 -0
- package/lib/batch-runs.js +168 -0
- package/lib/batch-sheet-render.js +206 -0
- package/lib/brief-compiler.js +598 -0
- package/lib/brief-render.js +205 -0
- package/lib/critique-engine.js +413 -0
- package/lib/critique-render.js +171 -0
- package/lib/errors.js +82 -0
- package/lib/generated-provenance.js +87 -0
- package/lib/log.js +36 -0
- package/lib/paths.js +31 -0
- package/lib/refine-briefs.js +329 -0
- package/lib/reingest-selected.js +173 -0
- package/lib/run-summary.js +131 -0
- package/lib/runtime-runs.js +168 -0
- package/lib/selections.js +323 -0
- package/lib/snapshot.js +5 -2
- package/lib/split.js +5 -2
- package/lib/workflow-profiles.js +161 -0
- package/package.json +8 -6
- package/scripts/batch-generate.js +167 -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 +52 -0
- package/scripts/critique-show.js +50 -0
- package/scripts/critique.js +70 -0
- package/scripts/doctor.js +169 -1
- package/scripts/init.js +22 -0
- package/scripts/refine.js +119 -0
- package/scripts/reingest-selected.js +78 -0
- package/scripts/reingest.js +7 -1
- 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 +118 -0
- package/scripts/selection-show.js +88 -0
- package/scripts/snapshot.js +27 -23
- package/scripts/split.js +3 -1
- package/scripts/workflow-list.js +61 -0
- package/scripts/workflow-show.js +96 -0
- 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/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/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/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/workflows/profiles/variant-pack.json +60 -0
- 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, '&')
|
|
193
|
+
.replace(/</g, '<')
|
|
194
|
+
.replace(/>/g, '>')
|
|
195
|
+
.replace(/"/g, '"');
|
|
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
|
+
}
|