| name | create-end-card-ffmpeg |
| description | Build an end card deterministically with ffmpeg — typewriter / fade-in text lines + logo overlay over a backdrop frame. No Playwright, no Remotion, no AI. Use when the end card is a static composition with simple fades. |
create-end-card-ffmpeg
Purpose
Render a clean end card with ffmpeg primitives only. No browser, no Node, no AI. Takes a backdrop image + up to N text lines + an optional logo and produces an mp4 with sequential fade-in animation.
The opinionated alternatives — create-end-card-html, create-end-card-hyperframes, create-end-card-pil — remain available for end cards that need richer motion graphics (kinetic typography, spring physics, particle effects, image transitions). Use this atom when you want a deterministic, fast, zero-dependency build for a static-with-fades end card.
Inputs
| Field | Required | Notes |
|---|
backdrop | yes | Path to a backdrop image (jpg/png). Will be scaled to width×height. Often the last frame of the prior scene. |
text_lines | no | List of { text, fade_in_start_s, fade_in_duration_s, font_size, y_offset_from_bottom } entries. Sequenced for stagger. |
logo_path | no | Path to a logo image (PNG, ideally pre-recolored white/black/etc.). If SVG, the atom converts via cairosvg first. |
logo_options | no | { width, fade_in_start_s, fade_in_duration_s, position } |
duration_s | yes | Total output duration (seconds) |
width | no, default 720 | Output width |
height | no, default 1280 | Output height (9:16 = 720×1280, 1:1 = 1080×1080, etc.) |
fps | no, default 24 | Output frame rate |
crf | no, default 18 | x264 quality (lower = better, 18 is visually lossless-ish) |
backdrop_dim | no, default 0 | Brightness offset to apply to backdrop (-0.15 darkens 15%; useful when text needs contrast) |
output_path | yes | Where to write the mp4 |
Workflow
- Prepare the logo. If
logo_path is .svg, run cairosvg to convert to PNG at logo_options.width (default 240px wide). If the logo needs a color shift (e.g. brand mark must be white), pre-process the alpha channel via PIL to swap RGB to the target color while preserving alpha.
- Build the ffmpeg filter graph. Single command, all overlays in one pass:
- Scale backdrop to target dimensions + apply optional
eq=brightness=<dim>
- For each
text_lines entry, add a drawtext filter with an alpha expression that fades in from fade_in_start_s over fade_in_duration_s
- For the logo, add an
overlay with the logo stream piped through a fade=t=in:st=<start>:d=<dur>:alpha=1 filter
- Run ffmpeg with
-loop 1 -t <duration_s> on each input.
- Validate the output is the correct duration + has video stream.
Alpha-fade expression cheat-sheet
The drawtext filter doesn't support fade= directly — it uses an alpha expression that you build manually:
alpha='if(lt(t, <start>), 0, if(lt(t, <start> + <dur>), (t - <start>) / <dur>, 1))'
This: holds invisible until start, ramps 0→1 over dur seconds, holds at 1 afterward.
For a fade-out at the end:
alpha='if(lt(t, <fade_out_start>), 1, if(lt(t, <fade_out_start> + <fade_out_dur>), 1 - (t - <fade_out_start>) / <fade_out_dur>, 0))'
For a fade-in + hold + fade-out:
alpha='if(lt(t, <start>), 0, if(lt(t, <start> + <in_dur>), (t - <start>) / <in_dur>, if(lt(t, <out_start>), 1, if(lt(t, <out_start> + <out_dur>), 1 - (t - <out_start>) / <out_dur>, 0))))'
Output
<output_path> — the mp4 end card
<output_path>.manifest.json — recording inputs + computed filter + duration
Quality Checks
- Output duration matches
duration_s ± 0.05s
- All text lines fade in at the correct times (verify with
ffmpeg -i out.mp4 -ss <line_start> -frames:v 1 and check alpha)
- Logo position is within the safe area (don't let it crop off screen)
- No transparency artifacts at fade boundaries
Failure Modes
- SVG conversion fails.
cairosvg requires Cairo + libffi installed. On macOS: brew install cairo libffi. On Linux: apt install libcairo2-dev libffi-dev. If install fails, fall back to manual SVG → PNG conversion via ImageMagick (convert -background none -density 300 logo.svg -resize 240x logo.png).
- drawtext font not found.
drawtext=fontfile=<path> requires an absolute path to a TTF/OTF file. On macOS use /System/Library/Fonts/HelveticaNeue.ttc. On Linux use one of /usr/share/fonts/truetype/dejavu/*.ttf. If the font has spaces or special chars, escape with backslashes.
- Alpha expression syntax error. ffmpeg's filtergraph is unforgiving with nested
if(...) — quote the entire alpha= value and escape internal commas/colons if needed. Easier: build the expression as a Python f-string and pass via -filter_complex_script if it gets long.
- Logo PNG has wrong color. SVG → PNG via cairosvg preserves the SVG's fill color. If you need a white-recolored logo, pre-process with PIL: load PNG → convert to RGBA → replace all non-transparent pixels with white preserving alpha.
Example invocation
from scripts.create_end_card import create_end_card
create_end_card(
backdrop='generated/callback-frames/end-card-bg.png',
text_lines=[
{'text': 'THE WORLD IS', 'fade_in_start_s': 0.0, 'fade_in_duration_s': 0.75,
'font_size': 72, 'y_offset_from_bottom': 360},
{'text': 'YOUR COURT.', 'fade_in_start_s': 0.9, 'fade_in_duration_s': 0.7,
'font_size': 72, 'y_offset_from_bottom': 270},
],
logo_path='nike/brand-assets/logos/jumpman.svg',
logo_options={'width': 240, 'fade_in_start_s': 2.0, 'fade_in_duration_s': 0.3,
'position': 'center', 'recolor': '#ffffff'},
duration_s=4.0,
width=720, height=1280,
backdrop_dim=-0.15,
output_path='clips/end-card.mp4',
)
When NOT to use this
- The end card has motion graphics (animated logos, kinetic type, particle effects). Use
create-end-card-hyperframes.
- The end card has interactive elements or web-style layout (links, CTAs, complex grids). Use
create-end-card-html.
- The end card needs gradient sampling from a reference image. Use
create-end-card-pil.
This atom is for the case where you want a clean fade-in end card produced deterministically in ~5 seconds with no AI or browser dependencies.