@mcptoolshop/style-dataset-lab 3.0.1 → 3.2.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 +77 -0
- package/README.es.md +154 -41
- package/README.fr.md +153 -40
- package/README.hi.md +160 -47
- package/README.it.md +155 -42
- package/README.ja.md +159 -46
- package/README.md +40 -0
- package/README.pt-BR.md +154 -41
- package/README.zh.md +154 -41
- package/bin/sdlab.js +81 -2
- package/lib/adapters/ai-toolkit.js +209 -0
- package/lib/adapters/comfyui-runner.js +11 -15
- package/lib/adapters/comfyui-workflows.js +133 -2
- package/lib/adapters/diffusers-lora.js +46 -66
- package/lib/adapters/generic-image-caption.js +22 -50
- package/lib/batch-runs.js +62 -0
- package/lib/canon-build/build.js +450 -0
- package/lib/canon-build/emitters/base.js +122 -0
- package/lib/canon-build/emitters/character.js +167 -0
- package/lib/canon-build/emitters/deity.js +165 -0
- package/lib/canon-build/emitters/faction.js +161 -0
- package/lib/canon-build/emitters/index.js +38 -0
- package/lib/canon-build/emitters/location.js +153 -0
- package/lib/canon-build/emitters/monster.js +131 -0
- package/lib/canon-build/emitters/relic.js +142 -0
- package/lib/canon-build/emitters/ship.js +154 -0
- package/lib/canon-build/emitters/species.js +165 -0
- package/lib/canon-build/frontmatter.js +76 -0
- package/lib/canon-build/lane-resolve.js +64 -0
- package/lib/canon-build/load-config.js +117 -0
- package/lib/canon-build/load-entry.js +62 -0
- package/lib/canon-build/load-schema.js +46 -0
- package/lib/captions.js +197 -0
- package/lib/comfyui-output.js +70 -0
- package/lib/config.js +19 -0
- package/lib/freeze-events.js +109 -0
- package/lib/freeze-gate.js +165 -0
- package/lib/freeze-stamp.js +161 -0
- package/lib/rows.js +228 -0
- package/lib/training-adapters.js +84 -0
- package/lib/training-packages.js +25 -32
- package/lib/training-profiles.js +43 -0
- package/package.json +29 -2
- package/scripts/_batch_cut.py +71 -0
- package/scripts/_seam_cutout.py +74 -0
- package/scripts/batch-generate.js +56 -13
- package/scripts/canon-build.js +88 -0
- package/scripts/canon-drift.js +193 -0
- package/scripts/canon-freeze-status.js +85 -0
- package/scripts/canon-freeze.js +211 -0
- package/scripts/canon-unfreeze.js +137 -0
- package/scripts/curate.js +12 -0
- package/scripts/generate-controlnet.js +12 -0
- package/scripts/generate-identity.js +20 -4
- package/scripts/generate-ipadapter.js +11 -0
- package/scripts/generate.js +250 -102
- package/scripts/qwen_generate.py +124 -0
|
@@ -1,60 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Diffusers LoRA adapter.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Consumes canonical rows (per D6) — either produced in memory by
|
|
5
|
+
* training-packages.js from records, or read from a canon-build
|
|
6
|
+
* dataset.jsonl. The row shape is defined in lib/rows.js and pinned by
|
|
7
|
+
* ROW_SCHEMA_VERSION.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Output layout (unchanged from pre-refactor):
|
|
10
|
+
* dataset/<partition>/<entity_id>.<ext> — image (copied or symlinked)
|
|
11
|
+
* dataset/<partition>/<entity_id>.txt — caption sidecar
|
|
12
|
+
* metadata/<partition>.jsonl — per-row audit
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
import { writeFile, mkdir, symlink, copyFile } from 'node:fs/promises';
|
|
13
|
-
import { join, relative
|
|
16
|
+
import { join, relative } from 'node:path';
|
|
14
17
|
import { existsSync } from 'node:fs';
|
|
18
|
+
import { groupRowsByPartition } from '../rows.js';
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
|
-
* Build
|
|
21
|
+
* Build training package in diffusers LoRA format from canonical rows.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} args
|
|
24
|
+
* @param {string} args.packageDir
|
|
25
|
+
* @param {Array<Object>} args.rows — canonical rows; partition field groups them
|
|
26
|
+
* @param {Object} args.profile
|
|
27
|
+
* @param {Object} args.manifest
|
|
28
|
+
* @param {Object} args.config
|
|
29
|
+
* @param {string} args.projectRoot
|
|
30
|
+
* @param {boolean} [args.copy]
|
|
18
31
|
*/
|
|
19
|
-
function
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
// Trigger word for LoRA activation
|
|
23
|
-
if (profile.prompt_strategy === 'trigger-word' && profile.profile_id) {
|
|
24
|
-
parts.push(profile.profile_id.replace(/-/g, '_'));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Subject identity if available
|
|
28
|
-
if (record.identity?.subject_name) {
|
|
29
|
-
parts.push(record.identity.subject_name.replace(/_/g, ' '));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Faction / group
|
|
33
|
-
if (group && group !== 'unknown') {
|
|
34
|
-
parts.push(`${group} faction`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Lane context
|
|
38
|
-
if (lane) parts.push(lane);
|
|
32
|
+
export async function buildPackage({ packageDir, rows, profile, manifest, config, projectRoot, copy }) {
|
|
33
|
+
const partitioned = groupRowsByPartition(rows || []);
|
|
39
34
|
|
|
40
|
-
// Use original prompt if available, otherwise build from ID
|
|
41
|
-
const prompt = record.provenance?.prompt;
|
|
42
|
-
if (prompt) {
|
|
43
|
-
parts.push(prompt.slice(0, 300));
|
|
44
|
-
} else {
|
|
45
|
-
parts.push(record.id.replace(/_/g, ' '));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return parts.join(', ');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Build training package in diffusers LoRA format.
|
|
53
|
-
*/
|
|
54
|
-
export async function buildPackage({ packageDir, partitions, profile, manifest, config, projectRoot, copy }) {
|
|
55
35
|
let imageCount = 0;
|
|
56
36
|
|
|
57
|
-
for (const [partition,
|
|
37
|
+
for (const [partition, partitionRows] of Object.entries(partitioned)) {
|
|
58
38
|
const datasetDir = join(packageDir, 'dataset', partition);
|
|
59
39
|
const metadataDir = join(packageDir, 'metadata');
|
|
60
40
|
await mkdir(datasetDir, { recursive: true });
|
|
@@ -62,19 +42,17 @@ export async function buildPackage({ packageDir, partitions, profile, manifest,
|
|
|
62
42
|
|
|
63
43
|
const metadataRows = [];
|
|
64
44
|
|
|
65
|
-
for (const
|
|
66
|
-
if (!
|
|
45
|
+
for (const row of partitionRows) {
|
|
46
|
+
if (!row.asset_path) continue;
|
|
67
47
|
|
|
68
|
-
const sourcePath = join(projectRoot,
|
|
48
|
+
const sourcePath = join(projectRoot, row.asset_path);
|
|
69
49
|
if (!existsSync(sourcePath)) continue;
|
|
70
50
|
|
|
71
|
-
|
|
72
|
-
// avoid silent collisions when two records share a filename stem.
|
|
73
|
-
const sourceBasename = record.asset_path.split(/[\\/]/).pop();
|
|
51
|
+
const sourceBasename = row.asset_path.split(/[\\/]/).pop();
|
|
74
52
|
const extMatch = sourceBasename.match(/\.[^.]+$/);
|
|
75
53
|
const ext = extMatch ? extMatch[0] : '';
|
|
76
|
-
const filename = `${
|
|
77
|
-
const stem =
|
|
54
|
+
const filename = `${row.entity_id}${ext}`;
|
|
55
|
+
const stem = row.entity_id;
|
|
78
56
|
const destPath = join(datasetDir, filename);
|
|
79
57
|
|
|
80
58
|
try {
|
|
@@ -88,28 +66,30 @@ export async function buildPackage({ packageDir, partitions, profile, manifest,
|
|
|
88
66
|
continue;
|
|
89
67
|
}
|
|
90
68
|
|
|
91
|
-
|
|
92
|
-
const caption = buildCaption(record, lane, group, profile);
|
|
93
|
-
await writeFile(join(datasetDir, `${stem}.txt`), caption + '\n');
|
|
69
|
+
await writeFile(join(datasetDir, `${stem}.txt`), row.caption + '\n');
|
|
94
70
|
|
|
95
|
-
metadataRows.push(
|
|
96
|
-
file: `dataset/${partition}/${filename}`,
|
|
97
|
-
caption_file: `dataset/${partition}/${stem}.txt`,
|
|
98
|
-
record_id: record.id,
|
|
99
|
-
caption,
|
|
100
|
-
lane,
|
|
101
|
-
group,
|
|
102
|
-
subject: record.identity?.subject_name || null,
|
|
103
|
-
pass_ratio: record.canon?.assertion_count > 0
|
|
104
|
-
? +(record.canon.pass_count / record.canon.assertion_count).toFixed(3) : null,
|
|
105
|
-
});
|
|
71
|
+
metadataRows.push(buildMetadataRow(row, partition, filename, stem));
|
|
106
72
|
}
|
|
107
73
|
|
|
108
74
|
await writeFile(
|
|
109
75
|
join(metadataDir, `${partition}.jsonl`),
|
|
110
|
-
metadataRows.map(r => JSON.stringify(r)).join('\n') + '\n'
|
|
76
|
+
metadataRows.map(r => JSON.stringify(r)).join('\n') + (metadataRows.length ? '\n' : '')
|
|
111
77
|
);
|
|
112
78
|
}
|
|
113
79
|
|
|
114
80
|
return { imageCount };
|
|
115
81
|
}
|
|
82
|
+
|
|
83
|
+
function buildMetadataRow(row, partition, filename, stem) {
|
|
84
|
+
const out = {
|
|
85
|
+
file: `dataset/${partition}/${filename}`,
|
|
86
|
+
caption_file: `dataset/${partition}/${stem}.txt`,
|
|
87
|
+
record_id: row.entity_id,
|
|
88
|
+
caption: row.caption,
|
|
89
|
+
lane: row.lane,
|
|
90
|
+
group: row.group ?? null,
|
|
91
|
+
subject: row.subject_filter_key ?? null,
|
|
92
|
+
pass_ratio: row.pass_ratio ?? null,
|
|
93
|
+
};
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
@@ -1,49 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generic image+caption adapter.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Consumes canonical rows (per D6) and emits:
|
|
5
|
+
* dataset/<partition>/<entity_id>.<ext> — image (copied or symlinked)
|
|
6
|
+
* metadata/<partition>.jsonl — per-row audit (caption inline)
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* No caption sidecar files — this adapter keeps metadata flat and
|
|
9
|
+
* machine-readable. Use diffusers-lora or ai-toolkit for trainer-specific
|
|
10
|
+
* caption-file conventions.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { writeFile, mkdir, symlink, copyFile } from 'node:fs/promises';
|
|
12
14
|
import { join, relative } from 'node:path';
|
|
13
15
|
import { existsSync } from 'node:fs';
|
|
16
|
+
import { groupRowsByPartition } from '../rows.js';
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
function buildCaption(record, lane, group, profile) {
|
|
19
|
-
const parts = [];
|
|
20
|
-
|
|
21
|
-
if (profile.prompt_strategy === 'trigger-word' && profile.profile_id) {
|
|
22
|
-
parts.push(profile.profile_id.replace(/-/g, '_'));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (group) parts.push(group);
|
|
26
|
-
if (lane) parts.push(lane);
|
|
27
|
-
|
|
28
|
-
// Add key provenance if available
|
|
29
|
-
const prompt = record.provenance?.prompt;
|
|
30
|
-
if (prompt) {
|
|
31
|
-
// Use first 200 chars of prompt as caption body
|
|
32
|
-
parts.push(prompt.slice(0, 200));
|
|
33
|
-
} else {
|
|
34
|
-
parts.push(record.id.replace(/_/g, ' '));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return parts.join(', ');
|
|
38
|
-
}
|
|
18
|
+
export async function buildPackage({ packageDir, rows, profile, manifest, config, projectRoot, copy }) {
|
|
19
|
+
const partitioned = groupRowsByPartition(rows || []);
|
|
39
20
|
|
|
40
|
-
/**
|
|
41
|
-
* Build training package in generic image+caption format.
|
|
42
|
-
*/
|
|
43
|
-
export async function buildPackage({ packageDir, partitions, profile, manifest, config, projectRoot, copy }) {
|
|
44
21
|
let imageCount = 0;
|
|
45
22
|
|
|
46
|
-
for (const [partition,
|
|
23
|
+
for (const [partition, partitionRows] of Object.entries(partitioned)) {
|
|
47
24
|
const datasetDir = join(packageDir, 'dataset', partition);
|
|
48
25
|
const metadataDir = join(packageDir, 'metadata');
|
|
49
26
|
await mkdir(datasetDir, { recursive: true });
|
|
@@ -51,18 +28,16 @@ export async function buildPackage({ packageDir, partitions, profile, manifest,
|
|
|
51
28
|
|
|
52
29
|
const metadataRows = [];
|
|
53
30
|
|
|
54
|
-
for (const
|
|
55
|
-
if (!
|
|
31
|
+
for (const row of partitionRows) {
|
|
32
|
+
if (!row.asset_path) continue;
|
|
56
33
|
|
|
57
|
-
const sourcePath = join(projectRoot,
|
|
34
|
+
const sourcePath = join(projectRoot, row.asset_path);
|
|
58
35
|
if (!existsSync(sourcePath)) continue;
|
|
59
36
|
|
|
60
|
-
|
|
61
|
-
// avoid silent collisions when two records share a filename stem.
|
|
62
|
-
const sourceBasename = record.asset_path.split(/[\\/]/).pop();
|
|
37
|
+
const sourceBasename = row.asset_path.split(/[\\/]/).pop();
|
|
63
38
|
const extMatch = sourceBasename.match(/\.[^.]+$/);
|
|
64
39
|
const ext = extMatch ? extMatch[0] : '';
|
|
65
|
-
const filename = `${
|
|
40
|
+
const filename = `${row.entity_id}${ext}`;
|
|
66
41
|
const destPath = join(datasetDir, filename);
|
|
67
42
|
|
|
68
43
|
try {
|
|
@@ -73,25 +48,22 @@ export async function buildPackage({ packageDir, partitions, profile, manifest,
|
|
|
73
48
|
}
|
|
74
49
|
imageCount++;
|
|
75
50
|
} catch {
|
|
76
|
-
continue;
|
|
51
|
+
continue;
|
|
77
52
|
}
|
|
78
53
|
|
|
79
|
-
const caption = buildCaption(record, lane, group, profile);
|
|
80
|
-
|
|
81
54
|
metadataRows.push({
|
|
82
55
|
file: `dataset/${partition}/${filename}`,
|
|
83
|
-
record_id:
|
|
84
|
-
caption,
|
|
85
|
-
lane,
|
|
86
|
-
group,
|
|
87
|
-
pass_ratio:
|
|
88
|
-
? +(record.canon.pass_count / record.canon.assertion_count).toFixed(3) : null,
|
|
56
|
+
record_id: row.entity_id,
|
|
57
|
+
caption: row.caption,
|
|
58
|
+
lane: row.lane,
|
|
59
|
+
group: row.group ?? null,
|
|
60
|
+
pass_ratio: row.pass_ratio ?? null,
|
|
89
61
|
});
|
|
90
62
|
}
|
|
91
63
|
|
|
92
64
|
await writeFile(
|
|
93
65
|
join(metadataDir, `${partition}.jsonl`),
|
|
94
|
-
metadataRows.map(r => JSON.stringify(r)).join('\n') + '\n'
|
|
66
|
+
metadataRows.map(r => JSON.stringify(r)).join('\n') + (metadataRows.length ? '\n' : '')
|
|
95
67
|
);
|
|
96
68
|
}
|
|
97
69
|
|
package/lib/batch-runs.js
CHANGED
|
@@ -85,6 +85,57 @@ export async function createBatchDir(projectRoot) {
|
|
|
85
85
|
return { batchId, batchDir };
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Compute which prior slot_ids are eligible to skip on resume.
|
|
90
|
+
* Truth source: status === 'ok'. For pre-status manifests we infer success
|
|
91
|
+
* from a truthy selected_output. Returns a Map<slot_id, prior_result>.
|
|
92
|
+
*/
|
|
93
|
+
export function buildCompletedSlotMap(priorResults) {
|
|
94
|
+
const completed = new Map();
|
|
95
|
+
if (!Array.isArray(priorResults)) return completed;
|
|
96
|
+
for (const r of priorResults) {
|
|
97
|
+
if (!r || !r.slot_id) continue;
|
|
98
|
+
const ok = r.status === 'ok' || (r.status === undefined && !!r.selected_output);
|
|
99
|
+
if (ok) completed.set(r.slot_id, r);
|
|
100
|
+
}
|
|
101
|
+
return completed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resume an existing batch directory. Loads its prior manifest so the caller
|
|
106
|
+
* can replay only the incomplete slots while keeping the same batch_id.
|
|
107
|
+
*
|
|
108
|
+
* Throws BATCH_NOT_FOUND if the batch dir or manifest is missing.
|
|
109
|
+
* Throws BATCH_NO_PROGRESS if the manifest predates the incremental-checkpoint
|
|
110
|
+
* format (no `slots` array) — those batches can't be partially resumed.
|
|
111
|
+
*/
|
|
112
|
+
export function openBatchDirForResume(projectRoot, batchId) {
|
|
113
|
+
const batchesDir = getBatchesDir(projectRoot);
|
|
114
|
+
const batchDir = join(batchesDir, batchId);
|
|
115
|
+
const manifestPath = join(batchDir, 'manifest.json');
|
|
116
|
+
|
|
117
|
+
if (!existsSync(manifestPath)) {
|
|
118
|
+
throw inputError(
|
|
119
|
+
'BATCH_NOT_FOUND',
|
|
120
|
+
`Batch "${batchId}" not found at ${manifestPath}`,
|
|
121
|
+
'Run `sdlab batch list` to see available batch IDs.'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
126
|
+
const priorResults = Array.isArray(manifest.slots) ? manifest.slots : null;
|
|
127
|
+
|
|
128
|
+
if (!priorResults) {
|
|
129
|
+
throw inputError(
|
|
130
|
+
'BATCH_NO_PROGRESS',
|
|
131
|
+
`Batch "${batchId}" has no slot-level progress to resume from`,
|
|
132
|
+
'This batch was created before incremental checkpointing — re-run from scratch instead.'
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { batchId, batchDir, priorResults, priorManifest: manifest };
|
|
137
|
+
}
|
|
138
|
+
|
|
88
139
|
/**
|
|
89
140
|
* Execute all slot briefs as coordinated runs.
|
|
90
141
|
*
|
|
@@ -95,6 +146,7 @@ export async function createBatchDir(projectRoot) {
|
|
|
95
146
|
* @param {Object} opts.mode — batch mode definition
|
|
96
147
|
* @param {Object[]} opts.slotBriefs — one brief per slot
|
|
97
148
|
* @param {boolean} [opts.dryRun]
|
|
149
|
+
* @param {Object[]} [opts.priorResults] — previously-completed slot results to carry forward (resume)
|
|
98
150
|
* @returns {Promise<Object[]>} — array of { slot, brief, manifest }
|
|
99
151
|
*/
|
|
100
152
|
export async function executeBatchRuns({
|
|
@@ -104,6 +156,7 @@ export async function executeBatchRuns({
|
|
|
104
156
|
mode,
|
|
105
157
|
slotBriefs,
|
|
106
158
|
dryRun = false,
|
|
159
|
+
priorResults = [],
|
|
107
160
|
}) {
|
|
108
161
|
// PB-005: fail fast on length mismatch instead of crashing deep in the loop.
|
|
109
162
|
if (!Array.isArray(mode?.variant_plan) || mode.variant_plan.length !== slotBriefs.length) {
|
|
@@ -115,6 +168,8 @@ export async function executeBatchRuns({
|
|
|
115
168
|
);
|
|
116
169
|
}
|
|
117
170
|
|
|
171
|
+
const completed = buildCompletedSlotMap(priorResults);
|
|
172
|
+
|
|
118
173
|
const results = [];
|
|
119
174
|
// PB-005: incremental batch-progress manifest — written after every slot
|
|
120
175
|
// so a crash never orphans the batch dir without a record of what ran.
|
|
@@ -125,6 +180,13 @@ export async function executeBatchRuns({
|
|
|
125
180
|
const brief = slotBriefs[i];
|
|
126
181
|
const slotLabel = slot.label || slot.slot_id;
|
|
127
182
|
|
|
183
|
+
if (completed.has(slot.slot_id)) {
|
|
184
|
+
const prior = completed.get(slot.slot_id);
|
|
185
|
+
info(`[${i + 1}/${slotBriefs.length}] Slot: ${slotLabel} \x1b[2m(resumed — already complete)\x1b[0m`);
|
|
186
|
+
results.push(prior);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
128
190
|
info(`[${i + 1}/${slotBriefs.length}] Slot: ${slotLabel}`);
|
|
129
191
|
|
|
130
192
|
// Save brief into batch/briefs/
|