← back to Visual Factory
src/stages/critic.js
53 lines
// Stage 6 — critic. Single Claude CLI subprocess call. Compares the llava description
// of the rendered image against the original brief + spec; returns a JSON review.
const { claudeCli } = require('../llm');
const PROMPT = (brief, spec, vision) => `You are reviewing an automated visual generation.
IMPORTANT CALIBRATION: The vision-model description below is from a small local model (llava). It frequently hallucinates colors and misses text. Treat \`vision.text_content\` as a LOWER BOUND on what's actually rendered, not the truth — the renderer wrote real HTML with the spec's content blocks, so they ARE present even if vision didn't read them. Treat \`vision.palette\` and \`vision.mood\` as soft signals, not facts. The strongest reliable signals are:
- \`vision.overall_quality\` (the model's holistic read — usually trustworthy)
- \`vision.issues\` (only if it flagged something concrete like clipping/overflow)
ORIGINAL BRIEF:
${brief}
EXTRACTED SPEC:
${JSON.stringify(spec, null, 2)}
VISION-MODEL REPORT (treat per the calibration above):
${JSON.stringify(vision, null, 2)}
Decision policy:
- If \`vision.overall_quality\` ≥ 75 AND \`vision.issues\` is empty/minor → APPROVE (score = vision.overall_quality)
- If \`vision.issues\` flags clipping, broken layout, or lorem ipsum → REJECT with concrete fixes
- Don't reject for "missing palette colors" or "missing text" based on vision alone — those are unreliable
- Don't reject for subjective mood mismatch alone
Return ONLY a JSON object, no prose, no fences:
{
"approved": <true|false>,
"score": <0-100>,
"issues": [<short string per concrete problem actually evidenced>],
"fix_instructions": [<short string per actionable HTML/CSS revision; empty if approved>]
}`;
async function runCritic({ brief, spec, vision, signal = null }) {
const raw = await claudeCli({ prompt: PROMPT(brief, spec, vision), timeoutMs: 180000, signal });
const m = raw.match(/\{[\s\S]*\}/);
if (!m) return { approved: false, score: 0, issues: ['critic returned no JSON'], fix_instructions: [], raw_excerpt: raw.slice(0, 300) };
try {
const r = JSON.parse(m[0]);
return {
approved: !!r.approved,
score: Number(r.score) || 0,
issues: Array.isArray(r.issues) ? r.issues : [],
fix_instructions: Array.isArray(r.fix_instructions) ? r.fix_instructions : []
};
} catch (e) {
return { approved: false, score: 0, issues: [`critic JSON parse failed: ${e.message}`], fix_instructions: [] };
}
}
module.exports = { runCritic };