@mcptoolshop/style-dataset-lab 2.3.0 → 3.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 +125 -0
- package/README.md +36 -8
- package/bin/sdlab.js +129 -0
- package/lib/adapters/comfyui-runner.js +170 -0
- package/lib/adapters/comfyui-workflows.js +163 -0
- package/lib/batch-compiler.js +118 -0
- package/lib/batch-modes.js +111 -0
- package/lib/batch-runs.js +168 -0
- package/lib/batch-sheet-render.js +206 -0
- package/lib/brief-compiler.js +598 -0
- package/lib/brief-render.js +205 -0
- package/lib/critique-engine.js +413 -0
- package/lib/critique-render.js +171 -0
- package/lib/generated-provenance.js +87 -0
- package/lib/paths.js +31 -0
- package/lib/refine-briefs.js +329 -0
- package/lib/reingest-selected.js +173 -0
- package/lib/run-summary.js +131 -0
- package/lib/runtime-runs.js +168 -0
- package/lib/selections.js +323 -0
- package/lib/workflow-profiles.js +161 -0
- package/package.json +6 -3
- package/scripts/batch-generate.js +167 -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 +52 -0
- package/scripts/critique-show.js +50 -0
- package/scripts/critique.js +70 -0
- package/scripts/doctor.js +169 -1
- package/scripts/init.js +22 -0
- package/scripts/refine.js +119 -0
- package/scripts/reingest-selected.js +78 -0
- package/scripts/reingest.js +7 -1
- 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 +118 -0
- package/scripts/selection-show.js +88 -0
- package/scripts/workflow-list.js +61 -0
- package/scripts/workflow-show.js +96 -0
- 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/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/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/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/workflows/profiles/variant-pack.json +60 -0
- package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* batch-sheet.js — Re-render a batch sheet from saved manifest.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab batch sheet batch_2025-06-30_001 --project star-freight
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { parseArgs } from '../lib/args.js';
|
|
11
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
12
|
+
import { loadBatchManifest } from '../lib/batch-runs.js';
|
|
13
|
+
import { getBatchMode } from '../lib/batch-modes.js';
|
|
14
|
+
import { renderSheetHTML, saveSheet } from '../lib/batch-sheet-render.js';
|
|
15
|
+
import { getBatchesDir } from '../lib/batch-runs.js';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { inputError } from '../lib/errors.js';
|
|
18
|
+
import { info } from '../lib/log.js';
|
|
19
|
+
|
|
20
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
21
|
+
const { flags, positionals } = parseArgs(argv, {
|
|
22
|
+
flags: {
|
|
23
|
+
project: { type: 'string', default: 'star-freight' },
|
|
24
|
+
batch: { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
deprecated: { game: 'project' },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const projectName = flags.project;
|
|
30
|
+
const projectRoot = getProjectRoot(projectName);
|
|
31
|
+
const batchId = positionals[0] || flags.batch;
|
|
32
|
+
|
|
33
|
+
if (!batchId) {
|
|
34
|
+
console.log('Usage: sdlab batch sheet <batch-id> [--project <name>]');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const manifest = loadBatchManifest(projectRoot, batchId);
|
|
39
|
+
const mode = getBatchMode(projectRoot, manifest.mode_id);
|
|
40
|
+
const batchDir = join(getBatchesDir(projectRoot), batchId);
|
|
41
|
+
|
|
42
|
+
const html = renderSheetHTML({ manifest, mode, projectRoot, batchDir });
|
|
43
|
+
const sheetPath = await saveSheet(batchDir, 'primary-sheet.html', html);
|
|
44
|
+
|
|
45
|
+
info(`Sheet written: ${sheetPath}`);
|
|
46
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* batch-show.js — List batches or show batch details.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab batch show --project star-freight # list all batches
|
|
8
|
+
* sdlab batch show batch_2025-06-30_001 # show specific batch
|
|
9
|
+
* sdlab batch show batch_2025-06-30_001 --json # as JSON
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseArgs } from '../lib/args.js';
|
|
13
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
14
|
+
import { listBatches, loadBatchManifest } from '../lib/batch-runs.js';
|
|
15
|
+
|
|
16
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
17
|
+
const { flags, positionals } = parseArgs(argv, {
|
|
18
|
+
flags: {
|
|
19
|
+
project: { type: 'string', default: 'star-freight' },
|
|
20
|
+
batch: { type: 'string' },
|
|
21
|
+
json: { type: 'boolean' },
|
|
22
|
+
},
|
|
23
|
+
deprecated: { game: 'project' },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const projectName = flags.project;
|
|
27
|
+
const projectRoot = getProjectRoot(projectName);
|
|
28
|
+
const batchId = positionals[0] || flags.batch;
|
|
29
|
+
|
|
30
|
+
// List mode
|
|
31
|
+
if (!batchId) {
|
|
32
|
+
const batches = listBatches(projectRoot);
|
|
33
|
+
if (batches.length === 0) {
|
|
34
|
+
console.log('No batches found.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m batches (${projectName})`);
|
|
38
|
+
console.log('');
|
|
39
|
+
for (const id of batches) {
|
|
40
|
+
try {
|
|
41
|
+
const manifest = loadBatchManifest(projectRoot, id);
|
|
42
|
+
const mode = manifest.mode_id || '?';
|
|
43
|
+
const slots = manifest.summary?.total_slots || manifest.slots?.length || 0;
|
|
44
|
+
const ok = manifest.summary?.successful_slots ?? '?';
|
|
45
|
+
const dry = manifest.summary?.dry_run ? ' [dry]' : '';
|
|
46
|
+
const subject = manifest.subject_id ? ` (${manifest.subject_id})` : '';
|
|
47
|
+
console.log(` ${id} ${mode.padEnd(24)} ${ok}/${slots} slots${dry}${subject}`);
|
|
48
|
+
} catch {
|
|
49
|
+
console.log(` ${id} (manifest unreadable)`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
console.log('');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Detail mode
|
|
57
|
+
const manifest = loadBatchManifest(projectRoot, batchId);
|
|
58
|
+
|
|
59
|
+
if (flags.json) {
|
|
60
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m batch ${batchId}`);
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(` Mode: ${manifest.mode_id}`);
|
|
67
|
+
console.log(` Project: ${manifest.project_id}`);
|
|
68
|
+
if (manifest.subject_id) console.log(` Subject: ${manifest.subject_id}`);
|
|
69
|
+
if (manifest.theme) console.log(` Theme: ${manifest.theme}`);
|
|
70
|
+
console.log(` Created: ${manifest.created_at}`);
|
|
71
|
+
if (manifest.summary?.dry_run) console.log(' ⚠ DRY RUN');
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log(' Slots:');
|
|
74
|
+
for (const slot of manifest.slots || []) {
|
|
75
|
+
const status = slot.selected_output ? '✓' : '○';
|
|
76
|
+
console.log(` ${status} ${(slot.label || slot.slot_id).padEnd(24)} run=${slot.run_id} ${slot.selected_output || ''}`);
|
|
77
|
+
}
|
|
78
|
+
console.log('');
|
|
79
|
+
if (manifest.sheet_paths?.length) {
|
|
80
|
+
console.log(' Sheets:');
|
|
81
|
+
for (const p of manifest.sheet_paths) {
|
|
82
|
+
console.log(` ${p}`);
|
|
83
|
+
}
|
|
84
|
+
console.log('');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* brief-compile.js — Compile a generation brief from project truth.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab brief compile --project star-freight --workflow character-portrait-set --subject kael_maren
|
|
8
|
+
* sdlab brief compile --project star-freight --workflow environment-moodboard
|
|
9
|
+
* sdlab brief compile --project star-freight --workflow character-portrait-set --subject kael_maren --reference sf_001,sf_018
|
|
10
|
+
* sdlab brief compile --project star-freight --workflow character-portrait-set --subject kael_maren --output-count 8 --json
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { parseArgs } from '../lib/args.js';
|
|
14
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
15
|
+
import { compileBrief, saveCompiledBrief } from '../lib/brief-compiler.js';
|
|
16
|
+
import { info } from '../lib/log.js';
|
|
17
|
+
|
|
18
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
19
|
+
const { flags } = parseArgs(argv, {
|
|
20
|
+
flags: {
|
|
21
|
+
project: { type: 'string', default: 'star-freight' },
|
|
22
|
+
workflow: { type: 'string' },
|
|
23
|
+
subject: { type: 'string' },
|
|
24
|
+
asset: { type: 'string' },
|
|
25
|
+
'implementation-pack': { type: 'string' },
|
|
26
|
+
reference: { type: 'string' },
|
|
27
|
+
'output-count': { type: 'string' },
|
|
28
|
+
width: { type: 'string' },
|
|
29
|
+
height: { type: 'string' },
|
|
30
|
+
steps: { type: 'string' },
|
|
31
|
+
cfg: { type: 'string' },
|
|
32
|
+
sampler: { type: 'string' },
|
|
33
|
+
'seed-mode': { type: 'string' },
|
|
34
|
+
json: { type: 'boolean' },
|
|
35
|
+
'dry-run': { type: 'boolean' },
|
|
36
|
+
},
|
|
37
|
+
deprecated: { game: 'project' },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!flags.workflow) {
|
|
41
|
+
console.log('Usage: sdlab brief compile --workflow <id> [--subject <id>] [--project <name>]');
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log('Options:');
|
|
44
|
+
console.log(' --workflow <id> Workflow profile to use (required)');
|
|
45
|
+
console.log(' --subject <id> Subject ID for subject-driven workflows');
|
|
46
|
+
console.log(' --asset <id> Training asset reference');
|
|
47
|
+
console.log(' --implementation-pack <id> Implementation pack reference');
|
|
48
|
+
console.log(' --reference <ids> Comma-separated reference record IDs');
|
|
49
|
+
console.log(' --output-count <n> Override output count');
|
|
50
|
+
console.log(' --width <px> Override width');
|
|
51
|
+
console.log(' --height <px> Override height');
|
|
52
|
+
console.log(' --steps <n> Override steps');
|
|
53
|
+
console.log(' --cfg <n> Override CFG scale');
|
|
54
|
+
console.log(' --sampler <name> Override sampler');
|
|
55
|
+
console.log(' --seed-mode <mode> Override seed mode (fixed|increment|random)');
|
|
56
|
+
console.log(' --json Output raw JSON');
|
|
57
|
+
console.log(' --dry-run Preview without saving');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const projectName = flags.project;
|
|
62
|
+
const projectRoot = getProjectRoot(projectName);
|
|
63
|
+
|
|
64
|
+
// Parse reference IDs
|
|
65
|
+
const referenceIds = flags.reference
|
|
66
|
+
? flags.reference.split(',').map(s => s.trim()).filter(Boolean)
|
|
67
|
+
: undefined;
|
|
68
|
+
|
|
69
|
+
// Parse overrides
|
|
70
|
+
const overrides = {};
|
|
71
|
+
if (flags['output-count']) overrides.output_count = parseInt(flags['output-count'], 10);
|
|
72
|
+
if (flags.width) overrides.width = parseInt(flags.width, 10);
|
|
73
|
+
if (flags.height) overrides.height = parseInt(flags.height, 10);
|
|
74
|
+
if (flags.steps) overrides.steps = parseInt(flags.steps, 10);
|
|
75
|
+
if (flags.cfg) overrides.cfg = parseFloat(flags.cfg);
|
|
76
|
+
if (flags.sampler) overrides.sampler = flags.sampler;
|
|
77
|
+
if (flags['seed-mode']) overrides.seed_mode = flags['seed-mode'];
|
|
78
|
+
|
|
79
|
+
const brief = await compileBrief({
|
|
80
|
+
projectRoot,
|
|
81
|
+
projectId: projectName,
|
|
82
|
+
workflowId: flags.workflow,
|
|
83
|
+
subjectId: flags.subject,
|
|
84
|
+
assetRef: flags.asset,
|
|
85
|
+
implementationPackRef: flags['implementation-pack'],
|
|
86
|
+
referenceIds,
|
|
87
|
+
overrides,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (flags.json) {
|
|
91
|
+
console.log(JSON.stringify(brief, null, 2));
|
|
92
|
+
if (!flags['dry-run']) {
|
|
93
|
+
await saveCompiledBrief(projectRoot, brief);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!flags['dry-run']) {
|
|
99
|
+
const { jsonPath, mdPath } = await saveCompiledBrief(projectRoot, brief);
|
|
100
|
+
info(`\x1b[1msdlab brief compile\x1b[0m — ${projectName}\n`);
|
|
101
|
+
info(` Brief ID: ${brief.brief_id}`);
|
|
102
|
+
info(` Workflow: ${brief.workflow_id}`);
|
|
103
|
+
info(` Lane: ${brief.lane_id}`);
|
|
104
|
+
if (brief.subject_id) info(` Subject: ${brief.subject_id}`);
|
|
105
|
+
info(` Outputs: ${brief.expected_outputs.output_count}× ${brief.expected_outputs.output_mode}`);
|
|
106
|
+
info('');
|
|
107
|
+
info(` Saved: ${jsonPath}`);
|
|
108
|
+
info(` ${mdPath}`);
|
|
109
|
+
} else {
|
|
110
|
+
info(`\x1b[1msdlab brief compile\x1b[0m — dry run\n`);
|
|
111
|
+
info(` Brief ID: ${brief.brief_id}`);
|
|
112
|
+
info(` Workflow: ${brief.workflow_id}`);
|
|
113
|
+
info(` Lane: ${brief.lane_id}`);
|
|
114
|
+
if (brief.subject_id) info(` Subject: ${brief.subject_id}`);
|
|
115
|
+
info(` Outputs: ${brief.expected_outputs.output_count}× ${brief.expected_outputs.output_mode}`);
|
|
116
|
+
info('');
|
|
117
|
+
info(' Prompt:');
|
|
118
|
+
info(` ${brief.prompt}`);
|
|
119
|
+
info('');
|
|
120
|
+
info(' Negative:');
|
|
121
|
+
info(` ${brief.negative_prompt}`);
|
|
122
|
+
info('');
|
|
123
|
+
if (brief.drift_warnings?.length > 0) {
|
|
124
|
+
info(' Drift warnings:');
|
|
125
|
+
for (const w of brief.drift_warnings) {
|
|
126
|
+
info(` \x1b[33m⚠\x1b[0m ${w}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (process.argv[1] && (process.argv[1].endsWith('brief-compile.js') || process.argv[1].endsWith('brief-compile'))) {
|
|
133
|
+
run().catch((err) => { console.error(err.message || err); process.exit(1); });
|
|
134
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* brief-show.js — Show a compiled brief.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab brief show brief_2026-04-15_001 --project star-freight
|
|
8
|
+
* sdlab brief show brief_2026-04-15_001 --json
|
|
9
|
+
* sdlab brief show brief_2026-04-15_001 --md
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseArgs } from '../lib/args.js';
|
|
13
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
14
|
+
import { loadCompiledBrief } from '../lib/brief-compiler.js';
|
|
15
|
+
import { renderBriefText, renderBriefMarkdown } from '../lib/brief-render.js';
|
|
16
|
+
|
|
17
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
18
|
+
const { flags, positionals } = parseArgs(argv, {
|
|
19
|
+
flags: {
|
|
20
|
+
project: { type: 'string', default: 'star-freight' },
|
|
21
|
+
json: { type: 'boolean' },
|
|
22
|
+
md: { type: 'boolean' },
|
|
23
|
+
},
|
|
24
|
+
positionals: ['briefId'],
|
|
25
|
+
deprecated: { game: 'project' },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const briefId = positionals[0];
|
|
29
|
+
if (!briefId) {
|
|
30
|
+
console.log('Usage: sdlab brief show <brief-id> [--project <name>] [--json] [--md]');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const projectRoot = getProjectRoot(flags.project);
|
|
35
|
+
const brief = await loadCompiledBrief(projectRoot, briefId);
|
|
36
|
+
|
|
37
|
+
if (flags.json) {
|
|
38
|
+
console.log(JSON.stringify(brief, null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (flags.md) {
|
|
43
|
+
console.log(renderBriefMarkdown(brief));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(renderBriefText(brief));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (process.argv[1] && (process.argv[1].endsWith('brief-show.js') || process.argv[1].endsWith('brief-show'))) {
|
|
51
|
+
run().catch((err) => { console.error(err.message || err); process.exit(1); });
|
|
52
|
+
}
|
|
@@ -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 } 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', default: 'star-freight' },
|
|
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;
|
|
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 } 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', default: 'star-freight' },
|
|
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;
|
|
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/doctor.js
CHANGED
|
@@ -41,6 +41,13 @@ const REQUIRED_DIRS = [
|
|
|
41
41
|
'training/packages',
|
|
42
42
|
'training/eval-runs',
|
|
43
43
|
'training/implementations',
|
|
44
|
+
'workflows/profiles',
|
|
45
|
+
'workflows/batch-modes',
|
|
46
|
+
'briefs',
|
|
47
|
+
'runs',
|
|
48
|
+
'batches',
|
|
49
|
+
'selections',
|
|
50
|
+
'inbox/generated',
|
|
44
51
|
];
|
|
45
52
|
|
|
46
53
|
function pass(msg) { console.log(` \x1b[32m✓\x1b[0m ${msg}`); }
|
|
@@ -284,7 +291,168 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
284
291
|
}
|
|
285
292
|
}
|
|
286
293
|
|
|
287
|
-
// ── 9.
|
|
294
|
+
// ── 9. Workflow profile validation ──
|
|
295
|
+
const workflowDir = join(projectDir, 'workflows', 'profiles');
|
|
296
|
+
if (existsSync(workflowDir)) {
|
|
297
|
+
console.log('');
|
|
298
|
+
const wfFiles = readdirSync(workflowDir).filter(f => f.endsWith('.json'));
|
|
299
|
+
if (wfFiles.length > 0) {
|
|
300
|
+
const VALID_SUBJECT_MODES = ['required', 'optional', 'forbidden'];
|
|
301
|
+
const VALID_OUTPUT_MODES = ['portrait_set', 'expression_sheet', 'variant_pack', 'moodboard', 'silhouette_sheet', 'turnaround'];
|
|
302
|
+
const laneIds = new Set((lanes?.lanes || []).map(l => l.id));
|
|
303
|
+
|
|
304
|
+
let wfErrors = 0;
|
|
305
|
+
for (const file of wfFiles) {
|
|
306
|
+
const { data, error } = tryParseJson(join(workflowDir, file));
|
|
307
|
+
if (error) { fail(`workflows/profiles/${file} — invalid JSON: ${error}`); wfErrors++; continue; }
|
|
308
|
+
|
|
309
|
+
const missing = ['workflow_id', 'label', 'lane_id', 'output_mode', 'output_count',
|
|
310
|
+
'prompt_strategy', 'negative_strategy', 'canon_focus', 'drift_guards', 'runtime_defaults']
|
|
311
|
+
.filter(f => data[f] === undefined || data[f] === null);
|
|
312
|
+
if (missing.length > 0) {
|
|
313
|
+
fail(`workflows/profiles/${file} — missing fields: ${missing.join(', ')}`);
|
|
314
|
+
wfErrors++;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (data.lane_id && laneIds.size > 0 && !laneIds.has(data.lane_id)) {
|
|
319
|
+
fail(`workflows/profiles/${file} — lane_id "${data.lane_id}" not found in lanes.json`);
|
|
320
|
+
wfErrors++;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (data.subject_mode && !VALID_SUBJECT_MODES.includes(data.subject_mode)) {
|
|
324
|
+
fail(`workflows/profiles/${file} — invalid subject_mode "${data.subject_mode}"`);
|
|
325
|
+
wfErrors++;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!VALID_OUTPUT_MODES.includes(data.output_mode)) {
|
|
329
|
+
fail(`workflows/profiles/${file} — invalid output_mode "${data.output_mode}"`);
|
|
330
|
+
wfErrors++;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const ps = data.prompt_strategy;
|
|
334
|
+
if (!ps || !Array.isArray(ps.style_prefix) || !ps.structure || !Array.isArray(ps.must_include)) {
|
|
335
|
+
fail(`workflows/profiles/${file} — prompt_strategy needs style_prefix[], structure, must_include[]`);
|
|
336
|
+
wfErrors++;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const ns = data.negative_strategy;
|
|
340
|
+
if (!ns || !Array.isArray(ns.must_avoid)) {
|
|
341
|
+
fail(`workflows/profiles/${file} — negative_strategy needs must_avoid[]`);
|
|
342
|
+
wfErrors++;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!data.runtime_defaults?.adapter_target) {
|
|
346
|
+
fail(`workflows/profiles/${file} — runtime_defaults needs adapter_target`);
|
|
347
|
+
wfErrors++;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (wfErrors === 0) {
|
|
352
|
+
pass(`${wfFiles.length} workflow profiles — all valid`);
|
|
353
|
+
passes++;
|
|
354
|
+
} else {
|
|
355
|
+
failures += wfErrors;
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
pass('workflows/profiles/ — empty (no workflows yet)');
|
|
359
|
+
passes++;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── 10. Batch mode validation ──
|
|
364
|
+
const batchModesDir = join(projectDir, 'workflows', 'batch-modes');
|
|
365
|
+
if (existsSync(batchModesDir)) {
|
|
366
|
+
console.log('');
|
|
367
|
+
const bmFiles = readdirSync(batchModesDir).filter(f => f.endsWith('.json'));
|
|
368
|
+
if (bmFiles.length > 0) {
|
|
369
|
+
const VALID_BATCH_TYPES = ['expression_sheet', 'environment_board', 'silhouette_pack', 'continuity_variants'];
|
|
370
|
+
const VALID_SUBJECT_MODES_BM = ['required', 'optional', 'forbidden'];
|
|
371
|
+
const VALID_LAYOUTS = ['grid', 'moodboard', 'strip', 'freeform'];
|
|
372
|
+
const wfFiles = existsSync(join(projectDir, 'workflows', 'profiles'))
|
|
373
|
+
? readdirSync(join(projectDir, 'workflows', 'profiles')).filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''))
|
|
374
|
+
: [];
|
|
375
|
+
|
|
376
|
+
let bmErrors = 0;
|
|
377
|
+
for (const file of bmFiles) {
|
|
378
|
+
const { data, error } = tryParseJson(join(batchModesDir, file));
|
|
379
|
+
if (error) { fail(`workflows/batch-modes/${file} — invalid JSON: ${error}`); bmErrors++; continue; }
|
|
380
|
+
|
|
381
|
+
if (!data.mode_id) { fail(`workflows/batch-modes/${file} — missing mode_id`); bmErrors++; }
|
|
382
|
+
if (!data.batch_type || !VALID_BATCH_TYPES.includes(data.batch_type)) {
|
|
383
|
+
fail(`workflows/batch-modes/${file} — invalid batch_type "${data.batch_type}"`);
|
|
384
|
+
bmErrors++;
|
|
385
|
+
}
|
|
386
|
+
if (data.subject_mode && !VALID_SUBJECT_MODES_BM.includes(data.subject_mode)) {
|
|
387
|
+
fail(`workflows/batch-modes/${file} — invalid subject_mode "${data.subject_mode}"`);
|
|
388
|
+
bmErrors++;
|
|
389
|
+
}
|
|
390
|
+
if (data.base_workflow_id && wfFiles.length > 0 && !wfFiles.includes(data.base_workflow_id)) {
|
|
391
|
+
fail(`workflows/batch-modes/${file} — base_workflow_id "${data.base_workflow_id}" not found in profiles`);
|
|
392
|
+
bmErrors++;
|
|
393
|
+
}
|
|
394
|
+
if (!Array.isArray(data.variant_plan) || data.variant_plan.length === 0) {
|
|
395
|
+
fail(`workflows/batch-modes/${file} — variant_plan must be non-empty array`);
|
|
396
|
+
bmErrors++;
|
|
397
|
+
} else {
|
|
398
|
+
const slotIds = data.variant_plan.map(s => s.slot_id);
|
|
399
|
+
const dupes = slotIds.filter((id, i) => slotIds.indexOf(id) !== i);
|
|
400
|
+
if (dupes.length > 0) {
|
|
401
|
+
fail(`workflows/batch-modes/${file} — duplicate slot_ids: ${[...new Set(dupes)].join(', ')}`);
|
|
402
|
+
bmErrors++;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (data.assembly?.layout && !VALID_LAYOUTS.includes(data.assembly.layout)) {
|
|
406
|
+
fail(`workflows/batch-modes/${file} — invalid assembly.layout "${data.assembly.layout}"`);
|
|
407
|
+
bmErrors++;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (bmErrors === 0) {
|
|
412
|
+
pass(`${bmFiles.length} batch modes — all valid`);
|
|
413
|
+
passes++;
|
|
414
|
+
} else {
|
|
415
|
+
failures += bmErrors;
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
pass('workflows/batch-modes/ — empty (no batch modes yet)');
|
|
419
|
+
passes++;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── 11. Runtime templates check ──
|
|
424
|
+
{
|
|
425
|
+
console.log('');
|
|
426
|
+
const runtimeDir = join(REPO_ROOT, 'runtime', 'comfyui');
|
|
427
|
+
if (existsSync(runtimeDir)) {
|
|
428
|
+
const templateFiles = readdirSync(runtimeDir).filter(f => f.endsWith('.json'));
|
|
429
|
+
if (templateFiles.length > 0) {
|
|
430
|
+
let templateErrors = 0;
|
|
431
|
+
for (const file of templateFiles) {
|
|
432
|
+
const { data, error } = tryParseJson(join(runtimeDir, file));
|
|
433
|
+
if (error) { fail(`runtime/comfyui/${file}: invalid JSON — ${error}`); templateErrors++; continue; }
|
|
434
|
+
if (!data.template_id) { fail(`runtime/comfyui/${file}: missing template_id`); templateErrors++; }
|
|
435
|
+
if (!data.compatible_modes || !Array.isArray(data.compatible_modes)) {
|
|
436
|
+
fail(`runtime/comfyui/${file}: missing compatible_modes[]`); templateErrors++;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (templateErrors === 0) {
|
|
440
|
+
pass(`${templateFiles.length} runtime templates — all valid`);
|
|
441
|
+
passes++;
|
|
442
|
+
} else {
|
|
443
|
+
failures += templateErrors;
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
warn('runtime/comfyui/ — no templates found');
|
|
447
|
+
warnings++;
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
warn('runtime/comfyui/ — directory not found');
|
|
451
|
+
warnings++;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ── 12. Records sanity check ──
|
|
288
456
|
const recordsDir = join(projectDir, 'records');
|
|
289
457
|
if (existsSync(recordsDir)) {
|
|
290
458
|
console.log('');
|
package/scripts/init.js
CHANGED
|
@@ -40,6 +40,13 @@ const PROJECT_DIRS = [
|
|
|
40
40
|
'training/packages',
|
|
41
41
|
'training/eval-runs',
|
|
42
42
|
'training/implementations',
|
|
43
|
+
'workflows/profiles',
|
|
44
|
+
'workflows/batch-modes',
|
|
45
|
+
'briefs',
|
|
46
|
+
'runs',
|
|
47
|
+
'batches',
|
|
48
|
+
'selections',
|
|
49
|
+
'inbox/generated',
|
|
43
50
|
];
|
|
44
51
|
|
|
45
52
|
function isValidName(name) {
|
|
@@ -214,6 +221,21 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
214
221
|
console.log(` \x1b[32m✓\x1b[0m inputs/prompts/example-wave.json`);
|
|
215
222
|
}
|
|
216
223
|
|
|
224
|
+
// Copy domain workflow profiles if available
|
|
225
|
+
if (domainDir) {
|
|
226
|
+
const domainWorkflowsDir = join(domainDir, 'workflows', 'profiles');
|
|
227
|
+
if (existsSync(domainWorkflowsDir)) {
|
|
228
|
+
const wfFiles = (await readdir(domainWorkflowsDir)).filter(f => f.endsWith('.json'));
|
|
229
|
+
for (const file of wfFiles) {
|
|
230
|
+
await copyFile(
|
|
231
|
+
join(domainWorkflowsDir, file),
|
|
232
|
+
join(projectDir, 'workflows', 'profiles', file)
|
|
233
|
+
);
|
|
234
|
+
console.log(` \x1b[32m✓\x1b[0m workflows/profiles/${file}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
217
239
|
console.log('');
|
|
218
240
|
console.log(`\x1b[32m✓\x1b[0m Project "${projectName}" initialized`);
|
|
219
241
|
console.log('');
|