短剧运镜/动作量化拆解
把一段真人视频的镜头运动与动作节拍用可量化、可复刻的方式拆开。核心原则:不靠肉眼估运镜——用帧差与配准给出客观证据。
一、何时用
- 用户给一段 mp4/短视频,要"拆解前 N 秒""复刻运镜/动作/镜头语言"
- 需要判断某镜是"固定 / 推 / 拉 / 摇 / 横移 / 遮挡揭晓 / 跟拍"
- 要产出可直接给即梦/可灵/Seedance 的复刻提示词 + 分镜时间轴 JSON
二、工具链(本机固定路径)
FFMPEG = E:/Workbuddy/NarratoAI/.venv/Lib/site-packages/ffmpeg/binaries/bin/ffmpeg.exe
FFPROBE= E:/Workbuddy/NarratoAI/.venv/Lib/site-packages/ffmpeg/binaries/bin/ffprobe.exe
PY = E:/Workbuddy/NarratoAI/.venv/Scripts/python.exe # 自带 Pillow 10.4 + numpy 2.5
> ⚠️ 不要用托管 python(E:/WorkBuddyConfig/binaries/python/...)建 venv:其 pyvenv.cfg 指向失效的 C 盘路径;pip install --target 也会被沙箱 SIGTERM。直接用 NarratoAI 的 venv python,它已带图像库。
三、SOP
0. 元数据
"$FFPROBE" -v error -show_entries format=duration,size -show_entries stream=codec_type,width,height,r_frame_rate,nb_frames -of default=noprint_wrappers=1 "in.mp4"
记录:分辨率/画幅比、fps、总时长 → 算出"前N秒 = 0~N*fps 帧"。
1. 切点检测(关键:用 select + metadata,不要用 ffprobe scene)
# ✅ 有效(HEVC 也work)
"$FFMPEG" -hide_banner -i "in.mp4" -t 10 -vf "select='gt(scene\,0.2)',metadata=print:file=-" -an -f null - 2>/dev/null | grep -oE "pts_time:[0-9.]+"
# 再用更低的 0.10 阈值复检,抓快切/快运镜
> ❌ ffprobe -show_frames -show_entries frame=scene 在 HEVC 上只返回 SEI 噪声,无效。
> 注意:低阈值(0.10)会把快速运镜也报成"切点"(每 ~0.17s 一个)→ 那说明该段有快速镜头运动,不是多个硬切。
2. 抽帧(三档)
OUT=.../video_analysis/<name>; mkdir -p "$OUT"/{key_1fps,action_10fps,door_30fps,overview}
"$FFMPEG" -y -ss 0 -i "in.mp4" -t 10 -vf "fps=1" -q:v 2 "$OUT/overview/ov_%03d.jpg" # 概览
"$FFMPEG" -y -ss 0 -i "in.mp4" -t 10 -vf "fps=10" -q:v 2 "$OUT/action_10fps/f_%03d.jpg" # 运镜/动作分析
"$FFMPEG" -y -ss 0 -i "in.mp4" -t 3 -vf "fps=30" -q:v 2 "$OUT/door_30fps/d_%03d.jpg" # 最动作段逐帧
# 时间映射: f_001=0.0s, 每帧 0.1s ; d_001=0.0s, 每帧 0.033s
3. 量化:帧差(MAD) 分区 + 红青叠加
写 py 脚本(用 NarratoAI python):
import numpy as np; from PIL import Image
def L(p): return np.asarray(Image.open(p).convert('L'),dtype=np.float32)
A,B=L(a),L(b)
print(np.abs(A-B).mean()) # 全帧 MAD
print(np.abs(A[:380]-B[:380]).mean()) # 背景区(判断镜头是否动)
print(np.abs(A[500:1000]-B[500:1000]).mean()) # 主体区
print(np.abs(A[1000:]-B[1000:]).mean()) # 地面区
# 红青叠加: 红=前帧, 绿蓝=后帧 → 背景出现红青鬼影=镜头在动
Image.fromarray(np.dstack([np.asarray(Image.open(a).convert('L')),
np.asarray(Image.open(b).convert('L')),
np.asarray(Image.open(b).convert('L'))]).astype(np.uint8)).save('ana.jpg')
判读规则
| 现象 | 结论 |
|------|------|
| 全帧MAD尖峰(>40)且对齐切点 | 硬切 |
| 背景区MAD高(>15)、主体更高 | 镜头在动(推/摇/横移) |
| 背景区MAD≈0(<1) | 镜头完全静止,只有主体动 |
| 红青叠加图背景出现鬼影 | 该段时间内镜头位移 |
| 平移穷举配准改善>30% | 是横移/摇镜(读 dx,dy 方向) |
| 尺度配准最优 scale≠1.00 且改善明显 | 是推/拉镜 |
| 背景区平移可对齐(改善>70%、残差<2)但 dx,dy 仅 10-20px | 机位近固定 + 轻微漂移(上摇/手持呼吸) |
3b. 区分「人走近」 vs 「推镜」——两步法(本机实测有效)
单看"主体变大"会误判。先分区、再配准:
1. 分区帧差:算 上带(背景0-15%) / 中带(主体) / 下带。
- 背景上带低(≈5)、主体中带高(≈11-13) → 主体在动,机位不动(人在走近)
- 三带同时高 → 机位在动,进第 2 步
2. 背景区平移穷举配准(只取纯背景小块,如上方丝带/远处墙):
- 改善 >70% 且残差降到 <2、位移仅 10-20px → 机位近固定 + 轻微上摇/漂移(不是推镜)
- 改善 <10%(残差几乎不变)→ 机位完全静止
3. 尺度扫描(判推/拉镜,必做):对整帧按 scale∈[0.88…1.12] resize 后取中心裁剪再比。
- 最优 scale≈1.00 或改善 <5% → 无推镜("主体变大"是人物走近)
- 最优 scale 明显≠1 且改善 >30% → 推/拉镜
> ⚠️ 尺度扫描实现坑:直接对整帧 resize 后取中心裁剪,若裁剪区贴边会越界,best 保持初值 → 打印出 scale=None / 改善=-2e9% 的假结果。改法:先按比例裁掉外圈 10% 再用中心区比较,或对 resize 后做边界钳制。
3c. 识别「撞击 montage」(短剧打脸戏标志手法)
若在 0.3-0.4s 内检测到 2-3 个连续切点(如 13.37/13.53/13.70),且 MAD 尖峰 70/26/30:
- 这是"撞击快切 montage"——用 2-3 个不同角度/景别的 0.16s 极短切,把"接触瞬间"放大成暴力美学段落。
- 复刻提示:单镜 0.16-0.17s,切点必须砸在接触那一帧;AI 无法一次生成,需生成 1 段连续动作再在剪映裁成短切。
4. 目检帧(必做)
- 用 Read 工具直接看 jpg(它是多模态)
- 先看 overview 10 帧建结构,再看 action_10fps 的连续帧判"同镜或切"(若相邻帧构图巨变=切点)
- 快运镜段看 30fps 连续帧还原轨迹(如"垂帘→横移揭晓")
- 查字幕/名牌:把若干帧的上 12% 与下 12% 横带裁出来拼成
strip_top.jpg/strip_bottom.jpg一次目检(短剧字幕在下部、名牌在左上)。有些段落完全没有硬字幕(信息只靠配音),务必核实并写进报告。
from PIL import Image
frames=[12,18,20,26,32,38]; h,w=1280,720
for name,y0,y1 in [('strip_top',0,0.12),('strip_bottom',0.86,1.0)]:
canvas=Image.new('RGB',(w,int(h*(y1-y0))*len(frames)),(20,20,20))
for i,n in enumerate(frames):
canvas.paste(Image.open(f'{B}/f_{n:03d}.jpg').crop((0,int(h*y0),w,int(h*y1))),(0,i*int(h*(y1-y0))))
canvas.save(f'{OUT}/{name}.jpg',quality=92)
5. 产出
xxx_前N秒运镜动作拆解.md:镜头总表 + 逐镜深拆(画面/运镜/动作/构图/光影/字幕/声音/复刻要点) + 运镜总纲 + 动作节拍 + 中文AI提示词 + 执行清单xxx_前N秒_分镜.json:meta + characters + shots + 汇总- 校验:
json.loads通过;Σduration ≈ meta.duration ±0.5
5b. 多段拆解后合并成单一文档
用户常先要"拆前 10s",再补"拆 10-17s,合并到一个文档"。做法:
1. 沿用同一套帧目录规范(fan_10_17s/overview|action_10fps|_probe),时间映射写成 f_001=10.0s, 每帧0.1s(t(n)=start + (n-1)/10)
2. 镜头编号全局连续(0-10s=镜1-9,10-17s=镜10-18),JSON 的 shots 加 segment 字段("0-10s"/"10-17s")便于分组统计
3. 合并文档结构:统一元数据 → 全局镜头总表(18镜) → 逐镜深拆分「上半场/下半场」 → 统一运镜总纲/动作总纲(两段动作分别列 5 拍)→ 18镜提示词 → 执行清单
4. meta 里加 segment_avg(如 {0-10s:1.2, 10-17s:0.69})——两段节奏差本身就是结论
5. 保留原单段文件不删,另存合并版(xxx_0-17秒运镜动作完整拆解.md)
四、踩坑清单
- 托管 python 不能建 venv、pip 被拦 → 借
NarratoAI/.venv/Scripts/python.exe - ffprobe 的 scene 字段在 HEVC 上无效 → 用
select='gt(scene\,X)'+metadata=print - 肉眼会把"遮挡横移揭晓"误判成"固定机位" → 必须看 30fps 连续帧 + 背景区MAD
- 低阈值(0.1)会把快运镜报成多切点 → 每~0.17s 一个 = 运镜,不是硬切
- 内联 python 全帧穷举配准(90×40×大数组)会超时 SIGTERM → 降采样 step=2、限制区域、或写成文件再跑
- 段末镜可能跨过 N 秒 → JSON 里注明"末镜延伸至下一处切点",别硬凑时长
- 用户引用的源文件可能已从磁盘消失 → 先
ls确认,找不到就换用户新给的路径 - "主体变大"≠推镜 → 必须先分区帧差(背景带是否也变),再跑背景平移配准;只有尺度扫描改善>30% 才是推/拉
- 尺度扫描越界假结果 → 整帧 resize+中心裁剪会贴边越界,出现
scale=None/改善-2e9%;先裁外圈 10% 再比 - 切点算法会把 0.33s 内的 2-3 连切识别成多个切点 → 这不是误检,是"撞击 montage",要如实写成剪辑手法
- 别默认全片都有字幕 → 分段裁上下横带目检;同一视频不同段落可能一段有字幕一段没有
五、输出风格
- 结论要带证据(MAD 数值、切点秒数)
- 复刻建议要分实拍 vs AI生成两路
- 提示词中文(即梦/可灵),带角色锚点 + 负向词