import { mkdirSync } from "fs"; import path from "path"; import { prisma } from "./prisma"; import { runAgentStep } from "./claude"; import { getAllSettings } from "./settings"; import { scanOutputDirectory } from "./scanner"; import type { AppConfig } from "./claude"; const PIPELINE_ROOT = process.env.PIPELINE_ROOT || path.join(process.cwd(), "pipeline"); interface VariationRequest { assetId: string; count: number; appConfig?: AppConfig; } async function loadPipelineEnv(): Promise> { const settings = await getAllSettings(); const env: Record = {}; if (settings.TAVILY_API_KEY) env.TAVILY_API_KEY = settings.TAVILY_API_KEY; if (settings.GEMINI_API_KEY) env.GEMINI_API_KEY = settings.GEMINI_API_KEY; return env; } /** * Extract the "DNA" from an asset — hook, CTA, visual style, platform, dimensions. */ function extractAssetDNA(asset: { metadata: string | null; platform: string | null; dimensions: string | null; filePath: string; type: string; }) { const meta = asset.metadata ? JSON.parse(asset.metadata) : {}; return { hook: meta.hook || meta.headline || meta.hookText || null, caption: meta.caption || null, cta: meta.cta || meta.ctaText || null, style: meta.style || null, platform: asset.platform, dimensions: asset.dimensions, type: asset.type, isGemini: asset.filePath.toLowerCase().includes("/gemini/"), isPoster: asset.filePath.toLowerCase().includes("/posters/"), }; } /** * Build a prompt for the ad-creative-designer to generate variations of a winning asset. */ function buildVariationPrompt( dna: ReturnType, sourceFilePath: string, count: number, outputDir: string, appConfig?: AppConfig ): string { const appName = appConfig?.name ?? "the app"; const primaryColor = appConfig?.primaryColor ?? "#0079FF"; const accentColor = appConfig?.accentColor ?? "#FF9400"; const assetsDir = appConfig?.assetsDir ?? "assets"; return `You are the Ad Creative Designer agent generating VARIATIONS of a winning ad. ## Source Asset (the winner to riff on) - File: ${sourceFilePath} - Platform: ${dna.platform || "instagram"} - Dimensions: ${dna.dimensions || "1080x1920"} - Hook: "${dna.hook || "Unknown"}" - CTA: "${dna.cta || "Download Free"}" - Style: ${dna.isGemini ? "Gemini AI-generated" : dna.isPoster ? "Canvas Design poster" : "Unknown"} ## Your Task Generate ${count} NEW variations that keep the same emotional pattern as the winning hook but explore different angles. Rules: - Same platform and dimensions as the source - Same visual style (${dna.isGemini ? "Gemini AI photo-realistic" : "Canvas Design poster art"}) - Same CTA pattern - DIFFERENT hook text — same emotional structure, different scenarios - Brand colors: ${primaryColor} (primary), ${accentColor} (accent) - App name: ${appName} - App icon: ${assetsDir}/icon.png — must appear in every ad ${dna.isGemini ? `Use mcp__nanobanana__gemini_generate_image for each variation. Use the source ad as a reference_image so Gemini keeps visual consistency. Reference image path: \${CWD}/${sourceFilePath}` : `Create each poster programmatically using Playwright (HTML → PNG). Match the visual language of the source poster.`} Save all outputs to: ${outputDir}/ Name files: variation_${dna.platform || "ig"}_${count > 1 ? "{n}" : "1"}_${dna.dimensions || "1080x1920"}.png Save a manifest: ${outputDir}/variation_manifest.json with fields: fileName, hook, platform, dimensions, parentFile. ## Also Generate Copy For each variation, write a platform-tuned caption. Save to: ${outputDir}/captions.json as an array of { hook, caption, platform, fileName }.`; } /** * Spawn variations of a winning asset. * Runs ad-creative-designer agent with a focused variation prompt. */ export async function spawnVariations(request: VariationRequest) { const asset = await prisma.asset.findUnique({ where: { id: request.assetId }, include: { campaign: true }, }); if (!asset) throw new Error("Asset not found"); if (!asset.campaign) throw new Error("Asset has no campaign"); const dna = extractAssetDNA(asset); const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, ""); const outputDir = `outputs/variations_${asset.id.slice(0, 8)}_${dateStr}`; mkdirSync(path.join(PIPELINE_ROOT, outputDir), { recursive: true }); const prompt = buildVariationPrompt( dna, asset.filePath, request.count, outputDir, request.appConfig ); const env = await loadPipelineEnv(); const { output } = await runAgentStep("ad-creative-designer", prompt, PIPELINE_ROOT, env); // Scan for generated assets const scanResult = await scanOutputDirectory(asset.campaignId, outputDir, PIPELINE_ROOT); // Link new assets to the parent const newAssets = await prisma.asset.findMany({ where: { campaignId: asset.campaignId, filePath: { startsWith: outputDir }, parentAssetId: null, }, }); for (const newAsset of newAssets) { await prisma.asset.update({ where: { id: newAsset.id }, data: { parentAssetId: asset.id }, }); } return { output: output.slice(0, 500), outputDir, scanned: scanResult.scanned, created: scanResult.created, parentAssetId: asset.id, }; }