【手把手】CodeX + Remotion + Samantha 制作双语教学视频

看到参考视频的时候,我第一反应就是:这种视频我们应该可以仿出来吧。 故事不重要,就拿我们现在随便一个,主要看效果。 图有了,故事有了,语音我们也能合成,我当时的感觉就是……万事俱备了,先做一个出来再说。
故事图是怎么做的
故事很简单:孩子小时候总张手要抱,妈妈一边抱一边笑他重。后来有一天过马路,孩子第一次说“我自己走”。妈妈的手停在半空,忽然想不起上一次抱他是什么时候。当天晚上,她又把睡着的孩子抱起来一次。
我们把它拆成了 6 个画面:
  1. 孩子张开手跑向妈妈。
  2. 妈妈把他抱起来,嘴上说“都多重了”。
  3. 过马路时,孩子第一次说“我自己走”。
  4. 妈妈的手悬在半空,孩子的背影越来越远。
  5. 晚上,妈妈又抱起睡着的孩子。
  6. 一大一小两个背影走远,用来收住“最后一次从不提前通知”这句话。
6 张图各有用处。前两张先让人相信“他以前总要抱”,第三张把日常打断,第四张让妈妈反应过来,第五张给出她的动作,最后一张才有资格收尾。少掉中间任何一张,情绪都会变成硬煽。
接着固定人物。我们先准备了一张妈妈参考图和一张孩子参考图:妈妈是黑色齐肩外翘短发、豆沙粉紫毛衣;孩子五岁,黑色乱发、圆脸、缺一颗门牙,穿芥末黄 T 恤。6 张图都把这两张人物参考图一起交给图片生成工具,衣服全程不换。
场景也别只靠文字猜。客厅、街口和夜间卧室,各准备一张环境参考图。做客厅画面时就带客厅参考,做斑马线时就带街口参考。背景不用抄得一模一样,但同一个家、同一条街别在不同画面里突然换装修。
图片用的是 Codex 内置的图片生成功能,一场单独做一张,3:4 竖图,不在图里生成文字。共同提示词大致是这样:
同一位妈妈:黑色齐肩外翘短发,鹅蛋脸尖下巴,细长脖子,
豆沙粉紫圆领毛衣。

同一个五岁男孩:头大身小,黑色乱发,圆脸短脖,
缺一颗门牙,芥末黄 T 恤。全程不换衣服。

画风:1950 年代美式中古动画概念稿,传统水粉和彩铅,
画在白纸上,大面积留白,环境像舞台布景,不像照片。

构图:3:4 竖版,顶部至少 35% 留白,四个角保持白色,
不要日系动漫脸、3D、矢量插画、满构图、文字、水印、
额外人物和服装变化。
然后每张图只补这场真正需要的动作和环境。比如第 4 张:
同一街口。妈妈豆沙粉紫袖口和悬空的手占前景,
手指微微收拢;远处是儿子穿芥末黄 T 恤走远的小背影。
画面保留大片空白,色调比上一幕稍冷,情绪克制。
提示词不用写得多文学,先把会变化的东西固定好:同一个人、同一件衣服、同一种画风、同一类留白。一个场景只做一张图,也别拿一张图硬顶两个情节。
从零照做,最少要拿到 6 张合格插画。实际会生成多少次,取决于人物有没有变脸、手有没有坏、构图能不能留出文字位置。当时的制作文件只留了最终采用的 6 张图,没有完整保存每次重试和扣费,所以我不编一个“总共花了多少钱”。
6 张图做好以后,原版短片的做法也很简单:每张图 4.25 秒,先在白纸上逐字打出中文,再让同一张图的灰度版从左向右出现,最后让彩图从左向右覆盖上来。场景之间直接切换,不让人物动。6 张图合起来是 25.5 秒,配背景音乐和打字声,没有旁白。
到这里,原来的静态故事短片才算做完。
【手把手】CodeX + Remotion + Samantha 制作双语教学视频
视频的排法
先分析下视频: 每场版式基本不变:顶部一句话,中间一张图,底下中英文;声音只念英文。图片先从空白变成灰度,再从左往右扫成彩色。
看起来像图在现场生成,其实不用视频模型。同一张图复制两层,下面一层去掉颜色,上面一层保留彩图,再给两层各做一次从左到右的遮罩,也就是逐渐放开可见区域。 我用到的工具也解释一下:
  • Chrome 浏览器用来查看 X 原帖和参考视频。
  • macOS 自带的 say 把英文文字读成语音。
  • Remotion 是一个用网页代码(React)排版和生成视频的工具,负责图片、文字、遮罩和时间轴。
  • FFmpeg 是处理视频和音频的命令行工具,负责音频格式转换和最后混入背景音乐;它自带的 ffprobe 用来查分辨率、帧率、时长和音轨。
  • MLX Whisper 是本地语音识别模型。成片做好后,让它重新把英文听写出来,检查有没有漏词或截断。
