Podcast teaser
Vertical teaser cut from a podcast: one clip of the audio with its waveform, the show, and a pull quote.
PodcastTeaser, defaults
Installation
npx shadcn add https://www.reelcn.dev/r/podcast-teaser.jsonFiles land in src/reelcn/ with the items it builds on. Dependencies: remotion, zod.
Register the composition
<Composition
id="PodcastTeaser"
component={PodcastTeaser}
schema={podcastTeaserSchema}
defaultProps={{ ...podcastTeaserDefaults, audio: staticFile("episode-42.mp3"), clipStart: 734, clipEnd: 762 }}
calculateMetadata={podcastTeaserMetadata}
width={1080}
height={1920}
fps={30}
durationInFrames={1}
/>Render
npx remotion render PodcastTeaser # 16:9
npx remotion render PodcastTeaser --width=1080 --height=1920 # 9:16Props
PropTypeDescription
clipStart*numberSeconds into the audio where the clip starts.
clipEnd*numberSeconds into the audio where the clip ends.
quote*string
show*string
theme"daylight" | "midnight" | "mono" | "ledger" | "afterglow" | "chromewave" | "neon" | "paper" | "sunset"Theme preset. Inherits the surrounding `ThemeProvider` when omitted.
brand{ accent?: string; logo?: string; font?: string; }
sfxbooleanPlays a soft whoosh as each scene starts.
musicstringBackground music file, looped under everything at 25% volume.
audiostringThe episode audio. Beat-driven bars stand in when omitted.
coverstring
Built from
Use
- Promoting an episode with its best thirty seconds
Avoid
- Whole episodes or segments with captions. Use
audiogram
Make it with an agent
With the reelcn agent skill installed, ask for the video in plain words, such as "make a launch video for Relay with reelcn". The skill picks a template, sets its props, reviews a contact sheet and renders.
Guides
Components take their colors from a theme, their timing from motion props and their layout from the canvas.
Source
registry/items/podcast-teaser.tsx
/**
* @title Podcast Teaser
* @category templates
* @description Vertical teaser cut from a podcast: one clip of the audio with its waveform, the show, and a pull quote.
* @duration data-driven
* @use Promoting an episode with its best thirty seconds
* @avoid Whole episodes or segments with captions. Use `audiogram`
* @tags podcast, teaser, clip, short, template
* @example
* <Composition
* id="PodcastTeaser"
* component={PodcastTeaser}
* schema={podcastTeaserSchema}
* defaultProps={{ ...podcastTeaserDefaults, audio: staticFile("episode-42.mp3"), clipStart: 734, clipEnd: 762 }}
* calculateMetadata={podcastTeaserMetadata}
* width={1080}
* height={1920}
* fps={30}
* durationInFrames={1}
* />
*/
import type { CalculateMetadataFunction } from "remotion";
import { z } from "zod";
import { AudiogramLayout } from "./audiogram";
import { STORY_FPS } from "./story";
import { TemplateFrame, templateSchema } from "./storyboard";
export const podcastTeaserSchema = templateSchema
.extend({
/** The episode audio. Beat-driven bars stand in when omitted. */
audio: z.string().optional(),
/** Seconds into the audio where the clip starts. */
clipStart: z.number().min(0),
/** Seconds into the audio where the clip ends. */
clipEnd: z.number().positive(),
quote: z.string(),
show: z.string(),
cover: z.string().optional(),
})
.refine((props) => props.clipEnd > props.clipStart, {
message: "clipEnd must be after clipStart",
path: ["clipEnd"],
});
export type PodcastTeaserProps = z.infer<typeof podcastTeaserSchema>;
export const podcastTeaserDefaults: PodcastTeaserProps = {
clipStart: 0,
clipEnd: 8,
quote: "The best render farm is the one you never have to think about.",
show: "Frame by Frame",
};
export function PodcastTeaser({ audio, clipStart, quote, show, cover, ...fields }: PodcastTeaserProps) {
return (
<TemplateFrame fields={fields}>
<AudiogramLayout audio={audio} clipStart={clipStart} show={show} cover={cover} quote={quote} />
</TemplateFrame>
);
}
export const podcastTeaserMetadata: CalculateMetadataFunction<PodcastTeaserProps> = ({ props }) => ({
fps: STORY_FPS,
durationInFrames: Math.round((props.clipEnd - props.clipStart) * STORY_FPS),
});