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:
Trey t
2026-05-03 20:28:07 -05:00
parent b318798ca7
commit 807dfc539b
40 changed files with 3089 additions and 232 deletions
+132 -16
View File
@@ -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);