Files
ClaudeMarketing/lib/repurpose.ts
T
Trey t 807dfc539b feat: add asset preferences, video research, and Remotion ad assets
- Add thumbs-down feedback modal and preference API endpoint
- Add AI UGC video platforms research doc
- Add ReflectAd Remotion composition with public flow assets
- Add gemini-ad-designer and poster-ad-designer pipeline skills
- Add research_reflect_v1.1 pipeline script

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:28:07 -05:00

126 lines
5.6 KiB
TypeScript

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<string, { width: number; height: number; label: string; ratio: string }> = {
"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<Record<string, string>> {
const settings = await getAllSettings();
const env: Record<string, string> = {};
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<Array<{ filePath: string; fileName: string; dimensions: string; platform: string }>> {
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<string> {
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();
}