@mcptoolshop/style-dataset-lab 3.0.0 → 3.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 +108 -15
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +71 -0
- package/README.es.md +99 -8
- package/README.fr.md +100 -9
- package/README.hi.md +102 -9
- package/README.it.md +99 -8
- package/README.ja.md +100 -9
- package/README.md +64 -1
- package/README.pt-BR.md +99 -8
- package/README.zh.md +100 -9
- package/SECURITY.md +6 -5
- package/bin/sdlab.js +289 -62
- package/lib/adapters/comfyui-runner.js +11 -15
- 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 +7 -3
- package/lib/batch-runs.js +162 -31
- package/lib/batch-sheet-render.js +5 -2
- package/lib/brief-compiler.js +38 -11
- package/lib/card.js +11 -4
- package/lib/comfyui-output.js +70 -0
- package/lib/comfyui.js +100 -6
- package/lib/config.js +68 -3
- 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/implementation-packs.js +15 -1
- package/lib/log.js +41 -4
- package/lib/paths.js +41 -3
- package/lib/refine-briefs.js +16 -9
- package/lib/reingest-selected.js +14 -3
- package/lib/reingest.js +15 -1
- package/lib/run-summary.js +3 -2
- package/lib/runtime-runs.js +59 -9
- package/lib/selections.js +54 -2
- package/lib/snapshot.js +100 -7
- package/lib/split.js +175 -17
- package/lib/training-adapters.js +79 -0
- package/lib/training-manifests.js +17 -3
- package/lib/training-packages.js +29 -28
- package/lib/training-profiles.js +10 -0
- package/package.json +27 -3
- package/scripts/batch-generate.js +72 -20
- package/scripts/batch-sheet.js +3 -3
- package/scripts/batch-show.js +3 -3
- package/scripts/brief-compile.js +8 -8
- package/scripts/brief-show.js +4 -3
- package/scripts/canon-bind.js +7 -9
- package/scripts/card.js +22 -25
- package/scripts/compare.js +38 -33
- package/scripts/critique-show.js +3 -3
- package/scripts/critique.js +3 -3
- package/scripts/curate.js +91 -46
- package/scripts/doctor.js +3 -5
- 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 +96 -29
- package/scripts/implementation-pack.js +19 -14
- package/scripts/init.js +115 -17
- package/scripts/painterly.js +8 -10
- package/scripts/refine.js +3 -3
- package/scripts/reingest-selected.js +3 -3
- package/scripts/reingest.js +33 -20
- package/scripts/run-generate.js +4 -4
- package/scripts/run-list.js +3 -3
- package/scripts/run-show.js +3 -3
- package/scripts/select.js +5 -4
- package/scripts/selection-show.js +3 -3
- 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 +2 -2
- package/scripts/workflow-show.js +4 -3
- package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/architecture/terminology.json +10 -8
- package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/character-design/project.json +4 -2
- 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/game-art/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/game-art/project.json +4 -2
- package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/vehicle-mech/project.json +4 -2
- 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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Training adapter registry.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for training-package adapters. Each adapter
|
|
5
|
+
* lives in `lib/adapters/<id>.js` and exports `buildPackage(opts)`.
|
|
6
|
+
*
|
|
7
|
+
* To add a new adapter (e.g. `kohya-lora`, `onedtrainer`):
|
|
8
|
+
* 1. Add `lib/adapters/<id>.js` exporting `buildPackage`.
|
|
9
|
+
* 2. Add an entry to ADAPTER_REGISTRY below with a one-line description.
|
|
10
|
+
*
|
|
11
|
+
* Why explicit registration: the available adapter list shows up in error
|
|
12
|
+
* messages, training-profile validation, and (eventually) CLI completion.
|
|
13
|
+
* A static map keeps those views consistent and makes new adapters a
|
|
14
|
+
* one-line surface change rather than a directory-listing side effect.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { inputError } from './errors.js';
|
|
18
|
+
|
|
19
|
+
export const ADAPTER_REGISTRY = Object.freeze({
|
|
20
|
+
'generic-image-caption': {
|
|
21
|
+
id: 'generic-image-caption',
|
|
22
|
+
description: 'Image folders + caption JSONL — broadest trainer compatibility',
|
|
23
|
+
module: './adapters/generic-image-caption.js',
|
|
24
|
+
},
|
|
25
|
+
'diffusers-lora': {
|
|
26
|
+
id: 'diffusers-lora',
|
|
27
|
+
description: 'Diffusers fine-tuning layout: image + caption .txt sidecars + per-partition JSONL',
|
|
28
|
+
module: './adapters/diffusers-lora.js',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Names of all registered adapters.
|
|
34
|
+
* @returns {string[]}
|
|
35
|
+
*/
|
|
36
|
+
export function listAdapters() {
|
|
37
|
+
return Object.keys(ADAPTER_REGISTRY);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether `id` names a registered adapter.
|
|
42
|
+
* @param {string} id
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
export function isRegisteredAdapter(id) {
|
|
46
|
+
return Object.prototype.hasOwnProperty.call(ADAPTER_REGISTRY, id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve and dynamically import an adapter module.
|
|
51
|
+
*
|
|
52
|
+
* Throws `ADAPTER_NOT_REGISTERED` (input error, exit 1) for unknown ids
|
|
53
|
+
* — distinct from `MODULE_LOAD_FAILED` (runtime, exit 2) for the rare
|
|
54
|
+
* case where the file vanished after registration.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} id
|
|
57
|
+
* @returns {Promise<{ buildPackage: Function }>}
|
|
58
|
+
*/
|
|
59
|
+
export async function loadAdapter(id) {
|
|
60
|
+
if (!isRegisteredAdapter(id)) {
|
|
61
|
+
throw inputError(
|
|
62
|
+
'ADAPTER_NOT_REGISTERED',
|
|
63
|
+
`Adapter "${id}" is not registered. Available: ${listAdapters().join(', ')}`,
|
|
64
|
+
'Pick one of the registered adapters or add a new one to lib/training-adapters.js.'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const entry = ADAPTER_REGISTRY[id];
|
|
68
|
+
try {
|
|
69
|
+
return await import(entry.module);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
// The registry says it should exist but import failed — surface the
|
|
72
|
+
// underlying error so it doesn't get masked as a typo.
|
|
73
|
+
throw inputError(
|
|
74
|
+
'ADAPTER_MODULE_LOAD_FAILED',
|
|
75
|
+
`Adapter "${id}" is registered but its module failed to load: ${err.message}`,
|
|
76
|
+
`Check that ${entry.module} exists and exports buildPackage().`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -12,8 +12,9 @@ import { existsSync } from 'node:fs';
|
|
|
12
12
|
import { createHash, randomBytes } from 'node:crypto';
|
|
13
13
|
import { loadExport } from './export.js';
|
|
14
14
|
import { loadTrainingProfile } from './training-profiles.js';
|
|
15
|
-
import { computeConfigFingerprint } from './snapshot.js';
|
|
15
|
+
import { computeConfigFingerprint, SCHEMA_VERSION, checkManifestVersion } from './snapshot.js';
|
|
16
16
|
import { listEvalPacks } from './eval-pack.js';
|
|
17
|
+
import { inputError } from './errors.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Generate a training manifest ID: tm-YYYYMMDD-HHMMSS-XXXX
|
|
@@ -64,6 +65,8 @@ export async function createTrainingManifest(projectRoot, exportId, profileId, o
|
|
|
64
65
|
|
|
65
66
|
const manifestId = generateManifestId();
|
|
66
67
|
const manifest = {
|
|
68
|
+
// DB-001 / DB-006: stamp manifest schema version
|
|
69
|
+
schema_version: SCHEMA_VERSION,
|
|
67
70
|
training_manifest_id: manifestId,
|
|
68
71
|
created_at: new Date().toISOString(),
|
|
69
72
|
created_by: 'sdlab-training-manifest-v1',
|
|
@@ -89,7 +92,16 @@ export async function createTrainingManifest(projectRoot, exportId, profileId, o
|
|
|
89
92
|
// Write manifest
|
|
90
93
|
const dir = join(projectRoot, 'training', 'manifests');
|
|
91
94
|
await mkdir(dir, { recursive: true });
|
|
92
|
-
|
|
95
|
+
const manifestPath = join(dir, `${manifestId}.json`);
|
|
96
|
+
// D-005: refuse to silently overwrite on ID collision.
|
|
97
|
+
if (existsSync(manifestPath)) {
|
|
98
|
+
throw inputError(
|
|
99
|
+
'TRAINING_MANIFEST_ID_COLLISION',
|
|
100
|
+
`Training manifest file already exists: ${manifestPath}`,
|
|
101
|
+
'Retry to generate a fresh manifest ID.',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
93
105
|
|
|
94
106
|
return { manifestId, exportId, profileId };
|
|
95
107
|
}
|
|
@@ -139,7 +151,9 @@ export async function loadTrainingManifest(projectRoot, manifestId) {
|
|
|
139
151
|
if (!existsSync(path)) {
|
|
140
152
|
throw new Error(`Training manifest "${manifestId}" not found at ${path}`);
|
|
141
153
|
}
|
|
142
|
-
|
|
154
|
+
const manifest = JSON.parse(await readFile(path, 'utf-8'));
|
|
155
|
+
checkManifestVersion(manifest, 'training-manifest');
|
|
156
|
+
return manifest;
|
|
143
157
|
}
|
|
144
158
|
|
|
145
159
|
/**
|
package/lib/training-packages.js
CHANGED
|
@@ -15,6 +15,9 @@ import { loadTrainingProfile } from './training-profiles.js';
|
|
|
15
15
|
import { loadSplitPartition } from './split.js';
|
|
16
16
|
import { loadRecord } from './records.js';
|
|
17
17
|
import { loadProjectConfig, detectLane, detectGroup } from './config.js';
|
|
18
|
+
import { inputError } from './errors.js';
|
|
19
|
+
import { SCHEMA_VERSION, checkManifestVersion } from './snapshot.js';
|
|
20
|
+
import { loadAdapter } from './training-adapters.js';
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Generate a training package ID: tp-YYYYMMDD-HHMMSS-XXXX
|
|
@@ -31,20 +34,6 @@ async function sha256File(filePath) {
|
|
|
31
34
|
return createHash('sha256').update(data).digest('hex');
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
/**
|
|
35
|
-
* Load an adapter module by name.
|
|
36
|
-
*/
|
|
37
|
-
async function loadAdapter(adapterTarget) {
|
|
38
|
-
const adapterPath = `./adapters/${adapterTarget}.js`;
|
|
39
|
-
try {
|
|
40
|
-
return await import(adapterPath);
|
|
41
|
-
} catch {
|
|
42
|
-
throw new Error(
|
|
43
|
-
`Adapter "${adapterTarget}" not found. Available: generic-image-caption, diffusers-lora`
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
37
|
/**
|
|
49
38
|
* Build a training package from a manifest.
|
|
50
39
|
*
|
|
@@ -63,6 +52,14 @@ export async function buildTrainingPackage(projectRoot, manifestId, options = {}
|
|
|
63
52
|
const recordsDir = join(projectRoot, 'records');
|
|
64
53
|
const packageId = generatePackageId();
|
|
65
54
|
const packageDir = join(projectRoot, 'training', 'packages', packageId);
|
|
55
|
+
// D-005: refuse to silently overwrite on ID collision.
|
|
56
|
+
if (existsSync(packageDir)) {
|
|
57
|
+
throw inputError(
|
|
58
|
+
'TRAINING_PACKAGE_ID_COLLISION',
|
|
59
|
+
`Training package directory already exists: ${packageDir}`,
|
|
60
|
+
'Retry to generate a fresh package ID.',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
66
63
|
|
|
67
64
|
// Load split partitions
|
|
68
65
|
const partitions = {};
|
|
@@ -105,19 +102,10 @@ export async function buildTrainingPackage(projectRoot, manifestId, options = {}
|
|
|
105
102
|
|
|
106
103
|
totalImages = adapterResult.imageCount || 0;
|
|
107
104
|
|
|
108
|
-
//
|
|
109
|
-
const checksumEntries = [];
|
|
110
|
-
const allFiles = await collectFiles(packageDir);
|
|
111
|
-
for (const filePath of allFiles.sort()) {
|
|
112
|
-
const relPath = relative(packageDir, filePath);
|
|
113
|
-
if (relPath === 'checksums.txt') continue;
|
|
114
|
-
const hash = await sha256File(filePath);
|
|
115
|
-
checksumEntries.push(`SHA256 (${relPath}) = ${hash}`);
|
|
116
|
-
}
|
|
117
|
-
await writeFile(join(packageDir, 'checksums.txt'), checksumEntries.join('\n') + '\n');
|
|
118
|
-
|
|
119
|
-
// Write package manifest (last — references everything)
|
|
105
|
+
// D-015: Write manifest + README FIRST so they are covered by checksums.
|
|
120
106
|
const packageManifest = {
|
|
107
|
+
// DB-001 / DB-006: stamp manifest schema version
|
|
108
|
+
schema_version: SCHEMA_VERSION,
|
|
121
109
|
training_package_id: packageId,
|
|
122
110
|
created_at: new Date().toISOString(),
|
|
123
111
|
created_by: 'sdlab-training-package-v1',
|
|
@@ -134,7 +122,7 @@ export async function buildTrainingPackage(projectRoot, manifestId, options = {}
|
|
|
134
122
|
val: partitions.val.length,
|
|
135
123
|
test: partitions.test.length,
|
|
136
124
|
images: totalImages,
|
|
137
|
-
checksums:
|
|
125
|
+
checksums: 0, // updated after checksum pass below
|
|
138
126
|
},
|
|
139
127
|
};
|
|
140
128
|
await writeFile(join(packageDir, 'manifest.json'), JSON.stringify(packageManifest, null, 2) + '\n');
|
|
@@ -143,6 +131,17 @@ export async function buildTrainingPackage(projectRoot, manifestId, options = {}
|
|
|
143
131
|
const readme = generatePackageReadme(packageManifest, profile, manifest, adapterTarget);
|
|
144
132
|
await writeFile(join(packageDir, 'README.md'), readme);
|
|
145
133
|
|
|
134
|
+
// Generate checksums AFTER manifest+README are on disk so they are included.
|
|
135
|
+
const checksumEntries = [];
|
|
136
|
+
const allFiles = await collectFiles(packageDir);
|
|
137
|
+
for (const filePath of allFiles.sort()) {
|
|
138
|
+
const relPath = relative(packageDir, filePath);
|
|
139
|
+
if (relPath === 'checksums.txt') continue;
|
|
140
|
+
const hash = await sha256File(filePath);
|
|
141
|
+
checksumEntries.push(`SHA256 (${relPath}) = ${hash}`);
|
|
142
|
+
}
|
|
143
|
+
await writeFile(join(packageDir, 'checksums.txt'), checksumEntries.join('\n') + '\n');
|
|
144
|
+
|
|
146
145
|
return { packageId, records: totalRecords, images: totalImages };
|
|
147
146
|
}
|
|
148
147
|
|
|
@@ -237,5 +236,7 @@ export async function loadTrainingPackage(projectRoot, packageId) {
|
|
|
237
236
|
if (!existsSync(path)) {
|
|
238
237
|
throw new Error(`Training package "${packageId}" not found at ${path}`);
|
|
239
238
|
}
|
|
240
|
-
|
|
239
|
+
const manifest = JSON.parse(await readFile(path, 'utf-8'));
|
|
240
|
+
checkManifestVersion(manifest, 'training-package');
|
|
241
|
+
return manifest;
|
|
241
242
|
}
|
package/lib/training-profiles.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
|
+
import { isRegisteredAdapter, listAdapters } from './training-adapters.js';
|
|
12
13
|
|
|
13
14
|
const REQUIRED_FIELDS = ['profile_id', 'label', 'target_family', 'asset_type', 'eligible_lanes', 'adapter_targets'];
|
|
14
15
|
|
|
@@ -32,6 +33,15 @@ export function validateProfile(profile) {
|
|
|
32
33
|
if (profile.adapter_targets?.length === 0) {
|
|
33
34
|
errors.push('adapter_targets must have at least one target');
|
|
34
35
|
}
|
|
36
|
+
// Reject unregistered adapter targets early — surfaces typos at profile
|
|
37
|
+
// load time instead of at package-build time.
|
|
38
|
+
if (Array.isArray(profile.adapter_targets)) {
|
|
39
|
+
for (const target of profile.adapter_targets) {
|
|
40
|
+
if (!isRegisteredAdapter(target)) {
|
|
41
|
+
errors.push(`adapter_targets contains unregistered adapter "${target}". Available: ${listAdapters().join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
35
45
|
return { valid: errors.length === 0, errors };
|
|
36
46
|
}
|
|
37
47
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcptoolshop/style-dataset-lab",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Canon-aligned dataset production and generation workbench — define visual rules, build versioned training data, compile production briefs, run local workflows, batch-produce, select winners, and re-ingest into your corpus",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sdlab": "./bin/sdlab.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
+
"test": "node --test tests/**/*.test.js",
|
|
11
|
+
"coverage": "c8 npm test",
|
|
10
12
|
"verify": "node bin/sdlab.js --help && node bin/sdlab.js project doctor --project star-freight",
|
|
11
13
|
"generate": "node scripts/generate.js",
|
|
12
14
|
"curate": "node scripts/curate.js",
|
|
@@ -15,15 +17,34 @@
|
|
|
15
17
|
"painterly": "node scripts/painterly.js",
|
|
16
18
|
"generate:identity": "node scripts/generate-identity.js"
|
|
17
19
|
},
|
|
20
|
+
"c8": {
|
|
21
|
+
"include": ["lib/**/*.js"],
|
|
22
|
+
"exclude": [
|
|
23
|
+
"tests/**",
|
|
24
|
+
"scripts/**",
|
|
25
|
+
"site/**",
|
|
26
|
+
"projects/**",
|
|
27
|
+
"templates/**",
|
|
28
|
+
"**/*.test.js"
|
|
29
|
+
],
|
|
30
|
+
"reporter": ["text", "lcov", "text-summary"],
|
|
31
|
+
"report-dir": "coverage",
|
|
32
|
+
"all": true
|
|
33
|
+
},
|
|
18
34
|
"files": [
|
|
19
35
|
"bin/",
|
|
20
36
|
"lib/",
|
|
21
37
|
"scripts/",
|
|
38
|
+
"!scripts/bulk-curate-*.js",
|
|
39
|
+
"!scripts/curate-wave*.js",
|
|
40
|
+
"!scripts/migrate-records.js",
|
|
22
41
|
"templates/",
|
|
23
42
|
"README.md",
|
|
24
43
|
"LICENSE",
|
|
25
44
|
"SECURITY.md",
|
|
26
|
-
"CHANGELOG.md"
|
|
45
|
+
"CHANGELOG.md",
|
|
46
|
+
"CONTRIBUTING.md",
|
|
47
|
+
"CODE_OF_CONDUCT.md"
|
|
27
48
|
],
|
|
28
49
|
"keywords": [
|
|
29
50
|
"dataset",
|
|
@@ -52,5 +73,8 @@
|
|
|
52
73
|
"node": ">=20"
|
|
53
74
|
},
|
|
54
75
|
"author": "mcp-tool-shop",
|
|
55
|
-
"license": "MIT"
|
|
76
|
+
"license": "MIT",
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"c8": "^10.1.3"
|
|
79
|
+
}
|
|
56
80
|
}
|
|
@@ -9,61 +9,95 @@
|
|
|
9
9
|
* sdlab batch generate --mode silhouette-pack --project star-freight --dry-run
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { parseArgs } from '../lib/args.js';
|
|
12
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
13
13
|
import { getProjectRoot, getRunsDir } from '../lib/paths.js';
|
|
14
14
|
import { compileBatch } from '../lib/batch-compiler.js';
|
|
15
15
|
import { getBatchMode } from '../lib/batch-modes.js';
|
|
16
16
|
import {
|
|
17
17
|
createBatchDir,
|
|
18
|
+
openBatchDirForResume,
|
|
18
19
|
executeBatchRuns,
|
|
19
20
|
saveBatchManifest,
|
|
20
21
|
} from '../lib/batch-runs.js';
|
|
21
22
|
import { renderSheetHTML, saveSheet } from '../lib/batch-sheet-render.js';
|
|
22
|
-
import { info } from '../lib/log.js';
|
|
23
|
+
import { info, result } from '../lib/log.js';
|
|
23
24
|
import { join } from 'node:path';
|
|
24
25
|
import { writeFile } from 'node:fs/promises';
|
|
25
26
|
|
|
26
27
|
export async function run(argv = process.argv.slice(2)) {
|
|
27
28
|
const { flags } = parseArgs(argv, {
|
|
28
29
|
flags: {
|
|
29
|
-
project: { type: 'string'
|
|
30
|
+
project: { type: 'string' },
|
|
30
31
|
mode: { type: 'string' },
|
|
31
32
|
subject: { type: 'string' },
|
|
32
33
|
theme: { type: 'string' },
|
|
33
34
|
asset: { type: 'string' },
|
|
35
|
+
resume: { type: 'string' },
|
|
34
36
|
'dry-run': { type: 'boolean' },
|
|
35
37
|
json: { type: 'boolean' },
|
|
36
38
|
},
|
|
37
39
|
deprecated: { game: 'project' },
|
|
38
40
|
});
|
|
39
41
|
|
|
40
|
-
if (!flags.mode) {
|
|
42
|
+
if (!flags.mode && !flags.resume) {
|
|
41
43
|
console.log('Usage: sdlab batch generate --mode <id> [--subject <id>] [--theme <label>] [--project <name>]');
|
|
44
|
+
console.log(' sdlab batch generate --resume <batch_id> [--project <name>]');
|
|
42
45
|
console.log('');
|
|
43
46
|
console.log('Options:');
|
|
44
|
-
console.log(' --mode <id> Batch mode (required)');
|
|
47
|
+
console.log(' --mode <id> Batch mode (required unless --resume is given)');
|
|
45
48
|
console.log(' --subject <id> Subject for subject-driven modes');
|
|
46
49
|
console.log(' --theme <label> Theme label for environment modes');
|
|
47
50
|
console.log(' --asset <id> Training asset reference');
|
|
51
|
+
console.log(' --resume <id> Resume an existing batch — re-runs only failed/missing slots');
|
|
48
52
|
console.log(' --dry-run Prepare batch without submitting to ComfyUI');
|
|
49
53
|
console.log(' --json Output manifest as JSON');
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
const projectName = flags.project;
|
|
57
|
+
const projectName = flags.project || getProjectName(argv);
|
|
54
58
|
const projectRoot = getProjectRoot(projectName);
|
|
55
59
|
const dryRun = flags['dry-run'] || false;
|
|
60
|
+
const resumeId = flags.resume || null;
|
|
61
|
+
|
|
62
|
+
// Resume short-circuit: load prior manifest and reuse its mode_id + subject/theme
|
|
63
|
+
// so the user only needs to pass --resume <batch_id>.
|
|
64
|
+
let batchId;
|
|
65
|
+
let batchDir;
|
|
66
|
+
let priorResults = [];
|
|
67
|
+
let resumedMode = null;
|
|
68
|
+
let resumedSubject = null;
|
|
69
|
+
let resumedTheme = null;
|
|
70
|
+
let resumedAsset = null;
|
|
71
|
+
if (resumeId) {
|
|
72
|
+
const opened = openBatchDirForResume(projectRoot, resumeId);
|
|
73
|
+
batchId = opened.batchId;
|
|
74
|
+
batchDir = opened.batchDir;
|
|
75
|
+
priorResults = opened.priorResults;
|
|
76
|
+
resumedMode = opened.priorManifest.mode_id || null;
|
|
77
|
+
resumedSubject = opened.priorManifest.subject_id || null;
|
|
78
|
+
resumedTheme = opened.priorManifest.theme || null;
|
|
79
|
+
resumedAsset = opened.priorManifest.asset_ref || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const modeId = flags.mode || resumedMode;
|
|
83
|
+
const subjectId = flags.subject || resumedSubject;
|
|
84
|
+
const theme = flags.theme || resumedTheme;
|
|
85
|
+
const assetRef = flags.asset || resumedAsset;
|
|
56
86
|
|
|
57
87
|
// Load mode for display
|
|
58
|
-
const mode = getBatchMode(projectRoot,
|
|
88
|
+
const mode = getBatchMode(projectRoot, modeId);
|
|
59
89
|
|
|
60
90
|
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m batch generate`);
|
|
61
91
|
console.log(` Project: ${projectName}`);
|
|
62
92
|
console.log(` Mode: ${mode.label} (${mode.mode_id})`);
|
|
63
93
|
console.log(` Type: ${mode.batch_type}`);
|
|
64
94
|
console.log(` Slots: ${mode.variant_plan.length}`);
|
|
65
|
-
if (
|
|
66
|
-
if (
|
|
95
|
+
if (subjectId) console.log(` Subject: ${subjectId}`);
|
|
96
|
+
if (theme) console.log(` Theme: ${theme}`);
|
|
97
|
+
if (resumeId) {
|
|
98
|
+
const completed = priorResults.filter(r => r && r.status === 'ok').length;
|
|
99
|
+
console.log(` Resume: ${resumeId} (${completed}/${priorResults.length} slots already complete)`);
|
|
100
|
+
}
|
|
67
101
|
if (dryRun) console.log(' ⚠ DRY RUN');
|
|
68
102
|
console.log('');
|
|
69
103
|
|
|
@@ -71,17 +105,22 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
71
105
|
const { baseBrief, slotBriefs } = await compileBatch({
|
|
72
106
|
projectRoot,
|
|
73
107
|
projectId: projectName,
|
|
74
|
-
modeId
|
|
75
|
-
subjectId
|
|
76
|
-
theme
|
|
77
|
-
assetRef
|
|
108
|
+
modeId,
|
|
109
|
+
subjectId,
|
|
110
|
+
theme,
|
|
111
|
+
assetRef,
|
|
78
112
|
});
|
|
79
113
|
|
|
80
|
-
// 2. Create batch directory
|
|
81
|
-
|
|
82
|
-
|
|
114
|
+
// 2. Create or reuse batch directory
|
|
115
|
+
if (!resumeId) {
|
|
116
|
+
const created = await createBatchDir(projectRoot);
|
|
117
|
+
batchId = created.batchId;
|
|
118
|
+
batchDir = created.batchDir;
|
|
119
|
+
}
|
|
120
|
+
info('batch-generate', `Batch: ${batchId}`);
|
|
83
121
|
|
|
84
122
|
// 3. Execute runs
|
|
123
|
+
const batchStartMs = Date.now();
|
|
85
124
|
const results = await executeBatchRuns({
|
|
86
125
|
projectRoot,
|
|
87
126
|
projectName,
|
|
@@ -89,7 +128,14 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
89
128
|
mode,
|
|
90
129
|
slotBriefs,
|
|
91
130
|
dryRun,
|
|
131
|
+
priorResults,
|
|
92
132
|
});
|
|
133
|
+
const batchElapsedMs = Date.now() - batchStartMs;
|
|
134
|
+
if (!dryRun && results.length > 0) {
|
|
135
|
+
const avgSec = Math.round(batchElapsedMs / results.length / 1000);
|
|
136
|
+
const totalSec = Math.round(batchElapsedMs / 1000);
|
|
137
|
+
info('batch-generate', `Completed ${results.length} slot(s) in ${totalSec}s (avg ${avgSec}s/slot)`);
|
|
138
|
+
}
|
|
93
139
|
|
|
94
140
|
// 4. Build manifest
|
|
95
141
|
const manifest = {
|
|
@@ -104,7 +150,9 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
104
150
|
label: r.label,
|
|
105
151
|
brief_id: r.brief_id,
|
|
106
152
|
run_id: r.run_id,
|
|
153
|
+
status: r.status,
|
|
107
154
|
selected_output: r.selected_output,
|
|
155
|
+
...(r.error ? { error: r.error } : {}),
|
|
108
156
|
})),
|
|
109
157
|
sheet_paths: [],
|
|
110
158
|
summary: {
|
|
@@ -114,8 +162,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
114
162
|
},
|
|
115
163
|
};
|
|
116
164
|
|
|
117
|
-
if (
|
|
118
|
-
if (
|
|
165
|
+
if (subjectId) manifest.subject_id = subjectId;
|
|
166
|
+
if (theme) manifest.theme = theme;
|
|
167
|
+
if (assetRef) manifest.asset_ref = assetRef;
|
|
168
|
+
if (resumeId) manifest.resumed_from = resumeId;
|
|
119
169
|
|
|
120
170
|
// 5. Render sheet
|
|
121
171
|
const sheetHtml = renderSheetHTML({ manifest, mode, projectRoot, batchDir });
|
|
@@ -161,7 +211,9 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
161
211
|
console.log(` ${status} ${r.label.padEnd(24)} → ${r.run_id} ${r.selected_output || '(dry run)'}`);
|
|
162
212
|
}
|
|
163
213
|
console.log('');
|
|
164
|
-
|
|
165
|
-
|
|
214
|
+
// Primary artifact paths — always print (survives --quiet) so pipelines
|
|
215
|
+
// can parse the output location.
|
|
216
|
+
result(`Sheet: batches/${batchId}/sheets/primary-sheet.html`);
|
|
217
|
+
result(`Manifest: batches/${batchId}/manifest.json`);
|
|
166
218
|
}
|
|
167
219
|
}
|
package/scripts/batch-sheet.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* sdlab batch sheet batch_2025-06-30_001 --project star-freight
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { parseArgs } from '../lib/args.js';
|
|
10
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
11
11
|
import { getProjectRoot } from '../lib/paths.js';
|
|
12
12
|
import { loadBatchManifest } from '../lib/batch-runs.js';
|
|
13
13
|
import { getBatchMode } from '../lib/batch-modes.js';
|
|
@@ -20,13 +20,13 @@ import { info } from '../lib/log.js';
|
|
|
20
20
|
export async function run(argv = process.argv.slice(2)) {
|
|
21
21
|
const { flags, positionals } = parseArgs(argv, {
|
|
22
22
|
flags: {
|
|
23
|
-
project: { type: 'string'
|
|
23
|
+
project: { type: 'string' },
|
|
24
24
|
batch: { type: 'string' },
|
|
25
25
|
},
|
|
26
26
|
deprecated: { game: 'project' },
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
const projectName = flags.project;
|
|
29
|
+
const projectName = flags.project || getProjectName(argv);
|
|
30
30
|
const projectRoot = getProjectRoot(projectName);
|
|
31
31
|
const batchId = positionals[0] || flags.batch;
|
|
32
32
|
|
package/scripts/batch-show.js
CHANGED
|
@@ -9,21 +9,21 @@
|
|
|
9
9
|
* sdlab batch show batch_2025-06-30_001 --json # as JSON
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { parseArgs } from '../lib/args.js';
|
|
12
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
13
13
|
import { getProjectRoot } from '../lib/paths.js';
|
|
14
14
|
import { listBatches, loadBatchManifest } from '../lib/batch-runs.js';
|
|
15
15
|
|
|
16
16
|
export async function run(argv = process.argv.slice(2)) {
|
|
17
17
|
const { flags, positionals } = parseArgs(argv, {
|
|
18
18
|
flags: {
|
|
19
|
-
project: { type: 'string'
|
|
19
|
+
project: { type: 'string' },
|
|
20
20
|
batch: { type: 'string' },
|
|
21
21
|
json: { type: 'boolean' },
|
|
22
22
|
},
|
|
23
23
|
deprecated: { game: 'project' },
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
const projectName = flags.project;
|
|
26
|
+
const projectName = flags.project || getProjectName(argv);
|
|
27
27
|
const projectRoot = getProjectRoot(projectName);
|
|
28
28
|
const batchId = positionals[0] || flags.batch;
|
|
29
29
|
|
package/scripts/brief-compile.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* sdlab brief compile --project star-freight --workflow character-portrait-set --subject kael_maren --output-count 8 --json
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { parseArgs } from '../lib/args.js';
|
|
13
|
+
import { parseArgs, getProjectName, parseNumberFlag } from '../lib/args.js';
|
|
14
14
|
import { getProjectRoot } from '../lib/paths.js';
|
|
15
15
|
import { compileBrief, saveCompiledBrief } from '../lib/brief-compiler.js';
|
|
16
16
|
import { info } from '../lib/log.js';
|
|
@@ -18,7 +18,7 @@ import { info } from '../lib/log.js';
|
|
|
18
18
|
export async function run(argv = process.argv.slice(2)) {
|
|
19
19
|
const { flags } = parseArgs(argv, {
|
|
20
20
|
flags: {
|
|
21
|
-
project: { type: 'string'
|
|
21
|
+
project: { type: 'string' },
|
|
22
22
|
workflow: { type: 'string' },
|
|
23
23
|
subject: { type: 'string' },
|
|
24
24
|
asset: { type: 'string' },
|
|
@@ -58,7 +58,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const projectName = flags.project;
|
|
61
|
+
const projectName = flags.project || getProjectName(argv);
|
|
62
62
|
const projectRoot = getProjectRoot(projectName);
|
|
63
63
|
|
|
64
64
|
// Parse reference IDs
|
|
@@ -68,11 +68,11 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
68
68
|
|
|
69
69
|
// Parse overrides
|
|
70
70
|
const overrides = {};
|
|
71
|
-
if (flags['output-count']) overrides.output_count =
|
|
72
|
-
if (flags.width) overrides.width =
|
|
73
|
-
if (flags.height) overrides.height =
|
|
74
|
-
if (flags.steps) overrides.steps =
|
|
75
|
-
if (flags.cfg) overrides.cfg =
|
|
71
|
+
if (flags['output-count']) overrides.output_count = parseNumberFlag('output-count', flags['output-count'], { int: true, min: 1 });
|
|
72
|
+
if (flags.width) overrides.width = parseNumberFlag('width', flags.width, { int: true, min: 1 });
|
|
73
|
+
if (flags.height) overrides.height = parseNumberFlag('height', flags.height, { int: true, min: 1 });
|
|
74
|
+
if (flags.steps) overrides.steps = parseNumberFlag('steps', flags.steps, { int: true, min: 1 });
|
|
75
|
+
if (flags.cfg) overrides.cfg = parseNumberFlag('cfg', flags.cfg, { min: 0 });
|
|
76
76
|
if (flags.sampler) overrides.sampler = flags.sampler;
|
|
77
77
|
if (flags['seed-mode']) overrides.seed_mode = flags['seed-mode'];
|
|
78
78
|
|
package/scripts/brief-show.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* sdlab brief show brief_2026-04-15_001 --md
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { parseArgs } from '../lib/args.js';
|
|
12
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
13
13
|
import { getProjectRoot } from '../lib/paths.js';
|
|
14
14
|
import { loadCompiledBrief } from '../lib/brief-compiler.js';
|
|
15
15
|
import { renderBriefText, renderBriefMarkdown } from '../lib/brief-render.js';
|
|
@@ -17,7 +17,7 @@ import { renderBriefText, renderBriefMarkdown } from '../lib/brief-render.js';
|
|
|
17
17
|
export async function run(argv = process.argv.slice(2)) {
|
|
18
18
|
const { flags, positionals } = parseArgs(argv, {
|
|
19
19
|
flags: {
|
|
20
|
-
project: { type: 'string'
|
|
20
|
+
project: { type: 'string' },
|
|
21
21
|
json: { type: 'boolean' },
|
|
22
22
|
md: { type: 'boolean' },
|
|
23
23
|
},
|
|
@@ -31,7 +31,8 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const
|
|
34
|
+
const projectName = flags.project || getProjectName(argv);
|
|
35
|
+
const projectRoot = getProjectRoot(projectName);
|
|
35
36
|
const brief = await loadCompiledBrief(projectRoot, briefId);
|
|
36
37
|
|
|
37
38
|
if (flags.json) {
|
package/scripts/canon-bind.js
CHANGED
|
@@ -21,6 +21,7 @@ import { join } from 'path';
|
|
|
21
21
|
import { getProjectName } from '../lib/args.js';
|
|
22
22
|
import { REPO_ROOT } from '../lib/paths.js';
|
|
23
23
|
import { loadConstitution, loadLanes, loadRubric, loadTerminology, detectLane, detectGroup } from '../lib/config.js';
|
|
24
|
+
import { inputError, handleCliError } from '../lib/errors.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Build rationale string from rule's template, interpolating variables.
|
|
@@ -162,10 +163,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
162
163
|
|
|
163
164
|
// Load project config — fail loudly if missing
|
|
164
165
|
if (!existsSync(join(PROJECT_ROOT, 'constitution.json'))) {
|
|
165
|
-
throw
|
|
166
|
-
|
|
167
|
-
`
|
|
168
|
-
`Run: sdlab init ${projectName} --domain <domain>`
|
|
166
|
+
throw inputError(
|
|
167
|
+
'INPUT_MISSING_CONFIG',
|
|
168
|
+
`No constitution.json found in ${PROJECT_ROOT}.`,
|
|
169
|
+
`Canon binding requires per-project config files. Run: sdlab init ${projectName} --domain <domain>`
|
|
169
170
|
);
|
|
170
171
|
}
|
|
171
172
|
|
|
@@ -175,7 +176,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
175
176
|
const termConfig = loadTerminology(PROJECT_ROOT);
|
|
176
177
|
|
|
177
178
|
if (!constitutionConfig.rules || constitutionConfig.rules.length === 0) {
|
|
178
|
-
throw
|
|
179
|
+
throw inputError('INPUT_EMPTY_CONFIG', `constitution.json has no rules — cannot bind.`);
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
const constitutionRules = constitutionConfig.rules;
|
|
@@ -280,8 +281,5 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
280
281
|
|
|
281
282
|
// Direct execution guard
|
|
282
283
|
if (process.argv[1] && (process.argv[1].endsWith('canon-bind.js') || process.argv[1].endsWith('canon-bind'))) {
|
|
283
|
-
run().catch(
|
|
284
|
-
console.error(err.message || err);
|
|
285
|
-
process.exit(1);
|
|
286
|
-
});
|
|
284
|
+
run().catch(handleCliError);
|
|
287
285
|
}
|