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
+14
View File
@@ -0,0 +1,14 @@
{
"permissions": {
"allow": [
"mcp__nanobanana__generate_image",
"mcp__nanobanana__gemini_generate_image",
"mcp__nanobanana__(.*)",
"Bash(*)",
"Read(*)",
"Write(*)",
"Grep(*)",
"Glob(*)"
]
}
}
+9 -8
View File
@@ -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.
+1
View File
@@ -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"}]
+1
View File
@@ -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

+581
View File
@@ -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>
);
};
+177 -23
View File
@@ -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
+127
View File
@@ -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