@mcptoolshop/style-dataset-lab 1.2.1 → 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 (50) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +95 -116
  3. package/bin/sdlab.js +103 -0
  4. package/lib/args.js +106 -0
  5. package/lib/comfyui.js +109 -0
  6. package/lib/config.js +104 -0
  7. package/lib/deprecation.js +14 -0
  8. package/lib/paths.js +38 -0
  9. package/lib/records.js +69 -0
  10. package/package.json +13 -4
  11. package/scripts/bulk-curate-wave2-5.js +1 -1
  12. package/scripts/bulk-curate-waves11-18.js +1 -1
  13. package/scripts/canon-bind.js +268 -332
  14. package/scripts/compare.js +40 -30
  15. package/scripts/curate-wave25.js +1 -1
  16. package/scripts/curate.js +34 -28
  17. package/scripts/doctor.js +321 -0
  18. package/scripts/generate-controlnet.js +36 -121
  19. package/scripts/generate-identity.js +38 -106
  20. package/scripts/generate-ipadapter.js +42 -119
  21. package/scripts/generate.js +22 -81
  22. package/scripts/init.js +226 -0
  23. package/scripts/migrate-records.js +15 -21
  24. package/scripts/painterly-test.js +20 -47
  25. package/scripts/painterly.js +27 -106
  26. package/templates/domains/architecture/constitution.json +97 -0
  27. package/templates/domains/architecture/lanes.json +41 -0
  28. package/templates/domains/architecture/project.json +20 -0
  29. package/templates/domains/architecture/rubric.json +40 -0
  30. package/templates/domains/architecture/terminology.json +19 -0
  31. package/templates/domains/character-design/constitution.json +88 -0
  32. package/templates/domains/character-design/lanes.json +35 -0
  33. package/templates/domains/character-design/project.json +17 -0
  34. package/templates/domains/character-design/rubric.json +38 -0
  35. package/templates/domains/character-design/terminology.json +21 -0
  36. package/templates/domains/creature-design/constitution.json +87 -0
  37. package/templates/domains/creature-design/lanes.json +41 -0
  38. package/templates/domains/creature-design/project.json +18 -0
  39. package/templates/domains/creature-design/rubric.json +39 -0
  40. package/templates/domains/creature-design/terminology.json +20 -0
  41. package/templates/domains/game-art/constitution.json +113 -0
  42. package/templates/domains/game-art/lanes.json +47 -0
  43. package/templates/domains/game-art/project.json +17 -0
  44. package/templates/domains/game-art/rubric.json +36 -0
  45. package/templates/domains/game-art/terminology.json +30 -0
  46. package/templates/domains/vehicle-mech/constitution.json +87 -0
  47. package/templates/domains/vehicle-mech/lanes.json +41 -0
  48. package/templates/domains/vehicle-mech/project.json +17 -0
  49. package/templates/domains/vehicle-mech/rubric.json +40 -0
  50. package/templates/domains/vehicle-mech/terminology.json +21 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.0.0] - 2026-04-15
