跳到正文
返回

贴文卡(PostCard)短视频制作规范

更新于:

基于 Remotion + React 的竖屏短视频模板规范。

  • 中文名:贴文卡
  • 英文名:PostCard

「贴文卡」指模拟社交媒体帖文截图,将柔白色卡片居中放置在纯黑动态渐变背景上的竖屏短视频。


一、视频规格

项目规范
分辨率1080 × 1920(9:16 竖屏)
帧率30 fps
时长10 秒(300 帧)
编码H.264,CRF 18
格式.mp4
音频AAC,统一背景音乐 bgm.mp3

成片 .mp4 必须包含这条音轨。


二、画面结构

2.1 整体布局

┌─────────────────────────────┐
│  [头像]  昵称                │
│          YYYY-MM-DD         │
│                             │
│  正文第 1 段(整段渲染,     │
│  浏览器按宽度自动换行)      │
│  ...                        │
│  正文第 N 段                 │
│                             │
│                    HH:MM    │
│                    星期X     │
└─────────────────────────────┘

2.2 默认账号信息

元素默认值
头像https://ai-daily.tech/brand/logo.png
昵称高老师的分享局
头部日期动态取当前日期,格式 YYYY-MM-DD
底部时间戳动态取当前时间,格式 HH:MM + 换行 + 星期X

三、画布与背景

3.1 画布规格

属性值
宽度1080px
高度1920px
宽高比9:16(竖屏)
帧率30fps
时长10 秒(300 帧)

3.2 背景样式


四、卡片规范

4.1 卡片外观

属性值
背景色#F5F3F0(柔白)
宽度880px(固定)
高度由内容自动撑开
圆角36px
内边距50px
投影0 8px 40px rgba(0,0,0,0.35)
定位水平垂直居中(flexbox)

4.2 头部(头像 + 昵称 + 日期)

属性值
头像尺寸80×80px
头像形状圆形(borderRadius: 50%)
昵称字号40px
昵称字重700
昵称颜色#1A1A1A
日期字号28px
日期颜色#999999
布局头像在左,昵称+日期在右,垂直居中
与正文间距44px

4.3 正文

属性值
字号46px
字重700
颜色#1A1A1A
行距1.6
段间距18px
对齐左对齐
高亮词颜色#F5A074(橙色)
高亮词字重800
高亮词换行不拆行(whiteSpace: 'nowrap')

正文内容由调用方传入,支持纯文本和高亮词混合。高亮词用 <Hi> 组件包裹。

正文排版(硬性要求):

4.4 底部时间戳

属性值
字号24px
颜色#999999
对齐右对齐
格式HH:MM + 换行 + 星期X
位置卡片底部右下角,自动贴底

五、动画规范

时间线(帧):

元素开始帧结束帧
头部(头像+昵称+日期)09
正文第 1 段615
正文第 2 段1120
正文第 3 段1625
………
正文第 N 段6 + (N-1)*515 + (N-1)*5
底部时间戳5564

所有文字在第 60 帧(2 秒)内全部展示完毕。


六、色彩规范

用途色值说明
背景底色#000000纯黑
渐变光效rgba(245,160,116, 0.15~0.25)橙色半透明
卡片背景#F5F3F0柔白,不刺眼
正文颜色#1A1A1A近黑
高亮词#F5A074橙色,与背景光效呼应
辅助文字#999999灰色(日期、时间戳)

七、字体

font-family: "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", sans-serif;

八、背景音乐

每条贴文卡视频共用这一条背景音乐,不随成片更换。

项目规范
文件bgm.mp3(统一曲目)
项目内路径public/bgm.mp3
加载<Audio src={staticFile('bgm.mp3')} />(从 remotion 引入 Audio、staticFile)
对齐视频仍为 10 秒(300 帧),只用音频的前 10 秒
淡入淡出无
音量volume={1}(不写 volume 时默认就是 1)
成片.mp4 必须包含音轨

曲目已按响度标准化,真峰值 -1 dB,即不削波的最大响度。保持 volume={1},不要再压低,也不要重新编码、裁剪或加淡化。

文件参数:约 10.03 秒,44.1 kHz,立体声,MP3 320 kbps。<Audio> 从第 0 帧起播;合成在第 300 帧结束,多出的约 0.03 秒不会进入成片。

下载:bgm.mp3

拉进 Remotion 项目的 public/bgm.mp3:

mkdir -p public
curl -L -o public/bgm.mp3 https://ai-daily.tech/audio/postcard/bgm.mp3

九、从零搭建步骤

9.1 创建项目目录

mkdir postcard && cd postcard

9.2 创建 package.json

{
  "name": "postcard",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "studio": "remotion studio",
    "render": "remotion render PostCard out/video.mp4"
  },
  "dependencies": {
    "@remotion/cli": "4.0.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "remotion": "4.0.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.0",
    "typescript": "^5.0.0"
  }
}

9.3 创建 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src"]
}

