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:
+132
-16
@@ -13,7 +13,8 @@ const AGENT_STEPS = [
|
||||
"trend-scout",
|
||||
"marketing-research-agent",
|
||||
"script-writer",
|
||||
"ad-creative-designer",
|
||||
"gemini-ad-designer",
|
||||
"poster-ad-designer",
|
||||
"video-ad-producer",
|
||||
"copywriter-agent",
|
||||
"distribution-agent",
|
||||
@@ -31,6 +32,7 @@ export interface AppConfig {
|
||||
brandIdentity: string | null;
|
||||
productInfo: string | null;
|
||||
platformGuidelines: string | null;
|
||||
stylePreferences: string | null;
|
||||
}
|
||||
|
||||
interface CampaignConfig {
|
||||
@@ -122,6 +124,82 @@ Read CLAUDE.md first. Then execute each agent skill in order:
|
||||
CRITICAL: Read each skill's SKILL.md before executing. Follow the skill instructions exactly.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a style preferences prompt section from app config.
|
||||
* Groups liked references by style tag (with-people / without-people) set at
|
||||
* generation time so the correct reference images are used for matching ads.
|
||||
* Disliked reasons are text-only and universal.
|
||||
*/
|
||||
function buildStylePreferencesSection(appConfig?: AppConfig): string {
|
||||
if (!appConfig?.stylePreferences) return "";
|
||||
interface PrefEntry { filePath: string; fileName: string; style?: string | null }
|
||||
interface DislikeEntry { reason: string; fileName: string; style?: string | null }
|
||||
let prefs: { liked?: PrefEntry[]; disliked?: DislikeEntry[] };
|
||||
try {
|
||||
prefs = JSON.parse(appConfig.stylePreferences);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
const liked = prefs.liked || [];
|
||||
const disliked = prefs.disliked || [];
|
||||
if (liked.length === 0 && disliked.length === 0) return "";
|
||||
|
||||
const withPeople = liked.filter((e) => e.style === "with-people");
|
||||
const withoutPeople = liked.filter((e) => e.style === "without-people");
|
||||
const untagged = liked.filter((e) => !e.style);
|
||||
|
||||
const lines: string[] = ["## Style Preferences (learned from user feedback)", ""];
|
||||
|
||||
if (withPeople.length > 0) {
|
||||
lines.push("### DO — liked WITH-PEOPLE ads (use as reference_images ONLY for ads with people):");
|
||||
for (const entry of withPeople) {
|
||||
lines.push(`- "${entry.fileName}"`);
|
||||
}
|
||||
const refs = withPeople.slice(-2);
|
||||
lines.push("Reference image paths:");
|
||||
for (const entry of refs) {
|
||||
lines.push(`- ${entry.filePath}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
if (withoutPeople.length > 0) {
|
||||
lines.push("### DO — liked WITHOUT-PEOPLE ads (use as reference_images ONLY for product-focused ads):");
|
||||
for (const entry of withoutPeople) {
|
||||
lines.push(`- "${entry.fileName}"`);
|
||||
}
|
||||
const refs = withoutPeople.slice(-2);
|
||||
lines.push("Reference image paths:");
|
||||
for (const entry of refs) {
|
||||
lines.push(`- ${entry.filePath}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
if (untagged.length > 0) {
|
||||
lines.push("### DO — liked ads (general inspiration, use for any ad type):");
|
||||
for (const entry of untagged) {
|
||||
lines.push(`- "${entry.fileName}"`);
|
||||
}
|
||||
const refs = untagged.slice(-2);
|
||||
lines.push("Reference image paths:");
|
||||
for (const entry of refs) {
|
||||
lines.push(`- ${entry.filePath}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
if (disliked.length > 0) {
|
||||
lines.push("### DON'T — styles the user dislikes (applies to ALL ad types):");
|
||||
for (const entry of disliked) {
|
||||
lines.push(`- Avoid: "${entry.reason}" (from "${entry.fileName}")`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a focused prompt for a single agent step.
|
||||
*/
|
||||
@@ -198,9 +276,9 @@ Save outputs to: ${outputDir}/scripts/
|
||||
|
||||
${campaignBrief}`,
|
||||
|
||||
"ad-creative-designer": `You are the Ad Creative Designer agent.
|
||||
"gemini-ad-designer": `You are the Gemini Ad Designer agent.
|
||||
|
||||
Read and follow the skill instructions in skills/ad-creative-designer/SKILL.md exactly.
|
||||
Read and follow the skill instructions in skills/gemini-ad-designer/SKILL.md exactly.
|
||||
|
||||
First, read these knowledge files:
|
||||
- ${knowledgeDir}/brand_identity.md
|
||||
@@ -211,11 +289,7 @@ Read the upstream outputs:
|
||||
- ${outputDir}/scripts/scripts_all.json
|
||||
- ${outputDir}/research_brief.md
|
||||
|
||||
You MUST produce TWO SETS of image assets:
|
||||
|
||||
---
|
||||
|
||||
## SET 1: Gemini AI-Generated Ads (NanoBanana MCP)
|
||||
## Gemini AI-Generated Ads (NanoBanana MCP)
|
||||
|
||||
Use the NanoBanana MCP tools to create polished ad images.${screenshots?.length ? `
|
||||
|
||||
@@ -247,9 +321,31 @@ Generate exactly 4 Gemini ads with this mix:
|
||||
|
||||
IMPORTANT: For ads WITH people, show real-looking people naturally using the app — not stock photo poses. For ads WITHOUT people, focus on the phone/app in an environment (floating over house, on a counter, etc.)
|
||||
|
||||
---
|
||||
Platform dimensions:
|
||||
- Instagram Feed: 1080x1080
|
||||
- Instagram Stories: 1080x1920
|
||||
- Nextdoor Spotlight: 1200x1200
|
||||
- Nextdoor Display: 1200x628
|
||||
|
||||
## SET 2: Canvas Design Posters (Museum-quality art)
|
||||
Save ${outputDir}/ads/gemini/manifest.json listing all generated Gemini ads with fields: fileName, set ("gemini"), hook, platform, dimensions, headline, style ("with-people" or "without-people").
|
||||
|
||||
${buildStylePreferencesSection(appConfig)}
|
||||
${campaignBrief}`,
|
||||
|
||||
"poster-ad-designer": `You are the Poster Ad Designer agent.
|
||||
|
||||
Read and follow the skill instructions in skills/poster-ad-designer/SKILL.md exactly.
|
||||
|
||||
First, read these knowledge files:
|
||||
- ${knowledgeDir}/brand_identity.md
|
||||
- ${knowledgeDir}/platform_guidelines.md
|
||||
- ${knowledgeDir}/product_campaign.md
|
||||
|
||||
Read the upstream outputs:
|
||||
- ${outputDir}/scripts/scripts_all.json
|
||||
- ${outputDir}/research_brief.md
|
||||
|
||||
## Canvas Design Posters (Museum-quality art)
|
||||
|
||||
Create poster ads using the /skill canvas-design approach. This is a TWO-STEP process:
|
||||
|
||||
@@ -276,6 +372,16 @@ Using the philosophy, create each poster as a .png file. For each:
|
||||
8. The result should look like it could hang in a gallery or appear in a design magazine
|
||||
9. The ${appName} app icon (${assetsDir}/icon.png) MUST appear in every poster, placed near the branding or CTA area. Use it as an <img> element in the HTML.
|
||||
|
||||
### CRITICAL Layout Rule: Phone Must NOT Cover Text
|
||||
The phone mockup and text MUST occupy separate vertical zones — NEVER overlapping.
|
||||
Use a three-zone vertical layout:
|
||||
- **Top zone (15-30%):** Headline text only. No phone.
|
||||
- **Middle zone (40-55%):** Phone mockup, centered. No text overlapping.
|
||||
- **Bottom zone (15-25%):** Subtext, CTA, branding. No phone.
|
||||
For 9:16 (1080x1920): headline top ~380px, phone middle ~420-1400px, CTA bottom ~400px.
|
||||
For 1:1 (1080x1080): headline top ~200px, phone center max 45% width/500px tall, CTA bottom ~250px.
|
||||
Before rendering, verify bounding boxes don't overlap. If they do, shrink the phone.
|
||||
|
||||
### MANDATORY Typography & Sizing Rules (Social Media Readability)
|
||||
These are viewed on phones at arm's length. Text that looks fine on a monitor is INVISIBLE in a feed.
|
||||
|
||||
@@ -302,16 +408,17 @@ Generate at least 4 posters:
|
||||
- 2x Instagram (1 feed 1080x1080, 1 stories 1080x1920)
|
||||
- 2x TikTok cover images (1080x1920)
|
||||
|
||||
---
|
||||
|
||||
Platform dimensions:
|
||||
- Instagram Feed: 1080x1080
|
||||
- Instagram Stories: 1080x1920
|
||||
- Nextdoor Spotlight: 1200x1200
|
||||
- Nextdoor Display: 1200x628
|
||||
|
||||
Save ${outputDir}/ads/ad_manifest.json listing ALL generated ads from BOTH sets, with fields: fileName, set ("gemini" or "poster"), hook, platform, dimensions, headline.
|
||||
Save ${outputDir}/ads/posters/manifest.json listing all generated poster ads with fields: fileName, set ("poster"), hook, platform, dimensions, headline.
|
||||
|
||||
After generating posters, also create the combined ${outputDir}/ads/ad_manifest.json listing ALL ads from both sets (read ${outputDir}/ads/gemini/manifest.json for the Gemini ads). Fields: fileName, set ("gemini" or "poster"), hook, platform, dimensions, headline, style.
|
||||
|
||||
${buildStylePreferencesSection(appConfig)}
|
||||
${campaignBrief}`,
|
||||
|
||||
"video-ad-producer": `You are the Video Ad Producer agent.
|
||||
@@ -425,7 +532,8 @@ const AGENT_LABELS: Record<string, string> = {
|
||||
"trend-scout": "Trend Scout",
|
||||
"marketing-research-agent": "Research Agent",
|
||||
"script-writer": "Script Writer",
|
||||
"ad-creative-designer": "Ad Creative Designer",
|
||||
"gemini-ad-designer": "Gemini Ad Designer",
|
||||
"poster-ad-designer": "Poster Ad Designer",
|
||||
"video-ad-producer": "Video Ad Producer",
|
||||
"copywriter-agent": "Copywriter",
|
||||
"distribution-agent": "Distribution Agent",
|
||||
@@ -472,6 +580,13 @@ function humanizeAgentError(agentName: string, code: number | null, stderr: stri
|
||||
return `${label} failed unexpectedly`;
|
||||
}
|
||||
|
||||
const AGENT_TOOLS: Record<string, string> = {
|
||||
"gemini-ad-designer":
|
||||
"Read,Edit,Write,Bash,Grep,Glob,mcp__nanobanana__generate_image,mcp__nanobanana__set_aspect_ratio",
|
||||
};
|
||||
const DEFAULT_TOOLS =
|
||||
"Read,Edit,Write,Bash,Grep,Glob";
|
||||
|
||||
export async function runAgentStep(
|
||||
agentName: string,
|
||||
prompt: string,
|
||||
@@ -479,6 +594,7 @@ export async function runAgentStep(
|
||||
env: Record<string, string>
|
||||
): Promise<{ output: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const allowedTools = AGENT_TOOLS[agentName] || DEFAULT_TOOLS;
|
||||
const args = [
|
||||
"-p",
|
||||
prompt,
|
||||
@@ -486,7 +602,7 @@ export async function runAgentStep(
|
||||
"stream-json",
|
||||
"--verbose",
|
||||
"--allowedTools",
|
||||
"Read,Edit,Write,Bash,Grep,Glob,mcp__nanobanana__generate_image",
|
||||
allowedTools,
|
||||
];
|
||||
|
||||
const claude = spawn("claude", args, {
|
||||
@@ -736,7 +852,7 @@ export async function sendChatMessage(
|
||||
"stream-json",
|
||||
"--verbose",
|
||||
"--allowedTools",
|
||||
"Read,Edit,Write,Bash,Grep,Glob,mcp__nanobanana__generate_image",
|
||||
"Read,Edit,Write,Bash,Grep,Glob,mcp__nanobanana__generate_image,mcp__nanobanana__set_aspect_ratio",
|
||||
];
|
||||
if (sessionId) args.push("--resume", sessionId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user