电影节拍视频风格
核心理念
视觉主导节奏。音频紧随其后。 动画和 ASCII 场景按其自然运行时间播放——它们不会被截断、加速或定格在最后一帧以匹配音频长度。节拍之间的静默是有意设计,而非需要填充的空白。
- TTS: 每个剪辑一句话。最大信噪比。无填充式旁白。
- 视觉: 完整的自然运行时。让动画自由呼吸。
- 剪辑: 仅用硬切。无交叉淡入淡出。
- 音乐: 用户提供的曲目,在语音部分自动进行闪避处理。
- 检查点: 在每个制作阶段之后,将资产交付给用户审阅,然后再继续。
项目结构
~/videos/[project-name]/
project.md # 活文档——故事板、决策、修订记录
beats/ # 单个节拍资产
beat-01/
visual.mp4 # 生成的视觉素材(带运动的图像或动画)
audio.mp3 # 此节拍的 TTS 片段(仅 MP3——参见关键规则)
note.txt # 此节拍所展示内容的一行描述
beat-02/
...
music/ # 用户提供的背景音乐曲目
output/ # 中间和最终渲染输出
阶段 1:节拍脚本
创建 project.md 并包含节拍分解。每个节拍 = 一个视觉素材 + 一个 TTS 句子。
编写节拍
TTS 规则:
- 每个节拍仅一句话。句号。
- 最大信噪比——每个字都必须有其价值。
- 如果一个想法需要两句话,那就分成两个节拍。
- 目标:每个片段 5-15 个词。越短越好。
- 无介绍性填充词("所以...","现在让我们看看...")。直奔主题。
视觉规则:
- 描述视觉素材展示的内容——而不是持续多久。
- 视觉运行时由其自然完成时间决定,而非 TTS 长度。
- 图像剪辑:静态图像配合 Ken Burns 运动效果(缓慢缩放/平移)。运行时:至少 4-6 秒。
- 动画剪辑(p5js/manim/ascii):完整动画播放。需要多久就多久——8s、12s、15s 均可。
- 节拍时长 = max(视觉时长, 音频时长) + 暂停。
project.md 中的节拍模板:
# [项目名称]
## 概念
[一段话——此视频的内容和想要传达的感觉]
## 音乐
- 曲目:[文件名或用户将提供的音乐描述]
- 氛围:[描述]
## 节拍
### 节拍 01
**视觉:** [类型: image | p5js | manim | ascii] — [提示/描述]
**TTS:** "一句有力的台词。"
**状态:** [ ] 视觉 [ ] 音频 [ ] 已批准
### 节拍 02
...
## 备注
[决策、修订、用户反馈的运行日志]
剪辑类型
| 类型 | 工具 | 运行时 | 最佳用途 |
|---|---|---|---|
image | image_generate + Ken Burns | 4-6s | 照片、场景、肖像、定场镜头 |
p5js | p5js 技能 | 自然(8-15s) | 生成艺术、粒子、数据可视化、流场 |
manim | manim-video 技能 | 自然(8-20s) | 数学动画、图表、技术讲解 |
ascii | ascii-video 技能 | 自然(6-12s) | 终端美学、复古计算、黑客主题 |
caption | ffmpeg drawtext | 4-8s | 深色背景上居中的 TTS 文本——聚焦于文字本身 |
关键:在制作任何 p5js/manim/ascii 节拍之前,先用 skill_view(name) 加载技能。 这些技能包含品牌调色板、字体规则和必须精确遵循的实现模式。
阶段 2:脚本批准门
停。 将 project.md 呈现给用户。在获得明确批准之前,不要生成资产。
在 project.md 的备注部分更新批准时间戳。
阶段 3:生成视觉素材
按类型生成所有视觉资产。保存为 beats/beat-XX/visual.mp4。
图像节拍(VHS 滤镜 Ken Burns)
- 通过
image_generate生成图像 - 对图像应用 VHS 滤镜(颗粒、扫描线、暗角、色差):
python3 scripts/vhs-filter.py \
--input beat-XX-image.png \
--output beat-XX-vhs.png \
--seed 42 \
--intensity 1.0
要求: pip install numpy Pillow(numpy 用于数组效果,Pillow 用于图像 I/O)。
强度级别:0.7(轻微——仅颗粒和暗角),1.0(标准——所有效果),1.5(大量——强烈色差,可见扫描线)。默认值为 1.0。
- 对 VHS 滤镜后的图像应用 Ken Burns 运动以创建 MP4:
ffmpeg -loop 1 -i beat-XX-vhs.png -t 5 \
-vf "zoompan=z='min(zoom+0.001,1.3)':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=125:s=1920x1080:fps=25" \
-c:v libx264 -pix_fmt yuv420p -r 25 beats/beat-XX/visual.mp4
为什么先 VHS 后 Ken Burns? zoompan 滤镜会模糊颗粒和扫描线。先在静态图像上应用 VHS,然后添加运动效果。在 VHS 滤镜图像上叠加 Ken Burns 运动,营造出具有年代感的模拟质感——就像通过略微老化的 CRT 观看旧影像。
动画节拍(p5js/manim/ascii)
首先加载相关技能,然后生成。将输出保存为 beats/beat-XX/visual.mp4。
分辨率: 所有视觉素材必须为 1920x1080,25fps。缩放 manim 输出:
ffmpeg -y -i raw-manim.mp4 \
-vf "scale=1920:1080:flags=lanczos,setsar=1:1,fps=25" \
-c:v libx264 -pix_fmt yuv420p -r 25 beats/beat-XX/visual.mp4
以 6 个为一组批量调用 image_generate。 若被标记,请中立地重新措辞并重试一次。
VHS 滤镜参考
VHS 滤镜(scripts/vhs-filter.py)对单张图像应用以下效果:
| 效果 | 作用 | 视觉结果 |
|---|---|---|
| 色差 | R/B 通道偏移(0-2px) | 边缘出现彩色条纹,像未对齐的 CRT |
| 模拟噪点 | 每通道 ±8 颗粒(按强度缩放) | 阴影和中间调具有胶片般的质感 |
| 扫描线 | 每第 4 行变暗至 96% | CRT 磷光体网格叠加 |
| CRT 暗角 | 边缘变暗(基于强度 15-35%) | 圆角屏幕,像老电视 |
对于图像: 所有效果以单一随机种子一次性应用。色差和噪点可见;抖动/跟踪线因只有一帧而较微弱。
注意: 此滤镜仅适用于图像——它不接受视频输入。要获得运动效果,请先对静态帧应用滤镜,然后添加 Ken Burns/zoompan 效果。
阶段 3.5:视觉检查点门
停。将每个视觉片段发送给用户审阅。
包含带有 MEDIA: 路径的截图或 MP4。按编号呈现它们:
节拍 01:[MEDIA 路径] — [简短描述]
节拍 02:[MEDIA 路径] — [简短描述]
...
询问:"在进入音频之前需要任何修改吗?"
在用户批准视觉素材之前,不要生成 TTS。 将反馈记录在 project.md 的备注中。
如果要求修改,仅重新生成受影响的节拍并重新检查。
阶段 4:生成 TTS 音频
生成所有 TTS 片段——每个节拍一个。使用 text_to_speech 工具,并将 output_path 设置为 .mp3。保存为 beats/beat-XX/audio.mp3。
关键:始终对音频使用 MP3 格式。 OGG 与 Telegram 交付和 ffmpeg concat 操作存在编解码器兼容性问题。将 TTS 提供商设置为 OpenAI 并输出 MP3。
规则:
- 每次调用一句话。不要拼接。
- 生成后使用 ffprobe 测量每个片段的时长。
- 在 project.md 中每个节拍旁边记录时长。
ffprobe -v error -show_entries format=duration -of csv=p=0 beats/beat-XX/audio.mp3
阶段 4.5:音频检查点门
停。将音频片段呈现给用户。
应用语音滤镜(必需)
对每个 TTS 片段应用干湿比特压缩链——这是我们的标志性声音。有关完整技术和陷阱,请参见 references/dry-wet-bitcrush.md。
两种方法可用:
A) 静态混音(简单、一致):
# 步骤 1:通过 wav 中间格式创建压缩版本(保持时长)
ffmpeg -y -i beats/beat-XX/audio.mp3 \
-af "aresample=resampler=soxr:osr=2000,aformat=sample_fmts=u8,aresample=resampler=soxr,lowpass=f=2500" \
temp-crushed.wav
# 步骤 2:使用压缩粘合混合干音和湿音
ffmpeg -y -i beats/beat-XX/audio.mp3 -i temp-crushed.wav \
-filter_complex "[0:a]volume=0.7[dry];[1:a]volume=0.3[wet];[dry][wet]amix=inputs=2:duration=first:normalize=0,acompressor=threshold=0.05:ratio=4:attack=10:release=100[out]" \
-map "[out]" beats/beat-XX/audio-filtered.mp3
rm temp-crushed.wav
B) 动态混音(信号退化效果——首选):
使用 Python 脚本(scripts/vibey-mix.py),采用零阶保持上采样,以正确的音高实现真实的比特压缩效果。
要求: pip install numpy pydub(numpy 用于数组处理,pydub 用于 MP3 读写)。
锁定参数(dec5-bit8 变体):
- 抽取: 5 倍(每 5 个样本丢弃一个,然后通过零阶保持重复)
- 量化: 8 位(恒定)
- 干/湿基线: 80% 干音 / 20% 湿音
- 下降模式: 约每 1.2 秒,干音降至约 35%,湿音升至约 65%(持续约 0.25 秒)
- 仅对湿音施颤音: 4Hz 三角波,深度 70%(湿音在 30-100% 之间脉冲)
- 击拍延迟: 80ms 在 -18dB(约 0.12 音量),应用于最终混音
python3 scripts/vibey-mix.py beats/beat-XX/audio.mp3
cp beats/beat-XX/audio-dynamic.mp3 beats/beat-XX/audio-filtered.mp3
链式分解: 干净的干音信号与极端的数字伪影(5 倍抽取,通过零阶保持上采样的 8 位量化)分层,周期性下降提高湿音电平以达到"退化传输"效果,对湿音信号施颤音以产生节奏脉冲,以及拍击延迟营造空间感。保持链式结构完整——仅在需要时调整个别参数。
在所有后续阶段中使用 audio-filtered.mp3。保留原始 audio.mp3 以备参考。
将决定记录在 project.md 的备注中。
阶段 5:合成节拍
对于每个节拍,将视觉素材和音频以硬切(无过渡)合并。节拍时长由视觉和音频中较长者决定。
如果视觉>=音频:
音频在完整视觉素材下播放。无需填充。
ffmpeg -i beats/beat-XX/visual.mp4 -i beats/beat-XX/audio-filtered.mp3 \
-c:v libx264 -pix_fmt yuv420p -c:a aac \
-shortest -r 25 output/beat-XX-composed.mp4
如果音频>视觉:
视觉素材保持其最后一帧以填充剩余时间。在视频上使用 tpad:
AUDIO_DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 beats/beat-XX/audio-filtered.mp3)
ffmpeg -i beats/beat-XX/visual.mp4 -i beats/beat-XX/audio-filtered.mp3 \
-vf "tpad=stop_mode=clone:stop_duration=$AUDIO_DUR" \
-c:v libx264 -pix_fmt yuv420p -c:a aac \
-t $AUDIO_DUR -r 25 output/beat-XX-composed.mp4
如果未应用音频滤镜,则使用 audio.mp3 替代 audio-filtered.mp3。
节拍间暂停
在节拍之间添加短黑帧以提供呼吸空间:
ffmpeg -f lavfi -i color=c=black:s=1920x1080:d=0.15:r=25 \
-c:v libx264 -pix_fmt yuv420p output/pause-XX.mp4
ASCII 字幕节拍(文字焦点)
当旁白本身是焦点而非视觉素材时,使用 ASCII 风格的节拍,将 TTS 文本居中显示在屏幕上。在 project.md 中将其标记为 [caption] 类型:
节拍模板:
### 节拍 03 [caption]
**视觉:** ascii caption — 居中文本,深色背景,微妙的终端美学
**TTS:** "屏幕上出现的准确文字。"
**状态:** [ ] 视觉 [ ] 音频 [ ] 已批准
使用 ffmpeg 的 drawtext 在黑色/深色背景上生成,配合等宽字体和 ALLMIND 金色 (#C5A55A) 文本:
TEXT="您的 TTS 句子此处"
ffmpeg -f lavfi -i color=c=black:s=1920x1080:d=6:r=25 \
-vf "drawtext=text='${TEXT}':fontsize=42:fontcolor='#C5A55A':fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf:x=(w-tex