9.4 创建 src/index.ts

import { registerRoot } from 'remotion';
import { RemotionRoot } from './Root';

registerRoot(RemotionRoot);

9.5 创建 src/Root.tsx

import React from 'react';
import { Composition } from 'remotion';
import { PostCard } from './PostCard';

export const RemotionRoot: React.FC = () => {
  return (
    <>
      <Composition
        id="PostCard"
        component={PostCard}
        durationInFrames={300}
        fps={30}
        width={1080}
        height={1920}
      />
    </>
  );
};

9.6 创建 src/PostCard.tsx

import React from 'react';
import {
  AbsoluteFill,
  Audio,
  Img,
  interpolate,
  staticFile,
  useCurrentFrame,
} from 'remotion';

const FONT = '"Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", sans-serif';
const CARD = '#F5F3F0';
const TEXT = '#1A1A1A';
const ORANGE = '#F5A074';
const GRAY = '#999999';

const NICKNAME = '高老师的分享局';

const Hi: React.FC<{ children: React.ReactNode }> = ({ children }) => (
  <span style={{ color: ORANGE, fontWeight: 800, whiteSpace: 'nowrap' }}>{children}</span>
);

const now = new Date();
const DATE_STR = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
const TIME_STR = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
const WEEKDAYS = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const WEEKDAY = WEEKDAYS[now.getDay()];

// 正文按自然段落组织,每个元素是一段;段内换行交给浏览器自然排版,禁止手动分行
const paragraphs: React.ReactNode[] = [
  <>在经过了长期的思想斗争以后,现在正式不用 IDE 了,<Hi>Cursor</Hi> 也正式切换成了 <Hi>Agent View</Hi>,<Hi>VSCode</Hi> 也很少打开了。</>,
  <>代码只关注 <Hi>架构</Hi>,每次简单浏览 <Hi>Diff</Hi>,<Hi>Code Review</Hi> 交给多个 Agent 互相 review,验证就是完备的 E2E 了。</>,
  <>未来代码就是黑盒了,这是心酸但是没办法的事实了。</>,
];

