@mcptoolshop/style-dataset-lab 1.1.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 +37 -0
- package/LICENSE +21 -0
- package/README.es.md +142 -0
- package/README.fr.md +142 -0
- package/README.hi.md +142 -0
- package/README.it.md +142 -0
- package/README.ja.md +142 -0
- package/README.md +178 -0
- package/README.pt-BR.md +142 -0
- package/README.zh.md +142 -0
- package/SECURITY.md +28 -0
- package/package.json +45 -0
- package/scripts/bulk-curate-wave2-5.js +138 -0
- package/scripts/bulk-curate-waves11-18.js +413 -0
- package/scripts/canon-bind.js +437 -0
- package/scripts/compare.js +116 -0
- package/scripts/curate-wave25.js +209 -0
- package/scripts/curate.js +136 -0
- package/scripts/generate-controlnet.js +357 -0
- package/scripts/generate-identity.js +633 -0
- package/scripts/generate-ipadapter.js +339 -0
- package/scripts/generate.js +351 -0
- package/scripts/migrate-records.js +147 -0
- package/scripts/painterly-test.js +149 -0
- package/scripts/painterly.js +349 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* migrate-records.js — One-time migration for curator-divergent records (Shape 2 + Shape 4)
|
|
4
|
+
*
|
|
5
|
+
* Normalizes verdict/scores/explanation/failures/curated_at/curator at top level
|
|
6
|
+
* into the standard judgment.{status,criteria_scores,explanation,failure_modes,reviewed_at,reviewer} shape.
|
|
7
|
+
*
|
|
8
|
+
* Also fixes asset_path from outputs/candidates/ to outputs/approved/ when the image exists in approved/.
|
|
9
|
+
*
|
|
10
|
+
* DOES NOT TOUCH Shape 3 (identity packet) records — those have judgment=null by design.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node scripts/migrate-records.js --dry-run # Preview changes
|
|
14
|
+
* node scripts/migrate-records.js # Apply changes
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readdir, readFile, writeFile, access } from 'node:fs/promises';
|
|
18
|
+
import { join, basename } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
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');
|
|
27
|
+
|
|
28
|
+
async function fileExists(p) {
|
|
29
|
+
try { await access(p); return true; } catch { return false; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isCuratorDivergent(rec) {
|
|
33
|
+
// Shape 2/4: has verdict at top level, judgment is null
|
|
34
|
+
return rec.judgment === null && typeof rec.verdict === 'string' && !rec.identity;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isIdentityRecord(rec) {
|
|
38
|
+
// Shape 3: has identity block — Grounded pipeline, DO NOT TOUCH
|
|
39
|
+
return rec.identity != null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function migrateRecord(filePath) {
|
|
43
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
44
|
+
const rec = JSON.parse(raw);
|
|
45
|
+
|
|
46
|
+
// Skip identity records entirely — Grounded pipeline
|
|
47
|
+
if (isIdentityRecord(rec)) return { file: basename(filePath), action: 'skip-identity' };
|
|
48
|
+
|
|
49
|
+
// Skip records that are already in standard shape
|
|
50
|
+
if (!isCuratorDivergent(rec)) {
|
|
51
|
+
// Still check for stale asset_path on standard records
|
|
52
|
+
if (rec.asset_path && rec.asset_path.includes('outputs/candidates/')) {
|
|
53
|
+
const approvedPath = rec.asset_path.replace('outputs/candidates/', 'outputs/approved/');
|
|
54
|
+
const fullApproved = join(GAME_ROOT, approvedPath);
|
|
55
|
+
if (await fileExists(fullApproved)) {
|
|
56
|
+
rec.asset_path = approvedPath;
|
|
57
|
+
if (!DRY_RUN) await writeFile(filePath, JSON.stringify(rec, null, 2) + '\n');
|
|
58
|
+
return { file: basename(filePath), action: 'path-fix-only', from: 'candidates/', to: 'approved/' };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { file: basename(filePath), action: 'skip-standard' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Migrate curator-divergent record
|
|
65
|
+
const changes = [];
|
|
66
|
+
|
|
67
|
+
// Build judgment object from top-level fields
|
|
68
|
+
rec.judgment = {
|
|
69
|
+
status: rec.verdict || 'unknown',
|
|
70
|
+
reviewer: rec.curator || 'wave25-script',
|
|
71
|
+
reviewed_at: rec.curated_at || new Date().toISOString(),
|
|
72
|
+
explanation: rec.explanation || '',
|
|
73
|
+
criteria_scores: rec.scores || {},
|
|
74
|
+
failure_modes: rec.failures || [],
|
|
75
|
+
improvement_notes: null,
|
|
76
|
+
confidence: 0.8,
|
|
77
|
+
};
|
|
78
|
+
changes.push(`verdict="${rec.verdict}" → judgment.status`);
|
|
79
|
+
|
|
80
|
+
// Remove top-level divergent fields
|
|
81
|
+
delete rec.verdict;
|
|
82
|
+
delete rec.scores;
|
|
83
|
+
delete rec.explanation;
|
|
84
|
+
delete rec.failures;
|
|
85
|
+
delete rec.curated_at;
|
|
86
|
+
delete rec.curator;
|
|
87
|
+
changes.push('removed top-level verdict/scores/explanation/failures/curated_at/curator');
|
|
88
|
+
|
|
89
|
+
// Fix asset_path if image moved to approved/
|
|
90
|
+
if (rec.asset_path && rec.asset_path.includes('outputs/candidates/')) {
|
|
91
|
+
const approvedPath = rec.asset_path.replace('outputs/candidates/', 'outputs/approved/');
|
|
92
|
+
const fullApproved = join(GAME_ROOT, approvedPath);
|
|
93
|
+
if (await fileExists(fullApproved)) {
|
|
94
|
+
rec.asset_path = approvedPath;
|
|
95
|
+
changes.push(`asset_path: candidates/ → approved/`);
|
|
96
|
+
} else {
|
|
97
|
+
changes.push(`asset_path: candidates/ (image not in approved/, kept as-is)`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!DRY_RUN) {
|
|
102
|
+
await writeFile(filePath, JSON.stringify(rec, null, 2) + '\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { file: basename(filePath), action: 'migrated', changes };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function main() {
|
|
109
|
+
console.log(DRY_RUN ? '=== DRY RUN ===' : '=== APPLYING MIGRATION ===');
|
|
110
|
+
|
|
111
|
+
const files = (await readdir(RECORDS_DIR)).filter(f => f.endsWith('.json'));
|
|
112
|
+
console.log(`Found ${files.length} record files\n`);
|
|
113
|
+
|
|
114
|
+
const results = { skipped: 0, migrated: 0, pathFixed: 0, identity: 0, errors: 0 };
|
|
115
|
+
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
try {
|
|
118
|
+
const result = await migrateRecord(join(RECORDS_DIR, file));
|
|
119
|
+
switch (result.action) {
|
|
120
|
+
case 'skip-standard': results.skipped++; break;
|
|
121
|
+
case 'skip-identity': results.identity++; break;
|
|
122
|
+
case 'migrated':
|
|
123
|
+
results.migrated++;
|
|
124
|
+
console.log(`MIGRATE: ${result.file}`);
|
|
125
|
+
result.changes.forEach(c => console.log(` → ${c}`));
|
|
126
|
+
break;
|
|
127
|
+
case 'path-fix-only':
|
|
128
|
+
results.pathFixed++;
|
|
129
|
+
console.log(`PATH-FIX: ${result.file} (${result.from} → ${result.to})`);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
results.errors++;
|
|
134
|
+
console.error(`ERROR: ${file}: ${err.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`\n=== Summary ===`);
|
|
139
|
+
console.log(`Standard (unchanged): ${results.skipped}`);
|
|
140
|
+
console.log(`Identity (untouched): ${results.identity}`);
|
|
141
|
+
console.log(`Migrated (schema fix): ${results.migrated}`);
|
|
142
|
+
console.log(`Path-fixed only: ${results.pathFixed}`);
|
|
143
|
+
console.log(`Errors: ${results.errors}`);
|
|
144
|
+
console.log(`Total: ${files.length}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
main().catch(err => { console.error(err); process.exit(1); });
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* painterly-test.js — Test different denoise levels on a single image.
|
|
5
|
+
* Outputs: outputs/painterly-test/<filename>_d<denoise>.png
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
|
|
12
|
+
const REPO_ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
|
|
13
|
+
const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
|
|
14
|
+
const GAME_ROOT = join(REPO_ROOT, 'games', GAME);
|
|
15
|
+
|
|
16
|
+
const PAINTERLY_PROMPT = [
|
|
17
|
+
"oil painting, visible brushstrokes, painterly concept art,",
|
|
18
|
+
"semi-realistic, muted dusty palette, subtle dark edges,",
|
|
19
|
+
"atmospheric directional lighting, traditional media texture,",
|
|
20
|
+
"canvas grain, impasto highlights, soft color blending,",
|
|
21
|
+
"art station quality concept illustration"
|
|
22
|
+
].join(" ");
|
|
23
|
+
|
|
24
|
+
const PAINTERLY_NEGATIVE = [
|
|
25
|
+
"photorealistic, photograph, photo, 3d render, smooth CG, CGI,",
|
|
26
|
+
"octane render, sharp photo detail, camera noise, lens flare,",
|
|
27
|
+
"film grain, bokeh, depth of field, DSLR, ultra realistic,",
|
|
28
|
+
"text, watermark, signature, blurry, low quality"
|
|
29
|
+
].join(" ");
|
|
30
|
+
|
|
31
|
+
function buildWorkflow(imagePath, denoise, loraWeight, seed) {
|
|
32
|
+
const nodes = {};
|
|
33
|
+
let n = 1;
|
|
34
|
+
|
|
35
|
+
const ckpt = String(n++);
|
|
36
|
+
nodes[ckpt] = { class_type: "CheckpointLoaderSimple", inputs: { ckpt_name: "dreamshaperXL_v21TurboDPMSDE.safetensors" } };
|
|
37
|
+
|
|
38
|
+
let model = [ckpt, 0], clip = [ckpt, 1];
|
|
39
|
+
|
|
40
|
+
const lora = String(n++);
|
|
41
|
+
nodes[lora] = { class_type: "LoraLoader", inputs: { model, clip, lora_name: "classipeintxl_v21.safetensors", strength_model: loraWeight, strength_clip: loraWeight } };
|
|
42
|
+
model = [lora, 0]; clip = [lora, 1];
|
|
43
|
+
|
|
44
|
+
const img = String(n++);
|
|
45
|
+
nodes[img] = { class_type: "LoadImage", inputs: { image: imagePath } };
|
|
46
|
+
|
|
47
|
+
const enc = String(n++);
|
|
48
|
+
nodes[enc] = { class_type: "VAEEncode", inputs: { pixels: [img, 0], vae: [ckpt, 2] } };
|
|
49
|
+
|
|
50
|
+
const pos = String(n++);
|
|
51
|
+
nodes[pos] = { class_type: "CLIPTextEncode", inputs: { text: PAINTERLY_PROMPT, clip } };
|
|
52
|
+
|
|
53
|
+
const neg = String(n++);
|
|
54
|
+
nodes[neg] = { class_type: "CLIPTextEncode", inputs: { text: PAINTERLY_NEGATIVE, clip } };
|
|
55
|
+
|
|
56
|
+
const samp = String(n++);
|
|
57
|
+
nodes[samp] = { class_type: "KSampler", inputs: {
|
|
58
|
+
model, positive: [pos, 0], negative: [neg, 0], latent_image: [enc, 0],
|
|
59
|
+
seed, steps: 10, cfg: 2.5, sampler_name: "dpmpp_sde", scheduler: "karras", denoise
|
|
60
|
+
}};
|
|
61
|
+
|
|
62
|
+
const dec = String(n++);
|
|
63
|
+
nodes[dec] = { class_type: "VAEDecode", inputs: { samples: [samp, 0], vae: [ckpt, 2] } };
|
|
64
|
+
|
|
65
|
+
const save = String(n++);
|
|
66
|
+
nodes[save] = { class_type: "SaveImage", inputs: { images: [dec, 0], filename_prefix: "ptest" } };
|
|
67
|
+
|
|
68
|
+
return nodes;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function upload(filePath, filename) {
|
|
72
|
+
const data = await readFile(filePath);
|
|
73
|
+
const form = new FormData();
|
|
74
|
+
form.append("image", new Blob([data], { type: "image/png" }), filename);
|
|
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
|
+
}
|
|
106
|
+
|
|
107
|
+
async function main() {
|
|
108
|
+
const outDir = join(GAME_ROOT, "outputs/painterly-test");
|
|
109
|
+
await mkdir(outDir, { recursive: true });
|
|
110
|
+
|
|
111
|
+
// Pick two test images — one character, one environment
|
|
112
|
+
const testFiles = [
|
|
113
|
+
"outputs/approved/arch_keth_hex_chamber_v1.png",
|
|
114
|
+
"outputs/approved/anchor_04_pilot_v1.png",
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const denoiseTests = [0.50, 0.60, 0.70];
|
|
118
|
+
const loraWeight = 1.0; // Full LoRA weight for stronger effect
|
|
119
|
+
|
|
120
|
+
for (const testFile of testFiles) {
|
|
121
|
+
const srcPath = join(GAME_ROOT, testFile);
|
|
122
|
+
const fname = testFile.split("/").pop().replace(".png", "");
|
|
123
|
+
|
|
124
|
+
console.log(`\nTesting: ${fname}`);
|
|
125
|
+
const uploaded = await upload(srcPath, `test_${fname}.png`);
|
|
126
|
+
|
|
127
|
+
for (const denoise of denoiseTests) {
|
|
128
|
+
const label = `${fname}_d${String(denoise).replace(".", "")}`;
|
|
129
|
+
console.log(` denoise=${denoise}, lora=${loraWeight} → ${label}.png`);
|
|
130
|
+
|
|
131
|
+
const t = Date.now();
|
|
132
|
+
const wf = buildWorkflow(uploaded, denoise, loraWeight, 42);
|
|
133
|
+
const result = await run(wf);
|
|
134
|
+
|
|
135
|
+
let imgFile, imgSub = "";
|
|
136
|
+
for (const o of Object.values(result.outputs || {})) {
|
|
137
|
+
if (o.images?.length) { imgFile = o.images[0].filename; imgSub = o.images[0].subfolder || ""; break; }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const data = await download(imgFile, imgSub);
|
|
141
|
+
await writeFile(join(outDir, `${label}.png`), data);
|
|
142
|
+
console.log(` ✓ ${Date.now() - t}ms`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log("\nDone — check outputs/painterly-test/");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* painterly.js — img2img post-processing pass via ComfyUI.
|
|
5
|
+
*
|
|
6
|
+
* Takes existing candidate/approved images and runs them through an img2img
|
|
7
|
+
* pass with a painterly style prompt at low denoise to shift from
|
|
8
|
+
* photorealistic to concept-art aesthetic while preserving composition.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node scripts/painterly.js [--source outputs/approved] [--limit 100] [--offset 0]
|
|
12
|
+
* node scripts/painterly.js --dry-run
|
|
13
|
+
*
|
|
14
|
+
* Output: outputs/painterly/<original_filename>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFile, writeFile, mkdir, readdir, copyFile, stat } from "node:fs/promises";
|
|
18
|
+
import { join, basename, extname } from "node:path";
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
20
|
+
|
|
21
|
+
const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
|
|
22
|
+
const REPO_ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
|
|
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
|
+
|
|
26
|
+
// ── Config ──
|
|
27
|
+
|
|
28
|
+
const PAINTERLY_PROMPT = [
|
|
29
|
+
"oil painting, visible brushstrokes, painterly concept art,",
|
|
30
|
+
"semi-realistic, muted dusty palette, subtle dark edges,",
|
|
31
|
+
"atmospheric directional lighting, traditional media texture,",
|
|
32
|
+
"canvas grain, impasto highlights, soft color blending,",
|
|
33
|
+
"art station quality concept illustration"
|
|
34
|
+
].join(" ");
|
|
35
|
+
|
|
36
|
+
const PAINTERLY_NEGATIVE = [
|
|
37
|
+
"photorealistic, photograph, photo, 3d render, smooth CG, CGI,",
|
|
38
|
+
"octane render, sharp photo detail, camera noise, lens flare,",
|
|
39
|
+
"film grain, bokeh, depth of field, DSLR, ultra realistic,",
|
|
40
|
+
"text, watermark, signature, blurry, low quality"
|
|
41
|
+
].join(" ");
|
|
42
|
+
|
|
43
|
+
const DEFAULTS = {
|
|
44
|
+
checkpoint: "dreamshaperXL_v21TurboDPMSDE.safetensors",
|
|
45
|
+
lora: { name: "classipeintxl_v21.safetensors", weight: 1.0 },
|
|
46
|
+
denoise: 0.50, // Tested sweet spot — visible painterly texture, composition preserved
|
|
47
|
+
steps: 10,
|
|
48
|
+
cfg: 2.5,
|
|
49
|
+
sampler: "dpmpp_sde",
|
|
50
|
+
scheduler: "karras",
|
|
51
|
+
seed: 42, // Fixed seed for reproducibility across batch
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ── ComfyUI helpers ──
|
|
55
|
+
|
|
56
|
+
async function comfyHealth() {
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(`${COMFY_URL}/system_stats`);
|
|
59
|
+
return res.ok;
|
|
60
|
+
} catch { return false; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildImg2ImgWorkflow(imagePath, seed) {
|
|
64
|
+
const nodes = {};
|
|
65
|
+
let nextId = 1;
|
|
66
|
+
|
|
67
|
+
// Checkpoint loader
|
|
68
|
+
const ckptId = String(nextId++);
|
|
69
|
+
nodes[ckptId] = {
|
|
70
|
+
class_type: "CheckpointLoaderSimple",
|
|
71
|
+
inputs: { ckpt_name: DEFAULTS.checkpoint },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let modelOut = [ckptId, 0];
|
|
75
|
+
let clipOut = [ckptId, 1];
|
|
76
|
+
|
|
77
|
+
// LoRA — ClassipeintXL at reduced weight for style shift
|
|
78
|
+
const loraId = String(nextId++);
|
|
79
|
+
nodes[loraId] = {
|
|
80
|
+
class_type: "LoraLoader",
|
|
81
|
+
inputs: {
|
|
82
|
+
model: modelOut,
|
|
83
|
+
clip: clipOut,
|
|
84
|
+
lora_name: DEFAULTS.lora.name,
|
|
85
|
+
strength_model: DEFAULTS.lora.weight,
|
|
86
|
+
strength_clip: DEFAULTS.lora.weight,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
modelOut = [loraId, 0];
|
|
90
|
+
clipOut = [loraId, 1];
|
|
91
|
+
|
|
92
|
+
// Load source image
|
|
93
|
+
const loadImgId = String(nextId++);
|
|
94
|
+
nodes[loadImgId] = {
|
|
95
|
+
class_type: "LoadImage",
|
|
96
|
+
inputs: { image: imagePath },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// VAE Encode — source image to latent
|
|
100
|
+
const vaeEncId = String(nextId++);
|
|
101
|
+
nodes[vaeEncId] = {
|
|
102
|
+
class_type: "VAEEncode",
|
|
103
|
+
inputs: {
|
|
104
|
+
pixels: [loadImgId, 0],
|
|
105
|
+
vae: [ckptId, 2],
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// CLIP Text Encode — positive (painterly style)
|
|
110
|
+
const posId = String(nextId++);
|
|
111
|
+
nodes[posId] = {
|
|
112
|
+
class_type: "CLIPTextEncode",
|
|
113
|
+
inputs: { text: PAINTERLY_PROMPT, clip: clipOut },
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// CLIP Text Encode — negative
|
|
117
|
+
const negId = String(nextId++);
|
|
118
|
+
nodes[negId] = {
|
|
119
|
+
class_type: "CLIPTextEncode",
|
|
120
|
+
inputs: { text: PAINTERLY_NEGATIVE, clip: clipOut },
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// KSampler — img2img with low denoise
|
|
124
|
+
const samplerId = String(nextId++);
|
|
125
|
+
nodes[samplerId] = {
|
|
126
|
+
class_type: "KSampler",
|
|
127
|
+
inputs: {
|
|
128
|
+
model: modelOut,
|
|
129
|
+
positive: [posId, 0],
|
|
130
|
+
negative: [negId, 0],
|
|
131
|
+
latent_image: [vaeEncId, 0],
|
|
132
|
+
seed,
|
|
133
|
+
steps: DEFAULTS.steps,
|
|
134
|
+
cfg: DEFAULTS.cfg,
|
|
135
|
+
sampler_name: DEFAULTS.sampler,
|
|
136
|
+
scheduler: DEFAULTS.scheduler,
|
|
137
|
+
denoise: DEFAULTS.denoise,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// VAE Decode
|
|
142
|
+
const vaeDecId = String(nextId++);
|
|
143
|
+
nodes[vaeDecId] = {
|
|
144
|
+
class_type: "VAEDecode",
|
|
145
|
+
inputs: {
|
|
146
|
+
samples: [samplerId, 0],
|
|
147
|
+
vae: [ckptId, 2],
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Save Image
|
|
152
|
+
const saveId = String(nextId++);
|
|
153
|
+
nodes[saveId] = {
|
|
154
|
+
class_type: "SaveImage",
|
|
155
|
+
inputs: {
|
|
156
|
+
images: [vaeDecId, 0],
|
|
157
|
+
filename_prefix: "painterly",
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return { nodes, saveNodeId: saveId };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const MAX_POLL_MS = 600_000; // 10 minutes
|
|
165
|
+
|
|
166
|
+
async function submitAndWait(workflow) {
|
|
167
|
+
const clientId = `sdl-paint-${Date.now()}`;
|
|
168
|
+
const res = await fetch(`${COMFY_URL}/prompt`, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "Content-Type": "application/json" },
|
|
171
|
+
body: JSON.stringify({ prompt: workflow, client_id: clientId }),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
const text = await res.text();
|
|
176
|
+
throw new Error(`ComfyUI submit failed: ${res.status} ${text}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const { prompt_id: promptId } = await res.json();
|
|
180
|
+
|
|
181
|
+
const start = Date.now();
|
|
182
|
+
while (true) {
|
|
183
|
+
if (Date.now() - start > MAX_POLL_MS) throw new Error(`ComfyUI poll timeout after ${MAX_POLL_MS/1000}s for prompt ${promptId}`);
|
|
184
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
185
|
+
const histRes = await fetch(`${COMFY_URL}/history/${promptId}`);
|
|
186
|
+
if (!histRes.ok) continue;
|
|
187
|
+
const history = await histRes.json();
|
|
188
|
+
const entry = history[promptId];
|
|
189
|
+
if (!entry) continue;
|
|
190
|
+
if (entry.status?.completed) return entry;
|
|
191
|
+
if (entry.status?.status_str === "error") {
|
|
192
|
+
throw new Error(`ComfyUI generation failed: ${JSON.stringify(entry.status)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function downloadImage(filename, subfolder, type = "output") {
|
|
198
|
+
const url = `${COMFY_URL}/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder || "")}&type=${type}`;
|
|
199
|
+
const res = await fetch(url);
|
|
200
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
201
|
+
return Buffer.from(await res.arrayBuffer());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Upload an image to ComfyUI's input folder so LoadImage can find it.
|
|
206
|
+
*/
|
|
207
|
+
async function uploadImage(filePath, filename) {
|
|
208
|
+
const data = await readFile(filePath);
|
|
209
|
+
const formData = new FormData();
|
|
210
|
+
formData.append("image", new Blob([data], { type: "image/png" }), filename);
|
|
211
|
+
formData.append("overwrite", "true");
|
|
212
|
+
|
|
213
|
+
const res = await fetch(`${COMFY_URL}/upload/image`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
body: formData,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (!res.ok) {
|
|
219
|
+
const text = await res.text();
|
|
220
|
+
throw new Error(`Upload failed: ${res.status} ${text}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const result = await res.json();
|
|
224
|
+
return result.name; // The name ComfyUI assigned to the uploaded image
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Main ──
|
|
228
|
+
|
|
229
|
+
async function main() {
|
|
230
|
+
const args = process.argv.slice(2);
|
|
231
|
+
const dryRun = args.includes("--dry-run");
|
|
232
|
+
|
|
233
|
+
// Parse args
|
|
234
|
+
let sourceDir = "outputs/approved";
|
|
235
|
+
let limit = 100;
|
|
236
|
+
let offset = 0;
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < args.length; i++) {
|
|
239
|
+
if (args[i] === "--source" && args[i + 1]) sourceDir = args[++i];
|
|
240
|
+
if (args[i] === "--limit" && args[i + 1]) limit = parseInt(args[++i]);
|
|
241
|
+
if (args[i] === "--offset" && args[i + 1]) offset = parseInt(args[++i]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const fullSourceDir = join(GAME_ROOT, sourceDir);
|
|
245
|
+
const outDir = join(GAME_ROOT, "outputs/painterly");
|
|
246
|
+
await mkdir(outDir, { recursive: true });
|
|
247
|
+
|
|
248
|
+
// Collect source PNGs
|
|
249
|
+
const allFiles = (await readdir(fullSourceDir))
|
|
250
|
+
.filter(f => f.endsWith(".png"))
|
|
251
|
+
.sort();
|
|
252
|
+
|
|
253
|
+
// Skip already-processed files
|
|
254
|
+
const existingPainterly = new Set(
|
|
255
|
+
existsSync(outDir) ? (await readdir(outDir)).filter(f => f.endsWith(".png")) : []
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const toProcess = allFiles
|
|
259
|
+
.filter(f => !existingPainterly.has(f))
|
|
260
|
+
.slice(offset, offset + limit);
|
|
261
|
+
|
|
262
|
+
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m painterly pass`);
|
|
263
|
+
console.log(` Source: ${sourceDir}`);
|
|
264
|
+
console.log(` Total source images: ${allFiles.length}`);
|
|
265
|
+
console.log(` Already processed: ${existingPainterly.size}`);
|
|
266
|
+
console.log(` To process: ${toProcess.length}`);
|
|
267
|
+
console.log(` Denoise: ${DEFAULTS.denoise}`);
|
|
268
|
+
console.log(` LoRA weight: ${DEFAULTS.lora.weight}`);
|
|
269
|
+
console.log("");
|
|
270
|
+
|
|
271
|
+
if (toProcess.length === 0) {
|
|
272
|
+
console.log("Nothing to process.");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!dryRun) {
|
|
277
|
+
const online = await comfyHealth();
|
|
278
|
+
if (!online) {
|
|
279
|
+
console.error("\x1b[31mError:\x1b[0m ComfyUI not reachable at " + COMFY_URL);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
console.log("\x1b[32m✓\x1b[0m ComfyUI online");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let processed = 0;
|
|
286
|
+
let errors = 0;
|
|
287
|
+
|
|
288
|
+
for (const file of toProcess) {
|
|
289
|
+
const srcPath = join(fullSourceDir, file);
|
|
290
|
+
console.log(` [${processed + 1}/${toProcess.length}] ${file}`);
|
|
291
|
+
|
|
292
|
+
if (dryRun) {
|
|
293
|
+
processed++;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const startMs = Date.now();
|
|
299
|
+
|
|
300
|
+
// Upload source image to ComfyUI
|
|
301
|
+
const uploadedName = await uploadImage(srcPath, file);
|
|
302
|
+
|
|
303
|
+
// Build and submit img2img workflow
|
|
304
|
+
const { nodes } = buildImg2ImgWorkflow(uploadedName, DEFAULTS.seed);
|
|
305
|
+
const result = await submitAndWait(nodes);
|
|
306
|
+
const elapsed = Date.now() - startMs;
|
|
307
|
+
|
|
308
|
+
// Find output image
|
|
309
|
+
const outputs = result.outputs || {};
|
|
310
|
+
let imageFile = null;
|
|
311
|
+
let imageSubfolder = "";
|
|
312
|
+
for (const nodeOut of Object.values(outputs)) {
|
|
313
|
+
if (nodeOut.images?.length > 0) {
|
|
314
|
+
imageFile = nodeOut.images[0].filename;
|
|
315
|
+
imageSubfolder = nodeOut.images[0].subfolder || "";
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!imageFile) {
|
|
321
|
+
console.log(` \x1b[33m⚠\x1b[0m No output image`);
|
|
322
|
+
errors++;
|
|
323
|
+
processed++;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Download and save with original filename
|
|
328
|
+
const imgData = await downloadImage(imageFile, imageSubfolder);
|
|
329
|
+
await writeFile(join(outDir, file), imgData);
|
|
330
|
+
|
|
331
|
+
const kb = Math.round(imgData.length / 1024);
|
|
332
|
+
console.log(` \x1b[32m✓\x1b[0m outputs/painterly/${file} (${elapsed}ms, ${kb}KB)`);
|
|
333
|
+
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.log(` \x1b[31m✗\x1b[0m ${err.message}`);
|
|
336
|
+
errors++;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
processed++;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
console.log(`\n\x1b[32m✓\x1b[0m Painterly pass: ${processed - errors} success, ${errors} errors`);
|
|
343
|
+
if (dryRun) console.log(" (dry run — no images processed)");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
main().catch((err) => {
|
|
347
|
+
console.error(err);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
});
|