OpenMontage 这个名字是我去年十一月份随手起的,当时本来只是想在本地跑几百张旅行照片做一条转场混剪,结果被商业剪辑软件的脚本接口折腾了一整晚,又被自动剪辑工具那种全自动套版气得够呛。最后索性自己写了一个很小的、只做一件事的工具:把素材、时间轴和转场交给一套可读的配置文件,让批量合成蒙太奇视频变成一条可以复跑、可以参数化、可以塞进自动化流程的命令。这工具就叫 OpenMontage,开源在我的代码仓库里,核心代码两千多行,底层走 ffmpeg,在 Mac 和 Linux 上稳定跑通。
如果看完这段描述你还是没有画面,我换个说法:它可以把你硬盘里一堆顺序错乱的 JPEG、MOV、MP4,按一份 JSON 描述的顺序、时长、转场、字幕和背景声组织起来,渲染成一条完整成片。它会自动处理画幅统一、色彩格式转换、关键帧对齐这类脏活,也会在你改了某一段文字、某一张图之后,只重渲染受影响的部分而不会全部重来。适合谁来用?跟我类似的独立创作者、小团队里管视频产出的运营角色、或者想给自家剪辑流程做自动化的开发人员。你不需要很懂视频编码,但最好有一点命令行基础和一点点 JSON/脚本阅读能力。这篇文章不会写成标准教程文档,所有内容都是我在这几个月里换来的一手经验。
1. 先聊清楚:OpenMontage 到底在解决什么问题
1.1 从一次真实的“素材灾难”说起
事发出差回来。相机里躺着四百多张 RAW/JPG,手机里有十几段竖屏和横屏混着拍的视频,还有一首已经剪好前奏、打算做主线的 BGM。我的需求其实很简单:把这些素材按“清晨→公路→海边→日落”四个段落串成一条 32 秒左右的片子,每段匹配 BGM 的节拍,配上简短字幕,最后分别导出 16:9 的横屏版和 9:16 的竖屏版。
放到剪辑软件里,这件事就是机械劳动:把照片拖上时间轴、统一缩放、每张设 0.8 秒、加转场、再手动对鼓点。四百多张图,就算闭着眼睛拖也得拖两个小时。我一开始想走捷径,用剪辑软件的脚本端口自动导入,结果发现官方脚本接口对“批量设置每张图片的时长和裁切比例”这类操作支持非常蹩脚,要么得写一串恶心的 UI 操作模拟,要么干脆不支持从外部批量导入后再改片段时间线。
然后我又试了市面上的自动剪辑工具,导入素材库之后它倒是能出一版片子,但转场风格、卡点位置、字幕样式全是它自己的预设,改一处动全身。我想做的是“把素材丢进去,按我的规则拼出来”,不是“让 AI 替我决定怎么拼”。来回折腾之后,我决定自己做一个东西。
1.2 为什么现成方案都不顺手
我后来把这个问题抽象成了四个需求,才明白为什么现成工具不行。
第一,可复现。同一条片子,我改了一行字幕、换了一张照片,就应该能重新产生一条新旧一致的完整视频,而不是每次手动重拖时间轴。第二,可参数化。画幅比例、卡片风格、转场长度、素材时长,这些都应该能用变量控制,方便我同时产出横屏版、竖屏版和方屏版。第三,差异渲染。我只改动了一个片段,就别把整条视频重新渲染一遍,这样才能在迭代时节省时间。第四,可控的底层。视频编码、封装格式、色彩空间这些复杂东西,我不打算自己重新发明,也不想用黑盒封装,最合理的选择是直接建立在 ffmpeg 之上。
用下面这张表可以更直观地看到我当时对比的几个方案:
| 方案 | 可编程性 | 批量处理 | 细节可控程度 | 学习/维护成本 | 是否能差异渲染 |
|---|---|---|---|---|---|
| 商业剪辑软件 | 弱,脚本接口限制多 | 一般 | 高 | 高 | 否,通常全片重导 |
| 自动剪辑工具 | 无 | 高 | 低,预设为主 | 低 | 否 |
| 直接调 ffmpeg 命令 | 高 | 高 | 高 | 中高,滤镜链难维护 | 需自己实现 |
| OpenMontage 思路 | 高 | 高 | 高 | 中,配置文件清晰 | 是,片段级缓存 |
商业剪辑软件在“单条片子手工精剪”上依然是王者,这一点我完全承认。但这四个需求拼在一起,指向的是另一个方向:把剪辑当作装配逻辑来写,而不是当作手工活来拖。
1.3 需求收敛成四条设计原则
所以 OpenMontage 的定位从第一天就锁死了。它不是“又一个剪辑软件”,而是一个可编程的批量蒙太奇合成器。
第一条原则,一切皆配置:时间轴、素材路径、时长、转场、字幕、音频全部写在一份 JSON 里,这份 JSON 可以被脚本生成、被版本管理、被同事 review。第二条原则,默认值要聪明,覆盖要容易:比如照片默认显示 0.8 秒、默认缩放方式为 cover,但每一段都可以单独覆盖。第三条原则,渲染结果可增量:每个片段的产物基于素材指纹缓存,重复跑不浪费时间。第四条原则,把 ffmpeg 当引擎,不当黑盒:OpenMontage 负责拆解时间轴、生成滤镜图、调度任务,真正处理像素和音频的是 ffmpeg,但我保留所有中间命令的可视化输出,出问题能看到底层的真实命令。
这四条原则后来几乎没改过。OpenMontage 能在三个月里变得真正好用,靠的就是一开始把范围卡得足够窄。
2. 核心抽象:把“蒙太奇感觉”翻译成数据结构
2.1 一分钟看懂 OpenMontage 的配置
OpenMontage 的核心不是代码,是一份 JSON。你要什么片子,直接看配置就能明白大半。举个例子,一段 24 秒的测试片段:
{ "project": "road_trip_demo", "fps": 25, "width": 1920, "height": 1080, "audio": { "file": "assets/bgm.mp3", "start_offset": 1.2, "volume": 0.9 }, "tracks": [ { "id": "v0", "type": "video", "clips": [ { "type": "photo", "file": "photos/001.jpg", "start": 0, "duration": 2.0, "transition": "crossfade", "transition_duration": 0.4 }, { "type": "photo", "file": "photos/002.jpg", "start": 1.8, "duration": 2.0, "transition": "dip_to_black", "transition_duration": 0.3 }, { "type": "video", "file": "clips/coast.mp4", "start": 3.8, "duration": 4.0, "transition": "crossfade", "transition_duration": 0.5 }, { "type": "photo", "file": "photos/003.jpg", "start": 7.6, "duration": 2.0 } ] }, { "id": "title", "type": "text", "clips": [ { "text": "清晨", "start": 0.2, "duration": 1.6, "style": {"fontsize": 72, "fontcolor": "white", "x": "main_w/2", "y": "main_h*0.85"} }, { "text": "沿海公路", "start": 3.9, "duration": 3.5, "style": {"fontsize": 72, "fontcolor": "white", "x": "main_w/2", "y": "main_h*0.85"} } ] } ] }上层的tracks定义了若干条轨道,每条轨道里是一串clips。每个 clip 的根本字段只有四个:file(用什么素材)、start(什么时候出现)、duration(出现多久)、transition(怎么入场和退场)。视频剪辑中最常见的“拼接”和“叠化”,在这儿被简化成了时间轴上相邻片段的重叠区间。两条视频轨之间的叠加,可以通过多写一条video轨道并设置blend_mode来实现,比如让下一条轨道做画中画,或者做老电视那种扫描线效果。
audio块控制全局背景声。这里我用了一个很容易被忽略的参数start_offset,默认是从 BGM 开头对齐,但很多时候前奏太闷,我会直接设置偏移,让第一个鼓点刚好落在第一张照片切换的时间点上。这个字段在手工剪辑软件里根本不叫这个名字,但在配置驱动的方式里,它就是一行数字。
2.2 轨道模型:拼贴的常见层次
刚开始设计时我只想支持“一条视频轨 + 一条字幕轨”,后来跑了一个月的实际案例,才把轨道模型稳定成三种类型:video、text、audio。video轨可能有多条,支持上下叠加,底层用 ffmpeg 的 overlay 或者 blend 滤镜实现。text轨不直接读图片,而是在渲染时动态生成一个字幕层。audio轨负责 BGM、环境声、甚至一段配音。
这里的核心决策是:轨道是独立的,但片段不强制对齐轨道边界。也就是说,一个视频片段可以横跨两条轨道,字幕的进点出点可以和画面切换错开。这在蒙太奇里非常常用,因为“字幕比画面晚半拍出现”本身就是一个情绪处理手法。如果轨道模型设计得过于严格,这种错位就会变成噩梦。
顺带说一句,我见过一些类似项目把时间轴抽象成表格或者直接用 Python 函数描述,一旦用例复杂起来就难以维护。OpenMontage 选择 JSON,不是因为它最优雅,而是因为它可序列化、可 diff、可由其他程序生成。我甚至写过一个很短的 Python 脚本,读取飞书表格里的节目单,自动生成成片配置,这就完全绕开了手工编辑 JSON 的过程。
2.3 为什么不直接用 Python 写死剪辑逻辑
这是我被问过最多的问题。有人觉得,反正是开源项目,直接用 Python 脚本描述剪辑过程,调用 ffmpeg 命令,不更自由吗?
我的回答是:写死逻辑一时爽,改需求的时候就会哭。配置驱动和编程驱动的区别在于,配置是数据的形状,编程是流程的形状。当需要做三个不同风格的项目时,用配置驱动只需要写三个 JSON 文件,或者一个 JSON 模板加参数化;用编程驱动则需要改代码、提 PR、重新部署。而且配置可以交给一个完全不会 Python 的编导同事去维护,脚本不能。
当然,纯 JSON 也有缺点。条件分支和循环逻辑在配置里很难表达,要做到“如果某天素材量超过 30 张,就自动把每张照片时长缩减到 0.6 秒”,配置本身做不到。所以我在配置里留了一个extends字段,允许一个配置继承另一个配置,再叠加一个非常薄的 Jinja2 模板层。我的实际用法是:项目共享的模板放在templates/,具体的片子配置放在projects/,用 Jinja2 的{% if %}控制分支。这个组合干净利落,又不会把整个项目拖入“一切皆代码”的泥潭。
3. 渲染管线:从一份 JSON 到一条成片之间发生了什么
配置只是一张图纸。真正让 OpenMontage 值钱的,是它把图纸展开成渲染任务的那套管线。我把整个渲染过程分成三段:解析展开 → 并行渲染 → 编码封装。每段之间有明确的产物,方便调试和断点重跑。
3.1 Stage 1:把抽象时间轴展开成具体帧指令
第一段输入是 JSON,输出是“帧级任务清单”。每个片段都会被展开成一组离散任务,任务内容包含输入文件、需要应用的滤镜链逻辑、输出位置和渲染参数。举个最简单的例子,一张照片在时间轴上占 2 秒、每秒 25 帧,那这个 clip 就要生成约 50 个渲染任务,其中每个任务输出一帧 PNG。视频片段的展开方式略有不同,不需要真正渲染每一帧,而是从源视频中提取指定时间区间并做缩放裁切。
这里我最初犯过一个错误:把所有片段都当成“逐帧渲染”来做,导致一段 10 秒视频被拆成了 250 帧 PNG,再交给编码器,处理时间被白白拉长了好几倍。后来我把展开逻辑分成两类:静态素材展开成帧,例如图片和字幕;动态素材展开成片段,例如视频和带转场的片段。前者适合逐帧 caches,后者适合直接切一个局部片段再合成。这个改动让常规视频素材的处理速度提升了大约 8 倍。
展开阶段还会生成一张“画面描述表”,记录每一帧在最终成片时间轴的哪一秒出现、来源是什么、是否属于转场区间。这张表是后面差异渲染的基础,也方便我手动检查某帧是不是想的那样。
3.2 Stage 2:并行渲染与差异缓存
任务清单出来之后,OpenMontage 会丢给一个并发 worker 池。默认 worker 数量是CPU 核心数 - 1,因为我总得留一个核干别的。每个 worker 从队列里取任务,调用 ffmpeg 渲染那一帧或那一段,然后写入本地渲染缓存目录。
缓存策略是整个管线的灵魂。每个渲染任务会计算一个指纹,指纹生成逻辑是:素材文件的 SHA-256 前 16 位 + 素材修改时间 + 时间戳 + 该片段所有渲染参数的序列化哈希 + OpenMontage 版本号。只要指纹一致,就直接从缓存里取结果,否则重渲染并覆盖缓存。这样我修改任意一个片段参数,只影响与该参数相关的任务,其余片段统统走缓存,速度快到几乎察觉不到。
import hashlib, json, os def task_fingerprint(src_path, mtime, clip_options, mount_version): payload = f"{src_path}:{mtime}:{json.dumps(clip_options, sort_keys=True)}:{mount_version}" return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16] def cached_result(fp, cache_dir): target = os.path.join(cache_dir, f"{fp}.png") return target if os.path.exists(target) else None有一个细节值得单独说:缓存目录的命名不要用素材名,而是用指纹。因为素材名会重名、会乱改,但指纹稳定,而且天然避免并发冲突。真正跑起来之后,配合--cache-clear清理指令和--dry-run预测指令,我几乎没有再为缓存操过心。
3.3 Stage 3:无损中间流 + 最终编码
渲染出来的中间产物是无损的,默认是 PNG 帧序列。到这一步我会把它们喂给一个 ffmpeg 进程做最终封装。选用 PNG 而不是原始 YUV 是因为它的兼容性好、可排查问题,而且现代编码器对帧序列的读取速度已经足够快。最终编码命令类似:
ffmpeg -r 25 -start_number 0 -i cache/frame_%05d.png \ -i assets/bgm.mp3 \ -c:v libx264 -preset medium -crf 18 -pix_fmt yuv420p \ -c:a aac -b:a 192k -shortest -movflags +faststart \ out/road_trip_demo.mp4这里-pix_fmt yuv420p是最容易踩坑的一点,后面我会专门说。我实测过一组数据供参考,配置是 900 张图、32 秒 25fps、1080p:
| 阶段 | 全量渲染耗时 | 增量渲染(只改 3 张图 + 1 行字幕)耗时 |
|---|---|---|
| 素材解析展开 | 1.8s | 1.8s |
| 帧渲染 | 245s | 3.2s |
| 最终编码 | 12s | 12s |
| 总计 | 约 259s | 约 17s |
对于频繁改字、换图的工作流,差异渲染省下的时间是极度明显的。
4. 跑一个实际例子:三十秒混剪从准备到出片
我一直觉得,工具类项目最怕被写成“空中楼阁”,只讲架构不讲操作。所以这一节我用最近一个真实项目带你走一遍完整流程。目标:把 40 张自驾游照片、4 段行车视频和一首 BGM 合成一条 32 秒、1080p、25fps 的混剪,带 5 组字幕,最后导出横屏和竖屏两个版本。
4.1 素材归一化:先统一画幅和旋转
原始素材千奇百怪:手机竖屏 1080x1920、相机横屏 6000x4000、单车视频 3840x2160。OpenMontage 的第一步是扫描素材库,生成“资产清单”。如果照片带 EXIF 旋转信息但没有实际转正,就自动调用ffmpeg -autorotate做方向修正;如果视频编码不是常见的 H.264,就先用-c:v libx264统一重封装一次,避免后面滤镜链出兼容问题。这一步看起来费时,实际上躲掉了一半的后期 bug。归一化后的素材统一放到assets/normalized/目录。
4.2 用配置描述结构与节奏
这一步我先把 BGM 导入,用音频分析工具手动看了一下节拍位置,心里有数之后填配置。由于时间轴不长,我直接用最朴素的写法:
{ "project": "road_trip_32s", "fps": 25, "width": 1920, "height": 1080, "audio": { "file": "assets/bgm.mp3", "start_offset": 0.0, "volume": 1.0 }, "tracks": [ { "id": "main", "type": "video", "clips": [ { "type": "photo", "file": "normalized/001.jpg", "start": 0, "duration": 1.2 }, { "type": "photo", "file": "normalized/002.jpg", "start": 1.0, "duration": 1.4, "transition": "crossfade", "transition_duration": 0.4 }, { "type": "video", "file": "normalized/road1.mp4", "start": 2.2, "duration": 3.0, "transition": "dip_to_black", "transition_duration": 0.3 }, { "type": "photo", "file": "normalized/003.jpg", "start": 5.0, "duration": 1.2 } ] }, { "id": "captions", "type": "text", "clips": [ { "text": "出发", "start": 0.2, "duration": 0.8 }, { "text": "沿海公路", "start": 2.4, "duration": 2.4 } ] } ] }这里我想强调一个节奏技巧:照片的duration我说 1.2 秒不代表真的放满 1.2 秒,因为转场会吃掉一部分重叠区间。如果你希望某张图“实际可见”1 秒,那duration就要设成 1 秒加半个transition_duration。这个细节配置手册不写,但时间轴一旦长了非常影响观感。初次使用我建议先把所有transition_duration设成 0,跑一版无转场的出来看节奏,再加转场微调。
4.3 一行命令出片
素材归一化完成、配置写好后,出片命令非常简单:
openmontage render projects/road_trip_32s.json --out out/road_trip_32s.mp4OpenMontage 会先输出一份“渲染计划”:共 40 张照片、4 段视频、6 次转场、16 个字幕帧,预计耗时 80 秒。执行完毕后out/目录下会生成成片和一个同名的.log文本,里面记录着每一个调用的完整 ffmpeg 命令行。如果我想再出一个竖屏版,不需要新建配置,直接在命令行追加参数覆盖画幅即可:
openmontage render projects/road_trip_32s.json --override width=1080 --override height=1920 --out out/road_trip_32s_vertical.mp4这个覆盖机制非常重要,因为竖屏版不只是画幅变了,字幕的纵坐标、照片的裁切中心也需要跟着变。OpenMontage 的做法是在配置里允许设置响应式布局参数,比如字幕的y字段写"main_h*0.85",它会在渲染时根据最终画幅动态计算。我第一次做竖屏导出时,90% 的时间都花在手工调整这些坐标上,后来把坐标全部改成百分比表达式,才彻底解放。
4.4 出片后你还要检查什么
跑完命令不是结束。我的习惯是把成片拖到快速预览器里,先关掉声音看 1x 速度,重点检查三处:转场是否太突兀、字幕是否有溢出、照片裁切是否裁掉了关键主体;然后开声音再对一遍:卡点位置和 BGM 重拍是否对齐。如果发现某个片段不合适,我会直接在 JSON 里改对应 clip 的字段,然后重新跑render。因为差异缓存,整个过程通常只要 10 秒出头。
5. 踩坑记录:这些坑配置文档里不会教你
工具跑顺之后,给我留下最多印象的反而不是功能,而是那些排查过程。这里挑几个典型的坑,说清楚“问题的本质原因是什么、我为什么花了很久、最后怎么解决”。
5.1 缓存文件莫名损坏:并发写同一路径
最早的版本在并发渲染时经常出现成品花屏,一开始我以为是 ffmpeg 某条命令写坏了,单独跑又永远复现不了。直到有一天我开了一大堆 worker,边渲染边用find cache -size 0检查,才发现有零字节 PNG 落在缓存目录里。根因非常简单:两个 worker 同时处理了同一个素材的同一个渲染任务,指纹相同,都判定缓存未命中,然后同时写同一个路径,互相覆盖。解决办法是给缓存写入加一个“原子写”机制:先写到临时文件,写完再重命名到目标路径;同时在任务分发层做一个 in-memory 的去重集合,同指纹任务只允许入队一次。
import os, tempfile def atomic_write(fp, target_path, data): fd, tmp_path = tempfile.mkstemp(dir=target_path, suffix=".tmp") try: with os.fdopen(fd, "wb") as f: f.write(data) os.replace(tmp_path, target_path) except Exception: if os.path.exists(tmp_path): os.remove(tmp_path) raise这个问题给所有并行渲染类工具一个通用教训:缓存写操作必须是原子的,检查与写入之间必须考虑并发。
5.2 照片方向不对:EXIF 旋转和实际像素方向
第二批照片里大概有三分之一在预览时是歪的。照片在电脑里看是正的,因为看图软件会自动读 EXIF Orientation;但 ffmpeg 读取时如果没启用 autorotate,得到的就是“存下了旋转标记但像素本身还是躺着的”状态。排查链路是这样的:先怀疑 OpenMontage 裁切逻辑,后用 ffmpeg 单测某张图,发现直接输出就是歪的;再跑ffprobe看 metadata,确认Orientation: 6,问题定位到归一化阶段。解法是在素材归一化阶段对所有图片执行-autorotate,并把旋转标记从输出文件中清除,保证后续所有环节拿到的是物理上横平竖直的像素。
这也让我意识到,素材归一化不是一个可选的预处理,而是整个管线稳定性的地基。视频音频的格式统一、图片方向统一,都应该在做任何渲染前完成,否则问题会散布在各个片段里,极难集中排查。
5.3 编码器突然报错 width not divisible by 2
这坑藏得最深。有一次我把画幅从 1920x1080 改成 1280x720 后一切正常,改成 1350x1080 就开始报 H.264 编码错误。ffmpeg 输出第一行是width not divisible by 2,但 1350 明明是偶数,我当时整个人是懵的。后来打开完整日志才发现,实际喂给编码器的分辨率不是 1350x1080,而是 1350x1080 往下的某个奇数尺寸,因为有张照片的长宽比经过 cover 裁切后是 1200x675,在叠转场时被 scale 成了 1350x759,759 是奇数,H.264 直接拒收。
这类问题对高分辨率素材特别隐蔽,因为大部分 2K、4K 素材的宽高都是偶数,但裁切中心偏移后很容易出现奇偶性问题。解决办法是在所有 scale 滤镜后面手动加一个兜底表达式:
scale='trunc(iw/2)*2':'trunc(ih/2)*2'并且我在 OpenMontage 的最终编码阶段强制校验一次分辨率宽高奇偶性,如果不满足就直接报错提示,而不是让 ffmpeg 抛一个让人摸不着头脑的底层异常。
5.4 音频卡点不对:不是延迟,而是静音填充方式错了
最玄学的一集。BGM 卡点总是慢半拍,一开始以为是start_offset算错了,反复调偏移都没用。后来我把最终成片的音频波形单独导出来和 BGM 原文件对比,发现前半段波形对不上,但后半段又对得上。再进一步看,问题出在视频轨与音频轨的总长度上:视频轨因为转场重叠,实际渲染出的总长度比配置里各片段duration之和少了将近 0.5 秒,而我的音频在封装时用了-shortest,导致音频末尾被截掉,ffmpeg 又为了保持音视频同步,强迫音频从前面开始多做了 0.5 秒静音填充。
解法很直接:在渲染完成后先获取视频轨真实时长,再据此调整音频输入,在音频开头按差值做精确的adelay,而不是依赖 ffmpeg 默认的负片延迟对齐。这个坑告诉我:当你依赖 ffmpeg 做“自动对齐”时,一定要搞清它对齐的是什么时间基准。音视频时长不一致时,-shortest很容易掩盖真实的时序问题。
5.5 并发越多越快?并不一定
最后是一个性能方面的认知修正。我最初天真地认为 worker 数量等于 CPU 核心数最快,结果跑 32 线程时总耗时反而比 8 线程更慢。原因是大量 PNG 帧同时写入磁盘,I/O 带宽和内存缓冲都成了瓶颈,CPU 反而大部分时间在等待。最终我采用的策略是:图片密集项目 worker = 核心数一半,视频密集项目 worker = 核心数 - 1,并为每个 worker 限制内存占用上限。这个看起来不像是“核心功能”的细节,在实际项目中节省的时间可能是最多的。
6. 现在的 OpenMontage:哪些能力我用上了,哪些还在路上
三个月迭代下来,OpenMontage 目前已经是我日常做混剪的主力工具。说得更准确一点,只要是符合“批量素材 + 固定时间轴模板 + 可参数化输出”这个模式的内容,我都会优先拿它来做。比如我每周会把手机里的新照片自动同步到素材库,然后跑一条一周汇总的 15 秒混剪;比如做一个系列固定片头,片头配置复用同一个模板,只改日期和标题文字,渲染一条新视频只需要几秒。
它也有明显的边界。实时预览几乎为零,我调转场效果主要靠快速渲染小尺寸草稿,而不是像传统剪辑软件那样拖动播放头看效果。复杂的转场、关键帧动画、文字动态效果也基本不碰,这类需求我会老老实实回到剪辑软件里手工处理。老实讲,OpenMontage 真正擅长的是“大批量、轻结构”的蒙太奇生成,深度精剪不是它的使命。
如果你也想造一个类似的轮子,我只有一条最诚恳的建议:不要从解码器做起,也不要用 OpenCV 自己拼帧。把渲染脏活交给 ffmpeg,把设计重心放在时间轴抽象、缓存策略和配置格式上。视频编码的世界已经足够复杂,不值得重复造一遍。
我个人现在最常用的方式是:给 OpenMontage 写了一个极薄的 Python 封装,读取我表格里的节目单,自动生成配置,渲染完自动把成片上传到指定目录。整个流程跑起来之后,我一个月省下的时间足够我再研究两个新的转场手法或者拍一段更好的素材。工具不一定要多惊艳,能被顺手地嵌入自己每天的工作流,就已经值回票价了。