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>
This commit is contained in:
+74
-44
@@ -1,16 +1,16 @@
|
||||
import sharp from "sharp";
|
||||
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 }> = {
|
||||
"instagram-feed": { width: 1080, height: 1080, label: "Instagram Feed" },
|
||||
"instagram-stories": { width: 1080, height: 1920, label: "Instagram Stories" },
|
||||
tiktok: { width: 1080, height: 1920, label: "TikTok" },
|
||||
"nextdoor-spotlight": { width: 1200, height: 1200, label: "Nextdoor Spotlight" },
|
||||
"nextdoor-display": { width: 1200, height: 628, label: "Nextdoor Display" },
|
||||
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[] {
|
||||
@@ -24,34 +24,80 @@ export function getPlatformFormat(key: string) {
|
||||
return PLATFORM_FORMATS[key] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize an image to target platform dimensions using Sharp.
|
||||
* Uses cover fit + center crop for aspect ratio changes.
|
||||
*/
|
||||
export async function resizeImage(
|
||||
sourcePath: string,
|
||||
targetFormat: string,
|
||||
outputDir: string
|
||||
): Promise<{ filePath: string; fileName: string; dimensions: string; platform: string }> {
|
||||
const fmt = PLATFORM_FORMATS[targetFormat];
|
||||
if (!fmt) throw new Error(`Unknown format: ${targetFormat}`);
|
||||
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 fileName = `${sourceBase}_${targetFormat}_${fmt.width}x${fmt.height}.png`;
|
||||
const outputPath = path.join(outputDir, fileName);
|
||||
const fullOutputPath = path.join(PIPELINE_ROOT, outputPath);
|
||||
const fullSourcePath = path.join(PIPELINE_ROOT, sourcePath);
|
||||
const env = await loadPipelineEnv();
|
||||
|
||||
await sharp(fullSourcePath)
|
||||
.resize(fmt.width, fmt.height, { fit: "cover", position: "centre" })
|
||||
.png()
|
||||
.toFile(fullOutputPath);
|
||||
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 }>;
|
||||
|
||||
const platform = targetFormat.split("-")[0];
|
||||
// Resolve absolute paths so the agent doesn't have to guess
|
||||
const absPipelineRoot = path.resolve(PIPELINE_ROOT);
|
||||
const absSourcePath = path.join(absPipelineRoot, sourcePath);
|
||||
|
||||
return { filePath: outputPath, fileName, dimensions: `${fmt.width}x${fmt.height}`, platform };
|
||||
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],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,19 +123,3 @@ Rules:
|
||||
const { output } = await runAgentStep("caption-retone", prompt, PIPELINE_ROOT, {});
|
||||
return output.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Repurpose an image asset to specific platform formats.
|
||||
*/
|
||||
export async function repurposeImage(
|
||||
sourcePath: string,
|
||||
targetFormats: string[],
|
||||
outputDir: string
|
||||
): Promise<Array<{ filePath: string; fileName: string; dimensions: string; platform: string }>> {
|
||||
const results = [];
|
||||
for (const fmt of targetFormats) {
|
||||
const result = await resizeImage(sourcePath, fmt, outputDir);
|
||||
results.push(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user