@mcptoolshop/style-dataset-lab 1.2.0 → 2.0.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 (57) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.es.md +94 -54
  3. package/README.fr.md +86 -46
  4. package/README.hi.md +86 -46
  5. package/README.it.md +93 -53
  6. package/README.ja.md +86 -46
  7. package/README.md +95 -116
  8. package/README.pt-BR.md +86 -46
  9. package/README.zh.md +90 -50
  10. package/bin/sdlab.js +103 -0
  11. package/lib/args.js +106 -0
  12. package/lib/comfyui.js +109 -0
  13. package/lib/config.js +104 -0
  14. package/lib/deprecation.js +14 -0
  15. package/lib/paths.js +38 -0
  16. package/lib/records.js +69 -0
  17. package/package.json +13 -4
  18. package/scripts/bulk-curate-wave2-5.js +1 -1
  19. package/scripts/bulk-curate-waves11-18.js +1 -1
  20. package/scripts/canon-bind.js +268 -332
  21. package/scripts/compare.js +40 -30
  22. package/scripts/curate-wave25.js +1 -1
  23. package/scripts/curate.js +34 -28
  24. package/scripts/doctor.js +321 -0
  25. package/scripts/generate-controlnet.js +36 -121
  26. package/scripts/generate-identity.js +38 -106
  27. package/scripts/generate-ipadapter.js +42 -119
  28. package/scripts/generate.js +22 -81
  29. package/scripts/init.js +226 -0
  30. package/scripts/migrate-records.js +15 -21
  31. package/scripts/painterly-test.js +20 -47
  32. package/scripts/painterly.js +27 -106
  33. package/templates/domains/architecture/constitution.json +97 -0
  34. package/templates/domains/architecture/lanes.json +41 -0
  35. package/templates/domains/architecture/project.json +20 -0
  36. package/templates/domains/architecture/rubric.json +40 -0
  37. package/templates/domains/architecture/terminology.json +19 -0
  38. package/templates/domains/character-design/constitution.json +88 -0
  39. package/templates/domains/character-design/lanes.json +35 -0
  40. package/templates/domains/character-design/project.json +17 -0
  41. package/templates/domains/character-design/rubric.json +38 -0
  42. package/templates/domains/character-design/terminology.json +21 -0
  43. package/templates/domains/creature-design/constitution.json +87 -0
  44. package/templates/domains/creature-design/lanes.json +41 -0
  45. package/templates/domains/creature-design/project.json +18 -0
  46. package/templates/domains/creature-design/rubric.json +39 -0
  47. package/templates/domains/creature-design/terminology.json +20 -0
  48. package/templates/domains/game-art/constitution.json +113 -0
  49. package/templates/domains/game-art/lanes.json +47 -0
  50. package/templates/domains/game-art/project.json +17 -0
  51. package/templates/domains/game-art/rubric.json +36 -0
  52. package/templates/domains/game-art/terminology.json +30 -0
  53. package/templates/domains/vehicle-mech/constitution.json +87 -0
  54. package/templates/domains/vehicle-mech/lanes.json +41 -0
  55. package/templates/domains/vehicle-mech/project.json +17 -0
  56. package/templates/domains/vehicle-mech/rubric.json +40 -0
  57. package/templates/domains/vehicle-mech/terminology.json +21 -0
package/README.zh.md CHANGED
@@ -13,91 +13,133 @@
13
13
 
14
14
  # style-dataset-lab
15
15
 
16
- 视觉数据集工厂 -- 用于生成、整理和导出多模态训练数据,以进行大型视觉语言模型(VLM)的微调。
16
+ 一个用于创建视觉数据集的生产流水线,从规范规则到结构化提示,再到 ComfyUI 生成,最终得到经过筛选、符合规范的训练数据。
17
17
 
18
18
  ## 这是什么
19
19
 
