@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,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brief renderer — converts compiled brief JSON to human-readable formats.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Render a compiled brief as Markdown.
|
|
7
|
+
*/
|
|
8
|
+
export function renderBriefMarkdown(brief) {
|
|
9
|
+
const lines = [];
|
|
10
|
+
|
|
11
|
+
lines.push(`# Brief: ${brief.brief_id}`);
|
|
12
|
+
lines.push('');
|
|
13
|
+
lines.push(`**Workflow:** ${brief.workflow_id}`);
|
|
14
|
+
lines.push(`**Project:** ${brief.project_id}`);
|
|
15
|
+
lines.push(`**Lane:** ${brief.lane_id}`);
|
|
16
|
+
if (brief.subject_id) {
|
|
17
|
+
lines.push(`**Subject:** ${brief.subject_id}`);
|
|
18
|
+
}
|
|
19
|
+
if (brief.training_asset_ref) {
|
|
20
|
+
lines.push(`**Training Asset:** ${brief.training_asset_ref}`);
|
|
21
|
+
}
|
|
22
|
+
if (brief.implementation_pack_ref) {
|
|
23
|
+
lines.push(`**Implementation Pack:** ${brief.implementation_pack_ref}`);
|
|
24
|
+
}
|
|
25
|
+
lines.push('');
|
|
26
|
+
|
|
27
|
+
// Prompt
|
|
28
|
+
lines.push('## Prompt');
|
|
29
|
+
lines.push('');
|
|
30
|
+
lines.push('```');
|
|
31
|
+
lines.push(brief.prompt);
|
|
32
|
+
lines.push('```');
|
|
33
|
+
lines.push('');
|
|
34
|
+
|
|
35
|
+
// Negative prompt
|
|
36
|
+
lines.push('## Negative Prompt');
|
|
37
|
+
lines.push('');
|
|
38
|
+
lines.push('```');
|
|
39
|
+
lines.push(brief.negative_prompt);
|
|
40
|
+
lines.push('```');
|
|
41
|
+
lines.push('');
|
|
42
|
+
|
|
43
|
+
// Canon constraints
|
|
44
|
+
if (brief.canon_constraints?.length > 0) {
|
|
45
|
+
lines.push('## Canon Constraints');
|
|
46
|
+
lines.push('');
|
|
47
|
+
for (const c of brief.canon_constraints) {
|
|
48
|
+
lines.push(`- ${c}`);
|
|
49
|
+
}
|
|
50
|
+
lines.push('');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Subject constraints
|
|
54
|
+
if (brief.subject_constraints?.length > 0) {
|
|
55
|
+
lines.push('## Subject Constraints');
|
|
56
|
+
lines.push('');
|
|
57
|
+
for (const c of brief.subject_constraints) {
|
|
58
|
+
lines.push(`- ${c}`);
|
|
59
|
+
}
|
|
60
|
+
lines.push('');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Drift warnings
|
|
64
|
+
if (brief.drift_warnings?.length > 0) {
|
|
65
|
+
lines.push('## Drift Warnings');
|
|
66
|
+
lines.push('');
|
|
67
|
+
for (const w of brief.drift_warnings) {
|
|
68
|
+
lines.push(`- ⚠ ${w}`);
|
|
69
|
+
}
|
|
70
|
+
lines.push('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Runtime plan
|
|
74
|
+
lines.push('## Runtime Plan');
|
|
75
|
+
lines.push('');
|
|
76
|
+
lines.push('| Parameter | Value |');
|
|
77
|
+
lines.push('|-----------|-------|');
|
|
78
|
+
const rp = brief.runtime_plan;
|
|
79
|
+
if (rp) {
|
|
80
|
+
lines.push(`| Adapter | ${rp.adapter_target} |`);
|
|
81
|
+
lines.push(`| Width | ${rp.width} |`);
|
|
82
|
+
lines.push(`| Height | ${rp.height} |`);
|
|
83
|
+
lines.push(`| Steps | ${rp.steps} |`);
|
|
84
|
+
lines.push(`| CFG | ${rp.cfg} |`);
|
|
85
|
+
lines.push(`| Sampler | ${rp.sampler} |`);
|
|
86
|
+
lines.push(`| Seed Mode | ${rp.seed_mode} |`);
|
|
87
|
+
}
|
|
88
|
+
lines.push('');
|
|
89
|
+
|
|
90
|
+
// Expected outputs
|
|
91
|
+
lines.push('## Expected Outputs');
|
|
92
|
+
lines.push('');
|
|
93
|
+
const eo = brief.expected_outputs;
|
|
94
|
+
if (eo) {
|
|
95
|
+
lines.push(`- **Mode:** ${eo.output_mode}`);
|
|
96
|
+
lines.push(`- **Count:** ${eo.output_count}`);
|
|
97
|
+
if (eo.review_surface) {
|
|
98
|
+
lines.push(`- **Review Surface:** ${eo.review_surface}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
lines.push('');
|
|
102
|
+
|
|
103
|
+
// Selected references
|
|
104
|
+
if (brief.reference_selection?.length > 0) {
|
|
105
|
+
lines.push('## Selected References');
|
|
106
|
+
lines.push('');
|
|
107
|
+
for (const ref of brief.reference_selection) {
|
|
108
|
+
lines.push(`- **${ref.record_id}** (${ref.role}): ${ref.reason}`);
|
|
109
|
+
}
|
|
110
|
+
lines.push('');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Metadata
|
|
114
|
+
lines.push('---');
|
|
115
|
+
lines.push('');
|
|
116
|
+
lines.push(`Compiled: ${brief.compiled_at}`);
|
|
117
|
+
lines.push(`Config fingerprint: \`${brief.config_fingerprint}\``);
|
|
118
|
+
lines.push('');
|
|
119
|
+
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Render a compiled brief as plain text for terminal output.
|
|
125
|
+
*/
|
|
126
|
+
export function renderBriefText(brief) {
|
|
127
|
+
const lines = [];
|
|
128
|
+
|
|
129
|
+
lines.push(`\x1b[1mBrief: ${brief.brief_id}\x1b[0m`);
|
|
130
|
+
lines.push(` Workflow: ${brief.workflow_id}`);
|
|
131
|
+
lines.push(` Project: ${brief.project_id}`);
|
|
132
|
+
lines.push(` Lane: ${brief.lane_id}`);
|
|
133
|
+
if (brief.subject_id) {
|
|
134
|
+
lines.push(` Subject: ${brief.subject_id}`);
|
|
135
|
+
}
|
|
136
|
+
if (brief.training_asset_ref) {
|
|
137
|
+
lines.push(` Asset: ${brief.training_asset_ref}`);
|
|
138
|
+
}
|
|
139
|
+
lines.push('');
|
|
140
|
+
|
|
141
|
+
lines.push('\x1b[1mPrompt:\x1b[0m');
|
|
142
|
+
lines.push(` ${brief.prompt}`);
|
|
143
|
+
lines.push('');
|
|
144
|
+
|
|
145
|
+
lines.push('\x1b[1mNegative:\x1b[0m');
|
|
146
|
+
lines.push(` ${brief.negative_prompt}`);
|
|
147
|
+
lines.push('');
|
|
148
|
+
|
|
149
|
+
if (brief.canon_constraints?.length > 0) {
|
|
150
|
+
lines.push('\x1b[1mCanon Constraints:\x1b[0m');
|
|
151
|
+
for (const c of brief.canon_constraints) {
|
|
152
|
+
lines.push(` • ${c}`);
|
|
153
|
+
}
|
|
154
|
+
lines.push('');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (brief.subject_constraints?.length > 0) {
|
|
158
|
+
lines.push('\x1b[1mSubject Constraints:\x1b[0m');
|
|
159
|
+
for (const c of brief.subject_constraints) {
|
|
160
|
+
lines.push(` • ${c}`);
|
|
161
|
+
}
|
|
162
|
+
lines.push('');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (brief.drift_warnings?.length > 0) {
|
|
166
|
+
lines.push('\x1b[1mDrift Warnings:\x1b[0m');
|
|
167
|
+
for (const w of brief.drift_warnings) {
|
|
168
|
+
lines.push(` \x1b[33m⚠\x1b[0m ${w}`);
|
|
169
|
+
}
|
|
170
|
+
lines.push('');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
lines.push('\x1b[1mRuntime Plan:\x1b[0m');
|
|
174
|
+
const rp = brief.runtime_plan;
|
|
175
|
+
if (rp) {
|
|
176
|
+
lines.push(` Adapter: ${rp.adapter_target}`);
|
|
177
|
+
lines.push(` Size: ${rp.width}×${rp.height}`);
|
|
178
|
+
lines.push(` Steps: ${rp.steps}`);
|
|
179
|
+
lines.push(` CFG: ${rp.cfg}`);
|
|
180
|
+
lines.push(` Sampler: ${rp.sampler}`);
|
|
181
|
+
lines.push(` Seed mode: ${rp.seed_mode}`);
|
|
182
|
+
}
|
|
183
|
+
lines.push('');
|
|
184
|
+
|
|
185
|
+
lines.push('\x1b[1mExpected Outputs:\x1b[0m');
|
|
186
|
+
const eo = brief.expected_outputs;
|
|
187
|
+
if (eo) {
|
|
188
|
+
lines.push(` Mode: ${eo.output_mode}`);
|
|
189
|
+
lines.push(` Count: ${eo.output_count}`);
|
|
190
|
+
}
|
|
191
|
+
lines.push('');
|
|
192
|
+
|
|
193
|
+
if (brief.reference_selection?.length > 0) {
|
|
194
|
+
lines.push('\x1b[1mSelected References:\x1b[0m');
|
|
195
|
+
for (const ref of brief.reference_selection) {
|
|
196
|
+
lines.push(` ${ref.record_id} (${ref.role}) — ${ref.reason}`);
|
|
197
|
+
}
|
|
198
|
+
lines.push('');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
lines.push(` Compiled: ${brief.compiled_at}`);
|
|
202
|
+
lines.push(` Fingerprint: ${brief.config_fingerprint}`);
|
|
203
|
+
|
|
204
|
+
return lines.join('\n');
|
|
205
|
+
}
|
package/lib/card.js
CHANGED
|
@@ -70,6 +70,13 @@ export async function generateCard(projectRoot, snapshotId, splitId) {
|
|
|
70
70
|
return { markdown, json };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// D-025: safe percentage formatter — renders '—' for zero-total cases
|
|
74
|
+
// instead of 'NaN%'.
|
|
75
|
+
function pct(numerator, denominator) {
|
|
76
|
+
if (!denominator || denominator <= 0) return '—';
|
|
77
|
+
return `${(numerator / denominator * 100).toFixed(1)}%`;
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
function renderMarkdown(card, config) {
|
|
74
81
|
const lines = [];
|
|
75
82
|
const add = (s = '') => lines.push(s);
|
|
@@ -87,7 +94,7 @@ function renderMarkdown(card, config) {
|
|
|
87
94
|
add(`- **Snapshot:** \`${card.snapshot.id}\``);
|
|
88
95
|
add(`- **Config fingerprint:** \`${card.snapshot.config_fingerprint.slice(0, 16)}...\``);
|
|
89
96
|
add(`- **Total records evaluated:** ${card.snapshot.total_records}`);
|
|
90
|
-
add(`- **Included:** ${card.snapshot.included} (${(card.snapshot.included
|
|
97
|
+
add(`- **Included:** ${card.snapshot.included} (${pct(card.snapshot.included, card.snapshot.total_records)})`);
|
|
91
98
|
add(`- **Excluded:** ${card.snapshot.excluded}`);
|
|
92
99
|
add();
|
|
93
100
|
|
|
@@ -111,9 +118,9 @@ function renderMarkdown(card, config) {
|
|
|
111
118
|
add('| Partition | Records | % |');
|
|
112
119
|
add('|-----------|---------|---|');
|
|
113
120
|
const total = card.split.counts.total_records;
|
|
114
|
-
add(`| Train | ${card.split.counts.train} | ${(card.split.counts.train
|
|
115
|
-
add(`| Val | ${card.split.counts.val} | ${(card.split.counts.val
|
|
116
|
-
add(`| Test | ${card.split.counts.test} | ${(card.split.counts.test
|
|
121
|
+
add(`| Train | ${card.split.counts.train} | ${pct(card.split.counts.train, total)} |`);
|
|
122
|
+
add(`| Val | ${card.split.counts.val} | ${pct(card.split.counts.val, total)} |`);
|
|
123
|
+
add(`| Test | ${card.split.counts.test} | ${pct(card.split.counts.test, total)} |`);
|
|
117
124
|
add();
|
|
118
125
|
|
|
119
126
|
add('## Lane Balance');
|
package/lib/comfyui.js
CHANGED
|
@@ -6,9 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { info } from './log.js';
|
|
9
10
|
|
|
10
11
|
const DEFAULT_URL = 'http://127.0.0.1:8188';
|
|
11
12
|
const MAX_POLL_MS = 600_000; // 10 minutes
|
|
13
|
+
const HEARTBEAT_MS = 15_000; // emit a heartbeat at most every 15s
|
|
14
|
+
// PB-004: if we see an unfamiliar status shape for this many consecutive
|
|
15
|
+
// polls without completed/error, bail with a descriptive message rather
|
|
16
|
+
// than hang until MAX_POLL_MS.
|
|
17
|
+
const UNKNOWN_STATUS_BAIL_POLLS = 30; // ~30s of unrecognized shape
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Check if ComfyUI is reachable.
|
|
@@ -48,21 +54,102 @@ export async function submitAndWait(workflow, comfyUrl = DEFAULT_URL, opts = {})
|
|
|
48
54
|
|
|
49
55
|
const { prompt_id: promptId } = await res.json();
|
|
50
56
|
|
|
57
|
+
const MAX_CONSECUTIVE_HISTORY_FAILURES = 5;
|
|
58
|
+
let consecutiveHistoryFailures = 0;
|
|
59
|
+
// PB-001: heartbeats
|
|
60
|
+
let lastHeartbeatAt = Date.now();
|
|
61
|
+
let announcedStart = false;
|
|
62
|
+
let seenEntry = false;
|
|
63
|
+
// PB-004: count polls where entry exists but status shape is unfamiliar
|
|
64
|
+
let unknownStatusPolls = 0;
|
|
65
|
+
let lastObservedStatus = null;
|
|
51
66
|
const start = Date.now();
|
|
52
67
|
while (true) {
|
|
53
|
-
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
if (now - start > timeoutMs) {
|
|
54
70
|
throw new Error(`ComfyUI poll timeout after ${timeoutMs / 1000}s for prompt ${promptId}`);
|
|
55
71
|
}
|
|
56
72
|
await new Promise(r => setTimeout(r, 1000));
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
let histRes;
|
|
74
|
+
try {
|
|
75
|
+
histRes = await fetch(`${comfyUrl}/history/${promptId}`);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
consecutiveHistoryFailures++;
|
|
78
|
+
if (consecutiveHistoryFailures >= MAX_CONSECUTIVE_HISTORY_FAILURES) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`ComfyUI history endpoint unreachable after ${MAX_CONSECUTIVE_HISTORY_FAILURES} consecutive attempts for prompt ${promptId}: ${err.message}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!histRes.ok) {
|
|
86
|
+
consecutiveHistoryFailures++;
|
|
87
|
+
if (consecutiveHistoryFailures >= MAX_CONSECUTIVE_HISTORY_FAILURES) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`ComfyUI history endpoint returned ${histRes.status} ${MAX_CONSECUTIVE_HISTORY_FAILURES} times in a row for prompt ${promptId}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
consecutiveHistoryFailures = 0;
|
|
59
95
|
const history = await histRes.json();
|
|
60
96
|
const entry = history[promptId];
|
|
61
|
-
|
|
62
|
-
|
|
97
|
+
|
|
98
|
+
// PB-001: emit heartbeats so the user knows the pipeline isn't hung.
|
|
99
|
+
const elapsedSec = Math.round((Date.now() - start) / 1000);
|
|
100
|
+
const heartbeatDue = Date.now() - lastHeartbeatAt >= HEARTBEAT_MS;
|
|
101
|
+
if (!entry) {
|
|
102
|
+
// Queued on ComfyUI but not yet visible in /history.
|
|
103
|
+
if (!announcedStart) {
|
|
104
|
+
info(`ComfyUI: queued (prompt ${promptId})`);
|
|
105
|
+
announcedStart = true;
|
|
106
|
+
lastHeartbeatAt = Date.now();
|
|
107
|
+
} else if (heartbeatDue) {
|
|
108
|
+
info(`ComfyUI: queued, waiting... (${elapsedSec}s)`);
|
|
109
|
+
lastHeartbeatAt = Date.now();
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!seenEntry) {
|
|
115
|
+
seenEntry = true;
|
|
116
|
+
info(`ComfyUI: generating... (prompt ${promptId})`);
|
|
117
|
+
lastHeartbeatAt = Date.now();
|
|
118
|
+
} else if (heartbeatDue) {
|
|
119
|
+
info(`ComfyUI: generating... (${elapsedSec}s elapsed)`);
|
|
120
|
+
lastHeartbeatAt = Date.now();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// PB-004: success signals — accept both completed===true and status_str==='success'
|
|
124
|
+
// (future-proof for schema drift where one may change without the other).
|
|
125
|
+
if (entry.status?.completed === true || entry.status?.status_str === 'success') {
|
|
126
|
+
info(`ComfyUI: completed in ${elapsedSec}s`);
|
|
127
|
+
return entry;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// PB-004: known failure signals — bail immediately.
|
|
63
131
|
if (entry.status?.status_str === 'error') {
|
|
64
132
|
throw new Error(`ComfyUI generation failed: ${JSON.stringify(entry.status)}`);
|
|
65
133
|
}
|
|
134
|
+
if (entry.status?.execution_error || entry.execution_error) {
|
|
135
|
+
const errObj = entry.status?.execution_error || entry.execution_error;
|
|
136
|
+
throw new Error(`ComfyUI execution error for prompt ${promptId}: ${JSON.stringify(errObj)}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// PB-004: the entry exists but matches neither a known success nor
|
|
140
|
+
// a known failure. This is the schema-drift hang scenario — count
|
|
141
|
+
// consecutive unknown-shape polls and bail with context.
|
|
142
|
+
const statusStr = entry.status?.status_str ?? null;
|
|
143
|
+
lastObservedStatus = entry.status ?? null;
|
|
144
|
+
unknownStatusPolls++;
|
|
145
|
+
if (unknownStatusPolls >= UNKNOWN_STATUS_BAIL_POLLS) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`ComfyUI returned an unrecognized status shape for prompt ${promptId} ` +
|
|
148
|
+
`after ${unknownStatusPolls} polls (status_str=${JSON.stringify(statusStr)}). ` +
|
|
149
|
+
`This usually means the ComfyUI history schema changed. ` +
|
|
150
|
+
`Observed status: ${JSON.stringify(lastObservedStatus)}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
66
153
|
}
|
|
67
154
|
}
|
|
68
155
|
|
|
@@ -75,7 +162,14 @@ export async function submitAndWait(workflow, comfyUrl = DEFAULT_URL, opts = {})
|
|
|
75
162
|
* @returns {Promise<Buffer>}
|
|
76
163
|
*/
|
|
77
164
|
export async function downloadImage(filename, subfolder, comfyUrl = DEFAULT_URL, type = 'output') {
|
|
78
|
-
const
|
|
165
|
+
const ALLOWED_TYPES = ['output', 'input', 'temp'];
|
|
166
|
+
if (!ALLOWED_TYPES.includes(type)) {
|
|
167
|
+
throw new Error(`Invalid ComfyUI image type "${type}" — must be one of: ${ALLOWED_TYPES.join(', ')}`);
|
|
168
|
+
}
|
|
169
|
+
if (typeof comfyUrl !== 'string' || comfyUrl.length === 0 || !/^https?:\/\//i.test(comfyUrl)) {
|
|
170
|
+
throw new Error(`Invalid comfyUrl "${comfyUrl}" — must start with http:// or https://`);
|
|
171
|
+
}
|
|
172
|
+
const url = `${comfyUrl}/view?filename=${encodeURIComponent(filename)}&subfolder=${encodeURIComponent(subfolder || '')}&type=${encodeURIComponent(type)}`;
|
|
79
173
|
const res = await fetch(url);
|
|
80
174
|
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
81
175
|
return Buffer.from(await res.arrayBuffer());
|
package/lib/config.js
CHANGED
|
@@ -7,17 +7,70 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { readFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
10
11
|
import { join } from 'node:path';
|
|
12
|
+
import { inputError, runtimeError } from './errors.js';
|
|
11
13
|
|
|
12
14
|
function loadJsonFile(filePath) {
|
|
13
15
|
if (!existsSync(filePath)) return null;
|
|
14
16
|
try {
|
|
15
17
|
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
16
18
|
} catch (err) {
|
|
17
|
-
throw
|
|
19
|
+
throw runtimeError(
|
|
20
|
+
'INPUT_INVALID_JSON',
|
|
21
|
+
`Failed to parse ${filePath}: ${err.message}`,
|
|
22
|
+
'Check the file for JSON syntax errors (trailing commas, unquoted keys, etc).',
|
|
23
|
+
err
|
|
24
|
+
);
|
|
18
25
|
}
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Async-safe readFile + JSON.parse with structured error handling.
|
|
30
|
+
* Optionally validates that required top-level fields are present.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} filePath — absolute path to a JSON file
|
|
33
|
+
* @param {{requiredFields?: string[]}} [opts]
|
|
34
|
+
*/
|
|
35
|
+
export async function readJsonFile(filePath, opts = {}) {
|
|
36
|
+
let raw;
|
|
37
|
+
try {
|
|
38
|
+
raw = await readFile(filePath, 'utf-8');
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err && err.code === 'ENOENT') {
|
|
41
|
+
throw inputError(
|
|
42
|
+
'INPUT_FILE_NOT_FOUND',
|
|
43
|
+
`File not found: ${filePath}`,
|
|
44
|
+
'Check the path and try again.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
throw runtimeError('RUNTIME_READ_FAILED', `Could not read ${filePath}: ${err.message}`, null, err);
|
|
48
|
+
}
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(raw);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
throw runtimeError(
|
|
54
|
+
'INPUT_INVALID_JSON',
|
|
55
|
+
`Failed to parse ${filePath}: ${err.message}`,
|
|
56
|
+
'Check the file for JSON syntax errors.',
|
|
57
|
+
err
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const required = opts.requiredFields || [];
|
|
61
|
+
if (required.length) {
|
|
62
|
+
const missing = required.filter((f) => parsed == null || parsed[f] === undefined);
|
|
63
|
+
if (missing.length) {
|
|
64
|
+
throw inputError(
|
|
65
|
+
'INPUT_MISSING_FIELDS',
|
|
66
|
+
`${filePath} is missing required fields: ${missing.join(', ')}`,
|
|
67
|
+
`Add the fields to the file, or use a valid example as template.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return parsed;
|
|
72
|
+
}
|
|
73
|
+
|
|
21
74
|
/**
|
|
22
75
|
* Load project.json — project identity and generation defaults.
|
|
23
76
|
*/
|
|
@@ -42,7 +95,13 @@ export function loadConstitution(projectRoot) {
|
|
|
42
95
|
const data = loadJsonFile(join(projectRoot, 'constitution.json'));
|
|
43
96
|
if (!data) return { rules: [] };
|
|
44
97
|
if (Array.isArray(data)) return { rules: data };
|
|
45
|
-
if (!data.rules)
|
|
98
|
+
if (!data.rules) {
|
|
99
|
+
throw inputError(
|
|
100
|
+
'INPUT_BAD_CONFIG',
|
|
101
|
+
`constitution.json must have a "rules" array`,
|
|
102
|
+
'Wrap your rules in { "rules": [...] } or use an array at the top level.'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
46
105
|
return data;
|
|
47
106
|
}
|
|
48
107
|
|
|
@@ -54,7 +113,13 @@ export function loadLanes(projectRoot) {
|
|
|
54
113
|
if (!data) {
|
|
55
114
|
return { default_lane: 'default', lanes: [] };
|
|
56
115
|
}
|
|
57
|
-
if (!data.lanes)
|
|
116
|
+
if (!data.lanes) {
|
|
117
|
+
throw inputError(
|
|
118
|
+
'INPUT_BAD_CONFIG',
|
|
119
|
+
`lanes.json must have a "lanes" array`,
|
|
120
|
+
'Top-level shape: { "default_lane": "...", "lanes": [ ... ] }.'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
58
123
|
return data;
|
|
59
124
|
}
|
|
60
125
|
|