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
+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,
}}
/>
</>