@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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* reingest-selected.js — Re-ingest selected outputs as candidate records.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab reingest selected --selection selection_2026-04-16_001 --project star-freight
|
|
8
|
+
* sdlab reingest selected --selection selection_2026-04-16_001 --dry-run
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
12
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
13
|
+
import { reingestSelection } from '../lib/reingest-selected.js';
|
|
14
|
+
import { info } from '../lib/log.js';
|
|
15
|
+
|
|
16
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
17
|
+
const { flags } = parseArgs(argv, {
|
|
18
|
+
flags: {
|
|
19
|
+
project: { type: 'string' },
|
|
20
|
+
selection: { type: 'string' },
|
|
21
|
+
'dry-run': { type: 'boolean' },
|
|
22
|
+
json: { type: 'boolean' },
|
|
23
|
+
},
|
|
24
|
+
deprecated: { game: 'project' },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!flags.selection) {
|
|
28
|
+
console.log('Usage: sdlab reingest selected --selection <id> [--project <name>]');
|
|
29
|
+
console.log('');
|
|
30
|
+
console.log('Options:');
|
|
31
|
+
console.log(' --selection <id> Selection to re-ingest (required)');
|
|
32
|
+
console.log(' --dry-run Preview without writing records');
|
|
33
|
+
console.log(' --json Output as JSON');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const projectName = flags.project || getProjectName(argv);
|
|
38
|
+
const projectRoot = getProjectRoot(projectName);
|
|
39
|
+
const dryRun = flags['dry-run'] || false;
|
|
40
|
+
|
|
41
|
+
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m reingest selected`);
|
|
42
|
+
console.log(` Selection: ${flags.selection}`);
|
|
43
|
+
console.log(` Project: ${projectName}`);
|
|
44
|
+
if (dryRun) console.log(' ⚠ DRY RUN');
|
|
45
|
+
console.log('');
|
|
46
|
+
|
|
47
|
+
const { created, skipped } = await reingestSelection({
|
|
48
|
+
projectRoot,
|
|
49
|
+
projectId: projectName,
|
|
50
|
+
selectionId: flags.selection,
|
|
51
|
+
dryRun,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (flags.json) {
|
|
55
|
+
console.log(JSON.stringify({ created, skipped }, null, 2));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (created.length > 0) {
|
|
60
|
+
console.log(` Created ${created.length} candidate record(s):`);
|
|
61
|
+
for (const id of created) {
|
|
62
|
+
console.log(` ✓ ${id}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (skipped.length > 0) {
|
|
67
|
+
console.log(` Skipped ${skipped.length} (already exist):`);
|
|
68
|
+
for (const id of skipped) {
|
|
69
|
+
console.log(` ○ ${id}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log('');
|
|
74
|
+
if (!dryRun && created.length > 0) {
|
|
75
|
+
info('Records staged for review. Use sdlab curate to approve/reject.');
|
|
76
|
+
info(`Images copied to inbox/generated/`);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/scripts/reingest.js
CHANGED
|
@@ -5,38 +5,51 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* sdlab reingest generated --source <dir> --manifest <id> [--dry-run] [--project <name>]
|
|
8
|
+
* sdlab reingest selected --selection <id> [--project <name>]
|
|
8
9
|
* sdlab reingest audit [--project <name>]
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { join } from 'node:path';
|
|
12
|
-
import { getProjectName } from '../lib/args.js';
|
|
13
|
-
import { REPO_ROOT } from '../lib/paths.js';
|
|
13
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
14
|
+
import { REPO_ROOT, resolveSafeProjectPath } from '../lib/paths.js';
|
|
15
|
+
import { inputError, handleCliError } from '../lib/errors.js';
|
|
14
16
|
import { reingestGenerated, auditReingest } from '../lib/reingest.js';
|
|
15
17
|
|
|
16
18
|
export async function run(argv = process.argv.slice(2)) {
|
|
17
|
-
const
|
|
19
|
+
const { flags, positionals } = parseArgs(argv, {
|
|
20
|
+
flags: {
|
|
21
|
+
project: { type: 'string' },
|
|
22
|
+
source: { type: 'string' },
|
|
23
|
+
manifest: { type: 'string' },
|
|
24
|
+
selection: { type: 'string' },
|
|
25
|
+
'dry-run': { type: 'boolean', default: false },
|
|
26
|
+
},
|
|
27
|
+
deprecated: { game: 'project' },
|
|
28
|
+
allowUnknown: true, // forward unknowns to delegated 'selected' path
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const projectName = flags.project || getProjectName(argv);
|
|
18
32
|
const projectRoot = join(REPO_ROOT, 'projects', projectName);
|
|
19
|
-
const subcommand =
|
|
20
|
-
const args = argv.filter(a => a !== subcommand);
|
|
33
|
+
const subcommand = positionals[0] || 'audit';
|
|
21
34
|
|
|
22
35
|
if (subcommand === 'generated') {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const manifestId = args[manifestIdx + 1];
|
|
36
|
+
if (!flags.source) {
|
|
37
|
+
throw inputError('INPUT_MISSING_FLAG', '--source <dir> is required (directory of generated images)');
|
|
38
|
+
}
|
|
39
|
+
if (!flags.manifest) {
|
|
40
|
+
throw inputError('INPUT_MISSING_FLAG', '--manifest <id> is required (training manifest that produced the outputs)');
|
|
41
|
+
}
|
|
30
42
|
|
|
31
|
-
const
|
|
43
|
+
const safeSource = resolveSafeProjectPath(projectRoot, flags.source, { baseRoot: REPO_ROOT, flagName: 'source' });
|
|
44
|
+
const dryRun = flags['dry-run'] || flags.dryRun;
|
|
32
45
|
|
|
33
46
|
console.log(`\x1b[1msdlab reingest generated\x1b[0m — ${projectName}`);
|
|
34
|
-
console.log(` Source: ${
|
|
35
|
-
console.log(` Manifest: ${
|
|
47
|
+
console.log(` Source: ${flags.source}`);
|
|
48
|
+
console.log(` Manifest: ${flags.manifest}`);
|
|
36
49
|
if (dryRun) console.log(` Mode: DRY RUN (no records written)`);
|
|
37
50
|
console.log('');
|
|
38
51
|
|
|
39
|
-
const result = await reingestGenerated(projectRoot,
|
|
52
|
+
const result = await reingestGenerated(projectRoot, safeSource, flags.manifest, { dryRun });
|
|
40
53
|
|
|
41
54
|
console.log(` \x1b[32m✓\x1b[0m Created: ${result.created} records`);
|
|
42
55
|
if (result.skipped > 0) {
|
|
@@ -50,6 +63,12 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
50
63
|
console.log(' 3. Then eligible for future snapshots');
|
|
51
64
|
}
|
|
52
65
|
|
|
66
|
+
} else if (subcommand === 'selected') {
|
|
67
|
+
// Delegate to reingest-selected.js — pass the full argv so it can re-parse
|
|
68
|
+
const { run: runSelected } = await import('./reingest-selected.js');
|
|
69
|
+
await runSelected(argv.slice(argv.indexOf(subcommand) + 1));
|
|
70
|
+
return;
|
|
71
|
+
|
|
53
72
|
} else if (subcommand === 'audit') {
|
|
54
73
|
console.log(`\x1b[1msdlab reingest audit\x1b[0m — ${projectName}\n`);
|
|
55
74
|
|
|
@@ -77,10 +96,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
} else {
|
|
80
|
-
throw
|
|
99
|
+
throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: generated, selected, audit`);
|
|
81
100
|
}
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
if (process.argv[1] && (process.argv[1].endsWith('reingest.js') || process.argv[1].endsWith('reingest'))) {
|
|
85
|
-
run().catch(
|
|
104
|
+
run().catch(handleCliError);
|
|
86
105
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* run-generate.js — Execute a compiled brief through ComfyUI.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab run generate --brief brief_2026-04-15_001 --project star-freight
|
|
8
|
+
* sdlab run generate --brief brief_2026-04-15_001 --project star-freight --dry-run
|
|
9
|
+
* sdlab run generate --brief brief_2026-04-15_001 --project star-freight --seed 42
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseArgs, getProjectName, parseNumberFlag } from '../lib/args.js';
|
|
13
|
+
import { getProjectRoot, getRunsDir } from '../lib/paths.js';
|
|
14
|
+
import { loadCompiledBrief } from '../lib/brief-compiler.js';
|
|
15
|
+
import { executeRun } from '../lib/adapters/comfyui-runner.js';
|
|
16
|
+
import { saveRunSummary, renderRunText } from '../lib/run-summary.js';
|
|
17
|
+
import { info } from '../lib/log.js';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
|
|
20
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
21
|
+
const { flags } = parseArgs(argv, {
|
|
22
|
+
flags: {
|
|
23
|
+
project: { type: 'string' },
|
|
24
|
+
brief: { type: 'string' },
|
|
25
|
+
seed: { type: 'string' },
|
|
26
|
+
'dry-run': { type: 'boolean' },
|
|
27
|
+
json: { type: 'boolean' },
|
|
28
|
+
},
|
|
29
|
+
deprecated: { game: 'project' },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!flags.brief) {
|
|
33
|
+
console.log('Usage: sdlab run generate --brief <id> [--project <name>]');
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log('Options:');
|
|
36
|
+
console.log(' --brief <id> Compiled brief ID (required)');
|
|
37
|
+
console.log(' --seed <n> Override base seed');
|
|
38
|
+
console.log(' --dry-run Prepare run dir without submitting to ComfyUI');
|
|
39
|
+
console.log(' --json Output manifest as JSON');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const projectName = flags.project || getProjectName(argv);
|
|
44
|
+
const projectRoot = getProjectRoot(projectName);
|
|
45
|
+
|
|
46
|
+
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m run generate`);
|
|
47
|
+
console.log(` Project: ${projectName}`);
|
|
48
|
+
console.log(` Brief: ${flags.brief}`);
|
|
49
|
+
if (flags['dry-run']) console.log(' ⚠ DRY RUN');
|
|
50
|
+
console.log('');
|
|
51
|
+
|
|
52
|
+
// Load brief
|
|
53
|
+
const brief = await loadCompiledBrief(projectRoot, flags.brief);
|
|
54
|
+
|
|
55
|
+
// Execute
|
|
56
|
+
const manifest = await executeRun({
|
|
57
|
+
projectRoot,
|
|
58
|
+
projectName,
|
|
59
|
+
brief,
|
|
60
|
+
baseSeed: flags.seed ? parseNumberFlag('seed', flags.seed, { int: true, min: 0 }) : undefined,
|
|
61
|
+
dryRun: flags['dry-run'] || false,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Save summary
|
|
65
|
+
const runDir = join(getRunsDir(projectRoot), manifest.run_id);
|
|
66
|
+
await saveRunSummary(runDir, manifest);
|
|
67
|
+
|
|
68
|
+
console.log('');
|
|
69
|
+
if (flags.json) {
|
|
70
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
71
|
+
} else {
|
|
72
|
+
console.log(renderRunText(manifest));
|
|
73
|
+
console.log('');
|
|
74
|
+
info(`Run saved to: runs/${manifest.run_id}/`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* run-list.js — List all runs for a project.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab run list --project star-freight
|
|
8
|
+
* sdlab run list --project star-freight --json
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
12
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
13
|
+
import { listRuns } from '../lib/runtime-runs.js';
|
|
14
|
+
|
|
15
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
16
|
+
const { flags } = parseArgs(argv, {
|
|
17
|
+
flags: {
|
|
18
|
+
project: { type: 'string' },
|
|
19
|
+
json: { type: 'boolean' },
|
|
20
|
+
},
|
|
21
|
+
deprecated: { game: 'project' },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const projectName = flags.project || getProjectName(argv);
|
|
25
|
+
const projectRoot = getProjectRoot(projectName);
|
|
26
|
+
const runs = listRuns(projectRoot);
|
|
27
|
+
|
|
28
|
+
if (flags.json) {
|
|
29
|
+
console.log(JSON.stringify(runs, null, 2));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (runs.length === 0) {
|
|
34
|
+
console.log('No runs found. Run: sdlab run generate --brief <id>');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`\x1b[1mRuns for ${projectName}\x1b[0m (${runs.length})\n`);
|
|
39
|
+
|
|
40
|
+
const header = ' ' +
|
|
41
|
+
'Run ID'.padEnd(26) +
|
|
42
|
+
'Brief'.padEnd(26) +
|
|
43
|
+
'Mode'.padEnd(18) +
|
|
44
|
+
'Count'.padEnd(8) +
|
|
45
|
+
'Created';
|
|
46
|
+
console.log(header);
|
|
47
|
+
console.log(' ' + '─'.repeat(90));
|
|
48
|
+
|
|
49
|
+
for (const r of runs) {
|
|
50
|
+
const line = ' ' +
|
|
51
|
+
(r.run_id || '').padEnd(26) +
|
|
52
|
+
(r.brief_id || '').padEnd(26) +
|
|
53
|
+
(r.adapter_target || '?').padEnd(18) +
|
|
54
|
+
String(r.output_count ?? '?').padEnd(8) +
|
|
55
|
+
(r.created_at ? r.created_at.slice(0, 16) : '?');
|
|
56
|
+
console.log(line);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* run-show.js — Show details of a completed run.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab run show run_2026-04-16_001 --project star-freight
|
|
8
|
+
* sdlab run show run_2026-04-16_001 --project star-freight --json
|
|
9
|
+
* sdlab run show run_2026-04-16_001 --project star-freight --md
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
13
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
14
|
+
import { loadRun, listRuns } from '../lib/runtime-runs.js';
|
|
15
|
+
import { renderRunText, renderRunMarkdown } from '../lib/run-summary.js';
|
|
16
|
+
|
|
17
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
18
|
+
const { positionals, flags } = parseArgs(argv, {
|
|
19
|
+
flags: {
|
|
20
|
+
project: { type: 'string' },
|
|
21
|
+
json: { type: 'boolean' },
|
|
22
|
+
md: { type: 'boolean' },
|
|
23
|
+
},
|
|
24
|
+
deprecated: { game: 'project' },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const runId = positionals[0];
|
|
28
|
+
const projectName = flags.project || getProjectName(argv);
|
|
29
|
+
const projectRoot = getProjectRoot(projectName);
|
|
30
|
+
|
|
31
|
+
// No run ID → list runs
|
|
32
|
+
if (!runId) {
|
|
33
|
+
const runs = listRuns(projectRoot);
|
|
34
|
+
if (runs.length === 0) {
|
|
35
|
+
console.log('No runs found. Run: sdlab run generate --brief <id>');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(`\x1b[1mRuns for ${projectName}\x1b[0m (${runs.length})\n`);
|
|
40
|
+
console.log(' ID Brief Mode Outputs Adapter');
|
|
41
|
+
console.log(' ─'.padEnd(100, '─'));
|
|
42
|
+
for (const r of runs) {
|
|
43
|
+
const id = (r.run_id || '').padEnd(24);
|
|
44
|
+
const brief = (r.brief_id || '').padEnd(24);
|
|
45
|
+
const mode = (r.output_count != null ? `${r.output_count}` : '?').padStart(7);
|
|
46
|
+
const adapter = r.adapter_target || '?';
|
|
47
|
+
// output_mode if we have it
|
|
48
|
+
console.log(` ${id} ${brief} ${mode} ${adapter}`);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Show single run
|
|
54
|
+
const manifest = await loadRun(projectRoot, runId);
|
|
55
|
+
|
|
56
|
+
if (flags.json) {
|
|
57
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
58
|
+
} else if (flags.md) {
|
|
59
|
+
console.log(renderRunMarkdown(manifest));
|
|
60
|
+
} else {
|
|
61
|
+
console.log(renderRunText(manifest));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* select.js — Approve outputs from runs or batches.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab select --run run_2026-04-15_001 --approve 001.png,003.png
|
|
8
|
+
* sdlab select --batch batch_2026-04-16_001 --approve slot_neutral:001.png,slot_anger:001.png
|
|
9
|
+
* sdlab select --run run_2026-04-15_001 --approve 001.png --reason "best continuity" --tags portrait,keeper
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
13
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
14
|
+
import { inputError, handleCliError } from '../lib/errors.js';
|
|
15
|
+
import {
|
|
16
|
+
createSelectionFromRun,
|
|
17
|
+
createSelectionFromBatch,
|
|
18
|
+
saveSelection,
|
|
19
|
+
} from '../lib/selections.js';
|
|
20
|
+
import { info } from '../lib/log.js';
|
|
21
|
+
|
|
22
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
23
|
+
const { flags } = parseArgs(argv, {
|
|
24
|
+
flags: {
|
|
25
|
+
project: { type: 'string' },
|
|
26
|
+
run: { type: 'string' },
|
|
27
|
+
batch: { type: 'string' },
|
|
28
|
+
approve: { type: 'string' },
|
|
29
|
+
reason: { type: 'string' },
|
|
30
|
+
tags: { type: 'string' },
|
|
31
|
+
json: { type: 'boolean' },
|
|
32
|
+
},
|
|
33
|
+
deprecated: { game: 'project' },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!flags.approve || (!flags.run && !flags.batch)) {
|
|
37
|
+
console.log('Usage: sdlab select --run <id> --approve <files>');
|
|
38
|
+
console.log(' sdlab select --batch <id> --approve <slot:file,...>');
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log('Options:');
|
|
41
|
+
console.log(' --run <id> Source run');
|
|
42
|
+
console.log(' --batch <id> Source batch');
|
|
43
|
+
console.log(' --approve <list> Comma-separated files (run) or slot:file pairs (batch)');
|
|
44
|
+
console.log(' --reason <text> Why these outputs were chosen');
|
|
45
|
+
console.log(' --tags <list> Comma-separated tags');
|
|
46
|
+
console.log(' --json Output manifest as JSON');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const projectName = flags.project || getProjectName(argv);
|
|
51
|
+
const projectRoot = getProjectRoot(projectName);
|
|
52
|
+
const reason = flags.reason || undefined;
|
|
53
|
+
const tags = flags.tags ? flags.tags.split(',').map(t => t.trim()) : undefined;
|
|
54
|
+
|
|
55
|
+
let result;
|
|
56
|
+
|
|
57
|
+
if (flags.run) {
|
|
58
|
+
// Run selection: --approve 001.png,003.png
|
|
59
|
+
const approvedFiles = flags.approve.split(',').map(f => f.trim());
|
|
60
|
+
|
|
61
|
+
result = await createSelectionFromRun({
|
|
62
|
+
projectRoot,
|
|
63
|
+
projectId: projectName,
|
|
64
|
+
runId: flags.run,
|
|
65
|
+
approvedFiles,
|
|
66
|
+
reason,
|
|
67
|
+
tags,
|
|
68
|
+
});
|
|
69
|
+
} else if (flags.batch) {
|
|
70
|
+
// Batch selection: --approve slot_neutral:001.png,slot_anger:001.png
|
|
71
|
+
const pairs = flags.approve.split(',').map(p => p.trim());
|
|
72
|
+
const approvedSlots = pairs.map(pair => {
|
|
73
|
+
const colonIdx = pair.indexOf(':');
|
|
74
|
+
if (colonIdx < 0) {
|
|
75
|
+
throw inputError('INPUT_BAD_FORMAT', `Invalid batch approve format: "${pair}". Use slot_id:filename`);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
slotId: pair.slice(0, colonIdx),
|
|
79
|
+
filename: pair.slice(colonIdx + 1),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
result = await createSelectionFromBatch({
|
|
84
|
+
projectRoot,
|
|
85
|
+
projectId: projectName,
|
|
86
|
+
batchId: flags.batch,
|
|
87
|
+
approvedSlots,
|
|
88
|
+
reason,
|
|
89
|
+
tags,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { selectionId, selectionDir, manifest } = result;
|
|
94
|
+
|
|
95
|
+
// Save the selection
|
|
96
|
+
await saveSelection(selectionDir, manifest);
|
|
97
|
+
|
|
98
|
+
if (flags.json) {
|
|
99
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m select`);
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(` Selection: ${selectionId}`);
|
|
106
|
+
console.log(` Source: ${manifest.source_type} ${manifest.source_id}`);
|
|
107
|
+
if (manifest.workflow_id) console.log(` Workflow: ${manifest.workflow_id}`);
|
|
108
|
+
if (manifest.subject_id) console.log(` Subject: ${manifest.subject_id}`);
|
|
109
|
+
console.log(` Items: ${manifest.items.length}`);
|
|
110
|
+
console.log('');
|
|
111
|
+
|
|
112
|
+
for (const item of manifest.items) {
|
|
113
|
+
console.log(` ✓ ${item.slot_or_output.padEnd(28)} → ${item.filename} ${item.reason}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log('');
|
|
117
|
+
info(`Selection: selections/${selectionId}/manifest.json`);
|
|
118
|
+
info(`Chosen files: selections/${selectionId}/chosen/`);
|
|
119
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* selection-show.js — Show selection details or list all selections.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sdlab selection show --project star-freight
|
|
8
|
+
* sdlab selection show --selection selection_2026-04-16_001
|
|
9
|
+
* sdlab selection show selection_2026-04-16_001 --json
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
13
|
+
import { getProjectRoot } from '../lib/paths.js';
|
|
14
|
+
import { loadSelection, listSelections, getSelectionsDir } from '../lib/selections.js';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
19
|
+
const { flags, positionals } = parseArgs(argv, {
|
|
20
|
+
flags: {
|
|
21
|
+
project: { type: 'string' },
|
|
22
|
+
selection: { type: 'string' },
|
|
23
|
+
json: { type: 'boolean' },
|
|
24
|
+
},
|
|
25
|
+
deprecated: { game: 'project' },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const projectName = flags.project || getProjectName(argv);
|
|
29
|
+
const projectRoot = getProjectRoot(projectName);
|
|
30
|
+
const selectionId = positionals[0] || flags.selection;
|
|
31
|
+
|
|
32
|
+
// List mode
|
|
33
|
+
if (!selectionId) {
|
|
34
|
+
const selections = listSelections(projectRoot);
|
|
35
|
+
if (selections.length === 0) {
|
|
36
|
+
console.log('No selections found.');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m selections (${projectName})`);
|
|
40
|
+
console.log('');
|
|
41
|
+
for (const id of selections) {
|
|
42
|
+
try {
|
|
43
|
+
const m = loadSelection(projectRoot, id);
|
|
44
|
+
const items = m.items?.length || 0;
|
|
45
|
+
const src = `${m.source_type}:${m.source_id}`;
|
|
46
|
+
const ready = m.reingest_ready ? '✓' : '○';
|
|
47
|
+
const subject = m.subject_id ? ` (${m.subject_id})` : '';
|
|
48
|
+
console.log(` ${ready} ${id} ${src.padEnd(32)} ${items} items${subject}`);
|
|
49
|
+
} catch {
|
|
50
|
+
console.log(` ? ${id} (manifest unreadable)`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
console.log('');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Detail mode
|
|
58
|
+
const manifest = loadSelection(projectRoot, selectionId);
|
|
59
|
+
|
|
60
|
+
if (flags.json) {
|
|
61
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`\x1b[1mstyle-dataset-lab\x1b[0m selection ${selectionId}`);
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(` Source: ${manifest.source_type} ${manifest.source_id}`);
|
|
68
|
+
if (manifest.workflow_id) console.log(` Workflow: ${manifest.workflow_id}`);
|
|
69
|
+
if (manifest.subject_id) console.log(` Subject: ${manifest.subject_id}`);
|
|
70
|
+
console.log(` Created: ${manifest.created_at}`);
|
|
71
|
+
console.log(` Re-ingest ready: ${manifest.reingest_ready ? 'yes' : 'no'}`);
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log(' Chosen outputs:');
|
|
74
|
+
for (const item of manifest.items || []) {
|
|
75
|
+
const seedStr = item.seed !== undefined ? `seed=${item.seed}` : '';
|
|
76
|
+
console.log(` ✓ ${item.slot_or_output.padEnd(28)} → ${item.filename} ${seedStr}`);
|
|
77
|
+
if (item.reason) console.log(` ${item.reason}`);
|
|
78
|
+
if (item.tags?.length) console.log(` tags: ${item.tags.join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check provenance log
|
|
82
|
+
const provPath = join(getSelectionsDir(projectRoot), selectionId, 'provenance.jsonl');
|
|
83
|
+
if (existsSync(provPath)) {
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(' Provenance log: provenance.jsonl');
|
|
86
|
+
}
|
|
87
|
+
console.log('');
|
|
88
|
+
}
|
package/scripts/snapshot.js
CHANGED
|
@@ -12,30 +12,33 @@
|
|
|
12
12
|
|
|
13
13
|
import { readFile } from 'node:fs/promises';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
|
-
import {
|
|
16
|
-
import { getProjectName } from '../lib/args.js';
|
|
15
|
+
import { parseArgs, getProjectName } from '../lib/args.js';
|
|
17
16
|
import { REPO_ROOT } from '../lib/paths.js';
|
|
18
17
|
import { loadSelectionProfile } from '../lib/config.js';
|
|
18
|
+
import { inputError, handleCliError } from '../lib/errors.js';
|
|
19
19
|
import { createSnapshot, loadSnapshot, listSnapshots, diffSnapshots } from '../lib/snapshot.js';
|
|
20
|
-
import { evaluateEligibility } from '../lib/eligibility.js';
|
|
21
20
|
import { categorizeExclusions } from '../lib/eligibility.js';
|
|
22
21
|
|
|
23
22
|
export async function run(argv = process.argv.slice(2)) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const { flags, positionals } = parseArgs(argv, {
|
|
24
|
+
flags: {
|
|
25
|
+
project: { type: 'string' },
|
|
26
|
+
profile: { type: 'string' },
|
|
27
|
+
'dry-run': { type: 'boolean', default: false },
|
|
28
|
+
},
|
|
29
|
+
deprecated: { game: 'project' },
|
|
30
|
+
});
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
const
|
|
32
|
+
const projectName = flags.project || getProjectName(argv);
|
|
33
|
+
const projectRoot = join(REPO_ROOT, 'projects', projectName);
|
|
34
|
+
const subcommand = positionals[0] || 'list';
|
|
35
|
+
const dryRun = flags['dry-run'] || flags.dryRun;
|
|
30
36
|
|
|
31
37
|
if (subcommand === 'create') {
|
|
32
|
-
const
|
|
33
|
-
const profileId = profileIdx >= 0 ? args[profileIdx + 1] : null;
|
|
34
|
-
const profile = loadSelectionProfile(projectRoot, profileId);
|
|
35
|
-
const dryRun = argv.includes('--dry-run');
|
|
38
|
+
const profile = loadSelectionProfile(projectRoot, flags.profile);
|
|
36
39
|
|
|
37
40
|
console.log(`\x1b[1msdlab snapshot create\x1b[0m — ${projectName}`);
|
|
38
|
-
console.log(` Profile: ${
|
|
41
|
+
console.log(` Profile: ${flags.profile || 'training-default (built-in)'}`);
|
|
39
42
|
if (dryRun) console.log(' Mode: DRY RUN (no files written)');
|
|
40
43
|
console.log('');
|
|
41
44
|
|
|
@@ -46,7 +49,6 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
46
49
|
console.log(` Excluded: ${result.excluded}`);
|
|
47
50
|
|
|
48
51
|
if (!dryRun) {
|
|
49
|
-
// Read and display summary
|
|
50
52
|
const summary = JSON.parse(
|
|
51
53
|
await readFile(join(projectRoot, 'snapshots', result.snapshotId, 'summary.json'), 'utf-8')
|
|
52
54
|
);
|
|
@@ -59,7 +61,6 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
59
61
|
console.log(` ${f}: ${c}`);
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
// Read and categorize exclusions
|
|
63
64
|
const excludedRaw = await readFile(
|
|
64
65
|
join(projectRoot, 'snapshots', result.snapshotId, 'excluded.jsonl'), 'utf-8'
|
|
65
66
|
);
|
|
@@ -83,8 +84,8 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
} else if (subcommand === 'show') {
|
|
86
|
-
const snapshotId =
|
|
87
|
-
if (!snapshotId) throw
|
|
87
|
+
const snapshotId = positionals[1] || positionals.find(a => a.startsWith('snap-'));
|
|
88
|
+
if (!snapshotId) throw inputError('INPUT_MISSING_ARGS', 'Usage: sdlab snapshot show <snapshot-id>');
|
|
88
89
|
|
|
89
90
|
const manifest = await loadSnapshot(projectRoot, snapshotId);
|
|
90
91
|
console.log(`\x1b[1mSnapshot\x1b[0m — ${snapshotId}\n`);
|
|
@@ -102,8 +103,8 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
} else if (subcommand === 'diff') {
|
|
105
|
-
const snapIds =
|
|
106
|
-
if (snapIds.length < 2) throw
|
|
106
|
+
const snapIds = positionals.slice(1).filter(a => a.startsWith('snap-'));
|
|
107
|
+
if (snapIds.length < 2) throw inputError('INPUT_MISSING_ARGS', 'Usage: sdlab snapshot diff <id-a> <id-b>');
|
|
107
108
|
|
|
108
109
|
const diff = await diffSnapshots(projectRoot, snapIds[0], snapIds[1]);
|
|
109
110
|
console.log(`\x1b[1mSnapshot diff\x1b[0m — ${snapIds[0]} vs ${snapIds[1]}\n`);
|
|
@@ -123,14 +124,11 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
} else {
|
|
126
|
-
throw
|
|
127
|
+
throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: create, list, show, diff`);
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
// Direct execution guard
|
|
131
132
|
if (process.argv[1] && (process.argv[1].endsWith('snapshot.js') || process.argv[1].endsWith('snapshot'))) {
|
|
132
|
-
run().catch(
|
|
133
|
-
console.error(err.message || err);
|
|
134
|
-
process.exit(1);
|
|
135
|
-
});
|
|
133
|
+
run().catch(handleCliError);
|
|
136
134
|
}
|