@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.
- package/CHANGELOG.md +45 -0
- package/README.md +95 -116
- 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/scripts/init.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* init.js — Scaffold a new project from a domain template.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab init my-project --domain character-design
|
|
8
|
+
* sdlab init my-project # uses 'generic' domain
|
|
9
|
+
* node scripts/init.js my-project --domain game-art
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mkdir, readdir, readFile, writeFile, copyFile } from 'node:fs/promises';
|
|
13
|
+
import { join, basename } from 'node:path';
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { REPO_ROOT } from '../lib/paths.js';
|
|
16
|
+
|
|
17
|
+
const TEMPLATES_DIR = join(REPO_ROOT, 'templates');
|
|
18
|
+
const PROJECTS_DIR = join(REPO_ROOT, 'projects');
|
|
19
|
+
|
|
20
|
+
const PROJECT_DIRS = [
|
|
21
|
+
'canon',
|
|
22
|
+
'records',
|
|
23
|
+
'comparisons',
|
|
24
|
+
'inputs/prompts',
|
|
25
|
+
'inputs/identity-packets',
|
|
26
|
+
'inputs/references',
|
|
27
|
+
'inputs/control-guides',
|
|
28
|
+
'outputs/candidates',
|
|
29
|
+
'outputs/approved',
|
|
30
|
+
'outputs/rejected',
|
|
31
|
+
'outputs/borderline',
|
|
32
|
+
'outputs/painterly',
|
|
33
|
+
'outputs/painterly-test',
|
|
34
|
+
'exports',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function isValidName(name) {
|
|
38
|
+
return /^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name) || /^[a-z0-9]$/.test(name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function listDomains() {
|
|
42
|
+
const domainsDir = join(TEMPLATES_DIR, 'domains');
|
|
43
|
+
if (!existsSync(domainsDir)) return [];
|
|
44
|
+
const entries = await readdir(domainsDir, { withFileTypes: true });
|
|
45
|
+
return entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
49
|
+
const projectName = argv.find(a => !a.startsWith('--'));
|
|
50
|
+
|
|
51
|
+
// Parse --domain
|
|
52
|
+
let domain = 'generic';
|
|
53
|
+
const domainIdx = argv.indexOf('--domain');
|
|
54
|
+
if (domainIdx >= 0 && argv[domainIdx + 1]) {
|
|
55
|
+
domain = argv[domainIdx + 1];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!projectName) {
|
|
59
|
+
const domains = await listDomains();
|
|
60
|
+
console.log('Usage: sdlab init <project-name> [--domain <domain>]\n');
|
|
61
|
+
console.log('Available domains:');
|
|
62
|
+
console.log(' generic Minimal starter (default)');
|
|
63
|
+
for (const d of domains) {
|
|
64
|
+
console.log(` ${d.padEnd(20)} Domain-specific starter`);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!isValidName(projectName)) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Invalid project name "${projectName}".\n` +
|
|
72
|
+
`Use lowercase letters, numbers, and hyphens (e.g. "my-project").`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const projectDir = join(PROJECTS_DIR, projectName);
|
|
77
|
+
if (existsSync(projectDir)) {
|
|
78
|
+
throw new Error(`Project "${projectName}" already exists at ${projectDir}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Resolve domain template source
|
|
82
|
+
let domainDir = null;
|
|
83
|
+
if (domain !== 'generic') {
|
|
84
|
+
domainDir = join(TEMPLATES_DIR, 'domains', domain);
|
|
85
|
+
if (!existsSync(domainDir)) {
|
|
86
|
+
const domains = await listDomains();
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Domain "${domain}" not found.\n` +
|
|
89
|
+
`Available: generic, ${domains.join(', ')}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(`\x1b[1msdlab init\x1b[0m`);
|
|
95
|
+
console.log(` Project: ${projectName}`);
|
|
96
|
+
console.log(` Domain: ${domain}`);
|
|
97
|
+
console.log(` Path: ${projectDir}`);
|
|
98
|
+
console.log('');
|
|
99
|
+
|
|
100
|
+
// Create directory structure
|
|
101
|
+
for (const dir of PROJECT_DIRS) {
|
|
102
|
+
await mkdir(join(projectDir, dir), { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
console.log(` \x1b[32m✓\x1b[0m Created ${PROJECT_DIRS.length} directories`);
|
|
105
|
+
|
|
106
|
+
// Copy config files from domain template or generate generic ones
|
|
107
|
+
const configFiles = ['project.json', 'constitution.json', 'lanes.json', 'rubric.json', 'terminology.json'];
|
|
108
|
+
|
|
109
|
+
if (domainDir) {
|
|
110
|
+
for (const file of configFiles) {
|
|
111
|
+
const src = join(domainDir, file);
|
|
112
|
+
if (existsSync(src)) {
|
|
113
|
+
let content = await readFile(src, 'utf-8');
|
|
114
|
+
// Replace template project name
|
|
115
|
+
if (file === 'project.json') {
|
|
116
|
+
const parsed = JSON.parse(content);
|
|
117
|
+
parsed.name = projectName;
|
|
118
|
+
content = JSON.stringify(parsed, null, 2) + '\n';
|
|
119
|
+
}
|
|
120
|
+
await writeFile(join(projectDir, file), content);
|
|
121
|
+
console.log(` \x1b[32m✓\x1b[0m ${file} (from ${domain} template)`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// Generic starters
|
|
126
|
+
await writeFile(join(projectDir, 'project.json'), JSON.stringify({
|
|
127
|
+
name: projectName,
|
|
128
|
+
display_name: projectName.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
|
129
|
+
domain: 'generic',
|
|
130
|
+
version: '0.1.0',
|
|
131
|
+
defaults: {
|
|
132
|
+
checkpoint: '',
|
|
133
|
+
loras: [],
|
|
134
|
+
width: 1024,
|
|
135
|
+
height: 1024,
|
|
136
|
+
steps: 8,
|
|
137
|
+
cfg: 2.0,
|
|
138
|
+
sampler: 'dpmpp_sde',
|
|
139
|
+
scheduler: 'karras',
|
|
140
|
+
comfy_url: 'http://127.0.0.1:8188'
|
|
141
|
+
}
|
|
142
|
+
}, null, 2) + '\n');
|
|
143
|
+
|
|
144
|
+
await writeFile(join(projectDir, 'constitution.json'), JSON.stringify({
|
|
145
|
+
version: '0.1.0',
|
|
146
|
+
rules: [
|
|
147
|
+
{ id: 'STY-001', category: 'style', desc: 'Consistent visual style across all assets',
|
|
148
|
+
dims: ['style_consistency'], faction_specific: false,
|
|
149
|
+
rationale_pass: 'Style consistency achieved (${scorePct}%)',
|
|
150
|
+
rationale_fail: 'Style inconsistent (${scorePct}%)' },
|
|
151
|
+
{ id: 'CMP-001', category: 'composition', desc: 'Clean composition and framing',
|
|
152
|
+
dims: ['composition'], faction_specific: false,
|
|
153
|
+
rationale_pass: 'Composition reads clearly (${scorePct}%)',
|
|
154
|
+
rationale_fail: 'Composition unclear or cluttered (${scorePct}%)' },
|
|
155
|
+
]
|
|
156
|
+
}, null, 2) + '\n');
|
|
157
|
+
|
|
158
|
+
await writeFile(join(projectDir, 'lanes.json'), JSON.stringify({
|
|
159
|
+
default_lane: 'default',
|
|
160
|
+
lanes: [
|
|
161
|
+
{ id: 'default', label: 'Default', description: 'All assets', id_patterns: [] }
|
|
162
|
+
]
|
|
163
|
+
}, null, 2) + '\n');
|
|
164
|
+
|
|
165
|
+
await writeFile(join(projectDir, 'rubric.json'), JSON.stringify({
|
|
166
|
+
dimensions: ['style_consistency', 'composition', 'detail', 'originality'],
|
|
167
|
+
thresholds: {
|
|
168
|
+
approved: { pass: 0.7, partial: 0.5 },
|
|
169
|
+
borderline: { pass: 0.7, partial: 0.5 },
|
|
170
|
+
rejected: { fail_ceiling: 0.4, partial_ceiling: 0.6 }
|
|
171
|
+
},
|
|
172
|
+
failure_to_rules: {}
|
|
173
|
+
}, null, 2) + '\n');
|
|
174
|
+
|
|
175
|
+
await writeFile(join(projectDir, 'terminology.json'), JSON.stringify({
|
|
176
|
+
group_label: 'group',
|
|
177
|
+
id_detection_order: [],
|
|
178
|
+
prompt_detection_order: [],
|
|
179
|
+
groups: {},
|
|
180
|
+
cross_group_patterns: {},
|
|
181
|
+
edge_defaults: {},
|
|
182
|
+
null_faction_patterns: [],
|
|
183
|
+
id_fallbacks: {},
|
|
184
|
+
faction_context: {}
|
|
185
|
+
}, null, 2) + '\n');
|
|
186
|
+
|
|
187
|
+
for (const file of configFiles) {
|
|
188
|
+
console.log(` \x1b[32m✓\x1b[0m ${file} (generic starter)`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Copy canon markdown templates
|
|
193
|
+
const canonTemplates = ['constitution.md', 'review-rubric.md'];
|
|
194
|
+
for (const file of canonTemplates) {
|
|
195
|
+
const src = join(TEMPLATES_DIR, 'canon', file);
|
|
196
|
+
if (existsSync(src)) {
|
|
197
|
+
await copyFile(src, join(projectDir, 'canon', file));
|
|
198
|
+
console.log(` \x1b[32m✓\x1b[0m canon/${file}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Copy example prompt pack
|
|
203
|
+
const examplePack = join(TEMPLATES_DIR, 'inputs', 'prompts', 'example-wave.json');
|
|
204
|
+
if (existsSync(examplePack)) {
|
|
205
|
+
await copyFile(examplePack, join(projectDir, 'inputs', 'prompts', 'example-wave.json'));
|
|
206
|
+
console.log(` \x1b[32m✓\x1b[0m inputs/prompts/example-wave.json`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log(`\x1b[32m✓\x1b[0m Project "${projectName}" initialized`);
|
|
211
|
+
console.log('');
|
|
212
|
+
console.log('Next steps:');
|
|
213
|
+
console.log(` 1. Edit ${projectName}/canon/constitution.md — define your visual rules`);
|
|
214
|
+
console.log(` 2. Edit ${projectName}/constitution.json — encode rules for canon-bind`);
|
|
215
|
+
console.log(` 3. Create prompt packs in ${projectName}/inputs/prompts/`);
|
|
216
|
+
console.log(` 4. Run: sdlab generate <pack> --project ${projectName}`);
|
|
217
|
+
console.log(` 5. Run: sdlab project doctor --project ${projectName}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Direct execution guard
|
|
221
|
+
if (process.argv[1] && (process.argv[1].endsWith('init.js') || process.argv[1].endsWith('init'))) {
|
|
222
|
+
run().catch((err) => {
|
|
223
|
+
console.error(err.message || err);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -12,43 +12,33 @@
|
|
|
12
12
|
* Usage:
|
|
13
13
|
* node scripts/migrate-records.js --dry-run # Preview changes
|
|
14
14
|
* node scripts/migrate-records.js # Apply changes
|
|
15
|
+
* sdlab project migrate --dry-run --project star-freight
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import { readdir, readFile, writeFile, access } from 'node:fs/promises';
|
|
18
19
|
import { join, basename } from 'node:path';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
const __dirname = new URL('.', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
|
|
22
|
-
const REPO_ROOT = join(__dirname, '..');
|
|
23
|
-
const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
|
|
24
|
-
const GAME_ROOT = join(REPO_ROOT, 'games', GAME);
|
|
25
|
-
const RECORDS_DIR = join(GAME_ROOT, 'records');
|
|
26
|
-
const DRY_RUN = process.argv.includes('--dry-run');
|
|
20
|
+
import { getProjectName } from '../lib/args.js';
|
|
21
|
+
import { REPO_ROOT } from '../lib/paths.js';
|
|
27
22
|
|
|
28
23
|
async function fileExists(p) {
|
|
29
24
|
try { await access(p); return true; } catch { return false; }
|
|
30
25
|
}
|
|
31
26
|
|
|
32
27
|
function isCuratorDivergent(rec) {
|
|
33
|
-
// Shape 2/4: has verdict at top level, judgment is null
|
|
34
28
|
return rec.judgment === null && typeof rec.verdict === 'string' && !rec.identity;
|
|
35
29
|
}
|
|
36
30
|
|
|
37
31
|
function isIdentityRecord(rec) {
|
|
38
|
-
// Shape 3: has identity block — Grounded pipeline, DO NOT TOUCH
|
|
39
32
|
return rec.identity != null;
|
|
40
33
|
}
|
|
41
34
|
|
|
42
|
-
async function migrateRecord(filePath) {
|
|
35
|
+
async function migrateRecord(filePath, GAME_ROOT, DRY_RUN) {
|
|
43
36
|
const raw = await readFile(filePath, 'utf-8');
|
|
44
37
|
const rec = JSON.parse(raw);
|
|
45
38
|
|
|
46
|
-
// Skip identity records entirely — Grounded pipeline
|
|
47
39
|
if (isIdentityRecord(rec)) return { file: basename(filePath), action: 'skip-identity' };
|
|
48
40
|
|
|
49
|
-
// Skip records that are already in standard shape
|
|
50
41
|
if (!isCuratorDivergent(rec)) {
|
|
51
|
-
// Still check for stale asset_path on standard records
|
|
52
42
|
if (rec.asset_path && rec.asset_path.includes('outputs/candidates/')) {
|
|
53
43
|
const approvedPath = rec.asset_path.replace('outputs/candidates/', 'outputs/approved/');
|
|
54
44
|
const fullApproved = join(GAME_ROOT, approvedPath);
|
|
@@ -61,10 +51,8 @@ async function migrateRecord(filePath) {
|
|
|
61
51
|
return { file: basename(filePath), action: 'skip-standard' };
|
|
62
52
|
}
|
|
63
53
|
|
|
64
|
-
// Migrate curator-divergent record
|
|
65
54
|
const changes = [];
|
|
66
55
|
|
|
67
|
-
// Build judgment object from top-level fields
|
|
68
56
|
rec.judgment = {
|
|
69
57
|
status: rec.verdict || 'unknown',
|
|
70
58
|
reviewer: rec.curator || 'wave25-script',
|
|
@@ -77,7 +65,6 @@ async function migrateRecord(filePath) {
|
|
|
77
65
|
};
|
|
78
66
|
changes.push(`verdict="${rec.verdict}" → judgment.status`);
|
|
79
67
|
|
|
80
|
-
// Remove top-level divergent fields
|
|
81
68
|
delete rec.verdict;
|
|
82
69
|
delete rec.scores;
|
|
83
70
|
delete rec.explanation;
|
|
@@ -86,7 +73,6 @@ async function migrateRecord(filePath) {
|
|
|
86
73
|
delete rec.curator;
|
|
87
74
|
changes.push('removed top-level verdict/scores/explanation/failures/curated_at/curator');
|
|
88
75
|
|
|
89
|
-
// Fix asset_path if image moved to approved/
|
|
90
76
|
if (rec.asset_path && rec.asset_path.includes('outputs/candidates/')) {
|
|
91
77
|
const approvedPath = rec.asset_path.replace('outputs/candidates/', 'outputs/approved/');
|
|
92
78
|
const fullApproved = join(GAME_ROOT, approvedPath);
|
|
@@ -105,7 +91,12 @@ async function migrateRecord(filePath) {
|
|
|
105
91
|
return { file: basename(filePath), action: 'migrated', changes };
|
|
106
92
|
}
|
|
107
93
|
|
|
108
|
-
async function
|
|
94
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
95
|
+
const projectName = getProjectName(argv);
|
|
96
|
+
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
97
|
+
const RECORDS_DIR = join(GAME_ROOT, 'records');
|
|
98
|
+
const DRY_RUN = argv.includes('--dry-run');
|
|
99
|
+
|
|
109
100
|
console.log(DRY_RUN ? '=== DRY RUN ===' : '=== APPLYING MIGRATION ===');
|
|
110
101
|
|
|
111
102
|
const files = (await readdir(RECORDS_DIR)).filter(f => f.endsWith('.json'));
|
|
@@ -115,7 +106,7 @@ async function main() {
|
|
|
115
106
|
|
|
116
107
|
for (const file of files) {
|
|
117
108
|
try {
|
|
118
|
-
const result = await migrateRecord(join(RECORDS_DIR, file));
|
|
109
|
+
const result = await migrateRecord(join(RECORDS_DIR, file), GAME_ROOT, DRY_RUN);
|
|
119
110
|
switch (result.action) {
|
|
120
111
|
case 'skip-standard': results.skipped++; break;
|
|
121
112
|
case 'skip-identity': results.identity++; break;
|
|
@@ -144,4 +135,7 @@ async function main() {
|
|
|
144
135
|
console.log(`Total: ${files.length}`);
|
|
145
136
|
}
|
|
146
137
|
|
|
147
|
-
|
|
138
|
+
// Direct execution guard
|
|
139
|
+
if (process.argv[1] && (process.argv[1].endsWith('migrate-records.js') || process.argv[1].endsWith('migrate-records'))) {
|
|
140
|
+
run().catch(err => { console.error(err.message || err); process.exit(1); });
|
|
141
|
+
}
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* painterly-test.js — Test different denoise levels on a single image.
|
|
5
5
|
* Outputs: outputs/painterly-test/<filename>_d<denoise>.png
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node scripts/painterly-test.js
|
|
9
|
+
* sdlab painterly:test --project star-freight
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
|
-
import {
|
|
12
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
9
13
|
import { join } from "node:path";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
|
|
14
|
-
const GAME_ROOT = join(REPO_ROOT, 'games', GAME);
|
|
14
|
+
import { getProjectName } from "../lib/args.js";
|
|
15
|
+
import { REPO_ROOT } from "../lib/paths.js";
|
|
16
|
+
import { submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
|
|
15
17
|
|
|
16
18
|
const PAINTERLY_PROMPT = [
|
|
17
19
|
"oil painting, visible brushstrokes, painterly concept art,",
|
|
@@ -68,43 +70,11 @@ function buildWorkflow(imagePath, denoise, loraWeight, seed) {
|
|
|
68
70
|
return nodes;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
async function
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
form.append("overwrite", "true");
|
|
76
|
-
const res = await fetch(`${COMFY_URL}/upload/image`, { method: "POST", body: form });
|
|
77
|
-
if (!res.ok) throw new Error(`Upload failed: ${res.status} ${await res.text()}`);
|
|
78
|
-
return (await res.json()).name;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const MAX_POLL_MS = 600_000; // 10 minutes
|
|
82
|
-
|
|
83
|
-
async function run(workflow) {
|
|
84
|
-
const res = await fetch(`${COMFY_URL}/prompt`, {
|
|
85
|
-
method: "POST", headers: { "Content-Type": "application/json" },
|
|
86
|
-
body: JSON.stringify({ prompt: workflow, client_id: `test-${Date.now()}` }),
|
|
87
|
-
});
|
|
88
|
-
if (!res.ok) throw new Error(`ComfyUI submit failed: ${res.status} ${await res.text()}`);
|
|
89
|
-
const { prompt_id: promptId } = await res.json();
|
|
90
|
-
const start = Date.now();
|
|
91
|
-
while (true) {
|
|
92
|
-
if (Date.now() - start > MAX_POLL_MS) throw new Error(`ComfyUI poll timeout after ${MAX_POLL_MS/1000}s for prompt ${promptId}`);
|
|
93
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
94
|
-
const histRes = await fetch(`${COMFY_URL}/history/${promptId}`);
|
|
95
|
-
if (!histRes.ok) continue;
|
|
96
|
-
const h = await histRes.json();
|
|
97
|
-
if (h[promptId]?.status?.completed) return h[promptId];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function download(filename, subfolder) {
|
|
102
|
-
const res = await fetch(`${COMFY_URL}/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder || "")}&type=output`);
|
|
103
|
-
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
104
|
-
return Buffer.from(await res.arrayBuffer());
|
|
105
|
-
}
|
|
73
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
74
|
+
const projectName = getProjectName(argv);
|
|
75
|
+
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
76
|
+
const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
|
|
106
77
|
|
|
107
|
-
async function main() {
|
|
108
78
|
const outDir = join(GAME_ROOT, "outputs/painterly-test");
|
|
109
79
|
await mkdir(outDir, { recursive: true });
|
|
110
80
|
|
|
@@ -115,14 +85,14 @@ async function main() {
|
|
|
115
85
|
];
|
|
116
86
|
|
|
117
87
|
const denoiseTests = [0.50, 0.60, 0.70];
|
|
118
|
-
const loraWeight = 1.0;
|
|
88
|
+
const loraWeight = 1.0;
|
|
119
89
|
|
|
120
90
|
for (const testFile of testFiles) {
|
|
121
91
|
const srcPath = join(GAME_ROOT, testFile);
|
|
122
92
|
const fname = testFile.split("/").pop().replace(".png", "");
|
|
123
93
|
|
|
124
94
|
console.log(`\nTesting: ${fname}`);
|
|
125
|
-
const uploaded = await
|
|
95
|
+
const uploaded = await uploadImage(srcPath, `test_${fname}.png`, COMFY_URL);
|
|
126
96
|
|
|
127
97
|
for (const denoise of denoiseTests) {
|
|
128
98
|
const label = `${fname}_d${String(denoise).replace(".", "")}`;
|
|
@@ -130,14 +100,14 @@ async function main() {
|
|
|
130
100
|
|
|
131
101
|
const t = Date.now();
|
|
132
102
|
const wf = buildWorkflow(uploaded, denoise, loraWeight, 42);
|
|
133
|
-
const result = await
|
|
103
|
+
const result = await submitAndWait(wf, COMFY_URL, { clientPrefix: 'test' });
|
|
134
104
|
|
|
135
105
|
let imgFile, imgSub = "";
|
|
136
106
|
for (const o of Object.values(result.outputs || {})) {
|
|
137
107
|
if (o.images?.length) { imgFile = o.images[0].filename; imgSub = o.images[0].subfolder || ""; break; }
|
|
138
108
|
}
|
|
139
109
|
|
|
140
|
-
const data = await
|
|
110
|
+
const data = await downloadImage(imgFile, imgSub, COMFY_URL);
|
|
141
111
|
await writeFile(join(outDir, `${label}.png`), data);
|
|
142
112
|
console.log(` ✓ ${Date.now() - t}ms`);
|
|
143
113
|
}
|
|
@@ -146,4 +116,7 @@ async function main() {
|
|
|
146
116
|
console.log("\nDone — check outputs/painterly-test/");
|
|
147
117
|
}
|
|
148
118
|
|
|
149
|
-
|
|
119
|
+
// Direct execution guard
|
|
120
|
+
if (process.argv[1] && (process.argv[1].endsWith('painterly-test.js') || process.argv[1].endsWith('painterly-test'))) {
|
|
121
|
+
run().catch(e => { console.error(e.message || e); process.exit(1); });
|
|
122
|
+
}
|