export const PostCard: React.FC = () => {
  const frame = useCurrentFrame();

  const fade = (start: number, dur = 9) =>
    interpolate(frame, [start, start + dur], [0, 1], {
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
    });

  return (
    <AbsoluteFill style={{ backgroundColor: '#000000' }}>
      <Audio src={staticFile('bgm.mp3')} />
      {[0, 1, 2].map((i) => {
        const s = 0.15 + i * 0.08;
        const cx = 50 + Math.sin(frame * s * 0.03 + i * 2.1) * 25;
        const cy = 50 + Math.cos(frame * s * 0.025 + i * 1.7) * 30;
        return (
          <div
            key={i}
            style={{
              position: 'absolute',
              inset: 0,
              background: `radial-gradient(circle ${800 + i * 150}px at ${cx}% ${cy}%, rgba(245,160,116,${0.15 + i * 0.05}), transparent 70%)`,
            }}
          />
        );
      })}

      <div style={{ position: 'absolute', inset: 0, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
        <div style={{ width: 880, backgroundColor: CARD, borderRadius: 36, padding: 50, display: 'flex', flexDirection: 'column', boxShadow: '0 8px 40px rgba(0,0,0,0.35)' }}>
          <div style={{ display: 'flex', alignItems: 'center', marginBottom: 44, opacity: fade(0) }}>
            <Img src="https://ai-daily.tech/brand/logo.png" style={{ width: 80, height: 80, borderRadius: '50%', objectFit: 'cover' }} />
            <div style={{ marginLeft: 18 }}>
              <div style={{ fontFamily: FONT, fontSize: 40, fontWeight: 700, color: TEXT, lineHeight: 1.3 }}>{NICKNAME}</div>
              <div style={{ fontFamily: FONT, fontSize: 28, color: GRAY, lineHeight: 1.4 }}>{DATE_STR}</div>
            </div>
          </div>

          <div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
            {paragraphs.map((p, i) => (
              <p key={i} style={{ opacity: fade(6 + i * 5), fontFamily: FONT, fontSize: 46, fontWeight: 700, color: TEXT, lineHeight: 1.6, margin: '0 0 18px', transformOrigin: 'left center' }}>
                {p}
              </p>
            ))}
          </div>

          <div style={{ fontFamily: FONT, fontSize: 24, color: GRAY, marginTop: 'auto', paddingTop: 16, opacity: fade(55), textAlign: 'right' }}>
            {TIME_STR}<br />{WEEKDAY}
          </div>
        </div>
      </div>
    </AbsoluteFill>
  );
};

9.7 安装依赖并渲染

mkdir -p public
curl -L -o public/bgm.mp3 https://ai-daily.tech/audio/postcard/bgm.mp3
npm install
npx remotion render PostCard out/video.mp4

Remotion 会自动检测系统中的浏览器。如需指定,添加 --browser-executable=/path/to/chrome。

输出文件:out/video.mp4。成片里带有背景音乐;Remotion 写 mp4 时默认把音轨编码为 AAC。

确认音轨:

ffprobe -hide_banner out/video.mp4

应能看到一条 Audio 流(AAC)。没有这条流,说明背景音乐没有打进成片。


十、封面制作

每条贴文卡视频都要配一张封面图,直接取成片的最后一帧裁剪得到,不单独设计。

项目规范
取帧视频最后一帧:300 帧视频即第 299 帧(从 0 起算)
宽高比3:4
尺寸1080 × 1440:宽度保持 1080 不变,高度按 3:4 算为 1440
裁剪方式从中间裁剪:顶部、底部各裁掉 240px,保留 y=240 到 y=1680 的区域
格式.png
首帧成片第 0 帧替换为完整的最后一帧(1080 × 1920),见 10.3

视频画面是 1080 × 1920(9:16),1920 − 1440 = 480,上下各去掉 240px,正好留下画面中间的 3:4 区域。

卡片位于约 y≈330–1590,落在保留区域(240–1680)以内,裁剪后卡片完整保留。如果文案变长导致卡片超出这个范围,需要缩短文案,不要改裁剪区域。

10.1 方式一:用 Remotion 导出最后一帧再裁剪

npx remotion still src/index.ts PostCard out/cover-full.png --frame=299
ffmpeg -i out/cover-full.png -vf "crop=1080:1440:0:240" out/cover.png

crop=1080:1440:0:240 的含义是:宽 1080、高 1440,从 x=0、y=240 开始裁。

10.2 方式二:直接从成片 mp4 截取最后一帧

ffmpeg -sseof -0.1 -i out/video.mp4 -frames:v 1 -vf "crop=1080:1440:0:240" out/cover.png

-sseof -0.1 表示从视频结尾前 0.1 秒开始读,取到的第一帧就是最后一帧(第 299 帧)。

确认封面尺寸:

ffprobe -v error -show_entries stream=width,height -of csv=p=0 out/cover.png

应输出 1080,1440。再打开图片检查卡片上下边缘完整、没有被裁掉。

10.3 封面同时作为视频首帧

部分平台默认取视频第一帧当封面,所以成片的第 0 帧必须就是封面画面:把第 0 帧替换成完整的最后一帧(1080 × 1920,不裁剪)。平台按 3:4 从中间裁剪后,看到的就是上面的 cover.png。

渲染出 out/video.mp4 后执行:

ffmpeg -i out/video.mp4 -sseof -0.1 -i out/video.mp4 \
  -filter_complex "[1:v]trim=end_frame=1,setpts=PTS-STARTPTS[last];[0:v][last]overlay=0:0:format=yuv420:eof_action=repeat:enable='eq(n,0)'[v]" \
  -map "[v]" -map 0:a -c:v libx264 -crf 18 -x264-params "zones=0,0,q=6" -pix_fmt yuvj420p \
  -c:a copy -movflags +faststart out/video-final.mp4
mv out/video-final.mp4 out/video.mp4

参数说明:

检查:

ffprobe -v error -show_entries stream=codec_type,width,height,r_frame_rate,nb_frames -of compact out/video.mp4
ffmpeg -i out/video.mp4 -vf "select=eq(n\,0)" -fps_mode passthrough -frames:v 1 out/frame0.png

应看到视频流 1080×1920、30/1、nb_frames=300,并且有一条 audio 流。打开 out/frame0.png,应与 out/cover-full.png(最后一帧)一致。


十一、定制说明

制作新视频时,只需修改 src/PostCard.tsx 中的 paragraphs 数组,把新的正文按自然段落传入(一个元素对应一段,段尾有完整句号)。高亮词用 <Hi> 包裹。段内换行由浏览器按卡片宽度自然排版,不要手动分行、不要设置每行字数。背景音乐保持 public/bgm.mp3,不要更换。

const paragraphs: React.ReactNode[] = [
  <>第 1 段普通文字,浏览器自动折行。</>,
  <>第 2 段带 <Hi>高亮词</Hi> 的文字,同样整体渲染。</>,
  // ...
];

十二、项目结构

postcard/
├── public/
│   └── bgm.mp3                # 统一背景音乐
├── src/
│   ├── index.ts               # Remotion 入口
│   ├── Root.tsx               # 根组件
│   └── PostCard.tsx           # 贴文卡视频组件(核心)
├── package.json
├── tsconfig.json
└── out/
    ├── video.mp4              # 渲染输出(含音轨)
    ├── cover-full.png         # 最后一帧原图(1080×1920)
    └── cover.png              # 封面(1080×1440,3:4)

本文档作为 PostCard 贴文卡短视频模板的设计与开发规范,持续迭代中。


上一篇
主片卡(HeroClip)短视频制作规范
下一篇
字卡影快闪短视频(CaptionReel)制作与排版布局规范