@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.
- package/CHANGELOG.md +45 -0
- package/README.es.md +94 -54
- package/README.fr.md +86 -46
- package/README.hi.md +86 -46
- package/README.it.md +93 -53
- package/README.ja.md +86 -46
- package/README.md +95 -116
- package/README.pt-BR.md +86 -46
- package/README.zh.md +90 -50
- package/bin/sdlab.js +103 -0
- package/lib/args.js +106 -0
- package/lib/comfyui.js +109 -0
- package/lib/config.js +104 -0
- package/lib/deprecation.js +14 -0
- package/lib/paths.js +38 -0
- package/lib/records.js +69 -0
- package/package.json +13 -4
- package/scripts/bulk-curate-wave2-5.js +1 -1
- package/scripts/bulk-curate-waves11-18.js +1 -1
- package/scripts/canon-bind.js +268 -332
- package/scripts/compare.js +40 -30
- package/scripts/curate-wave25.js +1 -1
- package/scripts/curate.js +34 -28
- package/scripts/doctor.js +321 -0
- package/scripts/generate-controlnet.js +36 -121
- package/scripts/generate-identity.js +38 -106
- package/scripts/generate-ipadapter.js +42 -119
- package/scripts/generate.js +22 -81
- package/scripts/init.js +226 -0
- package/scripts/migrate-records.js +15 -21
- package/scripts/painterly-test.js +20 -47
- package/scripts/painterly.js +27 -106
- package/templates/domains/architecture/constitution.json +97 -0
- package/templates/domains/architecture/lanes.json +41 -0
- package/templates/domains/architecture/project.json +20 -0
- package/templates/domains/architecture/rubric.json +40 -0
- package/templates/domains/architecture/terminology.json +19 -0
- package/templates/domains/character-design/constitution.json +88 -0
- package/templates/domains/character-design/lanes.json +35 -0
- package/templates/domains/character-design/project.json +17 -0
- package/templates/domains/character-design/rubric.json +38 -0
- package/templates/domains/character-design/terminology.json +21 -0
- package/templates/domains/creature-design/constitution.json +87 -0
- package/templates/domains/creature-design/lanes.json +41 -0
- package/templates/domains/creature-design/project.json +18 -0
- package/templates/domains/creature-design/rubric.json +39 -0
- package/templates/domains/creature-design/terminology.json +20 -0
- package/templates/domains/game-art/constitution.json +113 -0
- package/templates/domains/game-art/lanes.json +47 -0
- package/templates/domains/game-art/project.json +17 -0
- package/templates/domains/game-art/rubric.json +36 -0
- package/templates/domains/game-art/terminology.json +30 -0
- package/templates/domains/vehicle-mech/constitution.json +87 -0
- package/templates/domains/vehicle-mech/lanes.json +41 -0
- package/templates/domains/vehicle-mech/project.json +17 -0
- package/templates/domains/vehicle-mech/rubric.json +40 -0
- 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
|
-
|
|
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
|
-
|
|
23
|
-
2. **规范解释** -- 解释为什么该图像符合或不符合特定风格,并基于风格规范进行判断。
|
|
24
|
-
3. **质量评估** -- 经过批准/拒绝,并提供每个维度的评分以及引用规则。
|
|
22
|
+
该流水线不依赖于特定的游戏。 每个游戏都有自己的数据目录,位于 `games/<name>/` 目录下;13 个脚本和空白模板是共享的。 每个生成的资源都包含以下三项:
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
1. **图像像素**:由 ComfyUI 生成,包含完整的溯源信息(检查点、LoRA、种子、采样器、CFG)。
|
|
25
|
+
2. **规范解释**:解释为什么该图像符合或不符合风格,并基于风格规范进行说明。
|
|
26
|
+
3. **质量判断**:通过批准/拒绝,并根据每个维度的评分引用相关规则。
|
|
27
27
|
|
|
28
28
|
## 安全模型
|
|
29
29
|
|
|
30
|
-
**仅本地运行。** style-dataset-lab 只与本地 `localhost:8188` 上的 ComfyUI
|
|
30
|
+
**仅本地运行。** `style-dataset-lab` 只与本地 `localhost:8188` 上的 ComfyUI 进行通信,不会进行任何外部网络请求。 没有遥测数据,没有分析功能,不会向外部发送数据。 图像生成完全在您的 GPU 上进行。 记录和规范数据都存储在您的文件系统中。
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## npm 包包含的内容
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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.
|
|
63
|
-
#
|
|
89
|
+
# 1. Write your canon -- style constitution + review rubric
|
|
90
|
+
# (start from templates/ or write from scratch)
|
|
64
91
|
|
|
65
|
-
# 2.
|
|
66
|
-
|
|
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.
|
|
70
|
-
npm run generate
|
|
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
|
|
73
|
-
npm run
|
|
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"
|
|
77
|
-
npm run curate -- <asset_id> rejected "explanation" --failures "too_clean
|
|
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
|
-
#
|
|
80
|
-
npm run canon-bind --
|
|
109
|
+
# 7. Bind canon explanations to curated assets
|
|
110
|
+
npm run canon-bind -- --game star-freight
|
|
81
111
|
|
|
82
|
-
#
|
|
83
|
-
npm run compare -- <asset_a> <asset_b> a "A has better faction read
|
|
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
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
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,
|
|
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
|
|
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
|
+
}
|