From searchpo
Remotion video composition best practices for searchpo. Use when editing src/ components, configuring rendering, optimizing video output, or adding animations/effects to the Remotion pipeline.
How this skill is triggered — by the user, by Claude, or both
Slash command
/searchpo:remotion-best-practicesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
- Remotion 4.0.468 (React 19)
SearchpoRadar — storyboard-driven video, vertical 1080×1920 or horizontal 1920×1080remotion render) or Lambda (@remotion/lambda)gemini-3.1-flash-tts-preview) → voiceover.wavdeepseek-v4-pro) polished copyscripts/video_quality_loop.py wraps Remotion render, metadata checks, deterministic audit, Gemini 3.5 Flash visual audit, automatic layout repair, and final artifact promotion.src/Composition.tsx owns the global video shell. Scene-specific blocks live under src/scenes/, and storyboard contract/timing helpers live in src/storyboard.ts.Payload interface. Data comes from remotion_input_props.json at render time and should include a validated storyboard; video_spec.json is the renderer-neutral planning artifact compiled into render_payload.json, and legacy cards are only a compatibility fallback.statement, context, stat, bullets, card, compare, compare_table, terminal, flow, chart, code_diff, quote, and cta. Keep each block inside the visual safe band above the bottom caption area.16:9 is valid when videoProfile.orientation, aspectRatio, width, height, Remotion metadata, and deterministic audit all agree.theme, density, motion, accent, and chrome toggles. Do not let provider output inject arbitrary CSS or remote assets.useCurrentFrame() + useVideoConfig() as the timing source. Never use setTimeout or CSS animations.interpolate() for linear/eased transitions. Use spring() for physics-based motion.@remotion/transitions TransitionSeries when chooseTransitionFrames() can preserve readable dwell time and keep the midpoint OCR sample out of a cross-fade; otherwise fall back to plain Series.// Frame-based interpolation (preferred for predictable timing)
const opacity = interpolate(frame, [0, 30], [0, 1], {extrapolateRight: 'clamp'});
// Spring for natural entrance (cards, titles)
const enter = spring({frame: frame - delay, fps, config: {damping: 18, stiffness: 110}});
// Looping animation (pulse, glow)
const pulse = interpolate(Math.sin(frame / 10), [-1, 1], [0.4, 1]);
// Stagger children
const stagger = (index: number, base: number, gap: number) =>
spring({frame: frame - base - index * gap, fps, config: {damping: 18, stiffness: 110}});
agent_visual TSX ComponentsProvider videos must use a custom, topic-specific agent_visual; storyboard scenes and legacy cards are compatibility data, not the main provider visual surface. Aggressive visual generation is enabled:
agent_visual.module_code: a complete TSX module written to src/generated/AgentVisual.generated.tsx. It must export a named AgentVisualGenerated component and may import React, Remotion APIs, installed @remotion/* packages, and local helpers under src/components or src/utils.agent_visual.component_code only as a compatibility path: it is a TSX function body inserted inside AgentVisualGenerated and receives the provided runtime values data, frame, fps, durationInFrames, width, height, progress, audioPulse, React, AbsoluteFill, interpolate, spring, useMemo, Img, staticFile, Series, and Sequence..env values.Hard visual contract:
data.cards, safeCards, full data.summary, data.evidenceLevel, source paths, or operator/internal metadata as the main visual.animation or transition; every movement must be frame-driven from frame, progress, audioPulse, interpolate(), spring(), or Math.sin(frame / N).560px; for horizontal output reserve roughly 180px.Deterministic motion target:
motion_check.active_pair_ratio >= 0.20 and at least 4 active pairs.180s+ is not a default target; it is the long-form quality threshold. If the model chooses 180 seconds or longer, author a chaptered visual plan with at least 9 storyboard scenes and enough agent_visual states to sustain the entire narration. A short 3-7 scene storyboard stretched over 3+ minutes is a quality failure.Topic-specific visual recipes:
Copy-paste body template for agent_visual.component_code:
const isVertical = height >= width;
const bottomSafe = isVertical ? 560 : 180;
const sidePad = isVertical ? 72 : 104;
const title = String(data.displayTitle || data.title || 'Searchpo signal').slice(0, 46);
const source = String(data.source || 'verified source').slice(0, 34);
const beats = useMemo(() => {
const scenes = Array.isArray(data.storyboard?.scenes) ? data.storyboard.scenes : [];
const mapped = scenes.slice(0, 4).map((scene, index) => {
const props = scene?.props || {};
return {
label: String(props.title || props.heading || props.label || scene.type || `Step ${index + 1}`).slice(0, 24),
detail: String(props.detail || props.text || props.summary || props.value || '').slice(0, 44),
};
});
return mapped.length > 0
? mapped
: [
{label: 'Problem', detail: 'What changed'},
{label: 'Mechanism', detail: 'How it works'},
{label: 'Boundary', detail: 'What to verify'},
];
}, [data.storyboard]);
const entrance = spring({frame, fps, config: {damping: 18, stiffness: 120}});
const breathing = interpolate(Math.sin(frame / 16), [-1, 1], [0.985, 1.025]);
const floatY = interpolate(Math.sin(frame / 22), [-1, 1], [-16, 16]);
const orbit = interpolate(Math.sin(frame / 29), [-1, 1], [-22, 22]);
const scan = interpolate((frame % Math.max(1, durationInFrames)) / Math.max(1, durationInFrames), [0, 1], [-18, 118]);
const audioScale = interpolate(audioPulse, [0, 1], [1, 1.055], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'});
return (
<AbsoluteFill
style={{
background: 'linear-gradient(135deg, #182433 0%, #263449 48%, #fff7df 100%)',
color: '#fffdf4',
padding: `72px ${sidePad}px ${bottomSafe}px`,
overflow: 'hidden',
}}
>
<div style={{height: '100%', display: 'grid', gridTemplateRows: 'auto minmax(0, 1fr)', gap: 34}}>
<div style={{opacity: entrance, transform: `translateY(${(1 - entrance) * -28}px)`}}>
<div style={{fontSize: 26, letterSpacing: 0, color: '#72e7ff'}}>{source}</div>
<div style={{fontSize: isVertical ? 58 : 48, fontWeight: 800, lineHeight: 1.04, marginTop: 12}}>{title}</div>
</div>
<div
style={{
minHeight: 0,
display: 'grid',
gridTemplateColumns: isVertical ? '1fr' : '1.05fr 0.95fr',
alignItems: 'center',
gap: 28,
transform: `scale(${breathing * audioScale}) translateY(${floatY}px)`,
}}
>
<div
style={{
minHeight: isVertical ? 520 : 420,
borderRadius: 28,
padding: 28,
background: `linear-gradient(90deg, rgba(13, 25, 40, 0.72) 0%, rgba(13, 25, 40, 0.72) ${scan - 12}%, rgba(255, 214, 10, 0.20) ${scan}%, rgba(13, 25, 40, 0.72) ${scan + 12}%)`,
border: '2px solid rgba(114, 231, 255, 0.36)',
boxShadow: '0 34px 90px rgba(5, 13, 24, 0.32)',
overflow: 'hidden',
}}
>
<div style={{display: 'grid', gap: 18}}>
{beats.map((beat, index) => {
const itemEnter = spring({frame: frame - 8 - index * 9, fps, config: {damping: 17, stiffness: 115}});
const active = Math.sin((frame + index * 13) / 18);
return (
<div
key={beat.label}
style={{
display: 'grid',
gridTemplateColumns: '54px 1fr',
gap: 18,
alignItems: 'center',
opacity: itemEnter,
transform: `translateX(${(1 - itemEnter) * 46 + active * 5}px)`,
}}
>
<div
style={{
width: 54,
height: 54,
borderRadius: 18,
display: 'grid',
placeItems: 'center',
background: index % 2 === 0 ? '#ffd60a' : '#72e7ff',
color: '#162232',
fontSize: 24,
fontWeight: 900,
}}
>
{index + 1}
</div>
<div>
<div style={{fontSize: 30, fontWeight: 800, lineHeight: 1.05}}>{beat.label}</div>
<div style={{fontSize: 21, color: 'rgba(255, 253, 244, 0.74)', marginTop: 6}}>{beat.detail}</div>
</div>
</div>
);
})}
</div>
</div>
<div
style={{
minHeight: isVertical ? 260 : 420,
display: 'grid',
placeItems: 'center',
transform: `translateX(${orbit}px)`,
}}
>
<div
style={{
width: isVertical ? 300 : 360,
height: isVertical ? 300 : 360,
borderRadius: 44,
display: 'grid',
placeItems: 'center',
background: 'radial-gradient(circle, #ffd60a 0%, #ff2d55 54%, rgba(255, 45, 85, 0.18) 70%)',
color: '#111827',
fontSize: 86,
fontWeight: 900,
transform: `rotate(${Math.sin(frame / 34) * 4}deg)`,
boxShadow: '0 26px 80px rgba(255, 45, 85, 0.32)',
}}
>
{Math.round(progress * 100)}
</div>
</div>
</div>
</div>
</AbsoluteFill>
);
Use the template as a structural starting point, then replace the central primitives with source-grounded visuals for the actual topic. For example, swap the numbered beat list for a terminal command stream, code diff, architecture nodes, chart bars, or matrix cells, but keep the same continuous motion layers and safe-area padding.
useMemo() for parsed data.staticFile() for assets in public/. Never use external URLs (they break in Lambda).<Audio src={staticFile(path)} />. Remotion handles sync automatically.# High-quality local render (CRF 18 = visually lossless H.264)
remotion render SearchpoRadar out/video.mp4 \
--props props.json \
--codec=h264 \
--pixel-format=yuv420p \
--crf=18 \
--audio-bitrate=192K
# Even higher quality (slower encode, better for archival)
--crf=15 --preset=slow
--crf 18 is the sweet spot for Douyin (high quality, reasonable file size ~5-8MB for 12s).--pixel-format=yuv420p is required for cross-device compatibility.renderMediaOnLambda() options.REMOTION_LAMBDA_PRIVACY=private, pass REMOTION_LAMBDA_DELETE_AFTER to both video and cover still commands, and default to REMOTION_LAMBDA_CONCURRENCY=4. Only set SEARCHPO_LAMBDA_FRAMES_PER_LAMBDA when explicitly choosing frame-count partitioning.@remotion/captions package or custom <Captions> component (we use custom in src/Captions.tsx).public/generated/voiceover.wav before render.<Audio> component auto-syncs with frame timeline.scripts/normalize_audio.sh).// Fade in/out for intro/outro (critical for Douyin — no hard cuts)
const introOpacity = interpolate(frame, [0, 18], [0, 1], {extrapolateRight: 'clamp'});
const outroOpacity = interpolate(frame, [durationInFrames - 30, durationInFrames - 6], [1, 0], {extrapolateLeft: 'clamp'});
// Slide transition
const slideX = interpolate(frame, [startFrame, startFrame + 20], [1080, 0], {extrapolateRight: 'clamp'});
Storyboard timing rule:
N = scenes.length and T = transitionFrames.durationInFrames + (N - 1) * T.TransitionSeries consumes sum(sceneDurations) - (N - 1) * T, so the final timeline still equals the real audio/caption duration.TransitionSeries children must be Sequence, not Transition.| Mistake | Fix |
|---|---|
Using window.setTimeout | Use frame-based logic: if (frame > N) |
| Fetching remote data in component | Pre-compute in prepare_remotion_input.py, pass via props |
CSS transition / animation | Use Remotion interpolate() / spring() |
| Font not rendering | Use offline fontsource CSS imports in src/fonts.ts and gate on document.fonts.ready |
| Audio out of sync | Check audio sample rate matches (24kHz WAV), normalize before render |
| Black frames at start | Add intro fade: interpolate(frame, [0, N], [0, 1]) |
| Lambda timeout | Increase REMOTION_LAMBDA_TIMEOUT_SEC or reduce composition complexity |
| Flickering text | Avoid conditional rendering mid-frame; use opacity instead |
src/
├── index.ts # registerRoot
├── Root.tsx # <Composition> registration (specs, defaultProps)
├── Composition.tsx # Main storyboard shell (SearchpoRadar)
├── storyboard.ts # Storyboard contract, timing, style tokens
├── scenes/ # Vetted scene block components
├── Captions.tsx # Dynamic subtitle overlay
└── styles.css # All styling (no CSS modules, no Tailwind)
public/
├── searchpo-content.json # Sample/dev props fixture
└── generated/
└── voiceover.wav # Runtime audio (copied by prepare_remotion_input.py)
render/
└── prepare_remotion_input.py # Transforms video_spec/render_payload run artifacts → Remotion props JSON
scripts/
├── remotion_render.sh # Local render wrapper (--crf, --audio-bitrate)
└── lambda_render.sh # Lambda site upload + render
src/storyboard.ts and agent_harness/storyboard.py.src/scenes/ with frame-based timing and safe-area constraints.styles.css using stable dimensions; avoid text overflow and arbitrary provider-controlled CSS.contracts/schemas/render_payload.schema.json, DeepSeek policy, and tests when the storyboard contract changes.npm run dev → preview at http://localhost:3000.python3 scripts/video_quality_loop.py "$(cat runtime/latest_run.txt)" --execution-profile provider --render-mode local --publish-mode contract-check.npx claudepluginhub sunflowerslwtech/sunkit --plugin searchpoProvides best practices for building videos programmatically with Remotion, covering project scaffolding, animation (interpolate/spring), sequencing, transitions, captions, audio, effects, and rendering.
Provides Remotion best practices for video creation in React: animation patterns (useCurrentFrame, interpolate, Easing), proper use of Sequence, staticFile, and media components (Img, Video, Audio). Warns against CSS transitions, Tailwind animations.
Guides Remotion video production with Apple-style design rules, animation patterns, project structure, constants-first setup, and export settings.