@mcptoolshop/style-dataset-lab 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.es.md +96 -63
  3. package/README.fr.md +97 -64
  4. package/README.hi.md +108 -77
  5. package/README.it.md +97 -64
  6. package/README.ja.md +105 -72
  7. package/README.md +33 -0
  8. package/README.pt-BR.md +94 -61
  9. package/README.zh.md +98 -65
  10. package/bin/sdlab.js +71 -0
  11. package/lib/adapters/ai-toolkit.js +209 -0
  12. package/lib/adapters/comfyui-runner.js +36 -2
  13. package/lib/adapters/comfyui-workflows.js +133 -2
  14. package/lib/adapters/diffusers-lora.js +46 -66
  15. package/lib/adapters/generic-image-caption.js +22 -50
  16. package/lib/canon-build/build.js +450 -0
  17. package/lib/canon-build/emitters/base.js +122 -0
  18. package/lib/canon-build/emitters/character.js +167 -0
  19. package/lib/canon-build/emitters/deity.js +165 -0
  20. package/lib/canon-build/emitters/faction.js +161 -0
  21. package/lib/canon-build/emitters/index.js +38 -0
  22. package/lib/canon-build/emitters/location.js +153 -0
  23. package/lib/canon-build/emitters/monster.js +131 -0
  24. package/lib/canon-build/emitters/relic.js +142 -0
  25. package/lib/canon-build/emitters/ship.js +154 -0
  26. package/lib/canon-build/emitters/species.js +165 -0
  27. package/lib/canon-build/frontmatter.js +76 -0
  28. package/lib/canon-build/lane-resolve.js +64 -0
  29. package/lib/canon-build/load-config.js +117 -0
  30. package/lib/canon-build/load-entry.js +62 -0
  31. package/lib/canon-build/load-schema.js +46 -0
  32. package/lib/captions.js +197 -0
  33. package/lib/config.js +19 -0
  34. package/lib/critique-engine.js +46 -1
  35. package/lib/critique-render.js +45 -0
  36. package/lib/freeze-events.js +109 -0
  37. package/lib/freeze-gate.js +208 -0
  38. package/lib/freeze-stamp.js +161 -0
  39. package/lib/rows.js +228 -0
  40. package/lib/run-manifest.js +308 -0
  41. package/lib/snapshot.js +7 -1
  42. package/lib/training-adapters.js +5 -0
  43. package/lib/training-packages.js +24 -18
  44. package/lib/training-profiles.js +33 -0
  45. package/lib/verifier.js +67 -0
  46. package/package.json +12 -3
  47. package/scripts/_batch_cut.py +71 -0
  48. package/scripts/_seam_cutout.py +74 -0
  49. package/scripts/canon-build.js +88 -0
  50. package/scripts/canon-drift.js +193 -0
  51. package/scripts/canon-freeze-status.js +85 -0
  52. package/scripts/canon-freeze.js +211 -0
  53. package/scripts/canon-unfreeze.js +137 -0
  54. package/scripts/critique.js +32 -7
  55. package/scripts/curate.js +33 -0
  56. package/scripts/generate-controlnet.js +12 -0
  57. package/scripts/generate-identity.js +20 -4
  58. package/scripts/generate-ipadapter.js +11 -0
  59. package/scripts/generate.js +262 -102
  60. package/scripts/qwen_generate.py +327 -0
  61. package/scripts/run-generate.js +3 -0
@@ -1,60 +1,40 @@
1
1
  /**
2
2
  * Diffusers LoRA adapter.
3
3
  *
4
- * Produces a layout compatible with diffusers fine-tuning scripts:
5
- * - dataset/<partition>/ with image files
6
- * - dataset/<partition>/<image>.txt caption sidecar files
7
- * - metadata/<partition>.jsonl with structured rows
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
- * Does not mutate inclusion or split truth.
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, basename } from 'node:path';
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 a diffusers-style caption from record metadata.
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 buildCaption(record, lane, group, profile) {
20
- const parts = [];
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, records] of Object.entries(partitions)) {
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 { record, lane, group } of records) {
66
- if (!record.asset_path) continue;
45
+ for (const row of partitionRows) {
46
+ if (!row.asset_path) continue;
67
47
 
68
- const sourcePath = join(projectRoot, record.asset_path);
48
+ const sourcePath = join(projectRoot, row.asset_path);
69
49
  if (!existsSync(sourcePath)) continue;
70
50
 
71
- // D-009: Name output files by record.id (not source basename) to
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 = `${record.id}${ext}`;
77
- const stem = record.id;
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
- // Write caption sidecar (.txt next to image)
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
- * Simple, inspectable, durable baseline:
5
- * - dataset/<partition>/ with image files
6
- * - metadata/<partition>.jsonl with one row per image
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
- * Does not mutate inclusion or split truth.
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
- * Build a caption string from a record and profile.
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, records] of Object.entries(partitions)) {
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 { record, lane, group } of records) {
55
- if (!record.asset_path) continue;
31
+ for (const row of partitionRows) {
32
+ if (!row.asset_path) continue;
56
33
 
57
- const sourcePath = join(projectRoot, record.asset_path);
34
+ const sourcePath = join(projectRoot, row.asset_path);
58
35
  if (!existsSync(sourcePath)) continue;
59
36
 
60
- // D-009: Name output files by record.id (not source basename) to
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 = `${record.id}${ext}`;
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; // skip duplicates or broken links
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: record.id,
84
- caption,
85
- lane,
86
- group,
87
- pass_ratio: record.canon?.assertion_count > 0
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