← back to Visual Factory

src/stages/intake.js

45 lines

// Stage 1 — intake. Brief → structured visual spec.

const { ollama } = require('../llm');

const SYSTEM = `You convert a visual brief into a structured spec for "The Visual Factory".
The Factory composes HTML+CSS, screenshots it via Playwright, and reviews the PNG.
Return ONLY a JSON object:
  visual_type     "social_post" | "hero_image" | "poster" | "diagram" | "card" | "ad_creative"
  artifact_name   short kebab-case identifier
  width           integer pixels (default 1200)
  height          integer pixels (default 1200; for social_post 1080; hero 1200x600; poster 1200x1500)
  palette         array of 3-5 hex colors that fit the mood
  mood            one short phrase (e.g. "minimalist editorial", "bold maximalist")
  typography      "serif" | "sans" | "display" | "mono"
  content_blocks  array of objects: {kind: "headline"|"subhead"|"body"|"cta"|"image_placeholder", text: "..."} (max 6)
  background      "solid" | "gradient" | "pattern"
No prose, no markdown, no code fences.`;

const KEBAB = /^[a-z0-9-]{2,60}$/;

async function runIntake({ brief, signal = null }) {
  const spec = await ollama({
    model: process.env.COMPOSE_MODEL || 'qwen3:14b',
    system: SYSTEM,
    prompt: brief,
    format: 'json',
    temperature: 0.2,
    signal
  });

  const errors = [];
  if (!spec.visual_type || !['social_post','hero_image','poster','diagram','card','ad_creative'].includes(spec.visual_type)) {
    errors.push(`visual_type invalid: ${JSON.stringify(spec.visual_type)}`);
  }
  if (!spec.artifact_name || !KEBAB.test(spec.artifact_name)) errors.push(`artifact_name invalid: ${JSON.stringify(spec.artifact_name)}`);
  spec.width = Number(spec.width) || 1200;
  spec.height = Number(spec.height) || 1200;
  spec.palette = Array.isArray(spec.palette) ? spec.palette.filter(c => /^#?[0-9a-fA-F]{6}$/.test(String(c))).map(c => c.startsWith('#') ? c : `#${c}`).slice(0, 5) : [];
  if (!spec.palette.length) spec.palette = ['#0e0e10', '#e8e8ea', '#d4af37'];
  spec.content_blocks = Array.isArray(spec.content_blocks) ? spec.content_blocks.slice(0, 6) : [];
  return { spec, errors };
}

module.exports = { runIntake };