运行脚本的环境是 Node.js 22.22.1,视频部分用了 Remotion 4.0.484 和 FFmpeg 8.1.2。工具名字看着多,其实画面归 Remotion,声音归 FFmpeg,其他工具主要负责检查。
每场的动画参数最后定成:先空 0.2 秒,灰度图用 1.45 秒扫完,彩图再用 1.2 秒扫进去。遮罩边缘留 10% 的柔化,图片从 1 倍缓慢放大到 1.022 倍。幅度很小,但一动不动的话,6 秒钟看着确实像 PPT。
typescript
const sketchProgress = interpolate(t, [0.2, 1.65], [0, 1]);
const colorProgress = interpolate(t, [1.65, 2.85], [0, 1]);

show(grayscale(image), leftToRightMask(sketchProgress));
show(image, leftToRightMask(colorProgress));
灰度层和彩色层一定要用同一张图、同一个裁切位置、同一个缩放值。不要再生成一张“更像线稿的图”放在下面。两张图只要五官差一点,彩图扫进来的时候脸就会跳,那一下特别明显。
我另外做了一份场景清单。每场只记录图片名、中文标题、中文正文、英文口播和图片上下偏移的位置:
json
{
  "image": "03-i-walk.png",
  "title": "后来,他说我自己走",
  "chinese": "那天过马路,他第一次说:「我自己走。」",
  "english": "Then one day, at the crosswalk, he said it for the first time. I can walk by myself.",
  "imagePosition": "center 64%"
}
最后这个位置参数别省。每张图里人物站的位置不同,全部机械地放在正中央,不是裁掉头,就是裁掉手。
英文声音和字幕怎么接上
英文口播先用 macOS 自带的 Samantha 声音,语速是每分钟 165 个英文单词,每场单独生成一段。这个过程叫文字转语音,也常缩写成 TTS。
如果机器配置高,用 CosyVoice 小模型合成语音的效果会更好
bash
say -v Samantha -r 165 -o scene.aiff "English narration here"
ffmpeg -i scene.aiff -ar 48000 -ac 1 scene.wav
别把 6 场一次念成一个大文件。第三场的发音有问题,就只改第三场,前两场不用跟着重做。
语音生成后,再读取每一段的真实时长。每场取“至少 6.4 秒”和“旁白时长再加 0.55 秒”里的较大值。短的至少停 6.4 秒,长的等人声念完再多留半秒。不要按英文有多少个字母猜时长,不准。
英文字幕没有另做逐词时间码。我用“已经播放的时间 ÷ 这段语音的总时长”,计算现在该露出多少个词。每段只有 18—20 个词,拿来做这种故事字幕够用了。如果要做正经的英语教学,要求声音念到哪个词就准确指到哪个词,那还是要给每个单词做时间标记。
中文只显示,不朗读。中英文各念一遍,39.6 秒很快就会拖到一分钟,而且观众会等得很烦。
画面和人声先生成一版,背景音乐最后再混。音乐音量先放到 0.08,人声出现时自动把音乐压低,最后限制最大音量峰值,避免突然爆音。原来的具体参数是 threshold 0.02、ratio 9、attack 18ms、release 300ms,峰值不超过 0.95。它们只适合这条片和这首音乐,换一首歌就得重新听,别当成祖传秘方。
还有一个容易自我感觉良好的地方:Whisper 能完整听写出来,只能说明机器听懂了,不代表声音自然。Samantha 这版拿来验证方法没问题,真发以前还是得用手机外放完整听一遍。累不累、像不像报站,人耳说了算。
总结
第一,人物参考图别省。提示词里写一百遍“同一个妈妈”,通常也不如把同一张人物参考图每次都带上。
第二,一场一张图。别拿一张图同时讲“孩子拒绝抱”和“妈妈晚上又抱他”,观众会不知道该看哪句话。
第三,灰度彩图不是真线稿。原图背景太复杂,去掉颜色以后照样很脏。换干净一点的原图,比疯狂拉对比度有用。
第四,不要让视频模型把整条片重做一次。这种形式要的是人物、构图和文字稳定,又没有必须依靠连续动作才能讲清楚的内容。视频模型把脸、手或衣服改掉一个,返工反而更多。
第五,背景音乐最后加。画面和口播没定之前别急着配音乐,不然后面你都不知道是故事慢、声音慢,还是音乐在抢人声。
第六,只认最终 MP4。我把成片重新交给本地语音识别,6 段英文全部对上;又自动检查了一遍画布、帧率、编码和音轨。编辑器里预览正常,不等于导出的文件一定正常。
这次我不准备把它变成一条新流水线。我拿这条成片只验证一件事:手里已经有一组人物稳定、构图干净的故事插画,换一种版式和时间轴,就能做出这种双语手绘效果。
后面真要继续,我会先换一个更自然的英文声音(CosyVoice),发出去看看数据,再决定值不值得多做。
© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
admin的头像-share
评论 共1条

请登录后发表评论

    • 头像A WordPress Commenter0