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:
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__nanobanana__generate_image",
|
||||
"mcp__nanobanana__gemini_generate_image",
|
||||
"mcp__nanobanana__(.*)",
|
||||
"Bash(*)",
|
||||
"Read(*)",
|
||||
"Write(*)",
|
||||
"Grep(*)",
|
||||
"Glob(*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
+9
-8
@@ -1,22 +1,23 @@
|
||||
# Marketing Content Pipeline
|
||||
|
||||
This project implements an AI-powered Social Media Content Automation System.
|
||||
Seven specialized agents research, generate, render, and distribute marketing content.
|
||||
Eight specialized agents research, generate, render, and distribute marketing content.
|
||||
|
||||
# System Architecture
|
||||
Seven agents running in sequence:
|
||||
Eight agents running in sequence:
|
||||
1. **Trend Scout** — trending content monitoring via Tavily
|
||||
2. **Marketing Research Agent** — deep market research via Tavily
|
||||
3. **Script Writer** — ad scripts from research output
|
||||
4. **Ad Creative Designer** — static ads via NanoBanana MCP + Playwright
|
||||
5. **Video Ad Producer** — video ads via Remotion
|
||||
6. **Copywriter Agent** — platform-specific copy
|
||||
7. **Distribution Agent** — publish manifest creation (gate-protected)
|
||||
4. **Gemini Ad Designer** — AI-generated image ads via NanoBanana MCP (Google Gemini)
|
||||
5. **Poster Ad Designer** — museum-quality poster ads via Playwright HTML-to-PNG
|
||||
6. **Video Ad Producer** — video ads via Remotion
|
||||
7. **Copywriter Agent** — platform-specific copy
|
||||
8. **Distribution Agent** — publish manifest creation (gate-protected)
|
||||
|
||||
# Folder Structure
|
||||
- `assets/` — brand images, logos, product shots (mood board)
|
||||
- `knowledge/` — brand identity, platform guidelines, product/campaign info
|
||||
- `skills/` — all 7 agent skills (each has SKILL.md)
|
||||
- `skills/` — all 8 agent skills (each has SKILL.md)
|
||||
- `outputs/` — generated content per campaign
|
||||
- `remotion-ad/` — Remotion video project with compositions
|
||||
|
||||
@@ -65,7 +66,7 @@ cd remotion-ad && npx remotion render src/index.ts CompositionId --output ../out
|
||||
You can modify or create new compositions in `remotion-ad/src/` before rendering.
|
||||
|
||||
# Pipeline Execution Order
|
||||
trend-scout → research → script-writer → ad-creative → video-producer → copywriter → distribution
|
||||
trend-scout → research → script-writer → gemini-ad-designer → poster-ad-designer → video-producer → copywriter → distribution
|
||||
|
||||
Each agent reads its SKILL.md from `skills/{agent-name}/SKILL.md` and follows it exactly.
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[{"name":"generate-buildid","duration":118,"timestamp":60500788710,"id":4,"parentId":1,"tags":{},"startTime":1774315255995,"traceId":"9f11bde72f6a7676"},{"name":"load-custom-routes","duration":220,"timestamp":60500788882,"id":5,"parentId":1,"tags":{},"startTime":1774315255996,"traceId":"9f11bde72f6a7676"},{"name":"create-dist-dir","duration":1140,"timestamp":60500789119,"id":6,"parentId":1,"tags":{},"startTime":1774315255996,"traceId":"9f11bde72f6a7676"},{"name":"clean","duration":278,"timestamp":60500790703,"id":7,"parentId":1,"tags":{},"startTime":1774315255997,"traceId":"9f11bde72f6a7676"},{"name":"next-build","duration":27478,"timestamp":60500763592,"id":1,"tags":{"buildMode":"default","version":"16.2.1","bundler":"turbopack","failed":true},"startTime":1774315255970,"traceId":"9f11bde72f6a7676"}]
|
||||
@@ -0,0 +1 @@
|
||||
[{"name":"next-build","duration":27478,"timestamp":60500763592,"id":1,"tags":{"buildMode":"default","version":"16.2.1","bundler":"turbopack","failed":true},"startTime":1774315255970,"traceId":"9f11bde72f6a7676"}]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 377 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 444 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 394 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 282 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
@@ -0,0 +1,581 @@
|
||||
import {
|
||||
AbsoluteFill,
|
||||
Img,
|
||||
interpolate,
|
||||
spring,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
staticFile,
|
||||
} from "remotion";
|
||||
|
||||
export interface ReflectAdProps {
|
||||
platform: "instagram" | "tiktok";
|
||||
hookText: string;
|
||||
bodyText: string;
|
||||
ctaText: string;
|
||||
proofText: string;
|
||||
screenshotSrc: string;
|
||||
}
|
||||
|
||||
const COLORS = {
|
||||
sage: "#3d5a4c",
|
||||
bronze: "#c4956b",
|
||||
cream: "#f5f1eb",
|
||||
dark: "#141210",
|
||||
white: "#ffffff",
|
||||
sageLight: "#5a7d6a",
|
||||
bronzeLight: "#d4ad85",
|
||||
};
|
||||
|
||||
export const ReflectAd: React.FC<ReflectAdProps> = ({
|
||||
platform,
|
||||
hookText,
|
||||
bodyText,
|
||||
ctaText,
|
||||
proofText,
|
||||
screenshotSrc,
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, durationInFrames, width, height } = useVideoConfig();
|
||||
|
||||
const isPolished = platform === "instagram";
|
||||
|
||||
// Scene boundaries (4 scenes per the brief)
|
||||
// Hook: 0 → ~20% | Phone reveal: ~20% → ~60% | Proof: ~60% → ~78% | CTA: ~78% → end
|
||||
const hookEnd = Math.floor(durationInFrames * 0.2);
|
||||
const phoneStart = hookEnd;
|
||||
const phoneEnd = Math.floor(durationInFrames * 0.6);
|
||||
const proofStart = phoneEnd;
|
||||
const proofEnd = Math.floor(durationInFrames * 0.78);
|
||||
const ctaStart = proofEnd;
|
||||
|
||||
// === HOOK SCENE ===
|
||||
const hookFadeIn = interpolate(frame, [0, 12], [0, 1], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const hookFadeOut = interpolate(
|
||||
frame,
|
||||
[hookEnd - 10, hookEnd],
|
||||
[1, 0],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
);
|
||||
const hookOpacity = Math.min(hookFadeIn, hookFadeOut);
|
||||
const hookY = isPolished
|
||||
? interpolate(frame, [0, 18], [30, 0], { extrapolateRight: "clamp" })
|
||||
: 0;
|
||||
// TikTok: pop-in scale
|
||||
const hookScale = isPolished
|
||||
? 1
|
||||
: spring({
|
||||
frame,
|
||||
fps,
|
||||
config: { damping: 14, stiffness: 120 },
|
||||
});
|
||||
|
||||
// === PHONE REVEAL SCENE ===
|
||||
const phoneSpring = spring({
|
||||
frame: Math.max(0, frame - phoneStart),
|
||||
fps,
|
||||
config: { damping: 16, stiffness: 60 },
|
||||
});
|
||||
const phoneOpacity = interpolate(
|
||||
frame,
|
||||
[phoneStart, phoneStart + 12, phoneEnd - 10, phoneEnd],
|
||||
[0, 1, 1, 0],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
);
|
||||
const bodyTextOpacity = interpolate(
|
||||
frame,
|
||||
[phoneStart + 18, phoneStart + 30],
|
||||
[0, 1],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
);
|
||||
const bodyTextFadeOut = interpolate(
|
||||
frame,
|
||||
[phoneEnd - 10, phoneEnd],
|
||||
[1, 0],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
);
|
||||
|
||||
// === PROOF SCENE ===
|
||||
const proofOpacity = interpolate(
|
||||
frame,
|
||||
[proofStart, proofStart + 12, proofEnd - 8, proofEnd],
|
||||
[0, 1, 1, 0],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
);
|
||||
const proofScale = spring({
|
||||
frame: Math.max(0, frame - proofStart),
|
||||
fps,
|
||||
config: { damping: 14 },
|
||||
});
|
||||
|
||||
// === CTA SCENE ===
|
||||
const ctaOpacity = interpolate(frame, [ctaStart, ctaStart + 14], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
const ctaScale = spring({
|
||||
frame: Math.max(0, frame - ctaStart),
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 80 },
|
||||
});
|
||||
const ctaPulse =
|
||||
frame > ctaStart + 24
|
||||
? 1 + 0.025 * Math.sin((frame - ctaStart - 24) * 0.12)
|
||||
: 1;
|
||||
|
||||
const phoneWidth = width * 0.52;
|
||||
const phoneHeight = phoneWidth * 2.05;
|
||||
|
||||
// Subtle background grain animation
|
||||
const grainOpacity = isPolished ? 0.03 : 0.06;
|
||||
|
||||
if (isPolished) {
|
||||
// ============================
|
||||
// POLISHED — Instagram Reels
|
||||
// Warm cream or dark bg, elegant serif, smooth fades
|
||||
// ============================
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
backgroundColor: COLORS.dark,
|
||||
fontFamily: '"Cormorant Garamond", "Georgia", serif',
|
||||
}}
|
||||
>
|
||||
{/* Warm radial gradient */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: `radial-gradient(ellipse at 50% 25%, ${COLORS.sage}20 0%, transparent 55%), radial-gradient(ellipse at 50% 75%, ${COLORS.bronze}15 0%, transparent 50%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Subtle grain texture */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
opacity: grainOpacity,
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='1'/%3E%3C/svg%3E")`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* === HOOK === */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "38%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) translateY(${hookY}px)`,
|
||||
opacity: hookOpacity,
|
||||
width: "82%",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 62,
|
||||
fontWeight: 400,
|
||||
fontStyle: "italic",
|
||||
color: COLORS.cream,
|
||||
lineHeight: 1.25,
|
||||
letterSpacing: 0.5,
|
||||
}}
|
||||
>
|
||||
{hookText}
|
||||
</div>
|
||||
{/* Decorative line */}
|
||||
<div
|
||||
style={{
|
||||
width: 60,
|
||||
height: 2,
|
||||
backgroundColor: COLORS.bronze,
|
||||
margin: "28px auto 0",
|
||||
opacity: hookFadeIn,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* === PHONE REVEAL + BODY TEXT === */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "6%",
|
||||
left: "50%",
|
||||
transform: `translateX(-50%) scale(${phoneSpring})`,
|
||||
opacity: phoneOpacity,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 24,
|
||||
}}
|
||||
>
|
||||
{/* Body text above phone */}
|
||||
<div
|
||||
style={{
|
||||
opacity: Math.min(bodyTextOpacity, bodyTextFadeOut),
|
||||
fontSize: 32,
|
||||
fontWeight: 300,
|
||||
color: COLORS.cream,
|
||||
textAlign: "center",
|
||||
maxWidth: width * 0.78,
|
||||
lineHeight: 1.4,
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
letterSpacing: 0.3,
|
||||
}}
|
||||
>
|
||||
{bodyText}
|
||||
</div>
|
||||
|
||||
{/* Phone mockup */}
|
||||
<div
|
||||
style={{
|
||||
width: phoneWidth,
|
||||
height: phoneHeight,
|
||||
position: "relative",
|
||||
filter: "drop-shadow(0 24px 48px rgba(0,0,0,0.45))",
|
||||
}}
|
||||
>
|
||||
{/* Screenshot behind the frame */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "3.2%",
|
||||
left: "4.2%",
|
||||
width: "91.6%",
|
||||
height: "93.6%",
|
||||
borderRadius: phoneWidth * 0.065,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={screenshotSrc}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
objectPosition: "top center",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Phone frame on top */}
|
||||
<Img
|
||||
src={staticFile("phone.png")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* === PROOF === */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "42%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${proofScale})`,
|
||||
opacity: proofOpacity,
|
||||
textAlign: "center",
|
||||
width: "80%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 40,
|
||||
fontWeight: 400,
|
||||
fontStyle: "italic",
|
||||
color: COLORS.bronze,
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
{proofText}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 26,
|
||||
fontWeight: 300,
|
||||
color: "rgba(245, 241, 235, 0.6)",
|
||||
marginTop: 16,
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
}}
|
||||
>
|
||||
Loved by thousands of mindful users
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* === CTA === */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
opacity: ctaOpacity,
|
||||
transform: `scale(${ctaScale})`,
|
||||
gap: 36,
|
||||
}}
|
||||
>
|
||||
{/* App name */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 52,
|
||||
fontWeight: 400,
|
||||
fontStyle: "italic",
|
||||
color: COLORS.cream,
|
||||
letterSpacing: 2,
|
||||
}}
|
||||
>
|
||||
Reflect
|
||||
</div>
|
||||
|
||||
{/* Tagline */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 26,
|
||||
fontWeight: 300,
|
||||
color: "rgba(245, 241, 235, 0.7)",
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
marginTop: -12,
|
||||
}}
|
||||
>
|
||||
A quiet space for your inner world
|
||||
</div>
|
||||
|
||||
{/* CTA button */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: COLORS.sage,
|
||||
color: COLORS.cream,
|
||||
fontSize: 32,
|
||||
fontWeight: 500,
|
||||
padding: "22px 64px",
|
||||
borderRadius: 28,
|
||||
textAlign: "center",
|
||||
boxShadow: `0 8px 32px ${COLORS.sage}60`,
|
||||
transform: `scale(${ctaPulse})`,
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
}}
|
||||
>
|
||||
{ctaText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Small logo watermark bottom-right */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 40,
|
||||
right: 40,
|
||||
opacity: interpolate(frame, [0, durationInFrames * 0.15], [0, 0.4], {
|
||||
extrapolateRight: "clamp",
|
||||
}),
|
||||
fontSize: 18,
|
||||
fontWeight: 300,
|
||||
color: COLORS.cream,
|
||||
fontFamily: '"Outfit", "Inter", sans-serif',
|
||||
letterSpacing: 1,
|
||||
}}
|
||||
>
|
||||
reflect
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================
|
||||
// AUTHENTIC — TikTok
|
||||
// Dark bg, bold text, quick cuts, max 6 words per frame, native feel
|
||||
// ============================
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
backgroundColor: "#0a0a0a",
|
||||
fontFamily: '"Inter", "SF Pro Display", system-ui, sans-serif',
|
||||
}}
|
||||
>
|
||||
{/* Subtle warm vignette */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: `radial-gradient(ellipse at 50% 50%, transparent 40%, rgba(0,0,0,0.4) 100%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* === HOOK === */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "40%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${hookScale})`,
|
||||
opacity: hookOpacity,
|
||||
width: "88%",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: 800,
|
||||
color: COLORS.white,
|
||||
lineHeight: 1.15,
|
||||
letterSpacing: -1,
|
||||
textShadow: "0 4px 20px rgba(0,0,0,0.5)",
|
||||
}}
|
||||
>
|
||||
{hookText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* === PHONE REVEAL + BODY TEXT === */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "5%",
|
||||
left: "50%",
|
||||
transform: `translateX(-50%) scale(${phoneSpring})`,
|
||||
opacity: phoneOpacity,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 20,
|
||||
}}
|
||||
>
|
||||
{/* Body text — bold, short */}
|
||||
<div
|
||||
style={{
|
||||
opacity: Math.min(bodyTextOpacity, bodyTextFadeOut),
|
||||
fontSize: 38,
|
||||
fontWeight: 700,
|
||||
color: COLORS.white,
|
||||
textAlign: "center",
|
||||
maxWidth: width * 0.85,
|
||||
lineHeight: 1.25,
|
||||
textShadow: "0 2px 12px rgba(0,0,0,0.4)",
|
||||
}}
|
||||
>
|
||||
{bodyText}
|
||||
</div>
|
||||
|
||||
{/* Phone mockup */}
|
||||
<div
|
||||
style={{
|
||||
width: phoneWidth,
|
||||
height: phoneHeight,
|
||||
position: "relative",
|
||||
filter: "drop-shadow(0 16px 40px rgba(0,0,0,0.6))",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "3.2%",
|
||||
left: "4.2%",
|
||||
width: "91.6%",
|
||||
height: "93.6%",
|
||||
borderRadius: phoneWidth * 0.065,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={screenshotSrc}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
objectPosition: "top center",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Img
|
||||
src={staticFile("phone.png")}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* === PROOF === */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "42%",
|
||||
left: "50%",
|
||||
transform: `translate(-50%, -50%) scale(${proofScale})`,
|
||||
opacity: proofOpacity,
|
||||
textAlign: "center",
|
||||
width: "85%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 48,
|
||||
fontWeight: 800,
|
||||
color: COLORS.bronze,
|
||||
lineHeight: 1.2,
|
||||
textShadow: "0 2px 16px rgba(0,0,0,0.4)",
|
||||
}}
|
||||
>
|
||||
{proofText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* === CTA === */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
opacity: ctaOpacity,
|
||||
transform: `scale(${ctaScale})`,
|
||||
gap: 28,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 58,
|
||||
fontWeight: 800,
|
||||
color: COLORS.white,
|
||||
letterSpacing: -1,
|
||||
}}
|
||||
>
|
||||
Reflect
|
||||
</div>
|
||||
|
||||
{/* CTA text (no button — native TikTok feel) */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 36,
|
||||
fontWeight: 700,
|
||||
color: COLORS.bronze,
|
||||
textAlign: "center",
|
||||
textShadow: "0 2px 12px rgba(0,0,0,0.3)",
|
||||
transform: `scale(${ctaPulse})`,
|
||||
}}
|
||||
>
|
||||
{ctaText}
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +1,176 @@
|
||||
import { Composition, staticFile } from "remotion";
|
||||
import { HoneyDueAd } from "./HoneyDueAd";
|
||||
import { ReflectAd } from "./ReflectAd";
|
||||
|
||||
const SCREENSHOT = staticFile("tasks_overdue.png");
|
||||
const HONEYDUESCREEN = staticFile("tasks_overdue.png");
|
||||
|
||||
// 15s @ 30fps for Instagram, 12s for TikTok
|
||||
const IG_FRAMES = 450;
|
||||
const TT_FRAMES = 360;
|
||||
// 15s @ 30fps for Instagram, various for TikTok
|
||||
const IG_FRAMES = 450; // 15s
|
||||
const TT_13_FRAMES = 390; // 13s
|
||||
const TT_12_FRAMES = 360; // 12s
|
||||
|
||||
export const RemotionRoot: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
{/* === GEMINI AD VIDEOS === */}
|
||||
{/* ============================================ */}
|
||||
{/* === REFLECT v1.1 — GEMINI AD VIDEOS ======= */}
|
||||
{/* ============================================ */}
|
||||
|
||||
{/* Gemini IG Hook 2 — "My therapist asked me to track my moods" */}
|
||||
<Composition
|
||||
id="Reflect-Gemini-IG-Hook2"
|
||||
component={ReflectAd}
|
||||
durationInFrames={IG_FRAMES}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
defaultProps={{
|
||||
platform: "instagram" as const,
|
||||
hookText: "My therapist asked me\nto track my moods.",
|
||||
bodyText: "She didn't expect an AI-powered mood report.",
|
||||
ctaText: "Begin reflecting",
|
||||
proofText: "Your check-ins become patterns\nyou can share with your therapist",
|
||||
screenshotSrc: staticFile("flow_07_detail_after.png"),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Gemini IG Hook 4 — "Two minutes of reflection" */}
|
||||
<Composition
|
||||
id="Reflect-Gemini-IG-Hook4"
|
||||
component={ReflectAd}
|
||||
durationInFrames={420}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
defaultProps={{
|
||||
platform: "instagram" as const,
|
||||
hookText: "Two minutes of reflection.\nZero blank pages.",
|
||||
bodyText: "Guided questions that adapt to your mood.",
|
||||
ctaText: "Start your journal",
|
||||
proofText: "AI-powered reports turn daily moments\ninto real understanding",
|
||||
screenshotSrc: staticFile("flow_03_q1.png"),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Gemini TT Hook 1 — "I stopped journaling" */}
|
||||
<Composition
|
||||
id="Reflect-Gemini-TT-Hook1"
|
||||
component={ReflectAd}
|
||||
durationInFrames={TT_13_FRAMES}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
defaultProps={{
|
||||
platform: "tiktok" as const,
|
||||
hookText: "I stopped journaling.",
|
||||
bodyText: "Then I found one that\nasks the questions for you.",
|
||||
ctaText: "Try Reflect free",
|
||||
proofText: "No blank pages.\nJust the right question.",
|
||||
screenshotSrc: staticFile("flow_q2_typing.png"),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Gemini TT Hook 3 — "POV: the app asks exactly what you needed" */}
|
||||
<Composition
|
||||
id="Reflect-Gemini-TT-Hook3"
|
||||
component={ReflectAd}
|
||||
durationInFrames={TT_12_FRAMES}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
defaultProps={{
|
||||
platform: "tiktok" as const,
|
||||
hookText: "POV: it asks exactly\nwhat you needed.",
|
||||
bodyText: "Questions based on how you feel.\nNot random. Not generic.",
|
||||
ctaText: "Download Reflect free",
|
||||
proofText: "The one that makes\nyou pause and think.",
|
||||
screenshotSrc: staticFile("flow_05_q3_scrolled.png"),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* === REFLECT v1.1 — POSTER AD VIDEOS ======= */}
|
||||
{/* ============================================ */}
|
||||
|
||||
{/* Poster IG Hook 2 — "My therapist asked me to track my moods" */}
|
||||
<Composition
|
||||
id="Reflect-Poster-IG-Hook2"
|
||||
component={ReflectAd}
|
||||
durationInFrames={IG_FRAMES}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
defaultProps={{
|
||||
platform: "instagram" as const,
|
||||
hookText: "My therapist asked me\nto track my moods.",
|
||||
bodyText: "She didn't expect an AI-powered mood report.",
|
||||
ctaText: "Begin reflecting",
|
||||
proofText: "Patterns you can understand\nand share with your therapist",
|
||||
screenshotSrc: staticFile("flow_06_q4.png"),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Poster IG Hook 5 — "What if your phone helped you feel" */}
|
||||
<Composition
|
||||
id="Reflect-Poster-IG-Hook5"
|
||||
component={ReflectAd}
|
||||
durationInFrames={IG_FRAMES}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
defaultProps={{
|
||||
platform: "instagram" as const,
|
||||
hookText: "What if your phone\nhelped you feel\ninstead of just scroll?",
|
||||
bodyText: "Guided reflection that adapts to your mood.",
|
||||
ctaText: "Begin reflecting",
|
||||
proofText: "AI-powered insights reveal\npatterns you can't see alone",
|
||||
screenshotSrc: staticFile("flow_07_detail_after.png"),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Poster TT Hook 1 — "I stopped journaling" */}
|
||||
<Composition
|
||||
id="Reflect-Poster-TT-Hook1"
|
||||
component={ReflectAd}
|
||||
durationInFrames={TT_13_FRAMES}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
defaultProps={{
|
||||
platform: "tiktok" as const,
|
||||
hookText: "I stopped journaling.",
|
||||
bodyText: "Tap your mood.\nIt asks the right question.",
|
||||
ctaText: "Try Reflect free",
|
||||
proofText: "No pressure.\nJust the right question.",
|
||||
screenshotSrc: staticFile("flow_03_q1.png"),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Poster TT Hook 3 — "POV: the app asks exactly what you needed" */}
|
||||
<Composition
|
||||
id="Reflect-Poster-TT-Hook3"
|
||||
component={ReflectAd}
|
||||
durationInFrames={TT_12_FRAMES}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
defaultProps={{
|
||||
platform: "tiktok" as const,
|
||||
hookText: "POV: it asks exactly\nwhat you needed.",
|
||||
bodyText: "Log your mood.\nGet a guided question.",
|
||||
ctaText: "Download Reflect free",
|
||||
proofText: "Not random.\nThe one you needed.",
|
||||
screenshotSrc: staticFile("flow_06_q4.png"),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* === HONEYDUE (LEGACY COMPOSITIONS) ========= */}
|
||||
{/* ============================================ */}
|
||||
<Composition
|
||||
id="Gemini-IG-Feed-Cost"
|
||||
component={HoneyDueAd}
|
||||
durationInFrames={IG_FRAMES}
|
||||
durationInFrames={450}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
@@ -24,13 +180,13 @@ export const RemotionRoot: React.FC = () => {
|
||||
bodyText: "honeyDue tracks every task so you never skip the small stuff.",
|
||||
ctaText: "Download Free",
|
||||
proofText: "Trusted by thousands of homeowners",
|
||||
screenshotSrc: SCREENSHOT,
|
||||
screenshotSrc: HONEYDUESCREEN,
|
||||
}}
|
||||
/>
|
||||
<Composition
|
||||
id="Gemini-IG-Stories-FirstTimer"
|
||||
component={HoneyDueAd}
|
||||
durationInFrames={IG_FRAMES}
|
||||
durationInFrames={450}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
@@ -40,13 +196,13 @@ export const RemotionRoot: React.FC = () => {
|
||||
bodyText: "This app tells you what to fix and when.",
|
||||
ctaText: "Try honeyDue",
|
||||
proofText: "First-time homeowners love this",
|
||||
screenshotSrc: SCREENSHOT,
|
||||
screenshotSrc: HONEYDUESCREEN,
|
||||
}}
|
||||
/>
|
||||
<Composition
|
||||
id="Gemini-TT-SilentTodo"
|
||||
component={HoneyDueAd}
|
||||
durationInFrames={TT_FRAMES}
|
||||
durationInFrames={360}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
@@ -56,13 +212,13 @@ export const RemotionRoot: React.FC = () => {
|
||||
bodyText: "HVAC filters. Gutters. Water heater. honeyDue sees it all.",
|
||||
ctaText: "Get Started Free",
|
||||
proofText: "Never miss maintenance again",
|
||||
screenshotSrc: SCREENSHOT,
|
||||
screenshotSrc: HONEYDUESCREEN,
|
||||
}}
|
||||
/>
|
||||
<Composition
|
||||
id="Gemini-TT-Forgetter"
|
||||
component={HoneyDueAd}
|
||||
durationInFrames={TT_FRAMES}
|
||||
durationInFrames={360}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
@@ -72,15 +228,13 @@ export const RemotionRoot: React.FC = () => {
|
||||
bodyText: "honeyDue would have reminded me 8 times by now.",
|
||||
ctaText: "Download Free",
|
||||
proofText: "Your home maintenance safety net",
|
||||
screenshotSrc: SCREENSHOT,
|
||||
screenshotSrc: HONEYDUESCREEN,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* === CANVAS POSTER VIDEOS === */}
|
||||
<Composition
|
||||
id="Poster-IG-Feed-Cost"
|
||||
component={HoneyDueAd}
|
||||
durationInFrames={IG_FRAMES}
|
||||
durationInFrames={450}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
@@ -90,13 +244,13 @@ export const RemotionRoot: React.FC = () => {
|
||||
bodyText: "The difference between a reminder and a disaster.",
|
||||
ctaText: "Download Free",
|
||||
proofText: "Join thousands of organized homeowners",
|
||||
screenshotSrc: SCREENSHOT,
|
||||
screenshotSrc: HONEYDUESCREEN,
|
||||
}}
|
||||
/>
|
||||
<Composition
|
||||
id="Poster-IG-Stories-FirstHouse"
|
||||
component={HoneyDueAd}
|
||||
durationInFrames={IG_FRAMES}
|
||||
durationInFrames={450}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
@@ -106,13 +260,13 @@ export const RemotionRoot: React.FC = () => {
|
||||
bodyText: "100+ maintenance tasks. One app to track them all.",
|
||||
ctaText: "Try honeyDue",
|
||||
proofText: "Built for first-time homeowners",
|
||||
screenshotSrc: SCREENSHOT,
|
||||
screenshotSrc: HONEYDUESCREEN,
|
||||
}}
|
||||
/>
|
||||
<Composition
|
||||
id="Poster-TT-HiddenTodo"
|
||||
component={HoneyDueAd}
|
||||
durationInFrames={TT_FRAMES}
|
||||
durationInFrames={360}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
@@ -122,13 +276,13 @@ export const RemotionRoot: React.FC = () => {
|
||||
bodyText: "Filters. Gutters. Drains. Vents. honeyDue tracks all of it.",
|
||||
ctaText: "Get Started Free",
|
||||
proofText: "See what you've been missing",
|
||||
screenshotSrc: SCREENSHOT,
|
||||
screenshotSrc: HONEYDUESCREEN,
|
||||
}}
|
||||
/>
|
||||
<Composition
|
||||
id="Poster-TT-HVAC2Years"
|
||||
component={HoneyDueAd}
|
||||
durationInFrames={TT_FRAMES}
|
||||
durationInFrames={360}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
@@ -138,7 +292,7 @@ export const RemotionRoot: React.FC = () => {
|
||||
bodyText: "That's 8 missed reminders honeyDue would have sent.",
|
||||
ctaText: "Download Free",
|
||||
proofText: "Never forget again",
|
||||
screenshotSrc: SCREENSHOT,
|
||||
screenshotSrc: HONEYDUESCREEN,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { tavily } from "@tavily/core";
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
const client = tavily({ apiKey: process.env.TAVILY_API_KEY });
|
||||
|
||||
async function runQueries() {
|
||||
const queries = [
|
||||
{
|
||||
query_id: 1,
|
||||
query_name: "Industry Trends & Market Landscape",
|
||||
search_terms: "mood tracking app market 2026 mental health app trends CBT guided journaling AI insights",
|
||||
options: {
|
||||
searchDepth: "advanced",
|
||||
topic: "news",
|
||||
days: 30,
|
||||
maxResults: 10,
|
||||
excludeDomains: ["pinterest.com", "etsy.com"]
|
||||
}
|
||||
},
|
||||
{
|
||||
query_id: 2,
|
||||
query_name: "Competitor Analysis",
|
||||
search_terms: "Daylio vs Finch vs Bearable vs How We Feel mood tracker 2026 marketing campaigns features comparison",
|
||||
options: {
|
||||
searchDepth: "advanced",
|
||||
topic: "general",
|
||||
maxResults: 10,
|
||||
includeDomains: ["reddit.com", "producthunt.com", "techcrunch.com", "theverge.com", "cnet.com", "choosingtherapy.com"]
|
||||
}
|
||||
},
|
||||
{
|
||||
query_id: 3,
|
||||
query_name: "Audience Pain Points & Conversations",
|
||||
search_terms: "mood tracking app frustrations blank journal overwhelm therapist homework mood diary share with therapist 2025 2026",
|
||||
options: {
|
||||
searchDepth: "advanced",
|
||||
topic: "general",
|
||||
maxResults: 10,
|
||||
includeDomains: ["reddit.com", "twitter.com", "quora.com"]
|
||||
}
|
||||
},
|
||||
{
|
||||
query_id: 4,
|
||||
query_name: "High-Performing Hooks & Ad Copy",
|
||||
search_terms: "best mental health app ad copy hooks 2026 wellness app Instagram TikTok ad examples high engagement",
|
||||
options: {
|
||||
searchDepth: "advanced",
|
||||
topic: "general",
|
||||
maxResults: 10
|
||||
}
|
||||
},
|
||||
{
|
||||
query_id: 5,
|
||||
query_name: "Viral Content & Cultural Moments",
|
||||
search_terms: "mental health app viral TikTok 2026 spring self-care trend therapy journal check-in cultural moment",
|
||||
options: {
|
||||
searchDepth: "advanced",
|
||||
topic: "news",
|
||||
days: 14,
|
||||
maxResults: 10
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const q of queries) {
|
||||
console.log(`\n🔍 Query ${q.query_id}: ${q.query_name}`);
|
||||
console.log(` Search: "${q.search_terms}"`);
|
||||
try {
|
||||
const res = await client.search(q.search_terms, q.options);
|
||||
console.log(` ✅ Got ${res.results?.length || 0} results`);
|
||||
results.push({
|
||||
...q,
|
||||
raw_results: res.results || [],
|
||||
answer: res.answer || null
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(` ❌ Error: ${err.message}`);
|
||||
results.push({
|
||||
...q,
|
||||
raw_results: [],
|
||||
answer: null,
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Write raw results for processing
|
||||
const outputPath = "outputs/reflect_v1.1_—_guided_reflection_&_ai_reports_20260325/raw_research.json";
|
||||
writeFileSync(outputPath, JSON.stringify(results, null, 2));
|
||||
console.log(`\n✅ Raw results saved to ${outputPath}`);
|
||||
}
|
||||
|
||||
runQueries().catch(console.error);
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: gemini-ad-designer
|
||||
description: >
|
||||
AI image ad designer agent. Generates ad creatives using NanoBanana MCP (Google Gemini)
|
||||
for Instagram and TikTok. Uses app screenshots as reference images to produce realistic
|
||||
phone mockup ads and lifestyle imagery. Outputs production-ready PNG files.
|
||||
---
|
||||
|
||||
# Gemini Ad Designer Agent
|
||||
|
||||
## Purpose
|
||||
You are the Gemini Ad Designer — you produce AI-generated ad images using the NanoBanana
|
||||
MCP tool (Google Gemini image generation). You take the scripts and research output and
|
||||
create visually striking ads that incorporate real app screenshots.
|
||||
|
||||
## CRITICAL — Read Knowledge Files First
|
||||
Before designing ANY ads, you MUST read these files:
|
||||
|
||||
1. `knowledge/brand_identity.md` — tone, voice, CTA patterns, brand personality
|
||||
2. `knowledge/platform_guidelines.md` — exact dimensions, aspect ratios, platform rules
|
||||
3. `knowledge/product_campaign.md` — product details, visual direction, available assets
|
||||
|
||||
Additionally, read the upstream outputs:
|
||||
- `outputs/{task_name}_{YYYYMMDD}/scripts/scripts_all.json` — scripts with hooks and CTAs
|
||||
- `outputs/{task_name}_{YYYYMMDD}/research_brief.md` — campaign strategy context
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Plan Ad Variants
|
||||
Based on the scripts, determine which hooks to produce. Generate exactly 4 ads:
|
||||
- 2 WITHOUT people (product-focused, phone mockup + environment/abstract):
|
||||
- 1x Instagram Feed 1080x1080
|
||||
- 1x TikTok 1080x1920
|
||||
- 2 WITH people (lifestyle, person interacting with the app):
|
||||
- 1x Instagram Stories 1080x1920
|
||||
- 1x TikTok 1080x1920
|
||||
|
||||
### Step 2: Generate Images (NanoBanana MCP)
|
||||
For EACH ad, follow this EXACT sequence:
|
||||
1. Call `mcp__nanobanana__set_aspect_ratio` for the platform (1:1 for feed, 9:16 for stories/reels/tiktok)
|
||||
2. Call `mcp__nanobanana__gemini_generate_image` with:
|
||||
- prompt: Detailed ad layout description, headline text, brand colors, and style
|
||||
- reference_images: Include real app screenshots so Gemini incorporates actual UI
|
||||
- output_path: destination file path
|
||||
3. Save to `outputs/{task_name}_{date}/ads/gemini/`
|
||||
4. Name files: `gemini_{platform}_{hook}_{dimensions}.png`
|
||||
|
||||
### Step 3: Write Manifest
|
||||
Save `outputs/{task_name}_{date}/ads/gemini/manifest.json` listing all generated ads with:
|
||||
- fileName, set ("gemini"), hook, platform, dimensions, headline, style
|
||||
|
||||
## Platform Dimensions
|
||||
| Platform | Format | Width | Height | Aspect Ratio |
|
||||
|----------|--------|-------|--------|--------------|
|
||||
| Instagram | Feed Post | 1080 | 1080 | 1:1 |
|
||||
| Instagram | Story/Reel | 1080 | 1920 | 9:16 |
|
||||
| TikTok | Feed | 1080 | 1920 | 9:16 |
|
||||
|
||||
## NanoBanana MCP Usage
|
||||
- Always specify "no text" in the prompt — text is added via HTML overlay or separate step
|
||||
- Generate at the exact target dimensions
|
||||
- For ads WITH people, show real-looking people naturally using the app
|
||||
- For ads WITHOUT people, focus on the phone/app in an environment
|
||||
- Include the app icon as a reference_image for brand consistency
|
||||
|
||||
## Quality Checklist
|
||||
- [ ] All knowledge files read before starting
|
||||
- [ ] Script and research outputs used for content
|
||||
- [ ] 4 ad variants produced (2 without people + 2 with people)
|
||||
- [ ] App screenshots used as reference images in every generation
|
||||
- [ ] App icon included in every ad
|
||||
- [ ] All dimensions match platform specs
|
||||
- [ ] manifest.json is valid JSON with all required fields
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
name: poster-ad-designer
|
||||
description: >
|
||||
Canvas design poster agent. Creates museum-quality poster ads using HTML/CSS rendered
|
||||
to PNG via Playwright. Develops a design philosophy per campaign, then expresses it
|
||||
as art-object posters with phone mockups and minimal typography.
|
||||
---
|
||||
|
||||
# Poster Ad Designer Agent
|
||||
|
||||
## Purpose
|
||||
You are the Poster Ad Designer — you create museum-quality poster ads using HTML/CSS
|
||||
rendered to pixel-perfect PNGs via Playwright. Each poster is an art object: 90% visual
|
||||
design, 10% essential text. You develop a design philosophy for the campaign and express
|
||||
it through systematic visual language.
|
||||
|
||||
## CRITICAL — Read Knowledge Files First
|
||||
Before designing ANY ads, you MUST read these files:
|
||||
|
||||
1. `knowledge/brand_identity.md` — tone, voice, CTA patterns, brand personality
|
||||
2. `knowledge/platform_guidelines.md` — exact dimensions, aspect ratios, platform rules
|
||||
3. `knowledge/product_campaign.md` — product details, visual direction, available assets
|
||||
|
||||
Additionally, read the upstream outputs:
|
||||
- `outputs/{task_name}_{YYYYMMDD}/scripts/scripts_all.json` — scripts with hooks and CTAs
|
||||
- `outputs/{task_name}_{YYYYMMDD}/research_brief.md` — campaign strategy context
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Design Philosophy
|
||||
Create a visual design philosophy (.md file) for this campaign's poster aesthetic.
|
||||
Save it to `outputs/{task_name}_{date}/ads/posters/design_philosophy.md`
|
||||
|
||||
The philosophy should:
|
||||
- Name the aesthetic movement (1-2 words, e.g. "Domestic Geometry" or "Maintenance Modernism")
|
||||
- Be 4-6 paragraphs articulating how the philosophy manifests through space, form, color, scale, rhythm, composition
|
||||
- Emphasize: visual expression over text, spatial communication, artistic interpretation
|
||||
- Stress meticulous craftsmanship
|
||||
- Brand palette as foundation
|
||||
|
||||
### Step 2: Express as Poster Art
|
||||
Using the philosophy, create each poster as a .png file. For each:
|
||||
1. Write a Node.js script that builds HTML and screenshots with Playwright
|
||||
2. Treat each poster as an ART OBJECT — 90% visual design, 10% essential text
|
||||
3. Use repeating patterns, perfect geometric shapes, systematic visual language
|
||||
4. Typography is minimal and design-forward — sparse labels, bold single phrases, never paragraphs
|
||||
5. The campaign's hook text appears as a visual accent, not a headline block
|
||||
6. Incorporate app screenshots inside the phone frame PNG (transparent screen area)
|
||||
7. Every element contained within canvas boundaries with proper margins
|
||||
8. The result should look like it could hang in a gallery or appear in a design magazine
|
||||
9. App icon MUST appear in every poster near the branding or CTA area
|
||||
|
||||
### CRITICAL Layout Rule: Phone Must NOT Cover Text
|
||||
The phone mockup and text must occupy separate zones — NEVER overlapping.
|
||||
|
||||
Use a **three-zone vertical layout:**
|
||||
- **Top zone (15-30% of canvas):** Headline text. No phone here.
|
||||
- **Middle zone (40-55% of canvas):** Phone mockup, centered. No text overlapping this zone.
|
||||
- **Bottom zone (15-25% of canvas):** Subtext, CTA, branding. No phone here.
|
||||
|
||||
For 1080x1920 (9:16) posters:
|
||||
- Headline: top 290-380px (y: 60 to ~380)
|
||||
- Phone: centered vertically in middle band (y: ~420 to ~1400), max width 55% of canvas
|
||||
- Subtext + CTA: bottom 400px (y: ~1520 to ~1860)
|
||||
|
||||
For 1080x1080 (1:1) posters:
|
||||
- Headline: top 200px
|
||||
- Phone: center, max width 45%, max height 500px
|
||||
- Subtext + CTA: bottom 250px
|
||||
|
||||
**Before rendering, calculate bounding boxes for all elements and assert no overlap between
|
||||
the phone rect and any text rect. If they overlap, shrink the phone or increase spacing.**
|
||||
|
||||
### Step 3: Write Manifests
|
||||
Save `outputs/{task_name}_{date}/ads/posters/manifest.json` listing all poster ads.
|
||||
|
||||
Also create the combined `outputs/{task_name}_{date}/ads/ad_manifest.json` merging both
|
||||
the Gemini manifest (`ads/gemini/manifest.json`) and the poster manifest.
|
||||
|
||||
## Output
|
||||
Generate at least 4 posters:
|
||||
- 2x Instagram (1 feed 1080x1080, 1 stories 1080x1920)
|
||||
- 2x TikTok cover images (1080x1920)
|
||||
|
||||
Save to `outputs/{task_name}_{date}/ads/posters/`
|
||||
Name files: `poster_{platform}_{hook}_{dimensions}.png`
|
||||
|
||||
## 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.
|
||||
|
||||
- **Minimum font size: 44px** — NO text below 44px, ever.
|
||||
- **Text scale multiplier: 1.13x** — apply 113% to all font sizes before rendering.
|
||||
- **Hero headline: 75-100px effective** (66-88px raw × 1.13)
|
||||
- **Body/subheadline: 44px minimum**
|
||||
- **Minimum gap between stacked text:**
|
||||
- Giant (100-140px) above + Large below: 50px gap
|
||||
- Large above + Medium below: 40px gap
|
||||
- Medium + Medium: 30px gap
|
||||
- Absolute minimum gap: 20px
|
||||
- **2x internal render recommended:** Render at 2160×3840 (9:16) or 2160×2160 (1:1), downsample to 1080.
|
||||
- **CTA block at fixed bottom position:** CTA_BOTTOM_MARGIN = 60px from bottom edge.
|
||||
- **Overflow check:** Calculate total vertical extent before rendering. If content exceeds canvas, reduce phone width first.
|
||||
|
||||
## Platform Dimensions
|
||||
| Platform | Format | Width | Height | Aspect Ratio |
|
||||
|----------|--------|-------|--------|--------------|
|
||||
| Instagram | Feed Post | 1080 | 1080 | 1:1 |
|
||||
| Instagram | Story/Reel | 1080 | 1920 | 9:16 |
|
||||
| TikTok | Feed | 1080 | 1920 | 9:16 |
|
||||
|
||||
## Playwright Usage
|
||||
- Set device scale factor to 1 (exact pixel dimensions)
|
||||
- Use `waitForLoadState('networkidle')` before screenshots
|
||||
- Disable animations for consistent renders
|
||||
- If fonts fail to load, use system fonts as fallback
|
||||
- Set viewport to match ad dimensions exactly
|
||||
|
||||
## Quality Checklist
|
||||
- [ ] All knowledge files read before starting
|
||||
- [ ] Design philosophy created and saved
|
||||
- [ ] At least 4 poster variants produced
|
||||
- [ ] All dimensions match platform specs
|
||||
- [ ] Typography follows minimum size rules
|
||||
- [ ] Phone mockups use real screenshots + phone frame
|
||||
- [ ] App icon visible in every poster
|
||||
- [ ] HTML source files saved alongside PNGs
|
||||
- [ ] posters/manifest.json and ads/ad_manifest.json are valid JSON
|
||||
Reference in New Issue
Block a user