6
+
7
+ ### Breaking changes
8
+
9
+ - **`games/` renamed to `projects/`** -- all project data now lives under `projects/<name>/`
10
+ - **`--game` renamed to `--project`** -- `--game` still works with a deprecation warning (will be removed in v3.0)
11
+ - **Canon-bind loads from config files** -- rules, lanes, rubric, and terminology are now per-project JSON, not hardcoded
12
+
13
+ ### Added
14
+
15
+ - **`sdlab` CLI** -- unified command-line interface (`bin/sdlab.js`)
16
+ - `sdlab init <name> --domain <domain>` -- scaffold projects from domain templates
17
+ - `sdlab project doctor` -- validate project config and structure
18
+ - `sdlab bind`, `sdlab curate`, `sdlab generate`, etc. -- all pipeline commands
19
+ - **Shared library** (`lib/`) -- 6 modules replacing duplicated code across 13 scripts
20
+ - `lib/args.js` -- unified argument parser with `--project` and deprecated `--game` support
21
+ - `lib/paths.js` -- centralized path resolution
22
+ - `lib/comfyui.js` -- ComfyUI HTTP client (extracted from 5 scripts)
23
+ - `lib/config.js` -- per-project JSON config loader
24
+ - `lib/records.js` -- record I/O utilities
25
+ - `lib/deprecation.js` -- one-shot deprecation warnings
26
+ - **Per-project config files** -- 5 JSON files per project:
27
+ - `project.json` -- identity, domain, generation defaults
28
+ - `constitution.json` -- rules with rationale templates
29
+ - `lanes.json` -- subject lanes with regex detection patterns
30
+ - `rubric.json` -- scoring dimensions, thresholds, failure-to-rule mappings
31
+ - `terminology.json` -- group vocabulary with separate `id_detection_order` and `prompt_detection_order`
32
+ - **5 domain starter templates** -- game-art, character-design, creature-design, architecture, vehicle-mech
33
+ - **Training profile placeholders** in domain templates (eligible lanes, thresholds, export format)
34
+
35
+ ### Changed
36
+
37
+ - Canon-bind is now fully config-driven -- loads rules, lanes, dimensions, thresholds, group vocabulary, and rationale templates from per-project JSON files
38
+ - Missing config files cause hard failure with actionable error messages (no silent fallback)
39
+ - All 10 CLI-exposed scripts refactored to export `run(argv)` for CLI dispatch
40
+ - Net code reduction: ~305 lines removed from deduplication
41
+
42
+ ### Migration
43
+
44
+ 1. Rename `games/` to `projects/`
45
+ 2. Use `--project` instead of `--game` (or keep `--game` with deprecation warning)
46
+ 3. For canon-bind: add 5 JSON config files to your project (use `sdlab init` as reference)
47
+
48
+ Star Freight config files are included as the canonical example.
49
+
5
50
  ## [1.2.0] - 2026-04-14
6
51
 
7
52
  - Added `templates/` with blank constitution, review rubric, and example prompt pack
package/README.md CHANGED
@@ -1,7 +1,3 @@
1
- <p align="center">
2
- <a href="README.ja.md">日本語</a> | <a href="README.zh.md">中文</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.hi.md">हिन्दी</a> | <a href="README.it.md">Italiano</a> | <a href="README.pt-BR.md">Português (BR)</a>
3
- </p>
4
-
5
1
  <p align="center">
6
2
  <img src="https://raw.githubusercontent.com/mcp-tool-shop-org/brand/main/logos/style-dataset-lab/readme.png" width="400">
7
3
  </p>
@@ -13,163 +9,146 @@
13
9
 
14
10
  # style-dataset-lab
15
11
 
16
- A production pipeline for visual dataset creation -- from canon rules to structured prompts to ComfyUI generation to curated, canon-bound training data.
12
+ A canon-bound visual dataset pipeline for directed image production.
17
13
 
18
- ## What this is
14
+ Define what your project looks like. Generate candidates. Curate against your canon. Bind approved work to constitution rules. Export trustworthy datasets for training, evaluation, and production reuse.
19
15
 
