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