@mcptoolshop/style-dataset-lab 1.2.1 → 2.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 +73 -0
- package/README.md +138 -113
- package/bin/sdlab.js +126 -0
- package/lib/args.js +106 -0
- package/lib/card.js +145 -0
- package/lib/comfyui.js +109 -0
- package/lib/config.js +248 -0
- package/lib/deprecation.js +14 -0
- package/lib/eligibility.js +110 -0
- package/lib/eval-pack.js +283 -0
- package/lib/export.js +259 -0
- package/lib/paths.js +38 -0
- package/lib/records.js +69 -0
- package/lib/snapshot.js +211 -0
- package/lib/split.js +409 -0
- package/package.json +20 -9
- package/scripts/bulk-curate-wave2-5.js +1 -1
- package/scripts/bulk-curate-waves11-18.js +1 -1
- package/scripts/canon-bind.js +188 -338
- package/scripts/card.js +73 -0
- package/scripts/compare.js +40 -30
- package/scripts/curate-wave25.js +1 -1
- package/scripts/curate.js +34 -28
- package/scripts/doctor.js +324 -0
- package/scripts/eligibility.js +91 -0
- package/scripts/eval-pack.js +92 -0
- package/scripts/export.js +85 -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 +229 -0
- package/scripts/migrate-records.js +15 -21
- package/scripts/painterly-test.js +20 -47
- package/scripts/painterly.js +27 -106
- package/scripts/snapshot.js +132 -0
- package/scripts/split.js +154 -0
- 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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* eligibility.js — Audit training/eval eligibility for all project records.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab eligibility audit [--project <name>]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { getProjectName } from '../lib/args.js';
|
|
13
|
+
import { REPO_ROOT } from '../lib/paths.js';
|
|
14
|
+
import { loadProjectConfig, loadSelectionProfile } from '../lib/config.js';
|
|
15
|
+
import { evaluateEligibility, categorizeExclusions } from '../lib/eligibility.js';
|
|
16
|
+
|
|
17
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
18
|
+
const projectName = getProjectName(argv);
|
|
19
|
+
const projectRoot = join(REPO_ROOT, 'projects', projectName);
|
|
20
|
+
|
|
21
|
+
const profileIdx = argv.indexOf('--profile');
|
|
22
|
+
const profileId = profileIdx >= 0 ? argv[profileIdx + 1] : null;
|
|
23
|
+
const profile = loadSelectionProfile(projectRoot, profileId);
|
|
24
|
+
const config = loadProjectConfig(projectRoot);
|
|
25
|
+
|
|
26
|
+
const recordsDir = join(projectRoot, 'records');
|
|
27
|
+
const files = (await readdir(recordsDir)).filter(f => f.endsWith('.json')).sort();
|
|
28
|
+
|
|
29
|
+
console.log(`\x1b[1msdlab eligibility audit\x1b[0m — ${projectName}`);
|
|
30
|
+
console.log(` Profile: ${profileId || 'training-default (built-in)'}`);
|
|
31
|
+
console.log(` Records: ${files.length}`);
|
|
32
|
+
console.log('');
|
|
33
|
+
|
|
34
|
+
const eligible = [];
|
|
35
|
+
const excluded = [];
|
|
36
|
+
const nearMiss = []; // Records with exactly 1 failing reason
|
|
37
|
+
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const raw = await readFile(join(recordsDir, file), 'utf-8');
|
|
40
|
+
const record = JSON.parse(raw);
|
|
41
|
+
|
|
42
|
+
const result = evaluateEligibility(record, profile, config.lanes, config.terminology);
|
|
43
|
+
|
|
44
|
+
if (result.eligible) {
|
|
45
|
+
eligible.push(record.id);
|
|
46
|
+
} else {
|
|
47
|
+
excluded.push({ record_id: record.id, reasons: result.reasons });
|
|
48
|
+
if (result.reasons.length === 1) {
|
|
49
|
+
nearMiss.push({ record_id: record.id, reason: result.reasons[0] });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(` \x1b[32m✓\x1b[0m Eligible: ${eligible.length}`);
|
|
55
|
+
console.log(` \x1b[31m✗\x1b[0m Excluded: ${excluded.length}`);
|
|
56
|
+
console.log('');
|
|
57
|
+
|
|
58
|
+
// Categorize exclusions
|
|
59
|
+
const categories = categorizeExclusions(excluded);
|
|
60
|
+
console.log(' Exclusion breakdown:');
|
|
61
|
+
for (const [cat, count] of Object.entries(categories)) {
|
|
62
|
+
if (count > 0) {
|
|
63
|
+
const bar = '█'.repeat(Math.min(Math.round(count / files.length * 50), 50));
|
|
64
|
+
console.log(` ${cat.padEnd(20)} ${String(count).padStart(4)} ${bar}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Near-miss improvement opportunities
|
|
69
|
+
if (nearMiss.length > 0) {
|
|
70
|
+
console.log(`\n Improvement opportunities (${nearMiss.length} records with 1 failing check):`);
|
|
71
|
+
const byReason = {};
|
|
72
|
+
for (const nm of nearMiss) {
|
|
73
|
+
byReason[nm.reason] = (byReason[nm.reason] || 0) + 1;
|
|
74
|
+
}
|
|
75
|
+
for (const [reason, count] of Object.entries(byReason).sort((a, b) => b[1] - a[1]).slice(0, 5)) {
|
|
76
|
+
console.log(` ${count} records: ${reason}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log('');
|
|
81
|
+
const pct = ((eligible.length / files.length) * 100).toFixed(1);
|
|
82
|
+
console.log(` Eligibility rate: ${pct}% (${eligible.length}/${files.length})`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Direct execution guard
|
|
86
|
+
if (process.argv[1] && (process.argv[1].endsWith('eligibility.js') || process.argv[1].endsWith('eligibility'))) {
|
|
87
|
+
run().catch((err) => {
|
|
88
|
+
console.error(err.message || err);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* eval-pack.js — Build and inspect canon-aware eval packs.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab eval-pack build [--project <name>]
|
|
8
|
+
* sdlab eval-pack list [--project <name>]
|
|
9
|
+
* sdlab eval-pack show <eval-id> [--project <name>]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { getProjectName } from '../lib/args.js';
|
|
15
|
+
import { REPO_ROOT } from '../lib/paths.js';
|
|
16
|
+
import { buildEvalPack, listEvalPacks, loadEvalPack } from '../lib/eval-pack.js';
|
|
17
|
+
|
|
18
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
19
|
+
const projectName = getProjectName(argv);
|
|
20
|
+
const projectRoot = join(REPO_ROOT, 'projects', projectName);
|
|
21
|
+
const subcommand = argv.find(a => !a.startsWith('--')) || 'list';
|
|
22
|
+
const args = argv.filter(a => a !== subcommand);
|
|
23
|
+
|
|
24
|
+
if (subcommand === 'build') {
|
|
25
|
+
console.log(`\x1b[1msdlab eval-pack build\x1b[0m — ${projectName}\n`);
|
|
26
|
+
|
|
27
|
+
const result = await buildEvalPack(projectRoot);
|
|
28
|
+
|
|
29
|
+
console.log(` \x1b[32m✓\x1b[0m Eval pack: ${result.evalId}`);
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log(' Tasks:');
|
|
32
|
+
for (const [task, count] of Object.entries(result.tasks)) {
|
|
33
|
+
console.log(` ${task.padEnd(24)} ${count} records`);
|
|
34
|
+
}
|
|
35
|
+
const total = Object.values(result.tasks).reduce((s, n) => s + n, 0);
|
|
36
|
+
console.log(` ${'─'.repeat(36)}`);
|
|
37
|
+
console.log(` ${'total'.padEnd(24)} ${total} records`);
|
|
38
|
+
|
|
39
|
+
} else if (subcommand === 'list') {
|
|
40
|
+
const packs = await listEvalPacks(projectRoot);
|
|
41
|
+
if (packs.length === 0) {
|
|
42
|
+
console.log('No eval packs found. Run: sdlab eval-pack build');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.log(`\x1b[1mEval packs\x1b[0m — ${projectName}\n`);
|
|
46
|
+
for (const p of packs) {
|
|
47
|
+
console.log(` ${p.id} total=${p.total} lane=${p.tasks.lane_coverage} drift=${p.tasks.forbidden_drift} gold=${p.tasks.anchor_gold} cont=${p.tasks.subject_continuity} ${p.created_at}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
} else if (subcommand === 'show') {
|
|
51
|
+
const evalId = args.find(a => a.startsWith('eval-'));
|
|
52
|
+
if (!evalId) throw new Error('Usage: sdlab eval-pack show <eval-id>');
|
|
53
|
+
|
|
54
|
+
const manifest = await loadEvalPack(projectRoot, evalId);
|
|
55
|
+
console.log(`\x1b[1mEval pack\x1b[0m — ${evalId}\n`);
|
|
56
|
+
console.log(` Created: ${manifest.created_at}`);
|
|
57
|
+
console.log(` Project: ${manifest.project}`);
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(' Tasks:');
|
|
60
|
+
for (const [task, count] of Object.entries(manifest.task_counts)) {
|
|
61
|
+
console.log(` ${task.padEnd(24)} ${count} records`);
|
|
62
|
+
}
|
|
63
|
+
console.log(`\n Total: ${manifest.total_records} records`);
|
|
64
|
+
|
|
65
|
+
// Show sample from each task file
|
|
66
|
+
const evalDir = join(projectRoot, 'eval-packs', evalId);
|
|
67
|
+
for (const taskFile of ['lane-coverage', 'forbidden-drift', 'anchor-gold', 'subject-continuity']) {
|
|
68
|
+
const raw = await readFile(join(evalDir, `${taskFile}.jsonl`), 'utf-8');
|
|
69
|
+
const entries = raw.trim().split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
70
|
+
if (entries.length > 0) {
|
|
71
|
+
console.log(`\n ${taskFile} (first 3):`);
|
|
72
|
+
for (const entry of entries.slice(0, 3)) {
|
|
73
|
+
const summary = entry.record_id || entry.subject_name;
|
|
74
|
+
const detail = entry.pass_ratio != null ? `ratio=${entry.pass_ratio}` :
|
|
75
|
+
entry.violations ? `violations=${entry.violations.length}` :
|
|
76
|
+
`records=${entry.record_count}`;
|
|
77
|
+
console.log(` ${summary} ${detail}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
} else {
|
|
83
|
+
throw new Error(`Unknown subcommand: ${subcommand}. Use: build, list, show`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (process.argv[1] && (process.argv[1].endsWith('eval-pack.js') || process.argv[1].endsWith('eval-pack'))) {
|
|
88
|
+
run().catch((err) => {
|
|
89
|
+
console.error(err.message || err);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* export.js — Build and list versioned export packages.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab export build [--snapshot <id>] [--split <id>] [--profile <name>] [--copy] [--project <name>]
|
|
8
|
+
* sdlab export list [--project <name>]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { getProjectName } from '../lib/args.js';
|
|
13
|
+
import { REPO_ROOT } from '../lib/paths.js';
|
|
14
|
+
import { listSnapshots } from '../lib/snapshot.js';
|
|
15
|
+
import { listSplits } from '../lib/split.js';
|
|
16
|
+
import { buildExport, listExports, loadExport } from '../lib/export.js';
|
|
17
|
+
|
|
18
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
19
|
+
const projectName = getProjectName(argv);
|
|
20
|
+
const projectRoot = join(REPO_ROOT, 'projects', projectName);
|
|
21
|
+
const subcommand = argv.find(a => !a.startsWith('--') && a !== 'true') || 'list';
|
|
22
|
+
const args = argv.filter(a => a !== subcommand);
|
|
23
|
+
|
|
24
|
+
if (subcommand === 'build') {
|
|
25
|
+
// Find snapshot
|
|
26
|
+
let snapshotId;
|
|
27
|
+
const snapIdx = args.indexOf('--snapshot');
|
|
28
|
+
if (snapIdx >= 0) {
|
|
29
|
+
snapshotId = args[snapIdx + 1];
|
|
30
|
+
} else {
|
|
31
|
+
const snapshots = await listSnapshots(projectRoot);
|
|
32
|
+
if (snapshots.length === 0) throw new Error('No snapshots found. Run: sdlab snapshot create');
|
|
33
|
+
snapshotId = snapshots[snapshots.length - 1].id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Find split
|
|
37
|
+
let splitId;
|
|
38
|
+
const splitIdx = args.indexOf('--split');
|
|
39
|
+
if (splitIdx >= 0) {
|
|
40
|
+
splitId = args[splitIdx + 1];
|
|
41
|
+
} else {
|
|
42
|
+
const splits = await listSplits(projectRoot);
|
|
43
|
+
if (splits.length === 0) throw new Error('No splits found. Run: sdlab split build');
|
|
44
|
+
splitId = splits[splits.length - 1].id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const profileIdx = args.indexOf('--profile');
|
|
48
|
+
const profileId = profileIdx >= 0 ? args[profileIdx + 1] : null;
|
|
49
|
+
const copy = args.includes('--copy');
|
|
50
|
+
|
|
51
|
+
console.log(`\x1b[1msdlab export build\x1b[0m — ${projectName}`);
|
|
52
|
+
console.log(` Snapshot: ${snapshotId}`);
|
|
53
|
+
console.log(` Split: ${splitId}`);
|
|
54
|
+
console.log(` Profile: ${profileId || 'canonical-package (built-in)'}`);
|
|
55
|
+
console.log(` Images: ${copy ? 'copy' : 'symlink'}`);
|
|
56
|
+
console.log('');
|
|
57
|
+
|
|
58
|
+
const result = await buildExport(projectRoot, snapshotId, splitId, { profileId, copy });
|
|
59
|
+
|
|
60
|
+
console.log(` \x1b[32m✓\x1b[0m Export: ${result.exportId}`);
|
|
61
|
+
console.log(` Records: ${result.recordCount}`);
|
|
62
|
+
console.log(` Images: ${result.imageCount}`);
|
|
63
|
+
|
|
64
|
+
} else if (subcommand === 'list') {
|
|
65
|
+
const exports = await listExports(projectRoot);
|
|
66
|
+
if (exports.length === 0) {
|
|
67
|
+
console.log('No exports found. Run: sdlab export build');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.log(`\x1b[1mExports\x1b[0m — ${projectName}\n`);
|
|
71
|
+
for (const e of exports) {
|
|
72
|
+
console.log(` ${e.id} snap=${e.snapshot_id} split=${e.split_id} records=${e.records} images=${e.images} ${e.created_at}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
} else {
|
|
76
|
+
throw new Error(`Unknown subcommand: ${subcommand}. Use: build, list`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (process.argv[1] && (process.argv[1].endsWith('export.js') || process.argv[1].endsWith('export'))) {
|
|
81
|
+
run().catch((err) => {
|
|
82
|
+
console.error(err.message || err);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -9,29 +9,14 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
11
|
* node scripts/generate-controlnet.js --subject naia --guide inputs/control-guides/naia_structural_guide.png --seeds 4
|
|
12
|
-
*
|
|
13
|
-
* node scripts/generate-controlnet.js --subject naia --guide ... --dry-run
|
|
14
|
-
*
|
|
15
|
-
* Options:
|
|
16
|
-
* --subject NAME Subject name (used for output file naming)
|
|
17
|
-
* --guide PATH Path to structural guide image (white-on-black silhouette)
|
|
18
|
-
* --prompt TEXT Generation prompt (or reads from --prompt-file)
|
|
19
|
-
* --prompt-file F JSON file with {prompt, negative} fields
|
|
20
|
-
* --negative TEXT Negative prompt override
|
|
21
|
-
* --seeds N Number of seeds (default: 4)
|
|
22
|
-
* --weight W ControlNet weight (default: 0.70)
|
|
23
|
-
* --guidance-end E ControlNet guidance end (default: 0.65)
|
|
24
|
-
* --model M ControlNet model filename (default: controlnet-canny-sdxl-1.0.safetensors)
|
|
25
|
-
* --dry-run Print workflow without running
|
|
12
|
+
* sdlab generate:controlnet --subject naia --guide <path> --seeds 4 --project star-freight
|
|
26
13
|
*/
|
|
27
14
|
|
|
28
15
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
29
16
|
import { join } from "node:path";
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const GAME = process.argv.find((a, i) => process.argv[i - 1] === '--game') || 'star-freight';
|
|
34
|
-
const GAME_ROOT = join(REPO_ROOT, 'games', GAME);
|
|
17
|
+
import { getProjectName } from "../lib/args.js";
|
|
18
|
+
import { REPO_ROOT } from "../lib/paths.js";
|
|
19
|
+
import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
|
|
35
20
|
|
|
36
21
|
const DEFAULTS = {
|
|
37
22
|
checkpoint: "dreamshaperXL_v21TurboDPMSDE.safetensors",
|
|
@@ -45,11 +30,10 @@ const DEFAULTS = {
|
|
|
45
30
|
base_seed: 27100,
|
|
46
31
|
};
|
|
47
32
|
|
|
48
|
-
function
|
|
49
|
-
const args = process.argv.slice(2);
|
|
33
|
+
function parseLocalArgs(argv) {
|
|
50
34
|
const get = (flag, def) => {
|
|
51
|
-
const i =
|
|
52
|
-
return i >= 0 && i + 1 <
|
|
35
|
+
const i = argv.indexOf(flag);
|
|
36
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def;
|
|
53
37
|
};
|
|
54
38
|
return {
|
|
55
39
|
subject: get("--subject", "unknown"),
|
|
@@ -61,7 +45,7 @@ function parseArgs() {
|
|
|
61
45
|
weight: parseFloat(get("--weight", "0.70")),
|
|
62
46
|
guidanceEnd: parseFloat(get("--guidance-end", "0.65")),
|
|
63
47
|
controlModel: get("--model", "controlnet-canny-sdxl-1.0.safetensors"),
|
|
64
|
-
dryRun:
|
|
48
|
+
dryRun: argv.includes("--dry-run"),
|
|
65
49
|
};
|
|
66
50
|
}
|
|
67
51
|
|
|
@@ -69,7 +53,6 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
|
|
|
69
53
|
const nodes = {};
|
|
70
54
|
let nextId = 1;
|
|
71
55
|
|
|
72
|
-
// Checkpoint
|
|
73
56
|
const ckptId = String(nextId++);
|
|
74
57
|
nodes[ckptId] = {
|
|
75
58
|
class_type: "CheckpointLoaderSimple",
|
|
@@ -79,17 +62,14 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
|
|
|
79
62
|
let modelOut = [ckptId, 0];
|
|
80
63
|
let clipOut = [ckptId, 1];
|
|
81
64
|
|
|
82
|
-
// LoRA
|
|
83
65
|
for (const lora of DEFAULTS.loras) {
|
|
84
66
|
const loraId = String(nextId++);
|
|
85
67
|
nodes[loraId] = {
|
|
86
68
|
class_type: "LoraLoader",
|
|
87
69
|
inputs: {
|
|
88
|
-
model: modelOut,
|
|
89
|
-
clip: clipOut,
|
|
70
|
+
model: modelOut, clip: clipOut,
|
|
90
71
|
lora_name: lora.name,
|
|
91
|
-
strength_model: lora.weight,
|
|
92
|
-
strength_clip: lora.weight,
|
|
72
|
+
strength_model: lora.weight, strength_clip: lora.weight,
|
|
93
73
|
},
|
|
94
74
|
};
|
|
95
75
|
modelOut = [loraId, 0];
|
|
@@ -97,34 +77,33 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
|
|
|
97
77
|
}
|
|
98
78
|
|
|
99
79
|
// Load guide image
|
|
100
|
-
const
|
|
101
|
-
nodes[
|
|
80
|
+
const guideId = String(nextId++);
|
|
81
|
+
nodes[guideId] = {
|
|
102
82
|
class_type: "LoadImage",
|
|
103
83
|
inputs: { image: guideImagePath },
|
|
104
84
|
};
|
|
105
85
|
|
|
106
|
-
//
|
|
86
|
+
// ControlNet loader
|
|
107
87
|
const cnLoadId = String(nextId++);
|
|
108
88
|
nodes[cnLoadId] = {
|
|
109
89
|
class_type: "ControlNetLoader",
|
|
110
90
|
inputs: { control_net_name: controlModel },
|
|
111
91
|
};
|
|
112
92
|
|
|
113
|
-
//
|
|
93
|
+
// Text encoders
|
|
114
94
|
const posId = String(nextId++);
|
|
115
95
|
nodes[posId] = {
|
|
116
96
|
class_type: "CLIPTextEncode",
|
|
117
97
|
inputs: { text: prompt, clip: clipOut },
|
|
118
98
|
};
|
|
119
99
|
|
|
120
|
-
// Negative prompt
|
|
121
100
|
const negId = String(nextId++);
|
|
122
101
|
nodes[negId] = {
|
|
123
102
|
class_type: "CLIPTextEncode",
|
|
124
103
|
inputs: { text: negativePrompt, clip: clipOut },
|
|
125
104
|
};
|
|
126
105
|
|
|
127
|
-
// Apply ControlNet
|
|
106
|
+
// Apply ControlNet
|
|
128
107
|
const cnApplyId = String(nextId++);
|
|
129
108
|
nodes[cnApplyId] = {
|
|
130
109
|
class_type: "ControlNetApplyAdvanced",
|
|
@@ -132,7 +111,7 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
|
|
|
132
111
|
positive: [posId, 0],
|
|
133
112
|
negative: [negId, 0],
|
|
134
113
|
control_net: [cnLoadId, 0],
|
|
135
|
-
image: [
|
|
114
|
+
image: [guideId, 0],
|
|
136
115
|
strength: weight,
|
|
137
116
|
start_percent: 0.0,
|
|
138
117
|
end_percent: guidanceEnd,
|
|
@@ -171,7 +150,6 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
|
|
|
171
150
|
inputs: { samples: [samplerId, 0], vae: [ckptId, 2] },
|
|
172
151
|
};
|
|
173
152
|
|
|
174
|
-
// Save
|
|
175
153
|
const saveId = String(nextId++);
|
|
176
154
|
nodes[saveId] = {
|
|
177
155
|
class_type: "SaveImage",
|
|
@@ -181,61 +159,15 @@ function buildControlNetWorkflow(prompt, negativePrompt, seed, guideImagePath, c
|
|
|
181
159
|
return nodes;
|
|
182
160
|
}
|
|
183
161
|
|
|
184
|
-
async function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
} catch {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const MAX_POLL_MS = 600_000; // 10 minutes
|
|
162
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
163
|
+
const projectName = getProjectName(argv);
|
|
164
|
+
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
165
|
+
const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
|
|
194
166
|
|
|
195
|
-
|
|
196
|
-
const clientId = `cn-${Date.now()}`;
|
|
197
|
-
const res = await fetch(`${COMFY_URL}/prompt`, {
|
|
198
|
-
method: "POST",
|
|
199
|
-
headers: { "Content-Type": "application/json" },
|
|
200
|
-
body: JSON.stringify({ prompt: workflow, client_id: clientId }),
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
if (!res.ok) {
|
|
204
|
-
const text = await res.text();
|
|
205
|
-
throw new Error(`ComfyUI submit failed: ${res.status} ${text}`);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const { prompt_id: promptId } = await res.json();
|
|
209
|
-
|
|
210
|
-
const start = Date.now();
|
|
211
|
-
while (true) {
|
|
212
|
-
if (Date.now() - start > MAX_POLL_MS) throw new Error(`ComfyUI poll timeout after ${MAX_POLL_MS/1000}s for prompt ${promptId}`);
|
|
213
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
214
|
-
const histRes = await fetch(`${COMFY_URL}/history/${promptId}`);
|
|
215
|
-
if (!histRes.ok) continue;
|
|
216
|
-
const history = await histRes.json();
|
|
217
|
-
const entry = history[promptId];
|
|
218
|
-
if (!entry) continue;
|
|
219
|
-
if (entry.status?.completed) return entry;
|
|
220
|
-
if (entry.status?.status_str === "error") {
|
|
221
|
-
throw new Error(`ComfyUI error: ${JSON.stringify(entry.status)}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function downloadImage(filename, subfolder, type = "output") {
|
|
227
|
-
const url = `${COMFY_URL}/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder || "")}&type=${type}`;
|
|
228
|
-
const res = await fetch(url);
|
|
229
|
-
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
230
|
-
return Buffer.from(await res.arrayBuffer());
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function main() {
|
|
234
|
-
const opts = parseArgs();
|
|
167
|
+
const opts = parseLocalArgs(argv);
|
|
235
168
|
|
|
236
169
|
if (!opts.guidePath) {
|
|
237
|
-
|
|
238
|
-
process.exit(1);
|
|
170
|
+
throw new Error("--guide <path> is required");
|
|
239
171
|
}
|
|
240
172
|
|
|
241
173
|
// Resolve prompt
|
|
@@ -249,8 +181,7 @@ async function main() {
|
|
|
249
181
|
}
|
|
250
182
|
|
|
251
183
|
if (!prompt) {
|
|
252
|
-
|
|
253
|
-
process.exit(1);
|
|
184
|
+
throw new Error("--prompt or --prompt-file required");
|
|
254
185
|
}
|
|
255
186
|
|
|
256
187
|
if (!negative) {
|
|
@@ -267,38 +198,19 @@ async function main() {
|
|
|
267
198
|
console.log("");
|
|
268
199
|
|
|
269
200
|
if (!opts.dryRun) {
|
|
270
|
-
const online = await comfyHealth();
|
|
201
|
+
const online = await comfyHealth(COMFY_URL);
|
|
271
202
|
if (!online) {
|
|
272
|
-
|
|
273
|
-
process.exit(1);
|
|
203
|
+
throw new Error("ComfyUI not reachable at " + COMFY_URL);
|
|
274
204
|
}
|
|
275
205
|
console.log("\x1b[32m+\x1b[0m ComfyUI online");
|
|
276
206
|
}
|
|
277
207
|
|
|
278
|
-
// The guide image needs to be in ComfyUI's input directory
|
|
279
|
-
// Copy it there
|
|
280
208
|
const guideFullPath = join(GAME_ROOT, opts.guidePath);
|
|
281
|
-
const guideData = await readFile(guideFullPath);
|
|
282
209
|
const guideFilename = `cn_guide_${opts.subject}.png`;
|
|
283
210
|
|
|
284
211
|
if (!opts.dryRun) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
formData.append("image", new Blob([guideData], { type: "image/png" }), guideFilename);
|
|
288
|
-
formData.append("overwrite", "true");
|
|
289
|
-
|
|
290
|
-
const uploadRes = await fetch(`${COMFY_URL}/upload/image`, {
|
|
291
|
-
method: "POST",
|
|
292
|
-
body: formData,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
if (!uploadRes.ok) {
|
|
296
|
-
const text = await uploadRes.text();
|
|
297
|
-
console.error(`Guide upload failed: ${uploadRes.status} ${text}`);
|
|
298
|
-
process.exit(1);
|
|
299
|
-
}
|
|
300
|
-
const uploadResult = await uploadRes.json();
|
|
301
|
-
console.log(`\x1b[32m+\x1b[0m Guide uploaded: ${uploadResult.name}`);
|
|
212
|
+
await uploadImage(guideFullPath, guideFilename, COMFY_URL);
|
|
213
|
+
console.log(`\x1b[32m+\x1b[0m Guide uploaded: ${guideFilename}`);
|
|
302
214
|
}
|
|
303
215
|
|
|
304
216
|
await mkdir(join(GAME_ROOT, "outputs/candidates"), { recursive: true });
|
|
@@ -320,7 +232,7 @@ async function main() {
|
|
|
320
232
|
);
|
|
321
233
|
|
|
322
234
|
try {
|
|
323
|
-
const result = await submitAndWait(workflow);
|
|
235
|
+
const result = await submitAndWait(workflow, COMFY_URL, { clientPrefix: 'cn' });
|
|
324
236
|
const elapsed = Date.now() - startMs;
|
|
325
237
|
|
|
326
238
|
const outputs = result.outputs || {};
|
|
@@ -339,7 +251,7 @@ async function main() {
|
|
|
339
251
|
continue;
|
|
340
252
|
}
|
|
341
253
|
|
|
342
|
-
const imgData = await downloadImage(imageFile, imageSubfolder);
|
|
254
|
+
const imgData = await downloadImage(imageFile, imageSubfolder, COMFY_URL);
|
|
343
255
|
await writeFile(join(GAME_ROOT, destPath), imgData);
|
|
344
256
|
console.log(` \x1b[32m+\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
|
|
345
257
|
} catch (err) {
|
|
@@ -351,7 +263,10 @@ async function main() {
|
|
|
351
263
|
if (opts.dryRun) console.log(" (dry run)");
|
|
352
264
|
}
|
|
353
265
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
266
|
+
// Direct execution guard
|
|
267
|
+
if (process.argv[1] && (process.argv[1].endsWith('generate-controlnet.js') || process.argv[1].endsWith('generate-controlnet'))) {
|
|
268
|
+
run().catch((err) => {
|
|
269
|
+
console.error(err.message || err);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
});
|
|
272
|
+
}
|