← 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 };