import path from "path"; import { mkdirSync } from "fs"; import { runAgentStep } from "./claude"; import { getAllSettings } from "./settings"; const PIPELINE_ROOT = process.env.PIPELINE_ROOT || path.join(process.cwd(), "pipeline"); const PLATFORM_FORMATS: Record = { "instagram-feed": { width: 1080, height: 1080, label: "Instagram Feed", ratio: "1:1" }, "instagram-stories": { width: 1080, height: 1920, label: "Instagram Stories", ratio: "9:16" }, tiktok: { width: 1080, height: 1920, label: "TikTok", ratio: "9:16" }, "nextdoor-spotlight": { width: 1200, height: 1200, label: "Nextdoor Spotlight", ratio: "1:1" }, "nextdoor-display": { width: 1200, height: 628, label: "Nextdoor Display", ratio: "191:100" }, }; export function getAvailableFormats(currentDimensions: string | null): string[] { return Object.keys(PLATFORM_FORMATS).filter((key) => { const fmt = PLATFORM_FORMATS[key]; return `${fmt.width}x${fmt.height}` !== currentDimensions; }); } export function getPlatformFormat(key: string) { return PLATFORM_FORMATS[key] || null; } async function loadPipelineEnv(): Promise> { const settings = await getAllSettings(); const env: Record = {}; if (settings.GEMINI_API_KEY) env.GEMINI_API_KEY = settings.GEMINI_API_KEY; return env; } /** * Repurpose an image to target platform formats using Gemini via Claude CLI. * Claude analyzes the original ad and instructs Gemini to regenerate it * at the new dimensions with proper layout adaptation. */ export async function repurposeImage( sourcePath: string, sourceDimensions: string | null, targetFormats: string[], outputDir: string ): Promise> { mkdirSync(path.join(PIPELINE_ROOT, outputDir), { recursive: true }); const sourceBase = path.basename(sourcePath, path.extname(sourcePath)); const env = await loadPipelineEnv(); const formatInstructions = targetFormats.map((key) => { const fmt = PLATFORM_FORMATS[key]; if (!fmt) return null; const fileName = `${sourceBase}_${key}_${fmt.width}x${fmt.height}.png`; return { key, fmt, fileName }; }).filter(Boolean) as Array<{ key: string; fmt: typeof PLATFORM_FORMATS[string]; fileName: string }>; // Resolve absolute paths so the agent doesn't have to guess const absPipelineRoot = path.resolve(PIPELINE_ROOT); const absSourcePath = path.join(absPipelineRoot, sourcePath); const prompt = `You are an ad creative reformatter. Your job is to take an existing ad image and recreate it at different dimensions, keeping ALL the same content — same text, same layout structure, same colors, same branding, same imagery — but properly recomposed for the new aspect ratio. ## Source Ad - File: ${absSourcePath} - Current dimensions: ${sourceDimensions || "unknown"} First, use the Read tool to look at the source image so you can see exactly what it contains. ## What To Do For EACH target format below, do this EXACT sequence: 1. Call mcp__nanobanana__set_aspect_ratio with the correct ratio string 2. Call mcp__nanobanana__gemini_generate_image with: - reference_images: ["${absSourcePath}"] - prompt: Describe in detail everything you see in the source ad — the exact headline text, body text, phone mockup, app screenshot, app icon, CTA button text and color, brand name, background color and style. Then say: "Recreate this ad with all these elements recomposed for the new aspect ratio. Keep all text word-for-word identical. Adapt the layout naturally for the new dimensions." - output_path: the ABSOLUTE output path shown below ## Target Formats ${formatInstructions.map((f) => `### ${f.fmt.label} (${f.fmt.width}x${f.fmt.height}) - Aspect ratio for set_aspect_ratio: "${f.fmt.ratio}" - output_path: "${path.join(absPipelineRoot, outputDir, f.fileName)}"`).join("\n\n")} CRITICAL RULES: - You MUST read the source image first to see what it contains - Every piece of text from the original MUST appear in the output, word for word - The visual style, colors, and branding must match exactly - The layout should be ADAPTED for the new dimensions, not just cropped or padded - Use the exact output_path values above — they are absolute paths - The reference_images array must use the absolute source path above`; await runAgentStep("repurpose-adapter", prompt, PIPELINE_ROOT, env); // Return expected output info — the agent writes files to disk return formatInstructions.map((f) => ({ filePath: path.join(outputDir, f.fileName), fileName: f.fileName, dimensions: `${f.fmt.width}x${f.fmt.height}`, platform: f.key.split("-")[0], })); } /** * Re-tone a caption for a different platform using Claude CLI. */ export async function retoneCaption( originalCaption: string, originalPlatform: string, targetPlatform: string ): Promise { const prompt = `You are a social media copywriter. Adapt this ${originalPlatform} caption for ${targetPlatform}. Original caption: ${originalCaption} Rules: - ${targetPlatform === "tiktok" ? "Raw, authentic tone. Short. Trending hashtags. Hook in first line." : ""} - ${targetPlatform === "instagram" ? "Polished, aspirational. Hook → Value → CTA → Hashtags. 150-300 chars." : ""} - ${targetPlatform === "nextdoor" ? "Warm, neighborly. No hashtags. Reference local/community context." : ""} - Keep the core message but match the platform's voice. - Return ONLY the new caption text, nothing else.`; const { output } = await runAgentStep("caption-retone", prompt, PIPELINE_ROOT, {}); return output.trim(); }