@mcptoolshop/style-dataset-lab 2.1.0 → 2.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/README.zh.md CHANGED
@@ -13,158 +13,187 @@
13
13
 
14
14
  # style-dataset-lab
15
15
 
16
- 一个用于创建视觉数据集的生产流水线,从规范规则到结构化提示,再到 ComfyUI 生成,最终得到经过筛选、符合规范的训练数据。
16
+ 将已批准的视觉素材转化为版本控制、经过审核的数据集、分片、导出包和评估包。
17
17
 
18
18
  ## 这是什么
19
19
 
20
- 一个用于构建结构化视觉训练数据集的**流水线**。 您可以编写风格规则(规范),编写提示,使用 ComfyUI 生成图像,并根据每个维度进行评分进行筛选,将判断结果与规范规则关联,并通过 [`@mcptoolshop/repo-dataset`](https://github.com/mcp-tool-shop-org/repo-dataset) 以 10 种格式导出。
20
+ 一个用于视觉数据规范和数据集生成的**流水线**。明确您的项目外观。根据既定规则进行筛选。生成可重复的数据集包,并确保数据分割的安全性。生成评估数据集,用于未来模型的验证。
21
21
 
22
- 该流水线不依赖于特定的游戏。 每个游戏都有自己的数据目录,位于 `games/<name>/` 目录下;13 个脚本和空白模板是共享的。 每个生成的资源都包含以下三项:
22
+ 流水线会生成四个产出物:
23
23
 
24
- 1. **图像像素**:由 ComfyUI 生成,包含完整的溯源信息(检查点、LoRA、种子、采样器、CFG)。
25
- 2. **规范解释**:解释为什么该图像符合或不符合风格,并基于风格规范进行说明。
26
- 3. **质量判断**:通过批准/拒绝,并根据每个维度的评分引用相关规则。
24
+ | 文物。 | 它是什么。 |
25
+ |----------|-----------|
26
+ | **Snapshot** | 经过筛选,符合条件的记录已冻结,并进行了指纹识别。所有被选入的记录都有明确的纳入理由。 |
27
+ | **Split** | 安全的训练/验证/测试数据集划分,防止数据泄露。共享相同主题的数据记录始终会被划分到同一个子集。 |
28
+ | **Export package** | 自包含的数据集:包含清单、元数据、图像、数据集划分、数据集描述文档以及校验和。 |
29
+ | **Eval pack** | 符合佳能标准的验证任务包括:车道覆盖、禁止漂移、锚点/黄金区域、主体连续性。 |
27
30
 
28
- ## 安全模型
29
-
30
- **仅本地运行。** `style-dataset-lab` 只与本地 `localhost:8188` 上的 ComfyUI 进行通信,不会进行任何外部网络请求。 没有遥测数据,没有分析功能,不会向外部发送数据。 图像生成完全在您的 GPU 上进行。 记录和规范数据都存储在您的文件系统中。
31
-
32
- ## npm 包包含的内容
33
-
34
- `npm install @mcptoolshop/style-dataset-lab` 会为您提供:
31
+ 流水线中的每个资产都包含以下三个要素:
35
32
 
36
- - **13 个脚本**:用于生成、筛选、比较、与规范关联、生成绘画风格图像、生成身份图像、使用 ControlNet/IP-Adapter 生成图像、批量筛选、数据迁移。
37
- - **空白模板**:位于 `templates/` 目录下,包含起始规范、审查标准和示例提示包。
33
+ 1. **来源信息** -- 完整的生成历史记录(检查点、LoRA、种子、采样器、配置参数、生成时间)。
34
+ 2. **合规性** -- 指示该资产是否符合相关规定,以及是否完全符合、不符合或部分符合。
35
+ 3. **质量评估** -- 采用每维度的评分标准,对质量进行评估,结果分为:通过/未通过/临界。
38
36
 
39
- 该 npm 包**不包含**游戏数据。 如果您想使用 Star Freight 示例,请克隆该仓库(包含 1182 条记录,28 个提示集,18 个视觉类别)。
37
+ 主要从事游戏美术、角色设计、生物设计、建筑设计、车辆/机械概念设计等工作,以及任何需要保持视觉风格一致性的领域。
40
38
 
41
- ## 安装
39
+ ## 快速入门
42
40
 
43
41
  ```bash
44
- # Get the pipeline scripts + templates
45
- npm install @mcptoolshop/style-dataset-lab
42
+ # Install the CLI + pipeline
43
+ npm install -g @mcptoolshop/style-dataset-lab
46
44
 
47
- # Or clone the repo for the Star Freight example data
48
- git clone https://github.com/mcp-tool-shop-org/style-dataset-lab
49
- cd style-dataset-lab
50
- npm install
45
+ # Scaffold a new project
46
+ sdlab init my-project --domain character-design
47
+
48
+ # Validate the project structure
49
+ sdlab project doctor --project my-project
51
50
  ```
52
51
 
53
- 要从模板开始一个新的游戏:
52
+ 可用的领域包括:`game-art`(游戏美术)、`character-design`(角色设计)、`creature-design`(生物设计)、`architecture`(架构)、`vehicle-mech`(车辆机械设计)或`generic`(通用)。
53
+
54
+ ## 命令行界面
54
55
 
55
56
  ```bash
56
- # Copy templates into your game directory
57
- mkdir -p games/my-game/{records,comparisons,inputs/prompts,outputs/{candidates,approved,rejected,borderline,painterly},exports}
58
- cp -r templates/canon games/my-game/canon
59
- cp templates/inputs/prompts/example-wave.json games/my-game/inputs/prompts/wave1.json
60
- # Edit the canon and prompts, then generate
57
+ sdlab init <name> [--domain <domain>] # Scaffold a new project
58
+ sdlab project doctor [--project <name>] # Validate project config
59
+
60
+ sdlab generate <pack> [--project <name>] # Generate candidates via ComfyUI
61
+ sdlab generate:identity <packet> # Named-subject identity images
62
+ sdlab generate:controlnet # ControlNet-guided generation
63
+ sdlab generate:ipadapter # IP-Adapter reference-guided
64
+
65
+ sdlab curate <id> <status> <explanation> # Record review judgment
66
+ sdlab compare <a> <b> <winner> <reason> # Pairwise A-vs-B comparison
67
+ sdlab bind [--project <name>] # Bind records to constitution rules
68
+ sdlab painterly [--project <name>] # Post-processing style pass
69
+
70
+ sdlab snapshot create [--profile <name>] # Create frozen dataset snapshot
71
+ sdlab snapshot list # List all snapshots
72
+ sdlab snapshot diff <a> <b> # Compare two snapshots
73
+ sdlab eligibility audit # Audit record training eligibility
74
+ sdlab split build [--snapshot <id>] # Build train/val/test split
75
+ sdlab split audit <id> # Audit split for leakage + balance
76
+ sdlab card generate # Generate dataset card (md + JSON)
77
+ sdlab export build [--snapshot <id>] # Build versioned export package
78
+ sdlab eval-pack build # Build canon-aware eval pack
61
79
  ```
62
80
 
63
- ## 单仓库结构
81
+ 所有命令都支持 `--project <名称>` 参数,默认值为 `star-freight`。
82
+
83
+ ## 项目模型
64
84
 
65
- 流水线位于 `scripts/` 和 `templates/` 目录下。 每个游戏位于 `games/<name>/` 目录下,包含自己的规范、记录和资源。 脚本接受 `--game <name>` 参数(默认为 `star-freight`)。
85
+ 每个项目都位于 `projects/` 目录下,作为一个独立的目录,拥有自己的规范、配置文件和数据。
66
86
 
67
87
  ```
68
- style-dataset-lab/
69
- scripts/ 13 pipeline scripts (generate, curate, compare, etc.)
70
- templates/ Blank starting point for new games
71
- canon/ Starter constitution + review rubric
72
- inputs/prompts/ Example prompt pack
73
- games/
74
- star-freight/ Star Freight example (1,182 records, repo-only)
75
- canon/ Style constitution, review rubric, species canon
76
- records/ Per-asset JSON (provenance + judgment + canon)
77
- comparisons/ A-vs-B preference judgments
78
- inputs/ Prompt packs, identity packets, references
79
- outputs/ Generated images (gitignored)
80
- exports/ repo-dataset output (gitignored)
81
- <your-game>/ Add more games with the same structure
88
+ projects/
89
+ my-project/
90
+ project.json Project identity + generation defaults
91
+ constitution.json Rules array with rationale templates
92
+ lanes.json Subject lanes with detection patterns
93
+ rubric.json Scoring dimensions + thresholds
94
+ terminology.json Group vocabulary + detection order
95
+ canon/ Style constitution (markdown)
96
+ records/ Per-asset JSON (provenance + judgment + canon)
97
+ inputs/prompts/ Prompt packs (JSON)
98
+ outputs/ Generated images (gitignored)
99
+ comparisons/ A-vs-B preference judgments
100
+ snapshots/ Frozen dataset snapshots
101
+ splits/ Train/val/test partitions
102
+ exports/ Versioned export packages
103
+ eval-packs/ Canon-aware eval instruments
82
104
  ```
83
105
 
84
- ## 流水线工作流程
106
+ ## 流水线
85
107
 
86
- 从规范到训练数据导出的完整流水线:
87
-
88
- ```bash
89
- # 1. Write your canon -- style constitution + review rubric
90
- # (start from templates/ or write from scratch)
108
+ ```
109
+ canon → generate → curate → bind → snapshot → split → export → eval
110
+ | | | | | | | |
111
+ rules ComfyUI judgment rules frozen subject package verify
112
+ selection isolation
113
+ ```
91
114
 
92
- # 2. Create prompt packs in inputs/prompts/
93
- # (see templates/inputs/prompts/example-wave.json)
115
+ 1. **定义规范**:编写您的风格指南和评估标准。
116
+ 2. **生成**:ComfyUI 生成具有完整溯源信息的候选结果。
117
+ 3. **筛选**:根据每个维度的评分和失败模式,进行批准/拒绝。
118
+ 4. **关联**:将每个资源与规范规则关联,并给出通过/失败/部分通过的判定。
119
+ 5. **快照**:将符合条件的记录冻结为具有确定性和唯一标识的选择。
120
+ 6. **划分**:划分为训练集/验证集/测试集,同时进行主题隔离和类别平衡。
121
+ 7. **导出**:构建一个独立的软件包,包含清单、元数据、图像和校验和。
122
+ 8. **评估**:生成符合规范要求的测试工具,用于模型验证。
94
123
 
95
- # 3. Start ComfyUI and generate candidates
96
- npm run generate -- --game star-freight inputs/prompts/wave1.json
97
- npm run generate -- --game star-freight inputs/prompts/wave1.json --dry-run
124
+ 下游格式转换(例如:TRL、LLaVA、Parquet等)由[`repo-dataset`](https://github.com/mcp-tool-shop-org/repo-dataset) 负责。`sdlab` 拥有数据集的原始数据,而 `repo-dataset` 将其转换为各种特定格式。
98
125
 
99
- # 4. Generate identity-packet characters (named subjects)
100
- npm run generate:identity -- --game star-freight inputs/identity-packets/wave27a.json
126
+ ## 域名模板
101
127
 
102
- # 5. Curate -- approve, reject, or mark borderline
103
- npm run curate -- --game star-freight <asset_id> approved "explanation"
104
- npm run curate -- --game star-freight <asset_id> rejected "explanation" --failures "too_clean"
128
+ 每个领域模板都包含针对特定生产环境设计的车道定义、规则、评分标准和术语结构。
105
129
 
106
- # 6. Generate painterly variants of approved assets
107
- npm run painterly -- --game star-freight
130
+ | 域名。 | 车道。 | 主要关注点。 |
131
+ |--------|-------|-------------|
132
+ | **game-art** | 角色、环境、道具、用户界面、飞船、内部、设备。 | 游戏中的角色轮廓、阵营区分、以及磨损/老化效果。 |
133
+ | **character-design** | 人像、全身照、360度展示图、表情图、动作姿势图。 | 比例准确性、服装的合理性、人物性格的刻画、动作的清晰度。 |
134
+ | **creature-design** | 概念、正字法、详细研究、动作、比例参考、栖息地。 | 解剖学上的合理性、进化逻辑、轮廓的独特性。 |
135
+ | **architecture** | 外观、内部、街道景观、结构细节、废墟、景观。 | 结构合理性、材料一致性、视角、时代背景的协调性。 |
136
+ | **vehicle-mech** | 外观、驾驶舱、部件、示意图、轮廓图、损坏类型。 | 机械逻辑,功能性设计语言,访问点,损坏描述。 |
108
137
 
109
- # 7. Bind canon explanations to curated assets
110
- npm run canon-bind -- --game star-freight
138
+ ## 数据集生成
111
139
 
112
- # 8. Record pairwise comparisons
113
- npm run compare -- --game star-freight <asset_a> <asset_b> a "A has better faction read"
140
+ 完整的数据集流程:快照、分割、导出、评估。
114
141
 
115
- # 9. Export training data via repo-dataset
116
- repo-dataset visual generate ./games/star-freight --format trl
117
- repo-dataset visual inspect ./games/star-freight
142
+ ```
143
+ snapshot --> split --> export --> eval-pack
144
+ | | | |
145
+ frozen subject package canon-aware
146
+ selection isolation (manifest, test instruments
147
+ + lane metadata, (4 task types)
148
+ balance images,
149
+ checksums,
150
+ card)
118
151
  ```
119
152
 
120
- ## 添加新的游戏
153
+ **快照**:冻结符合条件的记录的确定性选择。每个包含记录都有其原因的追踪。配置指纹确保可重复性。
154
+
155
+ **分割**:将记录分配到训练集/验证集/测试集,同时保证样本隔离(同一个样本家族不会出现在多个分割中),并实现通道平衡的分布。使用种子伪随机数生成器,确保从相同的种子生成相同的结果。
156
+
157
+ **导出包**:是自包含的:清单、metadata.jsonl、图像(符号链接或复制)、分割、数据集卡(Markdown + JSON)以及 BSD 格式的校验和。 包含重建数据集所需的所有内容。
158
+
159
+ **评估包**:是考虑规范的测试工具,包含四种任务类型:通道覆盖、禁止漂移、锚定/黄金标准、以及样本连续性。 它们证明数据集流程正在为未来的模型评估提供支持,而不仅仅是导出文件。
160
+
161
+ 通过 [`repo-dataset`](https://github.com/mcp-tool-shop-org/repo-dataset) 导出到下游格式(TRL, LLaVA, Qwen2-VL, JSONL, Parquet 等)。 `repo-dataset` 负责格式转换;`sdlab` 负责数据集的真实性。
162
+
163
+ ## Star Freight 示例
164
+
165
+ 克隆仓库以获取完整的示例:1182 条记录,28 个提示序列,5 个派系,7 个通道,24 条宪法规则,以及来自一个硬核科幻角色扮演游戏的 892 个已批准的资源。
121
166
 
122
167
  ```bash
123
- # Create structure and copy blank templates
124
- mkdir -p games/my-game/{records,comparisons,inputs/prompts,outputs/{candidates,approved,rejected,borderline,painterly},exports}
125
- cp -r templates/canon games/my-game/canon
126
- cp templates/inputs/prompts/example-wave.json games/my-game/inputs/prompts/wave1.json
168
+ git clone https://github.com/mcp-tool-shop-org/style-dataset-lab
169
+ cd style-dataset-lab
170
+
171
+ # Validate the project
172
+ sdlab project doctor --project star-freight
127
173
 
128
- # Edit your canon/constitution.md and canon/review-rubric.md
129
- # Edit your prompt pack, then run the pipeline with --game my-game
174
+ # Run the full dataset spine
175
+ sdlab snapshot create --project star-freight # 839 eligible records
176
+ sdlab split build --project star-freight # ~80/10/10, zero leakage
177
+ sdlab export build --project star-freight # package with checksums
178
+ sdlab eval-pack build --project star-freight # 78 eval records
130
179
  ```
131
180
 
132
- ## 每个游戏目录的布局
181
+ ## 从 v1.x 迁移
133
182
 
134
- 每个 `games/<name>/` 目录包含:
183
+ v2.0 将 `games/` 重命名为 `projects/`,并将 `--game` 重命名为 `--project`。
135
184
 
136
- ```
137
- canon/ Style constitution, review rubric, species canon, identity gates
138
- inputs/
139
- prompts/ Prompt packs per wave (JSON: subjects, variations, defaults)
140
- references/ IP-Adapter reference images
141
- control-guides/ ControlNet guide overlays
142
- identity-packets/ Named character identity spines
143
- outputs/
144
- candidates/ Raw generations (gitignored)
145
- approved/ Curated approved (gitignored)
146
- rejected/ Curated rejected (gitignored)
147
- borderline/ Curated borderline (gitignored)
148
- painterly/ Painterly-style variants (gitignored)
149
- records/ Per-asset JSON (provenance + judgment + canon binding)
150
- comparisons/ A-vs-B preference judgments
151
- exports/ repo-dataset output (gitignored)
152
- ```
185
+ ```bash
186
+ # Rename your data directory
187
+ mv games projects
153
188
 
154
- ## 生成设置
155
-
156
- ```yaml
157
- checkpoint: dreamshaperXL_v21TurboDPMSDE.safetensors
158
- lora: classipeintxl_v21.safetensors (weight: 1.0)
159
- resolution: 1024x1024
160
- steps: 8
161
- cfg: 2.0
162
- sampler: dpmpp_sde
163
- scheduler: karras
164
- speed: ~9s per image (RTX 5080)
189
+ # --game still works with a deprecation warning (removed in v3.0)
190
+ sdlab bind --game star-freight # works, prints warning
191
+ sdlab bind --project star-freight # canonical form
165
192
  ```
166
193
 
167
- 其他生成模式:ControlNet(姿势/深度引导)、IP-Adapter(参考驱动)和身份包(用于保持角色一致性)。
194
+ ## 安全模型
195
+
196
+ **仅本地。** 与运行在 `localhost:8188` 上的 ComfyUI 通信。 没有遥测,没有分析,没有外部请求。 图像保留在您的 GPU 和文件系统中。
168
197
 
169
198
  ## 要求
170
199
 
package/bin/sdlab.js CHANGED
@@ -26,6 +26,12 @@ const COMMANDS = {
26
26
  'card': '../scripts/card.js',
27
27
  'export': '../scripts/export.js',
28
28
  'eval-pack': '../scripts/eval-pack.js',
29
+ 'training-profile': '../scripts/training-profile.js',
30
+ 'training-manifest': '../scripts/training-manifest.js',
31
+ 'training-package': '../scripts/training-package.js',
32
+ 'eval-run': '../scripts/eval-run.js',
33
+ 'implementation-pack': '../scripts/implementation-pack.js',
34
+ 'reingest': '../scripts/reingest.js',
29
35
  };
30
36
 
31
37
  // Two-word commands under "project" namespace
@@ -70,6 +76,26 @@ function printHelp() {
70
76
  console.log(' eval-pack list List eval packs');
71
77
  console.log(' eval-pack show <id> Show eval pack details');
72
78
  console.log('');
79
+ console.log('Training:');
80
+ console.log(' training-profile list List training profiles');
81
+ console.log(' training-profile show <id> Show training profile');
82
+ console.log(' training-manifest create Create training manifest');
83
+ console.log(' training-manifest validate <id> Validate manifest integrity');
84
+ console.log(' training-manifest show <id> Show manifest details');
85
+ console.log(' training-manifest list List all manifests');
86
+ console.log(' training-package build Build trainer-ready package');
87
+ console.log(' training-package show <id> Show package details');
88
+ console.log(' training-package list List all packages');
89
+ console.log(' eval-run create Create eval run');
90
+ console.log(' eval-run score <id> --outputs <p> Score against eval pack');
91
+ console.log(' eval-run show <id> Show eval run details');
92
+ console.log(' eval-run list List all eval runs');
93
+ console.log(' implementation-pack build Build implementation example pack');
94
+ console.log(' implementation-pack show <id> Show pack details');
95
+ console.log(' implementation-pack list List all packs');
96
+ console.log(' reingest generated --source <dir> Re-ingest generated outputs');
97
+ console.log(' reingest audit Audit re-ingested record status');
98
+ console.log('');
73
99
  console.log('Options:');
74
100
  console.log(' --project <name> Project to operate on (default: star-freight)');
75
101
  console.log(' --game <name> Deprecated alias for --project');
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Diffusers LoRA adapter.
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
8
+ *
9
+ * Does not mutate inclusion or split truth.
10
+ */
11
+
12
+ import { writeFile, mkdir, symlink, copyFile } from 'node:fs/promises';
13
+ import { join, relative, basename } from 'node:path';
14
+ import { existsSync } from 'node:fs';
15
+
16
+ /**
17
+ * Build a diffusers-style caption from record metadata.
18
+ */
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);
39
+
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
+ let imageCount = 0;
56
+
57
+ for (const [partition, records] of Object.entries(partitions)) {
58
+ const datasetDir = join(packageDir, 'dataset', partition);
59
+ const metadataDir = join(packageDir, 'metadata');
60
+ await mkdir(datasetDir, { recursive: true });
61
+ await mkdir(metadataDir, { recursive: true });
62
+
63
+ const metadataRows = [];
64
+
65
+ for (const { record, lane, group } of records) {
66
+ if (!record.asset_path) continue;
67
+
68
+ const sourcePath = join(projectRoot, record.asset_path);
69
+ if (!existsSync(sourcePath)) continue;
70
+
71
+ const filename = record.asset_path.split('/').pop();
72
+ const stem = filename.replace(/\.[^.]+$/, '');
73
+ const destPath = join(datasetDir, filename);
74
+
75
+ try {
76
+ if (copy) {
77
+ await copyFile(sourcePath, destPath);
78
+ } else {
79
+ await symlink(relative(datasetDir, sourcePath), destPath);
80
+ }
81
+ imageCount++;
82
+ } catch {
83
+ continue;
84
+ }
85
+
86
+ // Write caption sidecar (.txt next to image)
87
+ const caption = buildCaption(record, lane, group, profile);
88
+ await writeFile(join(datasetDir, `${stem}.txt`), caption + '\n');
89
+
90
+ metadataRows.push({
91
+ file: `dataset/${partition}/${filename}`,
92
+ caption_file: `dataset/${partition}/${stem}.txt`,
93
+ record_id: record.id,
94
+ caption,
95
+ lane,
96
+ group,
97
+ subject: record.identity?.subject_name || null,
98
+ pass_ratio: record.canon?.assertion_count > 0
99
+ ? +(record.canon.pass_count / record.canon.assertion_count).toFixed(3) : null,
100
+ });
101
+ }
102
+
103
+ await writeFile(
104
+ join(metadataDir, `${partition}.jsonl`),
105
+ metadataRows.map(r => JSON.stringify(r)).join('\n') + '\n'
106
+ );
107
+ }
108
+
109
+ return { imageCount };
110
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Generic image+caption adapter.
3
+ *
4
+ * Simple, inspectable, durable baseline:
5
+ * - dataset/<partition>/ with image files
6
+ * - metadata/<partition>.jsonl with one row per image
7
+ *
8
+ * Does not mutate inclusion or split truth.
9
+ */
10
+
11
+ import { writeFile, mkdir, symlink, copyFile } from 'node:fs/promises';
12
+ import { join, relative } from 'node:path';
13
+ import { existsSync } from 'node:fs';
14
+
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
+ }
39
+
40
+ /**
41
+ * Build training package in generic image+caption format.
42
+ */
43
+ export async function buildPackage({ packageDir, partitions, profile, manifest, config, projectRoot, copy }) {
44
+ let imageCount = 0;
45
+
46
+ for (const [partition, records] of Object.entries(partitions)) {
47
+ const datasetDir = join(packageDir, 'dataset', partition);
48
+ const metadataDir = join(packageDir, 'metadata');
49
+ await mkdir(datasetDir, { recursive: true });
50
+ await mkdir(metadataDir, { recursive: true });
51
+
52
+ const metadataRows = [];
53
+
54
+ for (const { record, lane, group } of records) {
55
+ if (!record.asset_path) continue;
56
+
57
+ const sourcePath = join(projectRoot, record.asset_path);
58
+ if (!existsSync(sourcePath)) continue;
59
+
60
+ const filename = record.asset_path.split('/').pop();
61
+ const destPath = join(datasetDir, filename);
62
+
63
+ try {
64
+ if (copy) {
65
+ await copyFile(sourcePath, destPath);
66
+ } else {
67
+ await symlink(relative(datasetDir, sourcePath), destPath);
68
+ }
69
+ imageCount++;
70
+ } catch {
71
+ continue; // skip duplicates or broken links
72
+ }
73
+
74
+ const caption = buildCaption(record, lane, group, profile);
75
+
76
+ metadataRows.push({
77
+ file: `dataset/${partition}/${filename}`,
78
+ record_id: record.id,
79
+ caption,
80
+ lane,
81
+ group,
82
+ pass_ratio: record.canon?.assertion_count > 0
83
+ ? +(record.canon.pass_count / record.canon.assertion_count).toFixed(3) : null,
84
+ });
85
+ }
86
+
87
+ await writeFile(
88
+ join(metadataDir, `${partition}.jsonl`),
89
+ metadataRows.map(r => JSON.stringify(r)).join('\n') + '\n'
90
+ );
91
+ }
92
+
93
+ return { imageCount };
94
+ }