20
- 一个用于**构建可训练的视觉真理**的工具集。每个资源都包含以下三方面:
20
+ 一个用于构建结构化视觉训练数据集的**流水线**。 您可以编写风格规则(规范),编写提示,使用 ComfyUI 生成图像,并根据每个维度进行评分进行筛选,将判断结果与规范规则关联,并通过 [`@mcptoolshop/repo-dataset`](https://github.com/mcp-tool-shop-org/repo-dataset) 以 10 种格式导出。
21
21
 
22
- 1. **图像像素** -- 由 ComfyUI 生成,并包含完整的溯源信息(检查点、LoRA、种子、采样器、CFG)。
23
- 2. **规范解释** -- 解释为什么该图像符合或不符合特定风格,并基于风格规范进行判断。
24
- 3. **质量评估** -- 经过批准/拒绝,并提供每个维度的评分以及引用规则。
22
+ 该流水线不依赖于特定的游戏。 每个游戏都有自己的数据目录,位于 `games/<name>/` 目录下;13 个脚本和空白模板是共享的。 每个生成的资源都包含以下三项:
25
23
 
26
- 输出结果通过 [`@mcptoolshop/repo-dataset`](https://github.com/mcp-tool-shop-org/repo-dataset) 生成多模态训练数据,提供 10 种格式:TRL、LLaVA、Qwen2-VL、Axolotl、LLaMA-Factory、ShareGPT、RLHF 对、DPO、图像描述和分类。
24
+ 1. **图像像素**:由 ComfyUI 生成,包含完整的溯源信息(检查点、LoRA、种子、采样器、CFG)。
25
+ 2. **规范解释**:解释为什么该图像符合或不符合风格,并基于风格规范进行说明。
26
+ 3. **质量判断**:通过批准/拒绝,并根据每个维度的评分引用相关规则。
27
27
 
28
28
  ## 安全模型
29
29
 
30
- **仅本地运行。** style-dataset-lab 只与本地 `localhost:8188` 上的 ComfyUI 进行通信,不会进行任何外部网络请求。没有遥测数据,没有分析功能,不会向外部发送数据。图像生成完全在您的 GPU 上进行。记录和规范数据都存储在您的文件系统中。
30
+ **仅本地运行。** `style-dataset-lab` 只与本地 `localhost:8188` 上的 ComfyUI 进行通信,不会进行任何外部网络请求。 没有遥测数据,没有分析功能,不会向外部发送数据。 图像生成完全在您的 GPU 上进行。 记录和规范数据都存储在您的文件系统中。
31
31
 
32
- ## 数据集统计信息
32
+ ## npm 包包含的内容
33
33
 
34
- | 指标 | 数值 |
35
- |--------|-------|
36
- | 整理记录 | 1,182 |
37
- | 总资源数 | 2,571 (其中 893 个已批准,887 个为绘画风格变体) |
38
- | 提示词组合 | 28 |
39
- | 视觉类别 | 18 种 (包括:服装、船只、室内、设备、环境、物种、站点、标识、照明、货物、**架构**、生物、表面、日常生活、行星、损坏/修复、外星生物、细节)。 |
40
- | 成对比较 | 6 个人工标注 + 71 个合成数据 |
41
- | 不同类型的拒绝 | 30+ |
42
- | 身份包系统 | 具有派系 DNA、等级和视觉特征的命名角色。 |
43
- | 导出格式 | 10 种 (通过 `@mcptoolshop/repo-dataset`) |
34
+ `npm install @mcptoolshop/style-dataset-lab` 会为您提供:
35
+
36
+ - **13 个脚本**:用于生成、筛选、比较、与规范关联、生成绘画风格图像、生成身份图像、使用 ControlNet/IP-Adapter 生成图像、批量筛选、数据迁移。
37
+ - **空白模板**:位于 `templates/` 目录下,包含起始规范、审查标准和示例提示包。
38
+
39
+ 该 npm 包**不包含**游戏数据。 如果您想使用 Star Freight 示例,请克隆该仓库(包含 1182 条记录,28 个提示集,18 个视觉类别)。
44
40
 
45
41
  ## 安装
46
42
 
47
43
  ```bash
48
- npm install -g @mcptoolshop/style-dataset-lab
44
+ # Get the pipeline scripts + templates
45
+ npm install @mcptoolshop/style-dataset-lab
46
+
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
49
51
  ```
50
52
 
51
- 然后克隆一个项目或初始化一个新的数据集工作区:
53
+ 要从模板开始一个新的游戏:
52
54
 
53
55
  ```bash
54
- git clone https://github.com/mcp-tool-shop-org/style-dataset-lab my-dataset
55
- cd my-dataset
56
- npm install
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
61
+ ```
62
+
63
+ ## 单仓库结构
64
+
65
+ 流水线位于 `scripts/` 和 `templates/` 目录下。 每个游戏位于 `games/<name>/` 目录下,包含自己的规范、记录和资源。 脚本接受 `--game <name>` 参数(默认为 `star-freight`)。
66
+
57
67
  ```
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
82
+ ```
83
+
84
+ ## 流水线工作流程
58
85
 
59
- ## 工作流程
86
+ 从规范到训练数据导出的完整流水线:
60
87
 
61
88
  ```bash
62
- # 1. Start ComfyUI
63
- # (point it at your checkpoint + LoRA setup)
89
+ # 1. Write your canon -- style constitution + review rubric
90
+ # (start from templates/ or write from scratch)
64
91
 
65
- # 2. Generate candidates from a prompt pack
66
- npm run generate -- inputs/prompts/wave1.json
67
- npm run generate -- inputs/prompts/wave1.json --dry-run
92
+ # 2. Create prompt packs in inputs/prompts/
93
+ # (see templates/inputs/prompts/example-wave.json)
68
94
 
69
- # 3. Generate identity-packet characters
70
- npm run generate:identity -- inputs/identity-packets/wave27a-identity-spine.json
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
71
98
 
72
- # 4. Generate painterly variants of approved assets
73
- npm run painterly -- <asset_id>
99
+ # 4. Generate identity-packet characters (named subjects)
100
+ npm run generate:identity -- --game star-freight inputs/identity-packets/wave27a.json
74
101
 
75
102
  # 5. Curate -- approve, reject, or mark borderline
76
- npm run curate -- <asset_id> approved "explanation" --scores "silhouette:0.9,palette:0.8"
77
- npm run curate -- <asset_id> rejected "explanation" --failures "too_clean,wrong_material"
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"
105
+
106
+ # 6. Generate painterly variants of approved assets
107
+ npm run painterly -- --game star-freight
78
108
 
79
- # 6. Bind canon explanations to assets
80
- npm run canon-bind -- <asset_id>
109
+ # 7. Bind canon explanations to curated assets
110
+ npm run canon-bind -- --game star-freight
81
111
 
82
- # 7. Record pairwise comparisons
83
- npm run compare -- <asset_a> <asset_b> a "A has better faction read because..."
112
+ # 8. Record pairwise comparisons
113
+ npm run compare -- --game star-freight <asset_a> <asset_b> a "A has better faction read"
84
114
 
85
- # 8. Export training data via repo-dataset
86
- npm run export
87
- npm run inspect
88
- npm run validate
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
89
118
  ```
90
119
 
91
- ## 目录结构
120
+ ## 添加新的游戏
121
+
122
+ ```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
127
+
128
+ # Edit your canon/constitution.md and canon/review-rubric.md
129
+ # Edit your prompt pack, then run the pipeline with --game my-game
130
+ ```
131
+
132
+ ## 每个游戏目录的布局
133
+
134
+ 每个 `games/<name>/` 目录包含:
92
135
 
93
136
  ```
94
- canon/ Style constitution, review rubric, identity gates, species canon
137
+ canon/ Style constitution, review rubric, species canon, identity gates
95
138
  inputs/
96
139
  prompts/ Prompt packs per wave (JSON: subjects, variations, defaults)
97
140
  references/ IP-Adapter reference images
98
- control/ ControlNet control images
99
141
  control-guides/ ControlNet guide overlays
100
- identity-packets/ Named character identity spines (faction DNA, rank, visual anchors)
142
+ identity-packets/ Named character identity spines
101
143
  outputs/
102
144
  candidates/ Raw generations (gitignored)
103
145
  approved/ Curated approved (gitignored)
@@ -107,8 +149,6 @@ outputs/
107
149
  records/ Per-asset JSON (provenance + judgment + canon binding)
108
150
  comparisons/ A-vs-B preference judgments
109
151
  exports/ repo-dataset output (gitignored)
110
- scripts/ generate, curate, compare, canon-bind, painterly, identity gen
111
- workflows/ Reusable ComfyUI workflow templates
112
152
  ```
113
153
 
114
154
  ## 生成设置
@@ -124,7 +164,7 @@ scheduler: karras
124
164
  speed: ~9s per image (RTX 5080)
125
165
  ```
126
166
 
127
- 其他生成模式:ControlNet(姿势/深度引导)、IP-Adapter(参考驱动)以及身份包(命名角色一致性)。
167
+ 其他生成模式:ControlNet(姿势/深度引导)、IP-Adapter(参考驱动)和身份包(用于保持角色一致性)。
128
168
 
129
169
  ## 要求
130
170
 
package/bin/sdlab.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * sdlab — CLI entry point for Style Dataset Lab.
5
+ *
6
+ * Thin dispatcher that routes subcommands to existing scripts.
7
+ * Each script exports run(argv) and is lazy-loaded on demand.
8
+ */
9
+
10
+ const COMMANDS = {
11
+ 'init': '../scripts/init.js',
12
+ 'generate': '../scripts/generate.js',
13
+ 'generate:identity': '../scripts/generate-identity.js',
14
+ 'generate:controlnet': '../scripts/generate-controlnet.js',
15
+ 'generate:ipadapter': '../scripts/generate-ipadapter.js',
16
+ 'curate': '../scripts/curate.js',
17
+ 'compare': '../scripts/compare.js',
18
+ 'bind': '../scripts/canon-bind.js',
19
+ 'canon-bind': '../scripts/canon-bind.js',
20
+ 'painterly': '../scripts/painterly.js',
21
+ 'painterly:test': '../scripts/painterly-test.js',
22
+ 'migrate': '../scripts/migrate-records.js',
23
+ };
24
+
25
+ // Two-word commands under "project" namespace
26
+ const PROJECT_COMMANDS = {
27
+ 'doctor': '../scripts/doctor.js',
28
+ 'migrate': '../scripts/migrate-records.js',
29
+ };
30
+
31
+ function printHelp() {
32
+ console.log(`\x1b[1msdlab\x1b[0m — Style Dataset Lab CLI\n`);
33
+ console.log('Usage: sdlab <command> [options]\n');
34
+ console.log('Project:');
35
+ console.log(' init <name> Scaffold a new project from a domain template');
36
+ console.log(' project doctor Validate project config and structure');
37
+ console.log(' project migrate Migrate record schemas');
38
+ console.log('');
39
+ console.log('Pipeline:');
40
+ console.log(' generate Generate candidates from a prompt pack');
41
+ console.log(' generate:identity Generate named-subject identity images');
42
+ console.log(' generate:controlnet ControlNet-guided structural generation');
43
+ console.log(' generate:ipadapter IP-Adapter reference-guided generation');
44
+ console.log(' curate Move candidate to approved/rejected/borderline');
45
+ console.log(' compare Record pairwise A-vs-B comparison');
46
+ console.log(' bind Bind approved records to constitution rules');
47
+ console.log(' painterly Post-processing painterly style pass');
48
+ console.log(' painterly:test Test denoise levels on reference images');
49
+ console.log('');
50
+ console.log('Options:');
51
+ console.log(' --project <name> Project to operate on (default: star-freight)');
52
+ console.log(' --game <name> Deprecated alias for --project');
53
+ console.log(' --help Show this help');
54
+ console.log('');
55
+ console.log('Examples:');
56
+ console.log(' sdlab init my-project --domain character-design');
57
+ console.log(' sdlab generate inputs/prompts/wave1.json --project star-freight');
58
+ console.log(' sdlab bind --stats --project star-freight');
59
+ console.log(' sdlab project doctor --project star-freight');
60
+ }
61
+
62
+ async function main() {
63
+ const args = process.argv.slice(2);
64
+
65
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
66
+ printHelp();
67
+ process.exit(0);
68
+ }
69
+
70
+ let command = args[0];
71
+ let commandArgs = args.slice(1);
72
+
73
+ // Handle "project <subcommand>" two-word form
74
+ if (command === 'project' && args[1] && PROJECT_COMMANDS[args[1]]) {
75
+ commandArgs = args.slice(2);
76
+ const modulePath = PROJECT_COMMANDS[args[1]];
77
+ try {
78
+ const mod = await import(modulePath);
79
+ await mod.run(commandArgs);
80
+ } catch (err) {
81
+ console.error(`\x1b[31mError:\x1b[0m ${err.message}`);
82
+ process.exit(1);
83
+ }
84
+ return;
85
+ }
86
+
87
+ const modulePath = COMMANDS[command];
88
+ if (!modulePath) {
89
+ console.error(`Unknown command: ${command}`);
90
+ console.error(`Run "sdlab --help" for available commands.`);
91
+ process.exit(1);
92
+ }
93
+
94
+ try {
95
+ const mod = await import(modulePath);
96
+ await mod.run(commandArgs);
97
+ } catch (err) {
98
+ console.error(`\x1b[31mError:\x1b[0m ${err.message}`);
99
+ process.exit(1);
100
+ }
101
+ }
102
+
103
+ main();
package/lib/args.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Unified CLI argument parser.
3
+ *
4
+ * Supports:
5
+ * --flag value (space-separated)
6
+ * --flag=value (equals form)
7
+ * --boolean-flag (boolean)
8
+ * positional args (collected in order)
9
+ * deprecated aliases (warns once, maps to canonical name)
10
+ */
11
+
12
+ import { warnDeprecated } from './deprecation.js';
13
+
14
+ /**
15
+ * @typedef {Object} ArgSpec
16
+ * @property {Object<string, {type: 'string'|'boolean', default?: any, alias?: string}>} flags
17
+ * @property {string[]} [positionals] - names for positional args (in order)
18
+ * @property {Object<string, string>} [deprecated] - old flag name → new flag name
19
+ */
20
+
21
+ /**
22
+ * Parse CLI arguments.
23
+ * @param {string[]} argv — typically process.argv.slice(2)
24
+ * @param {ArgSpec} spec
25
+ * @returns {{ flags: Object, positionals: string[], rest: string[] }}
26
+ */
27
+ export function parseArgs(argv, spec = {}) {
28
+ const flagDefs = spec.flags || {};
29
+ const deprecated = spec.deprecated || {};
30
+ const positionalNames = spec.positionals || [];
31
+
32
+ const flags = {};
33
+ const positionals = [];
34
+
35
+ // Set defaults
36
+ for (const [name, def] of Object.entries(flagDefs)) {
37
+ if (def.default !== undefined) flags[name] = def.default;
38
+ else if (def.type === 'boolean') flags[name] = false;
39
+ else flags[name] = undefined;
40
+ }
41
+
42
+ let i = 0;
43
+ while (i < argv.length) {
44
+ const arg = argv[i];
45
+
46
+ if (arg.startsWith('--')) {
47
+ let key, value;
48
+
49
+ if (arg.includes('=')) {
50
+ const eqIdx = arg.indexOf('=');
51
+ key = arg.slice(2, eqIdx);
52
+ value = arg.slice(eqIdx + 1);
53
+ } else {
54
+ key = arg.slice(2);
55
+ value = undefined;
56
+ }
57
+
58
+ // Handle deprecated aliases
59
+ if (deprecated[key]) {
60
+ warnDeprecated(key, deprecated[key]);
61
+ key = deprecated[key];
62
+ }
63
+
64
+ // Handle short aliases defined in flagDefs
65
+ const canonical = Object.entries(flagDefs).find(([, def]) => def.alias === key);
66
+ if (canonical) key = canonical[0];
67
+
68
+ // Normalize kebab-case to camelCase for lookup
69
+ const camelKey = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
70
+ const def = flagDefs[key] || flagDefs[camelKey];
71
+
72
+ if (def?.type === 'boolean') {
73
+ flags[key in flagDefs ? key : camelKey] = true;
74
+ } else if (value !== undefined) {
75
+ flags[key in flagDefs ? key : camelKey] = value;
76
+ } else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
77
+ flags[key in flagDefs ? key : camelKey] = argv[++i];
78
+ } else {
79
+ flags[key in flagDefs ? key : camelKey] = true;
80
+ }
81
+ } else {
82
+ positionals.push(arg);
83
+ }
84
+ i++;
85
+ }
86
+
87
+ // Map positionals to named keys
88
+ const named = {};
89
+ for (let j = 0; j < positionalNames.length && j < positionals.length; j++) {
90
+ named[positionalNames[j]] = positionals[j];
91
+ }
92
+
93
+ return { flags, positionals, named, rest: positionals.slice(positionalNames.length) };
94
+ }
95
+
96
+ /**
97
+ * Extract the --project (or deprecated --game) value from argv.
98
+ * Convenience for scripts that just need the project name.
99
+ */
100
+ export function getProjectName(argv) {
101
+ const { flags } = parseArgs(argv, {
102
+ flags: { project: { type: 'string', default: 'star-freight' } },
103
+ deprecated: { game: 'project' },
104
+ });
105
+ return flags.project;
106
+ }
package/lib/comfyui.js ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * ComfyUI HTTP API client — extracted from 5 generation scripts.
3
+ *
4
+ * All functions take comfyUrl as a parameter so projects can override
5
+ * the default localhost:8188 endpoint.
6
+ */
7
+
8
+ import { readFile } from 'node:fs/promises';
9
+
10
+ const DEFAULT_URL = 'http://127.0.0.1:8188';
11
+ const MAX_POLL_MS = 600_000; // 10 minutes
12
+
13
+ /**
14
+ * Check if ComfyUI is reachable.
15
+ * @param {string} [comfyUrl]
16
+ * @returns {Promise<boolean>}
17
+ */
18
+ export async function comfyHealth(comfyUrl = DEFAULT_URL) {
19
+ try {
20
+ const res = await fetch(`${comfyUrl}/system_stats`);
21
+ return res.ok;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Submit a workflow and poll until complete.
29
+ * @param {Object} workflow — ComfyUI prompt graph
30
+ * @param {string} [comfyUrl]
31
+ * @param {{ clientPrefix?: string, timeoutMs?: number }} [opts]
32
+ * @returns {Promise<Object>} — history entry
33
+ */
34
+ export async function submitAndWait(workflow, comfyUrl = DEFAULT_URL, opts = {}) {
35
+ const { clientPrefix = 'sdl', timeoutMs = MAX_POLL_MS } = opts;
36
+ const clientId = `${clientPrefix}-${Date.now()}`;
37
+
38
+ const res = await fetch(`${comfyUrl}/prompt`, {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ prompt: workflow, client_id: clientId }),
42
+ });
43
+
44
+ if (!res.ok) {
45
+ const text = await res.text();
46
+ throw new Error(`ComfyUI submit failed: ${res.status} ${text}`);
47
+ }
48
+
49
+ const { prompt_id: promptId } = await res.json();
50
+
51
+ const start = Date.now();
52
+ while (true) {
53
+ if (Date.now() - start > timeoutMs) {
54
+ throw new Error(`ComfyUI poll timeout after ${timeoutMs / 1000}s for prompt ${promptId}`);
55
+ }
56
+ await new Promise(r => setTimeout(r, 1000));
57
+ const histRes = await fetch(`${comfyUrl}/history/${promptId}`);
58
+ if (!histRes.ok) continue;
59
+ const history = await histRes.json();
60
+ const entry = history[promptId];
61
+ if (!entry) continue;
62
+ if (entry.status?.completed) return entry;
63
+ if (entry.status?.status_str === 'error') {
64
+ throw new Error(`ComfyUI generation failed: ${JSON.stringify(entry.status)}`);
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Download a generated image from ComfyUI.
71
+ * @param {string} filename
72
+ * @param {string} [subfolder]
73
+ * @param {string} [comfyUrl]
74
+ * @param {string} [type='output']
75
+ * @returns {Promise<Buffer>}
76
+ */
77
+ export async function downloadImage(filename, subfolder, comfyUrl = DEFAULT_URL, type = 'output') {
78
+ const url = `${comfyUrl}/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder || '')}&type=${type}`;
79
+ const res = await fetch(url);
80
+ if (!res.ok) throw new Error(`Download failed: ${res.status}`);
81
+ return Buffer.from(await res.arrayBuffer());
82
+ }
83
+
84
+ /**
85
+ * Upload an image to ComfyUI's input folder.
86
+ * @param {string} filePath — local file to upload
87
+ * @param {string} filename — name to assign in ComfyUI
88
+ * @param {string} [comfyUrl]
89
+ * @returns {Promise<string>} — assigned name from ComfyUI
90
+ */
91
+ export async function uploadImage(filePath, filename, comfyUrl = DEFAULT_URL) {
92
+ const data = await readFile(filePath);
93
+ const formData = new FormData();
94
+ formData.append('image', new Blob([data], { type: 'image/png' }), filename);
95
+ formData.append('overwrite', 'true');
96
+
97
+ const res = await fetch(`${comfyUrl}/upload/image`, {
98
+ method: 'POST',
99
+ body: formData,
100
+ });
101
+
102
+ if (!res.ok) {
103
+ const text = await res.text();
104
+ throw new Error(`Upload failed: ${res.status} ${text}`);
105
+ }
106
+
107
+ const result = await res.json();
108
+ return result.name;
109
+ }
package/lib/config.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Per-project config loader.
3
+ *
4
+ * Loads JSON config files from a project root directory.
5
+ * Falls back to sensible defaults when files don't exist,
6
+ * so unmigrated projects (pre-v2.0) still work.
7
+ */
8
+
9
+ import { readFileSync, existsSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+
12
+ function loadJsonFile(filePath) {
13
+ if (!existsSync(filePath)) return null;
14
+ try {
15
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
16
+ } catch (err) {
17
+ throw new Error(`Failed to parse ${filePath}: ${err.message}`);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Load project.json — project identity and generation defaults.
23
+ */
24
+ export function loadProjectMeta(projectRoot) {
25
+ const data = loadJsonFile(join(projectRoot, 'project.json'));
26
+ if (!data) {
27
+ return {
28
+ name: 'unknown',
29
+ display_name: 'Unknown Project',
30
+ domain: 'generic',
31
+ version: '0.0.0',
32
+ defaults: {},
33
+ };
34
+ }
35
+ return data;
36
+ }
37
+
38
+ /**
39
+ * Load constitution.json — array of rule objects.
40
+ */
41
+ export function loadConstitution(projectRoot) {
42
+ const data = loadJsonFile(join(projectRoot, 'constitution.json'));
43
+ if (!data) return { rules: [] };
44
+ if (Array.isArray(data)) return { rules: data };
45
+ if (!data.rules) throw new Error(`constitution.json must have a "rules" array`);
46
+ return data;
47
+ }
48
+
49
+ /**
50
+ * Load lanes.json — lane definitions with detection patterns.
51
+ */
52
+ export function loadLanes(projectRoot) {
53
+ const data = loadJsonFile(join(projectRoot, 'lanes.json'));
54
+ if (!data) {
55
+ return { default_lane: 'default', lanes: [] };
56
+ }
57
+ if (!data.lanes) throw new Error(`lanes.json must have a "lanes" array`);
58
+ return data;
59
+ }
60
+
61
+ /**
62
+ * Load rubric.json — scoring dimensions, thresholds, failure modes.
63
+ */
64
+ export function loadRubric(projectRoot) {
65
+ const data = loadJsonFile(join(projectRoot, 'rubric.json'));
66
+ if (!data) {
67
+ return {
68
+ dimensions: [],
69
+ thresholds: { pass: 0.7, partial: 0.5, fail_ceiling: 0.4 },
70
+ failure_to_rules: {},
71
+ };
72
+ }
73
+ return data;
74
+ }
75
+
76
+ /**
77
+ * Load terminology.json — group vocabulary and detection patterns.
78
+ */
79
+ export function loadTerminology(projectRoot) {
80
+ const data = loadJsonFile(join(projectRoot, 'terminology.json'));
81
+ if (!data) {
82
+ return {
83
+ group_label: 'group',
84
+ groups: {},
85
+ cross_group_patterns: {},
86
+ edge_defaults: {},
87
+ prompt_fallbacks: {},
88
+ };
89
+ }
90
+ return data;
91
+ }
92
+
93
+ /**
94
+ * Load all project config files at once.
95
+ */
96
+ export function loadProjectConfig(projectRoot) {
97
+ return {
98
+ meta: loadProjectMeta(projectRoot),
99
+ constitution: loadConstitution(projectRoot),
100
+ lanes: loadLanes(projectRoot),
101
+ rubric: loadRubric(projectRoot),
102
+ terminology: loadTerminology(projectRoot),
103
+ };
104
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Deprecation warning helper — fires once per flag per process.
3
+ */
4
+
5
+ const warned = new Set();
6
+
7
+ export function warnDeprecated(oldFlag, newFlag) {
8
+ const key = `${oldFlag}→${newFlag}`;
9
+ if (warned.has(key)) return;
10
+ warned.add(key);
11
+ process.stderr.write(
12
+ `\x1b[33m⚠ --${oldFlag} is deprecated and will be removed in v3.0. Use --${newFlag} instead.\x1b[0m\n`
13
+ );
14
+ }
package/lib/paths.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Path resolution — single source of truth for repo root and project paths.
3
+ */
4
+
5
+ import { fileURLToPath } from 'node:url';
6
+ import { dirname, join } from 'node:path';
7
+ import { existsSync } from 'node:fs';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ /** Absolute path to the repository root. */
13
+ export const REPO_ROOT = join(__dirname, '..');
14
+
15
+ /**
16
+ * Resolve the root directory for a named project.
17
+ * @param {string} name — project name (e.g. 'star-freight')
18
+ * @returns {string} absolute path to projects/<name>/
19
+ */
20
+ export function getProjectRoot(name) {
21
+ const root = join(REPO_ROOT, 'projects', name);
22
+ if (!existsSync(root)) {
23
+ throw new Error(
24
+ `Project "${name}" not found at ${root}.\n` +
25
+ `Run: sdlab init ${name} --domain <domain>`
26
+ );
27
+ }
28
+ return root;
29
+ }
30
+
31
+ /**
32
+ * Join segments onto a project root.
33
+ * @param {string} name — project name
34
+ * @param {...string} segments — path segments
35
+ */
36
+ export function resolveProjectPath(name, ...segments) {
37
+ return join(getProjectRoot(name), ...segments);
38
+ }