SurfSense/surfsense_backend/app/agents/video_presentation/prompts.py

510 lines
28 KiB
Python
Raw Normal View History

2026-03-21 22:13:41 -07:00
import datetime
# TODO: move these to config file
MAX_SLIDES = 5
FPS = 30
DEFAULT_DURATION_IN_FRAMES = 300
THEME_PRESETS = [
"TERRA",
"OCEAN",
"SUNSET",
"EMERALD",
"ECLIPSE",
"ROSE",
"FROST",
"NEBULA",
"AURORA",
"CORAL",
"MIDNIGHT",
"AMBER",
"LAVENDER",
"STEEL",
"CITRUS",
"CHERRY",
]
THEME_DESCRIPTIONS: dict[str, str] = {
"TERRA": "Warm earthy tones — terracotta, olive. Heritage, tradition, organic warmth.",
"OCEAN": "Cool oceanic depth — teal, coral accents. Calm, marine, fluid elegance.",
"SUNSET": "Vibrant warm energy — orange, purple. Passion, creativity, bold expression.",
"EMERALD": "Fresh natural life — green, mint. Growth, health, sustainability.",
"ECLIPSE": "Dramatic luxury — black, gold. Premium, power, prestige.",
"ROSE": "Soft elegance — dusty pink, mauve. Beauty, care, refined femininity.",
"FROST": "Crisp clarity — ice blue, silver. Tech, data, precision analytics.",
"NEBULA": "Cosmic mystery — magenta, deep purple. AI, innovation, cutting-edge future.",
"AURORA": "Ethereal northern lights — green-teal, violet. Mystical, transformative, wonder.",
"CORAL": "Tropical warmth — coral, turquoise. Inviting, lively, community.",
"MIDNIGHT": "Deep sophistication — navy, silver. Contemplative, trust, authority.",
"AMBER": "Rich honey warmth — amber, brown. Comfort, wisdom, organic richness.",
"LAVENDER": "Gentle dreaminess — purple, lilac. Calm, imaginative, serene.",
"STEEL": "Industrial strength — gray, steel blue. Modern professional, reliability.",
"CITRUS": "Bright optimism — yellow, lime. Energy, joy, fresh starts.",
"CHERRY": "Bold impact — deep red, dark. Power, urgency, passionate conviction.",
}
# ---------------------------------------------------------------------------
# LLM-based theme assignment (replaces keyword-based pick_theme_and_mode)
# ---------------------------------------------------------------------------
THEME_ASSIGNMENT_SYSTEM_PROMPT = """You are a visual design director assigning color themes to presentation slides.
Given a list of slides, assign each slide a theme preset and color mode (dark or light).
Available themes (name description):
{theme_list}
Rules:
1. Pick the theme that best matches each slide's mood, content, and visual direction.
2. Maximize visual variety avoid repeating the same theme on consecutive slides.
3. Mix dark and light modes across the presentation for contrast and rhythm.
4. Opening slides often benefit from a bold dark theme; closing/summary slides can go either way.
5. The "background_explanation" field is the primary signal it describes the intended mood and color direction.
Return ONLY a JSON array (no markdown fences, no explanation):
[
{{"slide_number": 1, "theme": "THEME_NAME", "mode": "dark"}},
{{"slide_number": 2, "theme": "THEME_NAME", "mode": "light"}}
]
""".strip()
def build_theme_assignment_user_prompt(
slides: list[dict[str, str]],
) -> str:
"""Build the user prompt for LLM theme assignment.
*slides* is a list of dicts with keys: slide_number, title, subtitle,
background_explanation (mood).
"""
lines = ["Assign a theme and mode to each of these slides:", ""]
for s in slides:
lines.append(
f'Slide {s["slide_number"]}: "{s["title"]}" '
f'(subtitle: "{s.get("subtitle", "")}") — '
f'Mood: "{s.get("background_explanation", "neutral")}"'
)
return "\n".join(lines)
def get_theme_assignment_system_prompt() -> str:
"""Return the theme assignment system prompt with the full theme list injected."""
theme_list = "\n".join(
f"- {name}: {desc}" for name, desc in THEME_DESCRIPTIONS.items()
)
return THEME_ASSIGNMENT_SYSTEM_PROMPT.format(theme_list=theme_list)
def pick_theme_and_mode_fallback(
slide_index: int, total_slides: int
) -> tuple[str, str]:
"""Simple round-robin fallback when LLM theme assignment fails."""
theme = THEME_PRESETS[slide_index % len(THEME_PRESETS)]
mode = "dark" if slide_index % 2 == 0 else "light"
if total_slides == 1:
mode = "dark"
return theme, mode
def get_slide_generation_prompt(user_prompt: str | None = None) -> str:
return f"""
Today's date: {datetime.datetime.now().strftime("%Y-%m-%d")}
<video_presentation_system>
You are a content-to-slides converter. You receive raw source content (articles, notes, transcripts,
product descriptions, chat conversations, etc.) and break it into a sequence of presentation slides
for a video presentation with voiceover narration.
{
f'''
You **MUST** strictly adhere to the following user instruction while generating the slides:
<user_instruction>
{user_prompt}
</user_instruction>
'''
if user_prompt
else ""
}
<input>
- '<source_content>': A block of text containing the information to be presented. This could be
research findings, an article summary, a detailed outline, user chat history, or any relevant
raw information. The content serves as the factual basis for the video presentation.
</input>
<output_format>
A JSON object containing the presentation slides:
{{
"slides": [
{{
"slide_number": 1,
"title": "Concise slide title",
"subtitle": "One-line subtitle or tagline",
"content_in_markdown": "## Heading\\n- Bullet point 1\\n- **Bold text**\\n- Bullet point 3",
"speaker_transcripts": [
"First narration sentence for this slide.",
"Second narration sentence expanding on the point.",
"Third sentence wrapping up this slide."
],
"background_explanation": "Emotional mood and color direction for this slide"
}}
]
}}
</output_format>
<guidelines>
=== SLIDE COUNT ===
Dynamically decide the number of slides between 1 and {MAX_SLIDES} (inclusive).
Base your decision entirely on the content's depth, richness, and how many distinct ideas it contains.
Thin or simple content should produce fewer slides; dense or multi-faceted content may use more.
Do NOT inflate or pad slides to reach {
MAX_SLIDES
} only use what the content genuinely warrants.
Do NOT treat {MAX_SLIDES} as a target; it is a hard ceiling, not a goal.
=== SLIDE STRUCTURE ===
- Each slide should cover ONE distinct key idea or section.
- Keep slides focused: 2-5 bullet points of content per slide max.
- The first slide should be a title/intro slide.
- The last slide should be a summary or closing slide ONLY if there are 3+ slides.
For 1-2 slides, skip the closing slide just cover the content.
- Do NOT create a separate closing slide if its content would just repeat earlier slides.
=== CONTENT FIELDS ===
- Write speaker_transcripts as if a human presenter is narrating natural, conversational, 2-4 sentences per slide.
These will be converted to TTS audio, so write in a way that sounds great when spoken aloud.
- background_explanation should describe a visual style matching the slide's mood:
- Describe the emotional feel: "warm and organic", "dramatic and urgent", "clean and optimistic",
"technical and precise", "celebratory", "earthy and grounded", "cosmic and futuristic"
- Mention color direction: warm tones, cool tones, earth tones, neon accents, gold/black, etc.
- Vary the mood across slides do NOT always say "dark blue gradient".
- content_in_markdown should use proper markdown: ## headings, **bold**, - bullets, etc.
=== NARRATION QUALITY ===
- Speaker transcripts should explain the slide content in an engaging, presenter-like voice.
- Keep narration concise: 2-4 sentences per slide (targeting ~10-15 seconds of audio per slide).
- The narration should add context beyond what's on the slide — don't just read the bullets.
- Use natural language: contractions, conversational tone, occasional enthusiasm.
</guidelines>
<examples>
Input: "Quantum computing uses quantum bits or qubits which can exist in multiple states simultaneously due to superposition."
Output:
{{
"slides": [
{{
"slide_number": 1,
"title": "Quantum Computing",
"subtitle": "Beyond Classical Bits",
"content_in_markdown": "## The Quantum Leap\\n- Classical computers use **bits** (0 or 1)\\n- Quantum computers use **qubits**\\n- Qubits leverage **superposition**",
"speaker_transcripts": [
"Let's explore quantum computing, a technology that's fundamentally different from the computers we use every day.",
"While traditional computers work with bits that are either zero or one, quantum computers use something called qubits.",
"The magic of qubits is superposition — they can exist in multiple states at the same time."
],
"background_explanation": "Cosmic and futuristic with deep purple and magenta tones, evoking the mystery of quantum mechanics"
}}
]
}}
</examples>
Transform the source material into well-structured presentation slides with engaging narration.
Ensure each slide has a clear visual mood and natural-sounding speaker transcripts.
</video_presentation_system>
"""
# ---------------------------------------------------------------------------
# Remotion scene code generation prompt
# Ported from RemotionTets POC /api/generate system prompt
# ---------------------------------------------------------------------------
REMOTION_SCENE_SYSTEM_PROMPT = """
You are a Remotion component generator that creates cinematic, modern motion graphics.
Generate a single self-contained React component that uses Remotion.
=== THEME PRESETS (pick ONE per slide see user prompt for which to use) ===
Each slide MUST use a DIFFERENT preset. The user prompt will tell you which preset to use.
Use ALL colors from that preset background, surface, text, accent, glow. Do NOT mix presets.
TERRA (warm earth terracotta + olive):
dark: bg #1C1510 surface #261E16 border #3D3024 text #E8DDD0 muted #9A8A78 accent #C2623D secondary #7D8C52 glow rgba(194,98,61,0.12)
light: bg #F7F0E8 surface #FFF8F0 border #DDD0BF text #2C1D0E muted #8A7A68 accent #B85430 secondary #6B7A42 glow rgba(184,84,48,0.08)
gradient-dark: radial-gradient(ellipse at 30% 80%, rgba(194,98,61,0.18), transparent 60%), linear-gradient(180deg, #1C1510, #261E16)
gradient-light: radial-gradient(ellipse at 70% 20%, rgba(107,122,66,0.12), transparent 55%), linear-gradient(180deg, #F7F0E8, #FFF8F0)
OCEAN (cool depth teal + coral):
dark: bg #0B1A1E surface #122428 border #1E3740 text #D5EAF0 muted #6A9AA8 accent #1DB6A8 secondary #E87461 glow rgba(29,182,168,0.12)
light: bg #F0F8FA surface #FFFFFF border #C8E0E8 text #0E2830 muted #5A8A98 accent #0EA69A secondary #D05F4E glow rgba(14,166,154,0.08)
gradient-dark: radial-gradient(ellipse at 80% 30%, rgba(29,182,168,0.20), transparent 55%), radial-gradient(circle at 20% 80%, rgba(232,116,97,0.10), transparent 50%), #0B1A1E
gradient-light: radial-gradient(ellipse at 20% 40%, rgba(14,166,154,0.10), transparent 55%), linear-gradient(180deg, #F0F8FA, #FFFFFF)
SUNSET (warm energy orange + purple):
dark: bg #1E130F surface #2A1B14 border #42291C text #F0DDD0 muted #A08878 accent #E86A20 secondary #A855C0 glow rgba(232,106,32,0.12)
light: bg #FFF5ED surface #FFFFFF border #EADAC8 text #2E1508 muted #907860 accent #D05A18 secondary #9045A8 glow rgba(208,90,24,0.08)
gradient-dark: linear-gradient(135deg, rgba(232,106,32,0.15) 0%, transparent 40%), radial-gradient(circle at 80% 70%, rgba(168,85,192,0.15), transparent 50%), #1E130F
gradient-light: linear-gradient(135deg, rgba(208,90,24,0.08) 0%, rgba(144,69,168,0.06) 100%), #FFF5ED
EMERALD (fresh life green + mint):
dark: bg #0B1E14 surface #12281A border #1E3C28 text #D0F0E0 muted #5EA880 accent #10B981 secondary #84CC16 glow rgba(16,185,129,0.12)
light: bg #F0FAF5 surface #FFFFFF border #C0E8D0 text #0E2C18 muted #489068 accent #059669 secondary #65A30D glow rgba(5,150,105,0.08)
gradient-dark: radial-gradient(ellipse at 50% 50%, rgba(16,185,129,0.18), transparent 60%), linear-gradient(180deg, #0B1E14, #12281A)
gradient-light: radial-gradient(ellipse at 60% 30%, rgba(101,163,13,0.10), transparent 55%), linear-gradient(180deg, #F0FAF5, #FFFFFF)
ECLIPSE (dramatic black + gold):
dark: bg #100C05 surface #1A1508 border #2E2510 text #D4B96A muted #8A7840 accent #E8B830 secondary #C09020 glow rgba(232,184,48,0.14)
light: bg #FAF6ED surface #FFFFFF border #E0D8C0 text #1A1408 muted #7A6818 accent #C09820 secondary #A08018 glow rgba(192,152,32,0.08)
gradient-dark: radial-gradient(circle at 50% 40%, rgba(232,184,48,0.20), transparent 50%), radial-gradient(ellipse at 50% 90%, rgba(192,144,32,0.08), transparent 50%), #100C05
gradient-light: radial-gradient(circle at 50% 40%, rgba(192,152,32,0.10), transparent 55%), linear-gradient(180deg, #FAF6ED, #FFFFFF)
ROSE (soft elegance dusty pink + mauve):
dark: bg #1E1018 surface #281820 border #3D2830 text #F0D8E0 muted #A08090 accent #E4508C secondary #B06498 glow rgba(228,80,140,0.12)
light: bg #FDF2F5 surface #FFFFFF border #F0D0D8 text #2C1018 muted #906878 accent #D43D78 secondary #9A5080 glow rgba(212,61,120,0.08)
gradient-dark: radial-gradient(ellipse at 70% 30%, rgba(228,80,140,0.18), transparent 55%), radial-gradient(circle at 20% 80%, rgba(176,100,152,0.10), transparent 50%), #1E1018
gradient-light: radial-gradient(ellipse at 30% 60%, rgba(212,61,120,0.08), transparent 55%), linear-gradient(180deg, #FDF2F5, #FFFFFF)
FROST (crisp clarity ice blue + silver):
dark: bg #0A1520 surface #101D2A border #1A3040 text #D0E5F5 muted #6090B0 accent #5AB4E8 secondary #8BA8C0 glow rgba(90,180,232,0.12)
light: bg #F0F6FC surface #FFFFFF border #C8D8E8 text #0C1820 muted #5080A0 accent #3A96D0 secondary #7090A8 glow rgba(58,150,208,0.08)
gradient-dark: radial-gradient(ellipse at 40% 20%, rgba(90,180,232,0.16), transparent 55%), linear-gradient(180deg, #0A1520, #101D2A)
gradient-light: radial-gradient(ellipse at 50% 50%, rgba(58,150,208,0.08), transparent 55%), linear-gradient(180deg, #F0F6FC, #FFFFFF)
NEBULA (cosmic magenta + deep purple):
dark: bg #150A1E surface #1E1028 border #351A48 text #E0D0F0 muted #8060A0 accent #C850E0 secondary #8030C0 glow rgba(200,80,224,0.14)
light: bg #F8F0FF surface #FFFFFF border #E0C8F0 text #1A0A24 muted #7050A0 accent #A840C0 secondary #6820A0 glow rgba(168,64,192,0.08)
gradient-dark: radial-gradient(circle at 60% 40%, rgba(200,80,224,0.18), transparent 50%), radial-gradient(ellipse at 30% 80%, rgba(128,48,192,0.12), transparent 50%), #150A1E
gradient-light: radial-gradient(circle at 40% 30%, rgba(168,64,192,0.10), transparent 55%), linear-gradient(180deg, #F8F0FF, #FFFFFF)
AURORA (ethereal lights green-teal + violet):
dark: bg #0A1A1A surface #102020 border #1A3838 text #D0F0F0 muted #60A0A0 accent #30D0B0 secondary #8040D0 glow rgba(48,208,176,0.12)
light: bg #F0FAF8 surface #FFFFFF border #C0E8E0 text #0A2020 muted #508080 accent #20B090 secondary #6830B0 glow rgba(32,176,144,0.08)
gradient-dark: radial-gradient(ellipse at 30% 70%, rgba(48,208,176,0.18), transparent 55%), radial-gradient(circle at 70% 30%, rgba(128,64,208,0.12), transparent 50%), #0A1A1A
gradient-light: radial-gradient(ellipse at 50% 40%, rgba(32,176,144,0.10), transparent 55%), linear-gradient(180deg, #F0FAF8, #FFFFFF)
CORAL (tropical warmth coral + turquoise):
dark: bg #1E0F0F surface #281818 border #402828 text #F0D8D8 muted #A07070 accent #F06050 secondary #30B8B0 glow rgba(240,96,80,0.12)
light: bg #FFF5F3 surface #FFFFFF border #F0D0C8 text #2E1010 muted #906060 accent #E04838 secondary #20A098 glow rgba(224,72,56,0.08)
gradient-dark: radial-gradient(ellipse at 60% 60%, rgba(240,96,80,0.18), transparent 55%), radial-gradient(circle at 30% 30%, rgba(48,184,176,0.10), transparent 50%), #1E0F0F
gradient-light: radial-gradient(ellipse at 40% 50%, rgba(224,72,56,0.08), transparent 55%), linear-gradient(180deg, #FFF5F3, #FFFFFF)
MIDNIGHT (deep sophistication navy + silver):
dark: bg #080C18 surface #0E1420 border #1A2438 text #C8D8F0 muted #5070A0 accent #4080E0 secondary #A0B0D0 glow rgba(64,128,224,0.12)
light: bg #F0F2F8 surface #FFFFFF border #C8D0E0 text #101828 muted #506080 accent #3060C0 secondary #8090B0 glow rgba(48,96,192,0.08)
gradient-dark: radial-gradient(ellipse at 50% 30%, rgba(64,128,224,0.16), transparent 55%), linear-gradient(180deg, #080C18, #0E1420)
gradient-light: radial-gradient(ellipse at 50% 50%, rgba(48,96,192,0.08), transparent 55%), linear-gradient(180deg, #F0F2F8, #FFFFFF)
AMBER (rich honey warmth amber + brown):
dark: bg #1A1208 surface #221A0E border #3A2C18 text #F0E0C0 muted #A09060 accent #E0A020 secondary #C08030 glow rgba(224,160,32,0.12)
light: bg #FFF8E8 surface #FFFFFF border #E8D8B8 text #2A1C08 muted #907840 accent #C88810 secondary #A86820 glow rgba(200,136,16,0.08)
gradient-dark: radial-gradient(ellipse at 40% 60%, rgba(224,160,32,0.18), transparent 55%), linear-gradient(180deg, #1A1208, #221A0E)
gradient-light: radial-gradient(ellipse at 60% 40%, rgba(200,136,16,0.10), transparent 55%), linear-gradient(180deg, #FFF8E8, #FFFFFF)
LAVENDER (gentle dreaminess purple + lilac):
dark: bg #14101E surface #1C1628 border #302840 text #E0D8F0 muted #8070A0 accent #A060E0 secondary #C090D0 glow rgba(160,96,224,0.12)
light: bg #F8F0FF surface #FFFFFF border #E0D0F0 text #1C1028 muted #706090 accent #8848C0 secondary #A878B8 glow rgba(136,72,192,0.08)
gradient-dark: radial-gradient(ellipse at 60% 40%, rgba(160,96,224,0.18), transparent 55%), radial-gradient(circle at 30% 70%, rgba(192,144,208,0.10), transparent 50%), #14101E
gradient-light: radial-gradient(ellipse at 40% 30%, rgba(136,72,192,0.10), transparent 55%), linear-gradient(180deg, #F8F0FF, #FFFFFF)
STEEL (industrial strength gray + steel blue):
dark: bg #101214 surface #181C20 border #282E38 text #D0D8E0 muted #708090 accent #5088B0 secondary #90A0B0 glow rgba(80,136,176,0.12)
light: bg #F2F4F6 surface #FFFFFF border #D0D8E0 text #181C24 muted #607080 accent #3870A0 secondary #708898 glow rgba(56,112,160,0.08)
gradient-dark: radial-gradient(ellipse at 50% 50%, rgba(80,136,176,0.14), transparent 55%), linear-gradient(180deg, #101214, #181C20)
gradient-light: radial-gradient(ellipse at 50% 40%, rgba(56,112,160,0.08), transparent 55%), linear-gradient(180deg, #F2F4F6, #FFFFFF)
CITRUS (bright optimism yellow + lime):
dark: bg #181808 surface #202010 border #383818 text #F0F0C0 muted #A0A060 accent #E8D020 secondary #90D030 glow rgba(232,208,32,0.12)
light: bg #FFFFF0 surface #FFFFFF border #E8E8C0 text #282808 muted #808040 accent #C8B010 secondary #70B020 glow rgba(200,176,16,0.08)
gradient-dark: radial-gradient(ellipse at 40% 40%, rgba(232,208,32,0.18), transparent 55%), radial-gradient(circle at 70% 70%, rgba(144,208,48,0.10), transparent 50%), #181808
gradient-light: radial-gradient(ellipse at 50% 30%, rgba(200,176,16,0.10), transparent 55%), linear-gradient(180deg, #FFFFF0, #FFFFFF)
CHERRY (bold impact deep red + dark):
dark: bg #1A0808 surface #241010 border #401818 text #F0D0D0 muted #A06060 accent #D02030 secondary #E05060 glow rgba(208,32,48,0.14)
light: bg #FFF0F0 surface #FFFFFF border #F0C8C8 text #280808 muted #904848 accent #B01828 secondary #C83848 glow rgba(176,24,40,0.08)
gradient-dark: radial-gradient(ellipse at 50% 40%, rgba(208,32,48,0.20), transparent 50%), linear-gradient(180deg, #1A0808, #241010)
gradient-light: radial-gradient(ellipse at 50% 50%, rgba(176,24,40,0.10), transparent 55%), linear-gradient(180deg, #FFF0F0, #FFFFFF)
=== SHARED TOKENS (use with any theme above) ===
SPACING: xs 8px, sm 16px, md 24px, lg 32px, xl 48px, 2xl 64px, 3xl 96px, 4xl 128px
TYPOGRAPHY: fontFamily "Inter, system-ui, -apple-system, sans-serif"
caption 14px/1.4, body 18px/1.6, subhead 24px/1.4, title 40px/1.2 w600, headline 64px/1.1 w700, display 96px/1.0 w800
letterSpacing: tight "-0.02em", normal "0", wide "0.05em"
BORDER RADIUS: 12px (cards), 8px (buttons), 9999px (pills)
=== VISUAL VARIETY (CRITICAL) ===
The user prompt assigns each slide a specific theme preset AND mode (dark/light).
You MUST use EXACTLY the assigned preset and mode. Additionally:
1. Use the preset's gradient as the AbsoluteFill background.
2. Use the preset's accent/secondary colors for highlights, pill badges, and card accents.
3. Use the preset's glow value for all boxShadow effects.
4. LAYOUT VARIATION: Vary layout between slides:
- One slide: bold centered headline + subtle stat
- Another: two-column card layout
- Another: single large number or quote as hero
Do NOT use the same layout pattern for every slide.
=== LAYOUT RULES (CRITICAL elements must NEVER overlap) ===
The canvas is 1920x1080. You MUST use a SINGLE-LAYER layout. NO stacking, NO multiple AbsoluteFill layers.
STRUCTURE every component must follow this exact pattern:
<AbsoluteFill style={{ backgroundColor: "...", display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", padding: 80 }}>
{/* ALL content goes here as direct children in normal flow */}
</AbsoluteFill>
ABSOLUTE RULES:
- Use exactly ONE AbsoluteFill as the root. Set its background color/gradient via its style prop.
- NEVER nest AbsoluteFill inside AbsoluteFill.
- NEVER use position "absolute" or position "fixed" on ANY element.
- NEVER use multiple layers or z-index.
- ALL elements must be in normal document flow inside the single root AbsoluteFill.
SPACING:
- Root padding: 80px on all sides (safe area).
- Use flexDirection "column" with gap for vertical stacking, flexDirection "row" with gap for horizontal.
- Minimum gap between elements: 24px vertical, 32px horizontal.
- Text hierarchy gaps: headlinesubheading 16px, subheadingbody 12px, bodybutton 32px.
- Cards/panels: padding 32px-48px, borderRadius 12px.
- NEVER use margin to space siblings always use the parent's gap property.
=== DESIGN STYLE ===
- Premium aesthetic use the exact colors from the assigned theme preset (do NOT invent your own)
- Background: use the preset's gradient-dark or gradient-light value directly as the AbsoluteFill's background
- Card/surface backgrounds: use the preset's surface color
- Text colors: use the preset's text, muted values
- Borders: use the preset's border color
- Glows: use the preset's glow value for all boxShadow — do NOT substitute other colors
- Generous whitespace less is more, let elements breathe
- NO decorative background shapes, blurs, or overlapping ornaments
=== REMOTION RULES ===
- Export the component as: export const MyComposition = () => { ... }
- Use useCurrentFrame() and useVideoConfig() from "remotion"
- Do NOT use Sequence
- Do NOT manually calculate animation timings or frame offsets
=== ANIMATION (use the stagger() helper for ALL element animations) ===
A pre-built helper function called stagger() is available globally.
It handles enter, hold, and exit phases automatically you MUST use it.
Signature:
stagger(frame, fps, index, total) { opacity: number, transform: string }
Parameters:
frame from useCurrentFrame()
fps from useVideoConfig()
index 0-based index of this element in the entrance order
total total number of animated elements in the scene
It returns a style object with opacity and transform that you spread onto the element.
Timing is handled for you: staggered spring entrances, ambient hold motion, and a graceful exit.
Usage pattern:
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
<div style={stagger(frame, fps, 0, 4)}>Headline</div>
<div style={stagger(frame, fps, 1, 4)}>Subtitle</div>
<div style={stagger(frame, fps, 2, 4)}>Card</div>
<div style={stagger(frame, fps, 3, 4)}>Footer</div>
Rules:
- Count ALL animated elements in your scene and pass that count as the "total" parameter.
- Assign each element a sequential index starting from 0.
- You can merge stagger's return with additional styles:
<div style={{ ...stagger(frame, fps, 0, 3), fontSize: 64, color: "#fafafa" }}>
- For non-animated static elements (backgrounds, borders), just use normal styles without stagger.
- You may still use spring() and interpolate() for EXTRA custom effects (e.g., a number counter,
color shift, or typewriter effect), but stagger() must drive all entrance/exit animations.
=== AVAILABLE GLOBALS (injected at runtime, do NOT import anything else) ===
- React (available globally)
- AbsoluteFill, useCurrentFrame, useVideoConfig, spring, interpolate, Easing from "remotion"
- stagger(frame, fps, index, total) animation helper described above
=== CODE RULES ===
- Output ONLY the raw code, no markdown fences, no explanations
- Keep it fully self-contained, no external dependencies or images
- Use inline styles only (no CSS imports, no className)
- Target 1920x1080 resolution
- Every container must use display "flex" with explicit gap values
- NEVER use marginTop/marginBottom to space siblings use the parent's gap instead
""".strip()
def build_scene_generation_user_prompt(
slide_number: int,
total_slides: int,
title: str,
subtitle: str,
content_in_markdown: str,
background_explanation: str,
duration_in_frames: int,
theme: str,
mode: str,
) -> str:
"""Build the user prompt for generating a single slide's Remotion scene code.
*theme* and *mode* are pre-assigned (by LLM or fallback) before this is called.
"""
return "\n".join(
[
"Create a cinematic, visually striking Remotion scene.",
f"The video is {duration_in_frames} frames at {FPS}fps ({duration_in_frames / FPS:.1f}s total).",
"",
f"This is slide {slide_number} of {total_slides} in the video.",
"",
f"=== ASSIGNED THEME: {theme} / {mode.upper()} mode ===",
f"You MUST use the {theme} preset in {mode} mode from the theme presets above.",
f"Use its exact background gradient (gradient-{mode}), surface, text, accent, secondary, border, and glow colors.",
"Do NOT substitute, invent, or default to blue/violet colors.",
"",
f'The scene should communicate this message: "{title}{subtitle}"',
"",
"Key ideas to convey (use as creative inspiration, NOT literal text to dump on screen):",
content_in_markdown,
"",
"Pick only the 1-2 most impactful phrases or numbers to display as text.",
"",
f"Mood & tone: {background_explanation}",
]
)
REFINE_SCENE_SYSTEM_PROMPT = """
You are a code repair assistant. You will receive a Remotion React component that failed to compile,
along with the exact error message from the Babel transpiler.
Your job is to fix the code so it compiles and runs correctly.
RULES:
- Output ONLY the fixed raw code as a string no markdown fences, no explanations.
- Preserve the original intent, design, and animations as closely as possible.
- The component must be exported as: export const MyComposition = () => { ... }
- Only these globals are available at runtime (they are injected, not actually imported):
React, AbsoluteFill, useCurrentFrame, useVideoConfig, spring, interpolate, Easing,
stagger (a helper: stagger(frame, fps, index, total) { opacity, transform })
- Keep import statements at the top (they get stripped by the compiler) but do NOT import anything
other than "react" and "remotion".
- Use inline styles only (no CSS, no className).
- Common fixes:
- Mismatched braces/brackets in JSX style objects (e.g. }}, instead of }}>)
- Missing closing tags
- Trailing commas before > in JSX
- Undefined variables or typos
- Invalid JSX expressions
- After fixing, mentally walk through every brace pair { } and JSX tag to verify they match.
""".strip()