@mcptoolshop/style-dataset-lab 1.2.1 → 2.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 +73 -0
- package/README.md +138 -113
- package/bin/sdlab.js +126 -0
- package/lib/args.js +106 -0
- package/lib/card.js +145 -0
- package/lib/comfyui.js +109 -0
- package/lib/config.js +248 -0
- package/lib/deprecation.js +14 -0
- package/lib/eligibility.js +110 -0
- package/lib/eval-pack.js +283 -0
- package/lib/export.js +259 -0
- package/lib/paths.js +38 -0
- package/lib/records.js +69 -0
- package/lib/snapshot.js +211 -0
- package/lib/split.js +409 -0
- package/package.json +20 -9
- package/scripts/bulk-curate-wave2-5.js +1 -1
- package/scripts/bulk-curate-waves11-18.js +1 -1
- package/scripts/canon-bind.js +188 -338
- package/scripts/card.js +73 -0
- package/scripts/compare.js +40 -30
- package/scripts/curate-wave25.js +1 -1
- package/scripts/curate.js +34 -28
- package/scripts/doctor.js +324 -0
- package/scripts/eligibility.js +91 -0
- package/scripts/eval-pack.js +92 -0
- package/scripts/export.js +85 -0
- package/scripts/generate-controlnet.js +36 -121
- package/scripts/generate-identity.js +38 -106
- package/scripts/generate-ipadapter.js +42 -119
- package/scripts/generate.js +22 -81
- package/scripts/init.js +229 -0
- package/scripts/migrate-records.js +15 -21
- package/scripts/painterly-test.js +20 -47
- package/scripts/painterly.js +27 -106
- package/scripts/snapshot.js +132 -0
- package/scripts/split.js +154 -0
- package/templates/domains/architecture/constitution.json +97 -0
- package/templates/domains/architecture/lanes.json +41 -0
- package/templates/domains/architecture/project.json +20 -0
- package/templates/domains/architecture/rubric.json +40 -0
- package/templates/domains/architecture/terminology.json +19 -0
- package/templates/domains/character-design/constitution.json +88 -0
- package/templates/domains/character-design/lanes.json +35 -0
- package/templates/domains/character-design/project.json +17 -0
- package/templates/domains/character-design/rubric.json +38 -0
- package/templates/domains/character-design/terminology.json +21 -0
- package/templates/domains/creature-design/constitution.json +87 -0
- package/templates/domains/creature-design/lanes.json +41 -0
- package/templates/domains/creature-design/project.json +18 -0
- package/templates/domains/creature-design/rubric.json +39 -0
- package/templates/domains/creature-design/terminology.json +20 -0
- package/templates/domains/game-art/constitution.json +113 -0
- package/templates/domains/game-art/lanes.json +47 -0
- package/templates/domains/game-art/project.json +17 -0
- package/templates/domains/game-art/rubric.json +36 -0
- package/templates/domains/game-art/terminology.json +30 -0
- package/templates/domains/vehicle-mech/constitution.json +87 -0
- package/templates/domains/vehicle-mech/lanes.json +41 -0
- package/templates/domains/vehicle-mech/project.json +17 -0
- package/templates/domains/vehicle-mech/rubric.json +40 -0
- package/templates/domains/vehicle-mech/terminology.json +21 -0
package/scripts/painterly.js
CHANGED
|
@@ -10,18 +10,17 @@
|
|
|
10
10
|
* Usage:
|
|
11
11
|
* node scripts/painterly.js [--source outputs/approved] [--limit 100] [--offset 0]
|
|
12
12
|
* node scripts/painterly.js --dry-run
|
|
13
|
+
* sdlab painterly --project star-freight --limit 50
|
|
13
14
|
*
|
|
14
15
|
* Output: outputs/painterly/<original_filename>
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
|
-
import {
|
|
18
|
-
import { join
|
|
18
|
+
import { writeFile, mkdir, readdir } from "node:fs/promises";
|
|
19
|
+
import { join } from "node:path";
|
|
19
20
|
import { existsSync } from "node:fs";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
|
|
24
|
-
const GAME_ROOT = join(REPO_ROOT, 'games', GAME);
|
|
21
|
+
import { getProjectName } from "../lib/args.js";
|
|
22
|
+
import { REPO_ROOT } from "../lib/paths.js";
|
|
23
|
+
import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
|
|
25
24
|
|
|
26
25
|
// ── Config ──
|
|
27
26
|
|
|
@@ -51,20 +50,10 @@ const DEFAULTS = {
|
|
|
51
50
|
seed: 42, // Fixed seed for reproducibility across batch
|
|
52
51
|
};
|
|
53
52
|
|
|
54
|
-
// ── ComfyUI helpers ──
|
|
55
|
-
|
|
56
|
-
async function comfyHealth() {
|
|
57
|
-
try {
|
|
58
|
-
const res = await fetch(`${COMFY_URL}/system_stats`);
|
|
59
|
-
return res.ok;
|
|
60
|
-
} catch { return false; }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
53
|
function buildImg2ImgWorkflow(imagePath, seed) {
|
|
64
54
|
const nodes = {};
|
|
65
55
|
let nextId = 1;
|
|
66
56
|
|
|
67
|
-
// Checkpoint loader
|
|
68
57
|
const ckptId = String(nextId++);
|
|
69
58
|
nodes[ckptId] = {
|
|
70
59
|
class_type: "CheckpointLoaderSimple",
|
|
@@ -74,7 +63,6 @@ function buildImg2ImgWorkflow(imagePath, seed) {
|
|
|
74
63
|
let modelOut = [ckptId, 0];
|
|
75
64
|
let clipOut = [ckptId, 1];
|
|
76
65
|
|
|
77
|
-
// LoRA — ClassipeintXL at reduced weight for style shift
|
|
78
66
|
const loraId = String(nextId++);
|
|
79
67
|
nodes[loraId] = {
|
|
80
68
|
class_type: "LoraLoader",
|
|
@@ -89,14 +77,12 @@ function buildImg2ImgWorkflow(imagePath, seed) {
|
|
|
89
77
|
modelOut = [loraId, 0];
|
|
90
78
|
clipOut = [loraId, 1];
|
|
91
79
|
|
|
92
|
-
// Load source image
|
|
93
80
|
const loadImgId = String(nextId++);
|
|
94
81
|
nodes[loadImgId] = {
|
|
95
82
|
class_type: "LoadImage",
|
|
96
83
|
inputs: { image: imagePath },
|
|
97
84
|
};
|
|
98
85
|
|
|
99
|
-
// VAE Encode — source image to latent
|
|
100
86
|
const vaeEncId = String(nextId++);
|
|
101
87
|
nodes[vaeEncId] = {
|
|
102
88
|
class_type: "VAEEncode",
|
|
@@ -106,21 +92,18 @@ function buildImg2ImgWorkflow(imagePath, seed) {
|
|
|
106
92
|
},
|
|
107
93
|
};
|
|
108
94
|
|
|
109
|
-
// CLIP Text Encode — positive (painterly style)
|
|
110
95
|
const posId = String(nextId++);
|
|
111
96
|
nodes[posId] = {
|
|
112
97
|
class_type: "CLIPTextEncode",
|
|
113
98
|
inputs: { text: PAINTERLY_PROMPT, clip: clipOut },
|
|
114
99
|
};
|
|
115
100
|
|
|
116
|
-
// CLIP Text Encode — negative
|
|
117
101
|
const negId = String(nextId++);
|
|
118
102
|
nodes[negId] = {
|
|
119
103
|
class_type: "CLIPTextEncode",
|
|
120
104
|
inputs: { text: PAINTERLY_NEGATIVE, clip: clipOut },
|
|
121
105
|
};
|
|
122
106
|
|
|
123
|
-
// KSampler — img2img with low denoise
|
|
124
107
|
const samplerId = String(nextId++);
|
|
125
108
|
nodes[samplerId] = {
|
|
126
109
|
class_type: "KSampler",
|
|
@@ -138,7 +121,6 @@ function buildImg2ImgWorkflow(imagePath, seed) {
|
|
|
138
121
|
},
|
|
139
122
|
};
|
|
140
123
|
|
|
141
|
-
// VAE Decode
|
|
142
124
|
const vaeDecId = String(nextId++);
|
|
143
125
|
nodes[vaeDecId] = {
|
|
144
126
|
class_type: "VAEDecode",
|
|
@@ -148,7 +130,6 @@ function buildImg2ImgWorkflow(imagePath, seed) {
|
|
|
148
130
|
},
|
|
149
131
|
};
|
|
150
132
|
|
|
151
|
-
// Save Image
|
|
152
133
|
const saveId = String(nextId++);
|
|
153
134
|
nodes[saveId] = {
|
|
154
135
|
class_type: "SaveImage",
|
|
@@ -161,84 +142,22 @@ function buildImg2ImgWorkflow(imagePath, seed) {
|
|
|
161
142
|
return { nodes, saveNodeId: saveId };
|
|
162
143
|
}
|
|
163
144
|
|
|
164
|
-
|
|
145
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
146
|
+
const projectName = getProjectName(argv);
|
|
147
|
+
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
148
|
+
const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
|
|
165
149
|
|
|
166
|
-
|
|
167
|
-
const clientId = `sdl-paint-${Date.now()}`;
|
|
168
|
-
const res = await fetch(`${COMFY_URL}/prompt`, {
|
|
169
|
-
method: "POST",
|
|
170
|
-
headers: { "Content-Type": "application/json" },
|
|
171
|
-
body: JSON.stringify({ prompt: workflow, client_id: clientId }),
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
if (!res.ok) {
|
|
175
|
-
const text = await res.text();
|
|
176
|
-
throw new Error(`ComfyUI submit failed: ${res.status} ${text}`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const { prompt_id: promptId } = await res.json();
|
|
180
|
-
|
|
181
|
-
const start = Date.now();
|
|
182
|
-
while (true) {
|
|
183
|
-
if (Date.now() - start > MAX_POLL_MS) throw new Error(`ComfyUI poll timeout after ${MAX_POLL_MS/1000}s for prompt ${promptId}`);
|
|
184
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
185
|
-
const histRes = await fetch(`${COMFY_URL}/history/${promptId}`);
|
|
186
|
-
if (!histRes.ok) continue;
|
|
187
|
-
const history = await histRes.json();
|
|
188
|
-
const entry = history[promptId];
|
|
189
|
-
if (!entry) continue;
|
|
190
|
-
if (entry.status?.completed) return entry;
|
|
191
|
-
if (entry.status?.status_str === "error") {
|
|
192
|
-
throw new Error(`ComfyUI generation failed: ${JSON.stringify(entry.status)}`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function downloadImage(filename, subfolder, type = "output") {
|
|
198
|
-
const url = `${COMFY_URL}/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder || "")}&type=${type}`;
|
|
199
|
-
const res = await fetch(url);
|
|
200
|
-
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
201
|
-
return Buffer.from(await res.arrayBuffer());
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Upload an image to ComfyUI's input folder so LoadImage can find it.
|
|
206
|
-
*/
|
|
207
|
-
async function uploadImage(filePath, filename) {
|
|
208
|
-
const data = await readFile(filePath);
|
|
209
|
-
const formData = new FormData();
|
|
210
|
-
formData.append("image", new Blob([data], { type: "image/png" }), filename);
|
|
211
|
-
formData.append("overwrite", "true");
|
|
212
|
-
|
|
213
|
-
const res = await fetch(`${COMFY_URL}/upload/image`, {
|
|
214
|
-
method: "POST",
|
|
215
|
-
body: formData,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
if (!res.ok) {
|
|
219
|
-
const text = await res.text();
|
|
220
|
-
throw new Error(`Upload failed: ${res.status} ${text}`);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const result = await res.json();
|
|
224
|
-
return result.name; // The name ComfyUI assigned to the uploaded image
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ── Main ──
|
|
228
|
-
|
|
229
|
-
async function main() {
|
|
230
|
-
const args = process.argv.slice(2);
|
|
231
|
-
const dryRun = args.includes("--dry-run");
|
|
150
|
+
const dryRun = argv.includes("--dry-run");
|
|
232
151
|
|
|
233
152
|
// Parse args
|
|
234
153
|
let sourceDir = "outputs/approved";
|
|
235
154
|
let limit = 100;
|
|
236
155
|
let offset = 0;
|
|
237
156
|
|
|
238
|
-
for (let i = 0; i <
|
|
239
|
-
if (
|
|
240
|
-
if (
|
|
241
|
-
if (
|
|
157
|
+
for (let i = 0; i < argv.length; i++) {
|
|
158
|
+
if (argv[i] === "--source" && argv[i + 1]) sourceDir = argv[++i];
|
|
159
|
+
if (argv[i] === "--limit" && argv[i + 1]) limit = parseInt(argv[++i]);
|
|
160
|
+
if (argv[i] === "--offset" && argv[i + 1]) offset = parseInt(argv[++i]);
|
|
242
161
|
}
|
|
243
162
|
|
|
244
163
|
const fullSourceDir = join(GAME_ROOT, sourceDir);
|
|
@@ -274,10 +193,9 @@ async function main() {
|
|
|
274
193
|
}
|
|
275
194
|
|
|
276
195
|
if (!dryRun) {
|
|
277
|
-
const online = await comfyHealth();
|
|
196
|
+
const online = await comfyHealth(COMFY_URL);
|
|
278
197
|
if (!online) {
|
|
279
|
-
|
|
280
|
-
process.exit(1);
|
|
198
|
+
throw new Error("ComfyUI not reachable at " + COMFY_URL);
|
|
281
199
|
}
|
|
282
200
|
console.log("\x1b[32m✓\x1b[0m ComfyUI online");
|
|
283
201
|
}
|
|
@@ -298,11 +216,11 @@ async function main() {
|
|
|
298
216
|
const startMs = Date.now();
|
|
299
217
|
|
|
300
218
|
// Upload source image to ComfyUI
|
|
301
|
-
const uploadedName = await uploadImage(srcPath, file);
|
|
219
|
+
const uploadedName = await uploadImage(srcPath, file, COMFY_URL);
|
|
302
220
|
|
|
303
221
|
// Build and submit img2img workflow
|
|
304
222
|
const { nodes } = buildImg2ImgWorkflow(uploadedName, DEFAULTS.seed);
|
|
305
|
-
const result = await submitAndWait(nodes);
|
|
223
|
+
const result = await submitAndWait(nodes, COMFY_URL, { clientPrefix: 'sdl-paint' });
|
|
306
224
|
const elapsed = Date.now() - startMs;
|
|
307
225
|
|
|
308
226
|
// Find output image
|
|
@@ -325,7 +243,7 @@ async function main() {
|
|
|
325
243
|
}
|
|
326
244
|
|
|
327
245
|
// Download and save with original filename
|
|
328
|
-
const imgData = await downloadImage(imageFile, imageSubfolder);
|
|
246
|
+
const imgData = await downloadImage(imageFile, imageSubfolder, COMFY_URL);
|
|
329
247
|
await writeFile(join(outDir, file), imgData);
|
|
330
248
|
|
|
331
249
|
const kb = Math.round(imgData.length / 1024);
|
|
@@ -343,7 +261,10 @@ async function main() {
|
|
|
343
261
|
if (dryRun) console.log(" (dry run — no images processed)");
|
|
344
262
|
}
|
|
345
263
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
264
|
+
// Direct execution guard
|
|
265
|
+
if (process.argv[1] && (process.argv[1].endsWith('painterly.js') || process.argv[1].endsWith('painterly'))) {
|
|
266
|
+
run().catch((err) => {
|
|
267
|
+
console.error(err.message || err);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* snapshot.js — Create, list, show, and diff dataset snapshots.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab snapshot create [--profile <name>] [--project <name>]
|
|
8
|
+
* sdlab snapshot list [--project <name>]
|
|
9
|
+
* sdlab snapshot show <snapshot-id> [--project <name>]
|
|
10
|
+
* sdlab snapshot diff <id-a> <id-b> [--project <name>]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFile } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { getProjectName } from '../lib/args.js';
|
|
17
|
+
import { REPO_ROOT } from '../lib/paths.js';
|
|
18
|
+
import { loadSelectionProfile } from '../lib/config.js';
|
|
19
|
+
import { createSnapshot, loadSnapshot, listSnapshots, diffSnapshots } from '../lib/snapshot.js';
|
|
20
|
+
import { evaluateEligibility } from '../lib/eligibility.js';
|
|
21
|
+
import { categorizeExclusions } from '../lib/eligibility.js';
|
|
22
|
+
|
|
23
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
24
|
+
const projectName = getProjectName(argv);
|
|
25
|
+
const projectRoot = join(REPO_ROOT, 'projects', projectName);
|
|
26
|
+
const subcommand = argv.find(a => !a.startsWith('--')) || 'list';
|
|
27
|
+
|
|
28
|
+
// Strip subcommand from argv for flag parsing
|
|
29
|
+
const args = argv.filter(a => a !== subcommand);
|
|
30
|
+
|
|
31
|
+
if (subcommand === 'create') {
|
|
32
|
+
const profileIdx = args.indexOf('--profile');
|
|
33
|
+
const profileId = profileIdx >= 0 ? args[profileIdx + 1] : null;
|
|
34
|
+
const profile = loadSelectionProfile(projectRoot, profileId);
|
|
35
|
+
|
|
36
|
+
console.log(`\x1b[1msdlab snapshot create\x1b[0m — ${projectName}`);
|
|
37
|
+
console.log(` Profile: ${profileId || 'training-default (built-in)'}`);
|
|
38
|
+
console.log('');
|
|
39
|
+
|
|
40
|
+
const result = await createSnapshot(projectRoot, profile);
|
|
41
|
+
|
|
42
|
+
console.log(` \x1b[32m✓\x1b[0m Snapshot: ${result.snapshotId}`);
|
|
43
|
+
console.log(` Included: ${result.included}`);
|
|
44
|
+
console.log(` Excluded: ${result.excluded}`);
|
|
45
|
+
|
|
46
|
+
// Read and display summary
|
|
47
|
+
const summary = JSON.parse(
|
|
48
|
+
await readFile(join(projectRoot, 'snapshots', result.snapshotId, 'summary.json'), 'utf-8')
|
|
49
|
+
);
|
|
50
|
+
console.log('\n Lane distribution:');
|
|
51
|
+
for (const [lane, count] of Object.entries(summary.lane_distribution).sort((a, b) => b[1] - a[1])) {
|
|
52
|
+
console.log(` ${lane}: ${count}`);
|
|
53
|
+
}
|
|
54
|
+
console.log('\n Faction distribution:');
|
|
55
|
+
for (const [f, c] of Object.entries(summary.faction_distribution).sort((a, b) => b[1] - a[1])) {
|
|
56
|
+
console.log(` ${f}: ${c}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Read and categorize exclusions
|
|
60
|
+
const excludedRaw = await readFile(
|
|
61
|
+
join(projectRoot, 'snapshots', result.snapshotId, 'excluded.jsonl'), 'utf-8'
|
|
62
|
+
);
|
|
63
|
+
const excluded = excludedRaw.trim().split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
64
|
+
const categories = categorizeExclusions(excluded);
|
|
65
|
+
console.log('\n Exclusion reasons:');
|
|
66
|
+
for (const [cat, count] of Object.entries(categories)) {
|
|
67
|
+
if (count > 0) console.log(` ${cat}: ${count}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
} else if (subcommand === 'list') {
|
|
71
|
+
const snapshots = await listSnapshots(projectRoot);
|
|
72
|
+
if (snapshots.length === 0) {
|
|
73
|
+
console.log('No snapshots found. Run: sdlab snapshot create');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log(`\x1b[1mSnapshots\x1b[0m — ${projectName}\n`);
|
|
77
|
+
for (const s of snapshots) {
|
|
78
|
+
console.log(` ${s.id} included=${s.included} excluded=${s.excluded} fp=${s.fingerprint} ${s.created_at}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
} else if (subcommand === 'show') {
|
|
82
|
+
const snapshotId = args.find(a => a.startsWith('snap-'));
|
|
83
|
+
if (!snapshotId) throw new Error('Usage: sdlab snapshot show <snapshot-id>');
|
|
84
|
+
|
|
85
|
+
const manifest = await loadSnapshot(projectRoot, snapshotId);
|
|
86
|
+
console.log(`\x1b[1mSnapshot\x1b[0m — ${snapshotId}\n`);
|
|
87
|
+
console.log(` Created: ${manifest.created_at}`);
|
|
88
|
+
console.log(` Project: ${manifest.project_name}`);
|
|
89
|
+
console.log(` Frozen: ${manifest.frozen}`);
|
|
90
|
+
console.log(` Records: ${manifest.counts.total_records}`);
|
|
91
|
+
console.log(` Included: ${manifest.counts.included}`);
|
|
92
|
+
console.log(` Excluded: ${manifest.counts.excluded}`);
|
|
93
|
+
console.log(` Config fingerprint: ${manifest.config_fingerprint}`);
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(' Selection profile:');
|
|
96
|
+
for (const [k, v] of Object.entries(manifest.selection_profile)) {
|
|
97
|
+
console.log(` ${k}: ${JSON.stringify(v)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
} else if (subcommand === 'diff') {
|
|
101
|
+
const snapIds = args.filter(a => a.startsWith('snap-'));
|
|
102
|
+
if (snapIds.length < 2) throw new Error('Usage: sdlab snapshot diff <id-a> <id-b>');
|
|
103
|
+
|
|
104
|
+
const diff = await diffSnapshots(projectRoot, snapIds[0], snapIds[1]);
|
|
105
|
+
console.log(`\x1b[1mSnapshot diff\x1b[0m — ${snapIds[0]} vs ${snapIds[1]}\n`);
|
|
106
|
+
console.log(` ${snapIds[0]}: ${diff.countA} records`);
|
|
107
|
+
console.log(` ${snapIds[1]}: ${diff.countB} records`);
|
|
108
|
+
console.log(` Shared: ${diff.shared.length}`);
|
|
109
|
+
console.log(` Added in ${snapIds[1]}: ${diff.added.length}`);
|
|
110
|
+
console.log(` Removed from ${snapIds[0]}: ${diff.removed.length}`);
|
|
111
|
+
|
|
112
|
+
if (diff.added.length > 0 && diff.added.length <= 20) {
|
|
113
|
+
console.log('\n Added:');
|
|
114
|
+
for (const id of diff.added) console.log(` + ${id}`);
|
|
115
|
+
}
|
|
116
|
+
if (diff.removed.length > 0 && diff.removed.length <= 20) {
|
|
117
|
+
console.log('\n Removed:');
|
|
118
|
+
for (const id of diff.removed) console.log(` - ${id}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
} else {
|
|
122
|
+
throw new Error(`Unknown subcommand: ${subcommand}. Use: create, list, show, diff`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Direct execution guard
|
|
127
|
+
if (process.argv[1] && (process.argv[1].endsWith('snapshot.js') || process.argv[1].endsWith('snapshot'))) {
|
|
128
|
+
run().catch((err) => {
|
|
129
|
+
console.error(err.message || err);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
|
132
|
+
}
|
package/scripts/split.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* split.js — Build, show, and audit dataset splits.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab split build [--snapshot <id>] [--profile <name>] [--project <name>]
|
|
8
|
+
* sdlab split list [--project <name>]
|
|
9
|
+
* sdlab split show <split-id> [--project <name>]
|
|
10
|
+
* sdlab split audit <split-id> [--project <name>]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFile } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { getProjectName } from '../lib/args.js';
|
|
16
|
+
import { REPO_ROOT } from '../lib/paths.js';
|
|
17
|
+
import { loadSplitProfile } from '../lib/config.js';
|
|
18
|
+
import { listSnapshots } from '../lib/snapshot.js';
|
|
19
|
+
import { createSplit, loadSplit, loadSplitAudit, listSplits } from '../lib/split.js';
|
|
20
|
+
|
|
21
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
22
|
+
const projectName = getProjectName(argv);
|
|
23
|
+
const projectRoot = join(REPO_ROOT, 'projects', projectName);
|
|
24
|
+
const subcommand = argv.find(a => !a.startsWith('--')) || 'list';
|
|
25
|
+
const args = argv.filter(a => a !== subcommand);
|
|
26
|
+
|
|
27
|
+
if (subcommand === 'build') {
|
|
28
|
+
// Find snapshot ID
|
|
29
|
+
let snapshotId;
|
|
30
|
+
const snapIdx = args.indexOf('--snapshot');
|
|
31
|
+
if (snapIdx >= 0) {
|
|
32
|
+
snapshotId = args[snapIdx + 1];
|
|
33
|
+
} else {
|
|
34
|
+
// Use latest snapshot
|
|
35
|
+
const snapshots = await listSnapshots(projectRoot);
|
|
36
|
+
if (snapshots.length === 0) {
|
|
37
|
+
throw new Error('No snapshots found. Run: sdlab snapshot create');
|
|
38
|
+
}
|
|
39
|
+
snapshotId = snapshots[snapshots.length - 1].id;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const profileIdx = args.indexOf('--profile');
|
|
43
|
+
const profileId = profileIdx >= 0 ? args[profileIdx + 1] : null;
|
|
44
|
+
const profile = loadSplitProfile(projectRoot, profileId);
|
|
45
|
+
|
|
46
|
+
console.log(`\x1b[1msdlab split build\x1b[0m — ${projectName}`);
|
|
47
|
+
console.log(` Snapshot: ${snapshotId}`);
|
|
48
|
+
console.log(` Profile: ${profileId || 'balanced-default (built-in)'}`);
|
|
49
|
+
console.log(` Strategy: ${profile.strategy}`);
|
|
50
|
+
console.log(` Ratios: ${profile.train_ratio}/${profile.val_ratio}/${profile.test_ratio}`);
|
|
51
|
+
console.log(` Seed: ${profile.seed}`);
|
|
52
|
+
console.log('');
|
|
53
|
+
|
|
54
|
+
const result = await createSplit(projectRoot, snapshotId, profile);
|
|
55
|
+
|
|
56
|
+
console.log(` \x1b[32m✓\x1b[0m Split: ${result.splitId}`);
|
|
57
|
+
console.log(` Train: ${result.train}`);
|
|
58
|
+
console.log(` Val: ${result.val}`);
|
|
59
|
+
console.log(` Test: ${result.test}`);
|
|
60
|
+
|
|
61
|
+
// Show audit summary
|
|
62
|
+
const audit = await loadSplitAudit(projectRoot, result.splitId);
|
|
63
|
+
console.log('');
|
|
64
|
+
if (audit.leakage_check.passed) {
|
|
65
|
+
console.log(` \x1b[32m✓\x1b[0m No subject leakage detected`);
|
|
66
|
+
} else {
|
|
67
|
+
console.log(` \x1b[31m✗\x1b[0m Subject leakage detected:`);
|
|
68
|
+
for (const issue of audit.leakage_check.issues) {
|
|
69
|
+
console.log(` family="${issue.family}" (${issue.record_count} records) → ${issue.leaked_to.join(', ')}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(` Families: ${audit.family_count} (train=${audit.families_per_split.train}, val=${audit.families_per_split.val}, test=${audit.families_per_split.test})`);
|
|
74
|
+
|
|
75
|
+
} else if (subcommand === 'list') {
|
|
76
|
+
const splits = await listSplits(projectRoot);
|
|
77
|
+
if (splits.length === 0) {
|
|
78
|
+
console.log('No splits found. Run: sdlab split build');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log(`\x1b[1mSplits\x1b[0m — ${projectName}\n`);
|
|
82
|
+
for (const s of splits) {
|
|
83
|
+
console.log(` ${s.id} snap=${s.snapshot_id} train=${s.train} val=${s.val} test=${s.test} families=${s.families} ${s.created_at}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
} else if (subcommand === 'show') {
|
|
87
|
+
const splitId = args.find(a => a.startsWith('split-'));
|
|
88
|
+
if (!splitId) throw new Error('Usage: sdlab split show <split-id>');
|
|
89
|
+
|
|
90
|
+
const manifest = await loadSplit(projectRoot, splitId);
|
|
91
|
+
console.log(`\x1b[1mSplit\x1b[0m — ${splitId}\n`);
|
|
92
|
+
console.log(` Created: ${manifest.created_at}`);
|
|
93
|
+
console.log(` Snapshot: ${manifest.snapshot_id}`);
|
|
94
|
+
console.log(` Records: ${manifest.counts.total_records}`);
|
|
95
|
+
console.log(` Families: ${manifest.counts.total_families}`);
|
|
96
|
+
console.log(` Train: ${manifest.counts.train}`);
|
|
97
|
+
console.log(` Val: ${manifest.counts.val}`);
|
|
98
|
+
console.log(` Test: ${manifest.counts.test}`);
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(' Profile:');
|
|
101
|
+
for (const [k, v] of Object.entries(manifest.profile)) {
|
|
102
|
+
console.log(` ${k}: ${JSON.stringify(v)}`);
|
|
103
|
+
}
|
|
104
|
+
if (manifest.warnings.length > 0) {
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(' Warnings:');
|
|
107
|
+
for (const w of manifest.warnings) {
|
|
108
|
+
console.log(` ⚠ ${w}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
} else if (subcommand === 'audit') {
|
|
113
|
+
const splitId = args.find(a => a.startsWith('split-'));
|
|
114
|
+
if (!splitId) throw new Error('Usage: sdlab split audit <split-id>');
|
|
115
|
+
|
|
116
|
+
const audit = await loadSplitAudit(projectRoot, splitId);
|
|
117
|
+
console.log(`\x1b[1mSplit audit\x1b[0m — ${splitId}\n`);
|
|
118
|
+
|
|
119
|
+
// Leakage check
|
|
120
|
+
if (audit.leakage_check.passed) {
|
|
121
|
+
console.log(` \x1b[32m✓\x1b[0m Leakage check: PASSED`);
|
|
122
|
+
} else {
|
|
123
|
+
console.log(` \x1b[31m✗\x1b[0m Leakage check: FAILED`);
|
|
124
|
+
for (const issue of audit.leakage_check.issues) {
|
|
125
|
+
console.log(` family="${issue.family}" (${issue.record_count} records) leaked to: ${issue.leaked_to.join(', ')}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Family distribution
|
|
130
|
+
console.log(`\n Families: ${audit.family_count}`);
|
|
131
|
+
console.log(` Train: ${audit.families_per_split.train}`);
|
|
132
|
+
console.log(` Val: ${audit.families_per_split.val}`);
|
|
133
|
+
console.log(` Test: ${audit.families_per_split.test}`);
|
|
134
|
+
|
|
135
|
+
// Lane balance
|
|
136
|
+
console.log('\n Lane balance:');
|
|
137
|
+
console.log(` ${'Lane'.padEnd(20)} ${'Total'.padStart(6)} ${'Train%'.padStart(8)} ${'Val%'.padStart(8)} ${'Test%'.padStart(8)}`);
|
|
138
|
+
console.log(` ${'─'.repeat(52)}`);
|
|
139
|
+
for (const [lane, b] of Object.entries(audit.lane_balance).sort((a, b) => b[1].total - a[1].total)) {
|
|
140
|
+
console.log(` ${lane.padEnd(20)} ${String(b.total).padStart(6)} ${String(b.train_pct + '%').padStart(8)} ${String(b.val_pct + '%').padStart(8)} ${String(b.test_pct + '%').padStart(8)}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
} else {
|
|
144
|
+
throw new Error(`Unknown subcommand: ${subcommand}. Use: build, list, show, audit`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Direct execution guard
|
|
149
|
+
if (process.argv[1] && (process.argv[1].endsWith('split.js') || process.argv[1].endsWith('split'))) {
|
|
150
|
+
run().catch((err) => {
|
|
151
|
+
console.error(err.message || err);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"rules": [
|
|
4
|
+
{
|
|
5
|
+
"id": "STR-001",
|
|
6
|
+
"category": "structure",
|
|
7
|
+
"desc": "Structural plausibility — buildings look like they could stand",
|
|
8
|
+
"dims": ["structural_plausibility"],
|
|
9
|
+
"faction_specific": false,
|
|
10
|
+
"rationale_pass": "Structure reads as plausible — load paths and massing are believable (${scorePct}%)",
|
|
11
|
+
"rationale_fail": "Structure implausible — walls, spans, or cantilevers defy credible engineering (${scorePct}%)"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "MAT-001",
|
|
15
|
+
"category": "material",
|
|
16
|
+
"desc": "Material consistency — stone reads as stone, wood as wood, metal as metal",
|
|
17
|
+
"dims": ["material_consistency"],
|
|
18
|
+
"faction_specific": false,
|
|
19
|
+
"rationale_pass": "Materials read correctly — surfaces have appropriate weight, texture, and joinery (${scorePct}%)",
|
|
20
|
+
"rationale_fail": "Material reads wrong — surface lacks expected weight, grain, or construction logic (${scorePct}%)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "PER-001",
|
|
24
|
+
"category": "perspective",
|
|
25
|
+
"desc": "Perspective accuracy — no impossible geometry or broken vanishing points",
|
|
26
|
+
"dims": ["perspective_accuracy"],
|
|
27
|
+
"faction_specific": false,
|
|
28
|
+
"rationale_pass": "Perspective is accurate — vanishing points consistent, no spatial contradictions (${scorePct}%)",
|
|
29
|
+
"rationale_fail": "Perspective broken — impossible geometry, misaligned vanishing points, or spatial contradictions (${scorePct}%)"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "LGT-001",
|
|
33
|
+
"category": "lighting",
|
|
34
|
+
"desc": "Lighting and mood — atmosphere serves narrative intent",
|
|
35
|
+
"dims": ["lighting_mood"],
|
|
36
|
+
"faction_specific": false,
|
|
37
|
+
"rationale_pass": "Lighting carries mood — shadow, bounce, and atmosphere serve the narrative intent (${scorePct}%)",
|
|
38
|
+
"rationale_fail": "Lighting flat or contradictory — no mood, multiple shadow sources, or atmosphere works against intent (${scorePct}%)"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "SCL-001",
|
|
42
|
+
"category": "scale",
|
|
43
|
+
"desc": "Scale consistency — human-scale reference readable",
|
|
44
|
+
"dims": ["scale_consistency"],
|
|
45
|
+
"faction_specific": false,
|
|
46
|
+
"rationale_pass": "Scale reads correctly — doors, windows, and elements imply consistent human proportion (${scorePct}%)",
|
|
47
|
+
"rationale_fail": "Scale inconsistent — elements contradict each other or lack human-scale reference (${scorePct}%)"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "ERA-001",
|
|
51
|
+
"category": "era",
|
|
52
|
+
"desc": "Era and tech-level coherence — no anachronisms within the depicted period",
|
|
53
|
+
"dims": ["era_coherence"],
|
|
54
|
+
"faction_specific": false,
|
|
55
|
+
"rationale_pass": "Era coherent — construction methods, ornament, and technology match the depicted period (${scorePct}%)",
|
|
56
|
+
"rationale_fail": "Anachronism present — elements from incompatible time periods or tech levels mixed (${scorePct}%)"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "WEA-001",
|
|
60
|
+
"category": "weathering",
|
|
61
|
+
"desc": "Weathering and aging appropriate to era, climate, and material",
|
|
62
|
+
"dims": ["weathering_logic"],
|
|
63
|
+
"faction_specific": false,
|
|
64
|
+
"rationale_pass": "Weathering reads correctly — aging pattern matches material, climate, and implied age (${scorePct}%)",
|
|
65
|
+
"rationale_fail": "Weathering illogical — wear doesn't match material behavior, climate, or timeframe (${scorePct}%)"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "STY-001",
|
|
69
|
+
"category": "style",
|
|
70
|
+
"desc": "Style consistency within the project — rendering and design language unified",
|
|
71
|
+
"dims": ["style_consistency"],
|
|
72
|
+
"faction_specific": false,
|
|
73
|
+
"rationale_pass": "Style consistent — rendering approach and design language match the project standard (${scorePct}%)",
|
|
74
|
+
"rationale_fail": "Style inconsistent — rendering or design language breaks from project standard (${scorePct}%)"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "DET-001",
|
|
78
|
+
"category": "detail",
|
|
79
|
+
"desc": "Detail density appropriate to distance and scale of view",
|
|
80
|
+
"dims": ["detail_density"],
|
|
81
|
+
"faction_specific": false,
|
|
82
|
+
"lanes": ["structural_detail"],
|
|
83
|
+
"rationale_pass": "Detail density appropriate — close-ups show construction logic, wide shots stay readable (${scorePct}%)",
|
|
84
|
+
"rationale_fail": "Detail density wrong — close-up lacks construction detail or wide shot is noisy and unreadable (${scorePct}%)"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "RUI-001",
|
|
88
|
+
"category": "ruin",
|
|
89
|
+
"desc": "Ruin logic — decay pattern follows structural weaknesses and material behavior",
|
|
90
|
+
"dims": ["ruin_logic"],
|
|
91
|
+
"faction_specific": false,
|
|
92
|
+
"lanes": ["ruin"],
|
|
93
|
+
"rationale_pass": "Ruin logic holds — collapse follows structural weakness, material decay is physically plausible (${scorePct}%)",
|
|
94
|
+
"rationale_fail": "Ruin logic fails — decay is arbitrary, structure collapses where it shouldn't or survives where it can't (${scorePct}%)"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"default_lane": "exterior",
|
|
3
|
+
"lanes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "exterior",
|
|
6
|
+
"label": "Building Exterior",
|
|
7
|
+
"description": "Building facades, elevations, full exterior views, architectural massing",
|
|
8
|
+
"id_patterns": ["^ext_", "^building_", "^facade_"]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "interior",
|
|
12
|
+
"label": "Interior Space",
|
|
13
|
+
"description": "Rooms, halls, chambers, functional interiors with spatial atmosphere",
|
|
14
|
+
"id_patterns": ["^int_", "^room_", "^hall_"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "streetscape",
|
|
18
|
+
"label": "Streetscape / Urban",
|
|
19
|
+
"description": "Street-level views, district overviews, urban fabric and city scenes",
|
|
20
|
+
"id_patterns": ["^street_", "^district_", "^urban_"]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "structural_detail",
|
|
24
|
+
"label": "Structural Detail",
|
|
25
|
+
"description": "Ornamental elements, material studies, construction details, decorative motifs",
|
|
26
|
+
"id_patterns": ["^detail_", "^ornament_", "^material_"]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "ruin",
|
|
30
|
+
"label": "Ruin / Decay",
|
|
31
|
+
"description": "Ruined structures, abandoned buildings, post-collapse environments",
|
|
32
|
+
"id_patterns": ["^ruin_", "^decay_", "^abandoned_"]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "landscape",
|
|
36
|
+
"label": "Landscape / Vista",
|
|
37
|
+
"description": "Wide establishing shots, panoramic views, architecture in landscape context",
|
|
38
|
+
"id_patterns": ["^landscape_", "^vista_", "^panorama_"]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|