@mcptoolshop/style-dataset-lab 2.3.0 → 3.0.1
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 +178 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +71 -0
- package/README.md +82 -9
- package/SECURITY.md +6 -5
- package/bin/sdlab.js +359 -11
- package/lib/adapters/comfyui-runner.js +170 -0
- package/lib/adapters/comfyui-workflows.js +163 -0
- package/lib/adapters/diffusers-lora.js +7 -2
- package/lib/adapters/generic-image-caption.js +6 -1
- package/lib/args.js +200 -13
- package/lib/batch-compiler.js +122 -0
- package/lib/batch-modes.js +111 -0
- package/lib/batch-runs.js +237 -0
- package/lib/batch-sheet-render.js +209 -0
- package/lib/brief-compiler.js +625 -0
- package/lib/brief-render.js +205 -0
- package/lib/card.js +11 -4
- package/lib/comfyui.js +100 -6
- package/lib/config.js +68 -3
- package/lib/critique-engine.js +413 -0
- package/lib/critique-render.js +171 -0
- package/lib/deprecation.js +1 -1
- package/lib/eval-pack.js +94 -4
- package/lib/eval-runs.js +42 -3
- package/lib/export.js +144 -34
- package/lib/generated-provenance.js +87 -0
- package/lib/implementation-packs.js +15 -1
- package/lib/log.js +41 -4
- package/lib/paths.js +72 -3
- package/lib/refine-briefs.js +336 -0
- package/lib/reingest-selected.js +184 -0
- package/lib/reingest.js +15 -1
- package/lib/run-summary.js +132 -0
- package/lib/runtime-runs.js +218 -0
- package/lib/selections.js +375 -0
- package/lib/snapshot.js +100 -7
- package/lib/split.js +175 -17
- package/lib/training-manifests.js +17 -3
- package/lib/training-packages.js +28 -14
- package/lib/workflow-profiles.js +161 -0
- package/package.json +13 -4
- package/scripts/batch-generate.js +176 -0
- package/scripts/batch-sheet.js +46 -0
- package/scripts/batch-show.js +86 -0
- package/scripts/brief-compile.js +134 -0
- package/scripts/brief-show.js +53 -0
- package/scripts/canon-bind.js +7 -9
- package/scripts/card.js +22 -25
- package/scripts/compare.js +38 -33
- package/scripts/critique-show.js +50 -0
- package/scripts/critique.js +70 -0
- package/scripts/curate.js +91 -46
- package/scripts/doctor.js +172 -6
- package/scripts/eligibility.js +17 -14
- package/scripts/eval-pack.js +13 -11
- package/scripts/eval-run.js +30 -29
- package/scripts/export.js +27 -31
- package/scripts/generate-controlnet.js +31 -17
- package/scripts/generate-identity.js +37 -20
- package/scripts/generate-ipadapter.js +22 -16
- package/scripts/generate.js +68 -13
- package/scripts/implementation-pack.js +19 -14
- package/scripts/init.js +137 -17
- package/scripts/painterly.js +8 -10
- package/scripts/refine.js +119 -0
- package/scripts/reingest-selected.js +78 -0
- package/scripts/reingest.js +37 -18
- package/scripts/run-generate.js +76 -0
- package/scripts/run-list.js +58 -0
- package/scripts/run-show.js +63 -0
- package/scripts/select.js +119 -0
- package/scripts/selection-show.js +88 -0
- package/scripts/snapshot.js +22 -24
- package/scripts/split.js +26 -31
- package/scripts/training-manifest.js +29 -24
- package/scripts/training-package.js +24 -20
- package/scripts/training-profile.js +13 -8
- package/scripts/workflow-list.js +61 -0
- package/scripts/workflow-show.js +97 -0
- package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/architecture/terminology.json +10 -8
- package/templates/domains/architecture/workflows/profiles/environment-moodboard.json +62 -0
- package/templates/domains/architecture/workflows/profiles/interior-variant-pack.json +60 -0
- package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/character-design/project.json +4 -2
- package/templates/domains/character-design/workflows/profiles/character-portrait-set.json +61 -0
- package/templates/domains/character-design/workflows/profiles/expression-sheet.json +59 -0
- package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
- package/templates/domains/creature-design/project.json +4 -2
- package/templates/domains/creature-design/terminology.json +1 -0
- package/templates/domains/creature-design/workflows/profiles/creature-portrait-set.json +61 -0
- package/templates/domains/creature-design/workflows/profiles/silhouette-exploration.json +58 -0
- package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/game-art/project.json +4 -2
- package/templates/domains/game-art/workflows/profiles/character-portrait-set.json +62 -0
- package/templates/domains/game-art/workflows/profiles/environment-moodboard.json +61 -0
- package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
- package/templates/domains/vehicle-mech/project.json +4 -2
- package/templates/domains/vehicle-mech/workflows/profiles/variant-pack.json +60 -0
- package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
- package/scripts/bulk-curate-wave2-5.js +0 -138
- package/scripts/bulk-curate-waves11-18.js +0 -413
- package/scripts/curate-wave25.js +0 -209
- package/scripts/migrate-records.js +0 -141
- package/templates/inputs/prompts/example-wave.json +0 -27
package/scripts/compare.js
CHANGED
|
@@ -8,62 +8,70 @@
|
|
|
8
8
|
* sdlab compare iron_sword_base iron_sword_icon_mid a "Better silhouette" --project star-freight
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { writeFile, readdir, mkdir, readFile } from "node:fs/promises";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { getProjectName } from "../lib/args.js";
|
|
13
|
+
import { parseArgs, getProjectName } from "../lib/args.js";
|
|
14
14
|
import { REPO_ROOT } from "../lib/paths.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const prefix = `--${flag}=`;
|
|
18
|
-
for (const a of args) {
|
|
19
|
-
if (a.startsWith(prefix)) return a.slice(prefix.length);
|
|
20
|
-
}
|
|
21
|
-
const idx = args.indexOf(`--${flag}`);
|
|
22
|
-
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
15
|
+
import { readJsonFile } from "../lib/config.js";
|
|
16
|
+
import { inputError, handleCliError } from "../lib/errors.js";
|
|
25
17
|
|
|
26
18
|
export async function run(argv = process.argv.slice(2)) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (i > 0 && (argv[i - 1] === '--project' || argv[i - 1] === '--game')) return false;
|
|
34
|
-
return true;
|
|
19
|
+
const { flags, positionals } = parseArgs(argv, {
|
|
20
|
+
flags: {
|
|
21
|
+
project: { type: 'string' },
|
|
22
|
+
scores: { type: 'string' },
|
|
23
|
+
},
|
|
24
|
+
deprecated: { game: 'project' },
|
|
35
25
|
});
|
|
36
26
|
|
|
37
|
-
const
|
|
27
|
+
const projectName = flags.project || getProjectName(argv);
|
|
28
|
+
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
29
|
+
|
|
30
|
+
const [assetAId, assetBId, winner, ...reasonParts] = positionals;
|
|
38
31
|
|
|
39
32
|
if (!assetAId || !assetBId || !winner) {
|
|
40
|
-
throw
|
|
33
|
+
throw inputError(
|
|
34
|
+
'INPUT_MISSING_ARGS',
|
|
35
|
+
'Usage: sdlab compare <asset_a_id> <asset_b_id> <a|b|tie> <reasoning>'
|
|
36
|
+
);
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
if (!["a", "b", "tie"].includes(winner)) {
|
|
44
|
-
throw
|
|
40
|
+
throw inputError(
|
|
41
|
+
'INPUT_BAD_WINNER',
|
|
42
|
+
`Invalid winner: ${winner}. Must be a, b, or tie.`
|
|
43
|
+
);
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
const reasoning = reasonParts.
|
|
46
|
+
const reasoning = reasonParts.join(" ") || null;
|
|
48
47
|
|
|
49
48
|
// Load records to get paths
|
|
50
49
|
const recordA = await loadRecord(GAME_ROOT, assetAId);
|
|
51
50
|
const recordB = await loadRecord(GAME_ROOT, assetBId);
|
|
52
51
|
|
|
53
52
|
if (!recordA || !recordB) {
|
|
54
|
-
throw
|
|
53
|
+
throw inputError('INPUT_UNKNOWN_RECORD', 'Could not load one or both records.');
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
// Parse optional scores
|
|
58
|
-
const scoresStr =
|
|
57
|
+
const scoresStr = flags.scores;
|
|
59
58
|
const criteria_scores = {};
|
|
60
59
|
if (scoresStr) {
|
|
61
60
|
for (const pair of scoresStr.split(",")) {
|
|
62
61
|
const [k, v] = pair.split(":");
|
|
63
62
|
// Format: "silhouette:0.9/0.6" → { silhouette: { a: 0.9, b: 0.6 } }
|
|
64
63
|
if (k && v) {
|
|
65
|
-
const [
|
|
66
|
-
|
|
64
|
+
const [aStr, bStr] = v.split("/");
|
|
65
|
+
const aScore = parseFloat(aStr);
|
|
66
|
+
const bScore = parseFloat(bStr);
|
|
67
|
+
if (!Number.isFinite(aScore) || !Number.isFinite(bScore)) {
|
|
68
|
+
throw inputError(
|
|
69
|
+
'INPUT_BAD_SCORE',
|
|
70
|
+
`Score for "${k.trim()}" is not numeric: "${v}"`,
|
|
71
|
+
'Use the form --scores silhouette:0.9/0.6,palette:0.8/0.7'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
criteria_scores[k.trim()] = { a: aScore, b: bScore };
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
77
|
}
|
|
@@ -101,7 +109,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
101
109
|
async function loadRecord(GAME_ROOT, id) {
|
|
102
110
|
try {
|
|
103
111
|
const path = join(GAME_ROOT, `records/${id}.json`);
|
|
104
|
-
return
|
|
112
|
+
return await readJsonFile(path);
|
|
105
113
|
} catch {
|
|
106
114
|
console.error(`Record not found: records/${id}.json`);
|
|
107
115
|
return null;
|
|
@@ -119,8 +127,5 @@ async function countComparisons(GAME_ROOT) {
|
|
|
119
127
|
|
|
120
128
|
// Direct execution guard
|
|
121
129
|
if (process.argv[1] && (process.argv[1].endsWith('compare.js') || process.argv[1].endsWith('compare'))) {
|
|
122
|
-
run().catch(
|
|
123
|
-
console.error(err.message || err);
|
|
124
|
-
process.exit(1);
|
|
125
|
-
});
|
|
130
|
+
run().catch(handleCliError);
|
|
126
131
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* critique-show.js — Display a saved critique report.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab critique show --project star-freight --run run_2026-04-15_001
|
|
8
|
+
* sdlab critique show --project star-freight --run run_2026-04-15_001 --json
|
|
9
|
+
* sdlab critique show --project star-freight --run run_2026-04-15_001 --md
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
13
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
14
|
+
import { loadCritique } from '../lib/critique-engine.js';
|
|
15
|
+
import { renderCritiqueMarkdown, renderCritiqueText } from '../lib/critique-render.js';
|
|
16
|
+
|
|
17
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
18
|
+
const { flags } = parseArgs(argv, {
|
|
19
|
+
flags: {
|
|
20
|
+
project: { type: 'string' },
|
|
21
|
+
run: { type: 'string' },
|
|
22
|
+
json: { type: 'boolean' },
|
|
23
|
+
md: { type: 'boolean' },
|
|
24
|
+
},
|
|
25
|
+
deprecated: { game: 'project' },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!flags.run) {
|
|
29
|
+
console.log('Usage: sdlab critique show --run <id> [--project <name>]');
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log('Options:');
|
|
32
|
+
console.log(' --run <id> Run ID (required)');
|
|
33
|
+
console.log(' --json Output raw JSON');
|
|
34
|
+
console.log(' --md Output full Markdown');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const projectName = flags.project || getProjectName(argv);
|
|
39
|
+
const projectRoot = getProjectRoot(projectName);
|
|
40
|
+
|
|
41
|
+
const report = loadCritique(projectRoot, flags.run);
|
|
42
|
+
|
|
43
|
+
if (flags.json) {
|
|
44
|
+
console.log(JSON.stringify(report, null, 2));
|
|
45
|
+
} else if (flags.md) {
|
|
46
|
+
console.log(renderCritiqueMarkdown(report));
|
|
47
|
+
} else {
|
|
48
|
+
console.log(renderCritiqueText(report));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* critique.js — Generate a critique report for a completed run.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab critique --project star-freight --run run_2026-04-15_001
|
|
8
|
+
* sdlab critique --project star-freight --run run_2026-04-15_001 --json
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
12
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
13
|
+
import { critiqueRun, saveCritique } from '../lib/critique-engine.js';
|
|
14
|
+
import { renderCritiqueMarkdown, renderCritiqueText } from '../lib/critique-render.js';
|
|
15
|
+
import { info } from '../lib/log.js';
|
|
16
|
+
|
|
17
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
18
|
+
const { flags } = parseArgs(argv, {
|
|
19
|
+
flags: {
|
|
20
|
+
project: { type: 'string' },
|
|
21
|
+
run: { type: 'string' },
|
|
22
|
+
json: { type: 'boolean' },
|
|
23
|
+
},
|
|
24
|
+
deprecated: { game: 'project' },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!flags.run) {
|
|
28
|
+
console.log('Usage: sdlab critique --run <id> [--project <name>]');
|
|
29
|
+
console.log('');
|
|
30
|
+
console.log('Options:');
|
|
31
|
+
console.log(' --run <id> Run to critique (required)');
|
|
32
|
+
console.log(' --json Output raw JSON');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const projectName = flags.project || getProjectName(argv);
|
|
37
|
+
const projectRoot = getProjectRoot(projectName);
|
|
38
|
+
|
|
39
|
+
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m critique`);
|
|
40
|
+
console.log(` Project: ${projectName}`);
|
|
41
|
+
console.log(` Run: ${flags.run}`);
|
|
42
|
+
console.log('');
|
|
43
|
+
|
|
44
|
+
// Generate critique
|
|
45
|
+
const report = await critiqueRun({
|
|
46
|
+
projectRoot,
|
|
47
|
+
projectId: projectName,
|
|
48
|
+
runId: flags.run,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Save
|
|
52
|
+
const md = renderCritiqueMarkdown(report);
|
|
53
|
+
await saveCritique(projectRoot, flags.run, report, md);
|
|
54
|
+
|
|
55
|
+
if (flags.json) {
|
|
56
|
+
console.log(JSON.stringify(report, null, 2));
|
|
57
|
+
} else {
|
|
58
|
+
console.log(renderCritiqueText(report));
|
|
59
|
+
const action = report.recommended_action;
|
|
60
|
+
console.log(`Critique saved to: runs/${flags.run}/critique.json`);
|
|
61
|
+
|
|
62
|
+
if (action.mode === 'refine_from_one' && action.preferred_candidate) {
|
|
63
|
+
console.log('');
|
|
64
|
+
info(`Next: sdlab refine --run ${flags.run} --pick ${action.preferred_candidate}`);
|
|
65
|
+
} else if (action.mode === 'accept_one' && action.preferred_candidate) {
|
|
66
|
+
console.log('');
|
|
67
|
+
info(`Preferred candidate: ${action.preferred_candidate}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
package/scripts/curate.js
CHANGED
|
@@ -7,59 +7,73 @@
|
|
|
7
7
|
* node scripts/curate.js <asset_id> approved "Clean silhouette, correct palette" --scores silhouette:0.9,palette:0.8
|
|
8
8
|
* node scripts/curate.js <asset_id> rejected "3D render look, alpha halo" --failures alpha_halo,3d_render_look
|
|
9
9
|
* node scripts/curate.js --list (show uncurated candidates)
|
|
10
|
-
* sdlab curate <asset_id> approved "explanation" --project star-freight
|
|
10
|
+
* sdlab curate <asset_id> approved "explanation" --project star-freight [--dry-run]
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { writeFile, rename, readdir, mkdir, readFile } from "node:fs/promises";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
-
import { getProjectName } from "../lib/args.js";
|
|
15
|
+
import { parseArgs, getProjectName } from "../lib/args.js";
|
|
16
16
|
import { REPO_ROOT } from "../lib/paths.js";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
for (const a of args) {
|
|
21
|
-
if (a.startsWith(prefix)) return a.slice(prefix.length);
|
|
22
|
-
}
|
|
23
|
-
const idx = args.indexOf(`--${flag}`);
|
|
24
|
-
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
17
|
+
import { readJsonFile } from "../lib/config.js";
|
|
18
|
+
import { inputError, runtimeError, handleCliError } from "../lib/errors.js";
|
|
19
|
+
import { result } from "../lib/log.js";
|
|
27
20
|
|
|
28
21
|
export async function run(argv = process.argv.slice(2)) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
const { flags, positionals } = parseArgs(argv, {
|
|
23
|
+
flags: {
|
|
24
|
+
project: { type: 'string' },
|
|
25
|
+
scores: { type: 'string' },
|
|
26
|
+
failures: { type: 'string' },
|
|
27
|
+
notes: { type: 'string' },
|
|
28
|
+
list: { type: 'boolean', default: false },
|
|
29
|
+
'dry-run': { type: 'boolean', default: false },
|
|
30
|
+
},
|
|
31
|
+
deprecated: { game: 'project' },
|
|
37
32
|
});
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
const projectName = flags.project || getProjectName(argv);
|
|
35
|
+
const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
|
|
36
|
+
const dryRun = flags['dry-run'] || flags.dryRun;
|
|
37
|
+
|
|
38
|
+
if (flags.list) {
|
|
40
39
|
return listUncurated(GAME_ROOT);
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
const [assetId, status, explanation
|
|
42
|
+
const [assetId, status, explanation] = positionals;
|
|
44
43
|
|
|
45
44
|
if (!assetId || !status || !explanation) {
|
|
46
|
-
throw
|
|
45
|
+
throw inputError(
|
|
46
|
+
'INPUT_MISSING_ARGS',
|
|
47
|
+
'Usage: sdlab curate <asset_id> <approved|rejected|borderline> <explanation> [--scores k:v,...] [--failures f1,f2] [--dry-run]'
|
|
48
|
+
);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
if (!["approved", "rejected", "borderline"].includes(status)) {
|
|
50
|
-
throw
|
|
52
|
+
throw inputError(
|
|
53
|
+
'INPUT_BAD_STATUS',
|
|
54
|
+
`Invalid status: ${status}. Must be approved, rejected, or borderline.`
|
|
55
|
+
);
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const notesStr = getFlagValue(rest, "notes");
|
|
58
|
+
const scoresStr = flags.scores;
|
|
59
|
+
const failuresStr = flags.failures;
|
|
60
|
+
const notesStr = flags.notes;
|
|
57
61
|
|
|
58
62
|
const criteria_scores = {};
|
|
59
63
|
if (scoresStr) {
|
|
60
64
|
for (const pair of scoresStr.split(",")) {
|
|
61
65
|
const [k, v] = pair.split(":");
|
|
62
|
-
if (k && v)
|
|
66
|
+
if (k && v) {
|
|
67
|
+
const score = parseFloat(v);
|
|
68
|
+
if (!Number.isFinite(score)) {
|
|
69
|
+
throw inputError(
|
|
70
|
+
'INPUT_BAD_SCORE',
|
|
71
|
+
`Score value for "${k.trim()}" is not a number: "${v}"`,
|
|
72
|
+
'Use the form --scores silhouette:0.9,palette:0.8'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
criteria_scores[k.trim()] = score;
|
|
76
|
+
}
|
|
63
77
|
}
|
|
64
78
|
}
|
|
65
79
|
|
|
@@ -69,17 +83,44 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
69
83
|
const recordPath = join(GAME_ROOT, `records/${assetId}.json`);
|
|
70
84
|
let record;
|
|
71
85
|
try {
|
|
72
|
-
record =
|
|
73
|
-
} catch {
|
|
74
|
-
|
|
86
|
+
record = await readJsonFile(recordPath);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err && err.code === 'INPUT_FILE_NOT_FOUND') {
|
|
89
|
+
throw inputError('INPUT_UNKNOWN_RECORD', `Record not found: ${recordPath}`);
|
|
90
|
+
}
|
|
91
|
+
throw err;
|
|
75
92
|
}
|
|
76
93
|
|
|
77
|
-
|
|
78
|
-
const oldPath = join(GAME_ROOT,
|
|
94
|
+
const originalAssetPath = record.asset_path;
|
|
95
|
+
const oldPath = join(GAME_ROOT, originalAssetPath);
|
|
79
96
|
const newDir = `outputs/${status}`;
|
|
80
97
|
const newPath = `${newDir}/${assetId}.png`;
|
|
98
|
+
const newFullPath = join(GAME_ROOT, newPath);
|
|
99
|
+
|
|
100
|
+
if (dryRun) {
|
|
101
|
+
console.log(`\x1b[33m(dry-run)\x1b[0m ${assetId}`);
|
|
102
|
+
console.log(` status: ${status}`);
|
|
103
|
+
console.log(` move: ${originalAssetPath} -> ${newPath}`);
|
|
104
|
+
console.log(` note: ${explanation}`);
|
|
105
|
+
if (failure_modes.length) console.log(` failures: ${failure_modes.join(", ")}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
81
109
|
await mkdir(join(GAME_ROOT, newDir), { recursive: true });
|
|
82
110
|
|
|
111
|
+
// Move image FIRST — if the rename fails we still have a clean record.
|
|
112
|
+
try {
|
|
113
|
+
await rename(oldPath, newFullPath);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
throw runtimeError(
|
|
116
|
+
'RUNTIME_MOVE_FAILED',
|
|
117
|
+
`Could not move ${oldPath} to ${newPath}: ${err.message}`,
|
|
118
|
+
'Check that the source image exists and the destination is writable.',
|
|
119
|
+
err
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Now safely update the record. If writing fails, try to restore the image.
|
|
83
124
|
record.asset_path = newPath;
|
|
84
125
|
record.judgment = {
|
|
85
126
|
status,
|
|
@@ -92,16 +133,23 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
92
133
|
confidence: 0.9,
|
|
93
134
|
};
|
|
94
135
|
|
|
95
|
-
await writeFile(recordPath, JSON.stringify(record, null, 2));
|
|
96
|
-
|
|
97
|
-
// Move image file
|
|
98
136
|
try {
|
|
99
|
-
await
|
|
100
|
-
} catch {
|
|
101
|
-
|
|
137
|
+
await writeFile(recordPath, JSON.stringify(record, null, 2));
|
|
138
|
+
} catch (err) {
|
|
139
|
+
// Attempt rollback: put the image back.
|
|
140
|
+
try { await rename(newFullPath, oldPath); } catch {}
|
|
141
|
+
throw runtimeError(
|
|
142
|
+
'RUNTIME_WRITE_FAILED',
|
|
143
|
+
`Could not write record ${recordPath}: ${err.message}`,
|
|
144
|
+
null,
|
|
145
|
+
err
|
|
146
|
+
);
|
|
102
147
|
}
|
|
103
148
|
|
|
104
|
-
|
|
149
|
+
// Final artifact path — always shown, even under --quiet.
|
|
150
|
+
result('curate', `${assetId} → ${status}`);
|
|
151
|
+
result(` image: ${newPath}`);
|
|
152
|
+
result(` record: records/${assetId}.json`);
|
|
105
153
|
console.log(` ${explanation}`);
|
|
106
154
|
if (failure_modes.length) console.log(` Failures: ${failure_modes.join(", ")}`);
|
|
107
155
|
}
|
|
@@ -135,8 +183,5 @@ async function listUncurated(GAME_ROOT) {
|
|
|
135
183
|
|
|
136
184
|
// Direct execution guard
|
|
137
185
|
if (process.argv[1] && (process.argv[1].endsWith('curate.js') || process.argv[1].endsWith('curate'))) {
|
|
138
|
-
run().catch(
|
|
139
|
-
console.error(err.message || err);
|
|
140
|
-
process.exit(1);
|
|
141
|
-
});
|
|
186
|
+
run().catch(handleCliError);
|
|
142
187
|
}
|
package/scripts/doctor.js
CHANGED
|
@@ -15,6 +15,7 @@ import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { getProjectName } from '../lib/args.js';
|
|
17
17
|
import { REPO_ROOT } from '../lib/paths.js';
|
|
18
|
+
import { runtimeError, handleCliError } from '../lib/errors.js';
|
|
18
19
|
|
|
19
20
|
const REQUIRED_CONFIG_FILES = [
|
|
20
21
|
'project.json',
|
|
@@ -41,6 +42,13 @@ const REQUIRED_DIRS = [
|
|
|
41
42
|
'training/packages',
|
|
42
43
|
'training/eval-runs',
|
|
43
44
|
'training/implementations',
|
|
45
|
+
'workflows/profiles',
|
|
46
|
+
'workflows/batch-modes',
|
|
47
|
+
'briefs',
|
|
48
|
+
'runs',
|
|
49
|
+
'batches',
|
|
50
|
+
'selections',
|
|
51
|
+
'inbox/generated',
|
|
44
52
|
];
|
|
45
53
|
|
|
46
54
|
function pass(msg) { console.log(` \x1b[32m✓\x1b[0m ${msg}`); }
|
|
@@ -284,7 +292,168 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
284
292
|
}
|
|
285
293
|
}
|
|
286
294
|
|
|
287
|
-
// ── 9.
|
|
295
|
+
// ── 9. Workflow profile validation ──
|
|
296
|
+
const workflowDir = join(projectDir, 'workflows', 'profiles');
|
|
297
|
+
if (existsSync(workflowDir)) {
|
|
298
|
+
console.log('');
|
|
299
|
+
const wfFiles = readdirSync(workflowDir).filter(f => f.endsWith('.json'));
|
|
300
|
+
if (wfFiles.length > 0) {
|
|
301
|
+
const VALID_SUBJECT_MODES = ['required', 'optional', 'forbidden'];
|
|
302
|
+
const VALID_OUTPUT_MODES = ['portrait_set', 'expression_sheet', 'variant_pack', 'moodboard', 'silhouette_sheet', 'turnaround'];
|
|
303
|
+
const laneIds = new Set((lanes?.lanes || []).map(l => l.id));
|
|
304
|
+
|
|
305
|
+
let wfErrors = 0;
|
|
306
|
+
for (const file of wfFiles) {
|
|
307
|
+
const { data, error } = tryParseJson(join(workflowDir, file));
|
|
308
|
+
if (error) { fail(`workflows/profiles/${file} — invalid JSON: ${error}`); wfErrors++; continue; }
|
|
309
|
+
|
|
310
|
+
const missing = ['workflow_id', 'label', 'lane_id', 'output_mode', 'output_count',
|
|
311
|
+
'prompt_strategy', 'negative_strategy', 'canon_focus', 'drift_guards', 'runtime_defaults']
|
|
312
|
+
.filter(f => data[f] === undefined || data[f] === null);
|
|
313
|
+
if (missing.length > 0) {
|
|
314
|
+
fail(`workflows/profiles/${file} — missing fields: ${missing.join(', ')}`);
|
|
315
|
+
wfErrors++;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (data.lane_id && laneIds.size > 0 && !laneIds.has(data.lane_id)) {
|
|
320
|
+
fail(`workflows/profiles/${file} — lane_id "${data.lane_id}" not found in lanes.json`);
|
|
321
|
+
wfErrors++;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (data.subject_mode && !VALID_SUBJECT_MODES.includes(data.subject_mode)) {
|
|
325
|
+
fail(`workflows/profiles/${file} — invalid subject_mode "${data.subject_mode}"`);
|
|
326
|
+
wfErrors++;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!VALID_OUTPUT_MODES.includes(data.output_mode)) {
|
|
330
|
+
fail(`workflows/profiles/${file} — invalid output_mode "${data.output_mode}"`);
|
|
331
|
+
wfErrors++;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const ps = data.prompt_strategy;
|
|
335
|
+
if (!ps || !Array.isArray(ps.style_prefix) || !ps.structure || !Array.isArray(ps.must_include)) {
|
|
336
|
+
fail(`workflows/profiles/${file} — prompt_strategy needs style_prefix[], structure, must_include[]`);
|
|
337
|
+
wfErrors++;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const ns = data.negative_strategy;
|
|
341
|
+
if (!ns || !Array.isArray(ns.must_avoid)) {
|
|
342
|
+
fail(`workflows/profiles/${file} — negative_strategy needs must_avoid[]`);
|
|
343
|
+
wfErrors++;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!data.runtime_defaults?.adapter_target) {
|
|
347
|
+
fail(`workflows/profiles/${file} — runtime_defaults needs adapter_target`);
|
|
348
|
+
wfErrors++;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (wfErrors === 0) {
|
|
353
|
+
pass(`${wfFiles.length} workflow profiles — all valid`);
|
|
354
|
+
passes++;
|
|
355
|
+
} else {
|
|
356
|
+
failures += wfErrors;
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
pass('workflows/profiles/ — empty (no workflows yet)');
|
|
360
|
+
passes++;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── 10. Batch mode validation ──
|
|
365
|
+
const batchModesDir = join(projectDir, 'workflows', 'batch-modes');
|
|
366
|
+
if (existsSync(batchModesDir)) {
|
|
367
|
+
console.log('');
|
|
368
|
+
const bmFiles = readdirSync(batchModesDir).filter(f => f.endsWith('.json'));
|
|
369
|
+
if (bmFiles.length > 0) {
|
|
370
|
+
const VALID_BATCH_TYPES = ['expression_sheet', 'environment_board', 'silhouette_pack', 'continuity_variants'];
|
|
371
|
+
const VALID_SUBJECT_MODES_BM = ['required', 'optional', 'forbidden'];
|
|
372
|
+
const VALID_LAYOUTS = ['grid', 'moodboard', 'strip', 'freeform'];
|
|
373
|
+
const wfFiles = existsSync(join(projectDir, 'workflows', 'profiles'))
|
|
374
|
+
? readdirSync(join(projectDir, 'workflows', 'profiles')).filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''))
|
|
375
|
+
: [];
|
|
376
|
+
|
|
377
|
+
let bmErrors = 0;
|
|
378
|
+
for (const file of bmFiles) {
|
|
379
|
+
const { data, error } = tryParseJson(join(batchModesDir, file));
|
|
380
|
+
if (error) { fail(`workflows/batch-modes/${file} — invalid JSON: ${error}`); bmErrors++; continue; }
|
|
381
|
+
|
|
382
|
+
if (!data.mode_id) { fail(`workflows/batch-modes/${file} — missing mode_id`); bmErrors++; }
|
|
383
|
+
if (!data.batch_type || !VALID_BATCH_TYPES.includes(data.batch_type)) {
|
|
384
|
+
fail(`workflows/batch-modes/${file} — invalid batch_type "${data.batch_type}"`);
|
|
385
|
+
bmErrors++;
|
|
386
|
+
}
|
|
387
|
+
if (data.subject_mode && !VALID_SUBJECT_MODES_BM.includes(data.subject_mode)) {
|
|
388
|
+
fail(`workflows/batch-modes/${file} — invalid subject_mode "${data.subject_mode}"`);
|
|
389
|
+
bmErrors++;
|
|
390
|
+
}
|
|
391
|
+
if (data.base_workflow_id && wfFiles.length > 0 && !wfFiles.includes(data.base_workflow_id)) {
|
|
392
|
+
fail(`workflows/batch-modes/${file} — base_workflow_id "${data.base_workflow_id}" not found in profiles`);
|
|
393
|
+
bmErrors++;
|
|
394
|
+
}
|
|
395
|
+
if (!Array.isArray(data.variant_plan) || data.variant_plan.length === 0) {
|
|
396
|
+
fail(`workflows/batch-modes/${file} — variant_plan must be non-empty array`);
|
|
397
|
+
bmErrors++;
|
|
398
|
+
} else {
|
|
399
|
+
const slotIds = data.variant_plan.map(s => s.slot_id);
|
|
400
|
+
const dupes = slotIds.filter((id, i) => slotIds.indexOf(id) !== i);
|
|
401
|
+
if (dupes.length > 0) {
|
|
402
|
+
fail(`workflows/batch-modes/${file} — duplicate slot_ids: ${[...new Set(dupes)].join(', ')}`);
|
|
403
|
+
bmErrors++;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (data.assembly?.layout && !VALID_LAYOUTS.includes(data.assembly.layout)) {
|
|
407
|
+
fail(`workflows/batch-modes/${file} — invalid assembly.layout "${data.assembly.layout}"`);
|
|
408
|
+
bmErrors++;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (bmErrors === 0) {
|
|
413
|
+
pass(`${bmFiles.length} batch modes — all valid`);
|
|
414
|
+
passes++;
|
|
415
|
+
} else {
|
|
416
|
+
failures += bmErrors;
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
pass('workflows/batch-modes/ — empty (no batch modes yet)');
|
|
420
|
+
passes++;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ── 11. Runtime templates check ──
|
|
425
|
+
{
|
|
426
|
+
console.log('');
|
|
427
|
+
const runtimeDir = join(REPO_ROOT, 'runtime', 'comfyui');
|
|
428
|
+
if (existsSync(runtimeDir)) {
|
|
429
|
+
const templateFiles = readdirSync(runtimeDir).filter(f => f.endsWith('.json'));
|
|
430
|
+
if (templateFiles.length > 0) {
|
|
431
|
+
let templateErrors = 0;
|
|
432
|
+
for (const file of templateFiles) {
|
|
433
|
+
const { data, error } = tryParseJson(join(runtimeDir, file));
|
|
434
|
+
if (error) { fail(`runtime/comfyui/${file}: invalid JSON — ${error}`); templateErrors++; continue; }
|
|
435
|
+
if (!data.template_id) { fail(`runtime/comfyui/${file}: missing template_id`); templateErrors++; }
|
|
436
|
+
if (!data.compatible_modes || !Array.isArray(data.compatible_modes)) {
|
|
437
|
+
fail(`runtime/comfyui/${file}: missing compatible_modes[]`); templateErrors++;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (templateErrors === 0) {
|
|
441
|
+
pass(`${templateFiles.length} runtime templates — all valid`);
|
|
442
|
+
passes++;
|
|
443
|
+
} else {
|
|
444
|
+
failures += templateErrors;
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
warn('runtime/comfyui/ — no templates found');
|
|
448
|
+
warnings++;
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
warn('runtime/comfyui/ — directory not found');
|
|
452
|
+
warnings++;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── 12. Records sanity check ──
|
|
288
457
|
const recordsDir = join(projectDir, 'records');
|
|
289
458
|
if (existsSync(recordsDir)) {
|
|
290
459
|
console.log('');
|
|
@@ -316,14 +485,11 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
316
485
|
console.log(`═══ ${status} — ${passes} passed, ${failures} failed, ${warnings} warnings ═══`);
|
|
317
486
|
|
|
318
487
|
if (failures > 0) {
|
|
319
|
-
throw
|
|
488
|
+
throw runtimeError('PROJECT_UNHEALTHY', `Project "${projectName}" has ${failures} issue(s).`);
|
|
320
489
|
}
|
|
321
490
|
}
|
|
322
491
|
|
|
323
492
|
// Direct execution guard
|
|
324
493
|
if (process.argv[1] && (process.argv[1].endsWith('doctor.js') || process.argv[1].endsWith('doctor'))) {
|
|
325
|
-
run().catch(
|
|
326
|
-
console.error(err.message || err);
|
|
327
|
-
process.exit(1);
|
|
328
|
-
});
|
|
494
|
+
run().catch(handleCliError);
|
|
329
495
|
}
|