20
- A **pipeline** for building structured visual training datasets. You write style rules (canon), compose prompts, generate with ComfyUI, curate with per-dimension scoring, bind judgments to canon rules, and export in 10 formats via [`@mcptoolshop/repo-dataset`](https://github.com/mcp-tool-shop-org/repo-dataset).
16
+ ## What this is
21
17
 
22
- The pipeline is game-agnostic. Each game gets its own data directory under `games/<name>/`; the 13 scripts and blank templates are shared. Every asset produced carries three things:
18
+ A **visual canon + dataset pipeline** for teams that care about style consistency, provenance, and reviewable judgment. Every asset carries three things:
23
19
 
24
- 1. **Image pixels** -- generated by ComfyUI with full provenance (checkpoint, LoRA, seed, sampler, cfg)
25
- 2. **Canon explanation** -- why this is on-style or off-style, grounded in a style constitution
26
- 3. **Quality judgment** -- approved/rejected with per-dimension scores and cited rules
20
+ 1. **Provenance** -- full generation history (checkpoint, LoRA, seed, sampler, cfg, timing)
21
+ 2. **Canon binding** -- which constitution rules this asset passes, fails, or partially meets
22
+ 3. **Quality judgment** -- approved/rejected/borderline with per-dimension scores
27
23
 
28
- ## Security model
24
+ The pipeline works for game art, character design, creature design, architecture, vehicle/mech concepts, and any domain where visual production needs to stay on-model.
29
25
 
30
- **Local-only.** style-dataset-lab talks to ComfyUI on `localhost:8188` and never makes external network requests. No telemetry, no analytics, no phoning home. Image generation happens entirely on your GPU. Records and canon data stay on your filesystem.
26
+ ## Quick start
31
27
 
32
- ## What the npm package ships
28
+ ```bash
29
+ # Install the CLI + pipeline
30
+ npm install -g @mcptoolshop/style-dataset-lab
33
31
 
34
- `npm install @mcptoolshop/style-dataset-lab` gives you:
32
+ # Scaffold a new project
33
+ sdlab init my-project --domain character-design
35
34
 
36
- - **13 scripts** -- generate, curate, compare, canon-bind, painterly, identity generation, ControlNet/IP-Adapter generation, bulk curation, migration
37
- - **Blank templates** -- starter constitution, review rubric, and example prompt pack in `templates/`
35
+ # Validate the project structure
36
+ sdlab project doctor --project my-project
37
+ ```
38
38
 
39
- The npm package does **not** include game data. Clone the repo if you want the Star Freight example (1,182 records, 28 prompt waves, 18 visual categories).
39
+ Available domains: `game-art`, `character-design`, `creature-design`, `architecture`, `vehicle-mech`, or `generic`.
40
40
 
41
- ## Install
41
+ ## CLI
42
42
 
43
43
  ```bash
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
44
+ sdlab init <name> [--domain <domain>] # Scaffold a new project
45
+ sdlab project doctor [--project <name>] # Validate project config
46
+
47
+ sdlab generate <pack> [--project <name>] # Generate candidates via ComfyUI
48
+ sdlab generate:identity <packet> # Named-subject identity images
49
+ sdlab generate:controlnet # ControlNet-guided generation
50
+ sdlab generate:ipadapter # IP-Adapter reference-guided
51
+
52
+ sdlab curate <id> <status> <explanation> # Record review judgment
53
+ sdlab compare <a> <b> <winner> <reason> # Pairwise A-vs-B comparison
54
+ sdlab bind [--project <name>] # Bind records to constitution rules
55
+ sdlab painterly [--project <name>] # Post-processing style pass
51
56
  ```
52
57
 
53
- To start a new game from the templates:
54
-
55
- ```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
61
- ```
58
+ All commands accept `--project <name>` (default: `star-freight`).
62
59
 
63
- ## Monorepo structure
60
+ ## Project model
64
61
 
65
- The pipeline lives in `scripts/` and `templates/`. Each game lives in `games/<name>/` with its own canon, records, and assets. Scripts accept `--game <name>` (defaults to `star-freight`).
62
+ Each project is a self-contained directory under `projects/` with its own canon, config, and data:
66
63
 
67
64
  ```
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
65
+ projects/
66
+ my-project/
67
+ project.json Project identity + generation defaults
68
+ constitution.json Rules array with rationale templates
69
+ lanes.json Subject lanes with detection patterns
70
+ rubric.json Scoring dimensions + thresholds
71
+ terminology.json Group vocabulary + detection order
72
+ canon/ Style constitution (markdown)
73
+ records/ Per-asset JSON (provenance + judgment + canon)
74
+ inputs/prompts/ Prompt packs (JSON)
75
+ outputs/ Generated images (gitignored)
76
+ comparisons/ A-vs-B preference judgments
77
+ exports/ Training data output (gitignored)
82
78
  ```
83
79
 
84
- ## Pipeline workflow
80
+ ## Pipeline
85
81
 
86
- The full pipeline from canon to training export:
82
+ ```
83
+ canon --> prompts --> generate --> curate --> bind --> compare --> export
84
+ | | | | | | |
85
+ rules packs ComfyUI judgment assertions preferences dataset
86
+ ```
87
87
 
88
- ```bash
89
- # 1. Write your canon -- style constitution + review rubric
90
- # (start from templates/ or write from scratch)
88
+ 1. **Define canon** -- write your style constitution and review rubric
89
+ 2. **Create prompt packs** -- structured subjects with style prefix and variations
90
+ 3. **Generate** -- ComfyUI produces candidates with full provenance
91
+ 4. **Curate** -- approve/reject with per-dimension scores and failure modes
92
+ 5. **Bind** -- link each asset to constitution rules with pass/fail/partial verdicts
93
+ 6. **Compare** -- record pairwise preferences for DPO/ranking datasets
94
+ 7. **Export** -- produce training data via [`repo-dataset`](https://github.com/mcp-tool-shop-org/repo-dataset) in 10+ formats
91
95
 
92
- # 2. Create prompt packs in inputs/prompts/
93
- # (see templates/inputs/prompts/example-wave.json)
96
+ ## Domain templates
94
97
 
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
98
+ Each domain template ships with lane definitions, constitution rules, scoring rubrics, and terminology structures designed for that production context:
98
99
 
99
- # 4. Generate identity-packet characters (named subjects)
100
- npm run generate:identity -- --game star-freight inputs/identity-packets/wave27a.json
100
+ | Domain | Lanes | Key concerns |
101
+ |--------|-------|-------------|
102
+ | **game-art** | character, environment, prop, UI, ship, interior, equipment | Silhouette at gameplay scale, faction differentiation, wear/aging |
103
+ | **character-design** | portrait, full_body, turnaround, expression_sheet, action_pose | Proportion accuracy, costume logic, personality read, gesture clarity |
104
+ | **creature-design** | concept, orthographic, detail_study, action, scale_reference, habitat | Anatomical plausibility, evolutionary logic, silhouette distinction |
105
+ | **architecture** | exterior, interior, streetscape, structural_detail, ruin, landscape | Structural plausibility, material consistency, perspective, era coherence |
106
+ | **vehicle-mech** | exterior, cockpit, component, schematic, silhouette_sheet, damage_variant | Mechanical logic, functional design language, access points, damage narrative |
101
107
 
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"
108
+ ## The dataset angle
105
109
 
106
- # 6. Generate painterly variants of approved assets
107
- npm run painterly -- --game star-freight
110
+ The pipeline produces more than organized images. It produces **trustworthy, structured datasets** with:
108
111
 
109
- # 7. Bind canon explanations to curated assets
110
- npm run canon-bind -- --game star-freight
112
+ - **Full provenance** on every asset (reproducible generations)
113
+ - **Canon-bound judgments** (not just labels -- reasoned verdicts against declared rules)
114
+ - **Pairwise preferences** (for DPO and ranking-based training)
115
+ - **Per-dimension scoring** (fine-grained quality signals, not just pass/fail)
116
+ - **Failure mode tagging** (what specifically went wrong and which rules were violated)
111
117
 
112
- # 8. Record pairwise comparisons
113
- npm run compare -- --game star-freight <asset_a> <asset_b> a "A has better faction read"
118
+ Export via `repo-dataset` to TRL, LLaVA, Qwen2-VL, JSONL, Parquet, and more.
114
119
 
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
118
- ```
120
+ ## Star Freight example
119
121
 
120
- ## Adding a new game
122
+ Clone the repo for a complete working example: 1,182 records, 28 prompt waves, 5 factions, 7 lanes, 24 constitution rules, and 892 approved assets from a gritty sci-fi RPG.
121
123
 
122
124
  ```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
125
+ git clone https://github.com/mcp-tool-shop-org/style-dataset-lab
126
+ cd style-dataset-lab
127
+ sdlab bind --stats --project star-freight
128
+ sdlab project doctor --project star-freight
130
129
  ```
131
130
 
132
- ## Per-game directory layout
131
+ ## Migrating from v1.x
133
132
 
134
- Each `games/<name>/` directory contains:
133
+ v2.0 renames `games/` to `projects/` and `--game` to `--project`:
135
134
 
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
- ```
135
+ ```bash
136
+ # Rename your data directory
137
+ mv games projects
153
138
 
154
- ## Generation setup
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)
139
+ # --game still works with a deprecation warning (removed in v3.0)
140
+ sdlab bind --game star-freight # works, prints warning
141
+ sdlab bind --project star-freight # canonical form
165
142
  ```
166
143
 
167
- Additional generation modes: ControlNet (pose/depth-guided), IP-Adapter (reference-driven), and identity packets (named character consistency).
144
+ ## Security model
145
+
146
+ **Local-only.** Talks to ComfyUI on `localhost:8188`. No telemetry, no analytics, no external requests. Images stay on your GPU and filesystem.
168
147
 
169
148
  ## Requirements
170
149
 
171
150
  - [ComfyUI](https://github.com/comfyanonymous/ComfyUI) running on localhost:8188
172
- - DreamShaper XL Turbo checkpoint + ClassipeintXL LoRA
151
+ - SDXL-compatible checkpoint + style LoRA
173
152
  - Node.js 20+
174
153
  - [`@mcptoolshop/repo-dataset`](https://github.com/mcp-tool-shop-org/repo-dataset) for training export
175
154
 
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
+ }