关于 fframes-video
fframes-video 是指导使用 Rust 库 fframes 以代码生成视频的技能,面向制作动画和视频的开发者。它覆盖场景、时间轴、音频、预览与成品检查流程。
主要功能
- 01用 Rust 代码生成视频帧
- 02以场景和时间轴组织动画
- 03通过音频映射安排声音
- 04使用 GPU 实时预览
- 05检查帧、联系表与响度
适用场景
- 01制作单镜头或多场景视频
- 02按音乐节奏设计动画
- 03检查视频布局和运动
- 04导出最终视频
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dmtrkovalenko/ fframes --skill fframes-video安装后在 Claude Code 中输入/fframes-video确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api.mdaudio.md3d.mddesign.md详细介绍
使用 fframes 制作视频
fframes 是一个用代码渲染视频的 Rust 库。视频是一个带有 render_frame(frame) -> Svgr 方法的 Rust 结构体:对于每一帧,它都会返回一个 SVG 树,该树由 svgr! 宏构建(SVG 标记中包含 {rust expressions})。
场景会拆分时间线,timeline! 和弹簧可为数值添加动画,音频映射用于放置声音,最后由 ffmpeg 编码输出。
每个项目都会获得一个命令行界面(来自 fframes::cli)。所有操作都使用它:渲染视频、打开实时预览窗口,以及查看帧、联系表和响度数值等可读取的文件。
设计前请先阅读 references/design.md,编写代码时参考 references/api.md,处理声音时参考 references/audio.md,制作 3D 镜头时参考 references/3d.md(fframes::three_d)。
如果要匹配给定的视频,请先遵循 references/design.md 中的参考视频分析指南。参考视频中的剪辑、节奏和运动优先于下方的设计默认设置;快速剪辑和有意设置的黑帧也可能是所需效果的一部分。
首要遵守的两条规则
编写代码前先规划
不要打开模板就开始制作动画。首先:
- 明确需求:观众是谁、在哪里观看(平台决定格式和时长)、观众必须记住的一件事、基调,以及已有素材(徽标、产品镜头、视频素材、音乐、旁白)。询问用户缺少且重要的信息。绝不要虚构产品事实、数字或引言。
- 如果视频要配合音乐或旁白剪辑,先选好声音。它的节奏、拍点和乐句决定其他所有内容的时间安排。
- 先规划动态设计,再让动态设计与声音的峰值同步,形成独特连贯的流程:元素将从哪里出现,以及如何变换。尽量为每个场景设计独特的动态效果,让视频看起来独特而精致。视频不应像幻灯片。
- 试着设想观众观看成片时的感受,并据此调整每个设计部分:流程、节奏、拍点、运动和色彩。
杜绝敷衍
敷衍的视频是由默认选项拼凑而成的:只要换掉文字,就能套用到任何产品上,观众一秒内就能看出来。除非用户提出要求,否则不要交付以下内容:
- 模板残留:起始布局、颜色、字体或装饰、占位文字、假徽标、虚构数字。
- 拍点滥用:每个拍点都让整个画面缩放、抖动或闪烁。让细小元素与拍点同步(例如一条线、一个词、一点火花),只在少数剪辑处改变整个画面。
- 所有元素都用同一种运动:每个元素都以相同的缓动和时长向上淡入 40 px。
- 静态场景:一个元素出现、停留后淡出,期间没有其他运动。
- 泛泛的文案:"Introducing"、"Unleash"、"Seamless"、"Next-level"、三个词的标语("Fast. Simple. Powerful.")。屏幕文字应符合自然说话方式。
- 总体而言,文字应控制数量并切中要点。
相反,每个元素都应有与主题相关的理由,并删去没有理由的元素。把精力花在细节和运动上,而不是文字上:次级运动、错落入场、描绘线条、相互响应的物体、跨剪辑延续的元素。向用户展示任何内容之前,先查看一组连续画面,并问问自己:如果只替换文字,这个视频是否也适用于其他产品?如果答案是肯定的,就该改变创意,而不是颜色。
1. 安装
先安装 https://rustup.rs 上的 Rust,然后安装构建 ffmpeg 时使用的系统库:
# macOS
brew install pkg-config ffmpeg x264 x265 opus nasm ninja
# Debian / Ubuntu
sudo apt-get install -y yasm nasm ffmpeg libx264-dev libx265-dev libopus-dev libclang-dev clang ninja-build libvpx-dev libasound2-dev
# Arch
sudo pacman -S ninja yasm nasm ffmpeg x264 x265 opus clang
Windows 会链接预构建的 FFmpeg 9 共享版本(将 FFMPEG_DIR 设置为从 BtbN/FFmpeg-Builds 解压得到的 ffmpeg-n9.0-latest-win64-gpl-shared 构建目录,将其 bin 添加到 PATH,使用 winget install LLVM.LLVM 安装 LLVM,并设置 LIBCLANG_PATH)。详情请参阅 fframes README。
2. 创建项目
curl -fsSL https://raw.githubusercontent.com/dmtrKovalenko/fframes/main/scripts/new-video.sh | bash -s -- my-video --yes
# or install the generator once and use it directly:
cargo install --locked cargo-fframes
cargo fframes new my-video --format landscape --fps 30 --yes
选项:--template single-scene|multi-scene、--format landscape|portrait|square|uhd、
--fps、--title、--backend、--dir。项目依赖与已安装的 cargo-fframes 相匹配的
fframes 发行版;--git 则使用代码库的 main 分支。始终传入 --yes,以免等待输入。
模板:
single-scene(默认):一个场景。它适用于所有格式;单镜头片段、标题卡和竖屏视频都可以从这里开始。multi-scene:两个场景。包含多个场景的内容可以从这里开始;它只支持landscape和uhd。
模板内容是用于展示 API 的占位代码。请将其布局、颜色、字体和装饰替换为适合用户所需视频的设计。
使用 Skia GPU 后端(默认)。 cargo fframes new 会在 macOS 上选择 Skia on Metal,在 Linux 和 Windows 上选择
Vulkan。它的渲染速度约为 CPU 后端的 10 倍,还能提供实时 preview 窗口。在 macOS 和 Linux 上,首次构建会下载预编译的 Skia 和 ffmpeg,并且只编译 Rust 依赖项(约需一分钟;其他目标平台,或同时使用 metal 和 vulkan 时,会从源码编译 Skia,约需 20 分钟)。请立即启动,并在运行期间编写视频:
cd my-video && cargo build --release # run in the background, the first build is the slow one
--backend cpu 无需 Skia,但没有预览窗口;仅在没有 GPU 时选择它。
项目生成的效果如下:
my-video/
src/lib.rs # the video (from the template)
src/main.rs # the command line
media/ # fonts, images and audio compiled into the binary (one starter font)
README.md # the commands below
以下每条命令都是 cargo run --release -- <command>。定义一个简短的 shell 函数(在 zsh(macOS 的 shell)中,像 R="cargo run --release --" 这样的变量不会拆分成多个单词):
R() { cargo run --release -- "$@"; }
3. 工作循环
使用此技能时完全不要编写测试。 不要创建单元测试、集成测试、快照测试、视觉回归测试、测试模块或测试框架。使用 inspect、渲染帧、联系表、音频检查和原生预览来验证视频。
预览,不要渲染。 如果视频使用 GPU 渲染后端,且此计算机上有 GPU,请运行原生预览(或向用户展示命令),不要在每次更改后都重新渲染视频。更改准备好供审阅后:
- 在可复制的 shell 代码块中展示完整命令,使用实际项目路径和包名;不要要求用户先定义
$R辅助变量。例如:cd /absolute/path/to/fframes && cargo run --release -p my-video -- preview 0s。 - 明确提出可以为用户运行该预览。如果用户已经授权打开或刷新预览,请展示命令并在该授权范围内继续操作。
用户提出更改后,将命令的开始时间设为更改部分所在的具体秒数,或略早于该部分,并留出足够的前置时间,以便看到其入场或转场。例如,preview 8.5s 可以让用户查看约 9 秒处的更改,而不必重放开头。说明他们将看到哪个部分。用户要求最终导出时,渲染完整视频;如果该请求已经明确,无需再次确认。
每次更改后:
$R timeline会列出各场景及其帧数和秒数范围,以及每条音轨。 在查看画面之前,先在这里检查结构和节奏。$R inspect每隔 0.25 s 检查一帧,并检查每个场景的第一帧和最后一帧。它会报告缺失的图像、字体或字形,文本被画布边缘截断,无效 SVG(矩形尺寸为零、半径错误),变换损坏和程序崩溃,并注明各问题出现的时间和场景。发现错误时会以代码 2 退出。修复报告中的所有问题。若警告只出现在场景的开头几帧,通常是入场动画;请用条带图检查。$R strip <scene or range> -n 12会生成一张联系表(strip.png):在一张带标签的图片中均匀排列多帧。用图像工具打开它。这是判断布局、节奏和运动最快的方法。$R strip 2s,5s,9s会将指定时间的画面并排显示。$R frame Intro@end,Outro@50%会将全尺寸 PNG 写入frames/,用于检查细节(排版、对齐、对比度),并报告这些画面中发现的问题。检查细节(元素之间的接触、边缘、小图形)时,应使用清晰的放大裁剪图,绝不要查看缩小后的画面:$R frame 3s --crop 1100,520,240,120(视频像素,放大到约 960 px 宽)。--ids会描出所有带有id的元素并标注名称,以便辨认各个图形。$R onion "[email protected]@1s" -n 6会将多帧叠加成一张图片(onion.png),展示运动轨迹和间距:缓动、过冲、错开。$R preview Intro会打开一个带声音的实时窗口,供用户观看(按空格键播放 / 暂停,按 h/l 快进或后退一秒,按 j/k 逐帧前进或后退,按 q 退出)。它会一直运行到关闭为止,因此替用户运行时请在后台启动。显示命令,并按上述说明提供启动选项;修改后,请使用靠近修改处的具体秒数进行检查。
需要编码样片时,$R render Intro --draft 会以半分辨率编码一个场景。$R render 会为指定的最终导出生成完整视频。
展示完成的视频之前,请运行 $R inspect --pops:它会以较小尺寸渲染每一帧,并警告画面跳变,也就是静止帧之间有某个画面元素在一帧内发生变化,例如元素突然出现、消失或跳到另一个元素前面,而不是平滑移动到那里。每条警告都会提供用于检查问题的 frame ... --crop ... 命令;同一区域的重复问题(计数器、闪烁的灯)会合并为一条警告。
规则:
- 在说画面效果不错之前,先查看 PNG。
- 优先使用
strip,而不是多次调用frame;如果只需检查构图,请添加--scale 0.5。 - 解析输出时添加
--json:结果会输出到 stdout,进度信息会输出到 stderr。 - 保留
--release:调试构建的渲染速度会慢很多。
指定时间
120(帧)、3.2s、500ms、1:05.5、50%、start、end;场景可用结构体名称指定,如 Intro(也匹配 IntroScene,不区分大小写)、#3(场景索引)、Intro[1](该类型的第二个场景);场景内可指定 [email protected]、Intro@12、Intro@50%、Intro@end。范围:a..b(不包含结束位置)、a..、..b、all,或者用场景名称表示整个场景。多个时间点用逗号分隔。
所有命令
| 命令 | 用途 |
|---|---|
timeline | 查看场景、时长、音轨及其混音设置 |
inspect [RANGE] [--every 0.1s | --all-frames] [--fail-on warning] [--pops] | 无需渲染像素即可查找问题;--pops 还会查找突然出现或消失的元素 |
strip [RANGE | TIMES] -n N [--columns 4] [--width 480] [--crop x,y,w,h] | 在一张图中检查视频节奏和运动 |
frame TIMES [-o dir] [--svg] [--crop x,y,w,h] [--zoom N] [--ids] | 获取所选帧的全尺寸 PNG(以及布局后的 SVG)、清晰裁剪图和带标签的元素 |
onion RANGE -n N | 查看运动轨迹和缓动效果 |
svg TIME | 以文本形式读取帧的最终 SVG(位置、文本、颜色) |
preview [TIME] [--paused] [--mute] | 为用户提供带声音的实时窗口 |
render [RANGE] [-o out.mp4] [--draft] | 对视频或其中一部分进行编码(音频会相应剪切) |
audio analyze [RANGE] [--waveform w.png] | 查看响度(LUFS)、真峰值、削波、静音和各场景的数据 |
audio at TIMES | 查看某一时刻播放的声音、它们在文件中的位置及音量 |
audio render [RANGE] -o a.wav | 将混音导出为 WAV 文件 |
浏览器编辑器
fframes 还提供带时间轴和拖动浏览功能的 Web 编辑器,适合用户以交互方式调整视频。它会运行编译为 WebAssembly 的视频,并需要 Node.js 和
wasm-pack;设置方法见 fframes 仓库中 hello-world 示例的 editor/ 文件夹(https://github.com/dmtrKovalenko/fframes/tree/main/examples/hello-world)。
用户需要 GUI 编辑器时,请提及此编辑器。观看视频请使用 preview,自行检查请使用 CLI。
4. 编写视频
impl Video for MyVideo<'_> {
const FPS: usize = 30; const WIDTH: usize = 1920; const HEIGHT: usize = 1080;
fn duration(&self) -> Duration<'_> { Duration::Auto } // sum of the scenes
fn audio(&self) -> AudioMap<'_> { AudioMap::none() } // see references/audio.md
fn define_scenes(&self) -> Scenes<'_> { Scenes::from(vec![&self.intro as &dyn Scene, &self.main]) }
fn render_frame<'a>(&'a self, frame: Frame, ctx: &FFramesContext<'a, '_>) -> Svgr<'a> {
fframes::svgr!(<svg xmlns="http://www.w3.org/2000/svg" width={Self::WIDTH} height={Self::HEIGHT}>
<rect width={Self::WIDTH} height={Self::HEIGHT} fill={BACKGROUND} />
{ctx.render_scenes(&frame)}
</svg>)
}
}
- 每个想法对应一个场景,每个场景 2-6 秒。在场景内,
frame.seconds()从场景起点开始计时。 - 对于固定值的运动,优先直接在
svgr!属性中使用frame.animate(fframes::timeline!(...))。svgr!会将这些动画提升为缓存的静态值。在时间轴中内联定义缓动效果,包括Easing::Spring { mass: 1.0, stiffness: 180.0, damping: 20.0 };命名缓动常量是可选的。弹簧效果无需指定结束时间,它会自行稳定下来。 - 对于动态值或不符合上述优化模式的调用(包括
animate_loop),请在self上仅构造一次时间轴,并通过引用对其采样。对于构造后起点会变化或使用自定义/子帧时钟的情况,请使用缓存的AnimationRuntime值。摄像机参数和着色器 uniform 也可以使用这些 API;不要重复实现弹簧或补间插值。请参阅references/design.md和references/api.md。 - 不含
{}的标记会跨帧缓存。装饰元素应保持为字面值,并将动画值放在外层<g transform={..} opacity={..}>中。 render_frame会在多个线程上为每一帧运行:其中不得出现 panic、文件读取或繁重操作。请在构造函数或OnceLock中准备数据。媒体查找会返回Option;请使用Svgr::empty()作为回退,而不要使用expect。- 将所有字体文件放在
media/中,并通过字体系列名称引用,同时指定数字font-weight。 - 请测量文本,不要猜测:段落可使用
frame.text_width、frame.text_fit(.., TextOverflow::Ellipsis)和frame.text_break_lines。inspect能检测文本是否超出画布,但无法检测文本是否超出自身文本框,因此请检查framePNG 中的文本框。 - GPU 着色器(SkSL 或 Shadertoy GLSL)通过 Skia 后端使用
fframes::Shader运行;它们可以接收图像和同步的视频帧作为输入(用于绿幕抠像、调色)。请参阅references/api.md。 - 3D 镜头(产品、设备、材质中的徽标、镜头穿过场景)由
fframes::three_d(特性3d)提供;它采用 three.js 风格的场景图,并渲染到<image>中,SVG 内容可位于图像下方或上方。场景只需构建一次,每帧调整其姿态。请参阅references/3d.md。
5. 让画面更出彩
references/design.md 的简要版本:
- 每个场景只表达一个想法,使用大字号,边距占宽度的 8-10%,使用两三种颜色,搭配中性色,并用一种强调色突出重点。
- 元素以弹簧或缓出效果进入,耗时 300-600 ms;退出速度更快,以缓入效果过渡,耗时 200-300 ms。相关项目依次错开 60-120 ms。动画停止后,留出 1-2 s 供观众阅读,并且不要让所有元素同时移动。
- 场景之间使用交叉淡化(
fn overlap(&self) -> Overlap { Overlap::Previous(0.4) };重叠时长会加到场景的duration()上,参见references/api.md),或让一个元素延续到下一个场景。静止期间也保留少量动效,避免画面看起来像静止不动。 - 在 1920x1080 分辨率下:标题为 96-140 px,正文为 44-60 px,每行最多约 8 个字,每张卡片最多 3 行。竖屏 1080x1920 视频会在手机上观看:字号保持相同或更大,内容放在中间 80% 区域内,因为平台 UI 会遮挡顶部和底部。
- 使用条带图,按照
references/design.md中的清单检查每个场景。
6. 音频
将音频文件放在 media/ 中(编译时会一并包含;除非使用 include_media_dir!(.., "media", stereo),否则为单声道),或者在运行时使用 MediaDirectory 加载文件夹,然后放置音频:
AudioMap::from([
AudioTrack::new("music.mp3", Second(0.)..Eof).gain_db(-18.).fade_in(1.).fade_out(2.).duck_under_voice(),
AudioTrack::new("vo.wav", Second(0.6)..Eof).voice(),
AudioTrack::new("whoosh.wav", Second(3.1)..Eof).gain_db(-8.),
])
使用驱动动画的同一组常量来安排音效时间。使用 $R audio analyze --waveform w.png 检查音量(网页视频综合响度约为 -14 LUFS,真峰值低于 -1 dBTP,且不能出现意外静音),并使用 $R audio at 3.1s 查看某个事件播放的内容。然后通过 $R preview 让用户试听。
7. 收尾
$R inspect --fail-on warning检查通过,或已了解剩余警告对应的是合理的入场效果。- 每个场景的条带图看起来都正确,并且关键帧已按原尺寸检查。
- 本文件顶部的“不要粗制滥造”清单中,没有任何问题出现在条带图里。
$R audio analyze显示的音量合适。- 对于等待审核的 GPU 作业,展示预览命令,并提出可以在相关时间点运行预览。用户要求最终导出时,运行
$R render -o out.mp4,然后使用ffprobe -v error -show_entries stream=codec_type,width,height,nb_frames,duration out.mp4确认文件大小、帧数和音频。
审核预览时,提供具体的预览命令、起始时间,并提出可以启动预览。最终导出后,提供输出路径和时长。
故障排除
- 在
ffmpeg-sys-fframes中构建失败:缺少第 1 步中的系统库(nasm、pkg-config、编解码器的-dev软件包)。 - 在 Skia 绑定(
skia-bindings)中构建失败,并出现 bindgen 或 libclang 错误(仅当没有匹配的预编译版本时,才会从源码编译 Skia,例如同时使用metal和vulkan):将LIBCLANG_PATH指向可用的 libclang(在 macOS 上可使用 Xcode 中的路径:export LIBCLANG_PATH=$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib)。 - 在 macOS 27 / Xcode 27 上编译 Skia 绑定失败,并在
std___hash_table___node_allocator中出现cannot find type `_Traits`:仅在从源码编译 Skia 时发生(预编译二进制文件已附带其绑定)。skia-bindings0.153.3 缺少相应的 bindgen 规则(rust-skia #1335);使用一种 GPU 后端,使预编译下载包能够匹配,或者通过[patch.crates-io]修补skia-bindings-0.153.3的副本(在build_support/skia_bindgen.rs的OPAQUE_TYPES中"std::__tree.*",后添加"std::__hash_table.*",)。 - 在 macOS 27 上,尽管文件存在,仍出现
can't find crate for `fframes_media_dir_macro`:该过程宏由较旧版本的 Rust 链接,其输出会被 macOS 27 加载器拒绝(报错为 “LINKEDIT string pool”)。更新 Rust(运行rustup update;1.98 可正常工作)并重新构建。 - 文本使用了错误的字体,或者完全没有显示:
inspect显示 “No match for ... font-family”;将字体文件添加到media/,并使用其准确的字体族名称。