关于 wjs-reframing-video
wjs-reframing-video 是用于将视频改为相反画面方向的处理技能。它通过嘴部运动追踪当前说话者,并按片段定位裁剪窗口;原始文件不会被修改。
主要功能
- 01根据嘴部运动检测说话者
- 02追踪采样帧中的人脸
- 03按说话者划分裁剪片段
- 04支持硬切或平滑移动
- 05音频流直接复制
- 06生成裁剪计划 JSON 旁文件
适用场景
- 01将横屏访谈转为竖屏短片
- 02将竖屏录像转为横屏视频
- 03重构单一说话者的视频画面
- 04查看旁文件中的裁剪决策
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jianshuo/ claude-skills --skill wjs-reframing-video安装后在 Claude Code 中输入/wjs-reframing-video确认已加载。
附带文件models/blaze_face_short_range.tflitescripts/crop.py
详细介绍
wjs-reframing-video
通过从源视频中裁剪一条窄带来转换视频方向,而不是实际旋转视频。裁剪窗口会跟随当前说话者(嘴部正在动的那张脸),而不只是最大或置信度最高的脸。.crop.json 伴随文件会记录裁剪方案、各片段的说话者判定以及所用参数。不会修改原始输入。
适用场景
- 将 16:9 播客/访谈/演讲重新剪辑为竖屏短视频平台内容(微信 Channels 视频号、Douyin 抖音、Xiaohongshu 小红书、YouTube Shorts、TikTok、Reels)。
- 将 9:16 手机录制视频重新剪辑为适合横屏播放器的内容(YouTube 长视频、博客嵌入视频)。
- 将 4:3 存档素材重新剪辑为 3:4 手机视频,或反向转换。
输出宽高比与源视频相反——16:9 → 9:16,而不是“在 9:16 画面中加黑边显示 16:9”。
不适用场景
- 多人问答,且每张脸都需要单独裁剪——此技能每个视频只选择一条裁剪轨迹。要按说话者分别渲染,请改用 wjs-editing-multicam。
- 动画内容/没有人脸的 B-roll——会回退到中心裁剪,通常不符合预期。
- 源视频有明显的摄像机运动(手持平移/变焦)——人脸跟踪器会放大摄像机抖动。请先进行防抖处理。
- 源视频已是目标宽高比——无需处理。
此技能的功能与非功能
| 功能 | 非功能 |
|---|---|
| 通过 MAR(嘴部宽高比)变化进行视觉说话者检测 | 视听融合(将音频能量与唇部运动进行互相关) |
| 通过中心距离匹配,在不同帧之间稳定跟踪人脸 | 在长时间间隔/遮挡后重新识别 |
| 采用滞后机制防止闪烁的说话者对齐片段 | 每次闪烁都逐帧切换 |
--face-pick speaker(默认)——选择嘴部正在动的人 | --face-pick largest(可选的旧版模式)——选择最大的人脸 |
片段之间硬切,每个片段内裁剪位置固定(--motion cut,默认) | 说话者发言期间平滑移动取景(可选的 --motion smooth) |
| 音频流复制(逐位精确) | 音频重新处理/重新编码 |
通过 ffmpeg 以 5 fps 采样的 MediaPipe Tasks FaceLandmarker(478 点网格) | 逐帧神经网络修复/扩图 |
一次 ffmpeg crop + scale 处理 | 逐帧 Python 合成 |
如果没有人在说话(静音、只有音乐的片段),会自动回退为“最大人脸”。
依赖项
pip install mediapipe opencv-python numpy
(MediaPipe 不属于标准 Python 发行版;ffmpeg 和 ffprobe 必须位于 PATH 中。)
首次运行时下载模型:MediaPipe 0.10+ 使用 Tasks API,需要 face_landmarker.task 模型文件(约 4 MB)。首次调用时,crop.py 会将其下载到 ~/.claude/skills/wjs-reframing-video/models/,供后续运行缓存使用。首次运行时若处于离线状态,脚本会失败。
适用范围限制:随附的 landmarker 针对距离摄像机约 2 米以内的人脸进行了调优(自拍/播客/访谈距离)。如果活动现场的广角镜头中人脸较小,可能无法检测到人脸——请先对一帧进行采样确认。
裁剪计算
源宽高比 = W / H。目标宽高比 = H / W(反转)。计算裁剪窗口:
| 源视频方向 | 裁剪窗口 |
|---|---|
| 横屏(W > H)→ 竖屏 | W_crop = H × H / W,H_crop = H(窄竖带) |
| 竖屏(W < H)→ 横屏 | W_crop = W,H_crop = W × W / H(窄横带) |
对于 1920×1080 → 竖屏,W_crop = 608,H_crop = 1080。最终缩放为 1080×1920(放大约 1.78 倍)。
对于 1080×1920 → 横屏,W_crop = 1080,H_crop = 608。最终缩放为 1920×1080。
如果希望使用原生裁剪尺寸而不是放大,可通过 --output-size 1080x1920 覆盖最终尺寸。
流程
- 探测:通过 ffprobe 获取输入尺寸、帧率和时长。
- 确定方向:根据宽高比自动判断(可用
--target portrait|landscape覆盖)。 - 按
--sample-fps抽取帧(默认值为 5;足以捕捉嘴部运动——语音的奈奎斯特频率约为 10 Hz,因此至少需要 4–5 fps)。 - 检测面部关键点:对每个抽取的帧使用 MediaPipe Tasks
FaceLandmarker(478 个关键点)。为每张检测到的脸记录:中心点、尺寸估值、MAR(嘴部纵横比 = 内唇垂直距离 / 嘴角水平距离)。 - 跟踪人脸:按中心点距离匹配各帧中的人脸 → 为每张脸分配稳定的
face_id。 - 确定每个采样点的活跃说话者:对每条人脸轨迹,计算滑动窗口内 MAR 的方差(
--mar-var-window-sec,默认值为 1 s)。方差最高的人脸即为“正在说话”。若方差低于--mar-var-threshold,则无人说话 → 回退到画面中最大的人脸。 - 滞后切换:只有当新的说话者稳定达到
--min-segment-sec(默认值为 1.5 s)时,候选切换才会生效。更短暂的切换会被合并,避免裁剪因单帧误检而来回跳动。 - 按说话者对齐分段 → 对每个片段,取该说话者在片段内人脸中心点的平均值(cx、cy)作为裁剪中心,并在整个片段时长内保持固定。
- 构建 ffmpeg 阶梯函数表达式(
--motion cut,默认值),使每个片段的裁剪位置保持不变,并在每个片段边界处瞬间跳转——呈现类似真实镜头切换的视觉效果。(--motion smooth会切换为在各片段中点之间进行分段线性平移;对于口播内容,这种方式很少合适,因为画面看起来会在句中漂移。) - 渲染:执行一次 ffmpeg 处理——
crop=W:H:x='expr':y='expr', scale=OUT_W:OUT_H。裁剪滤镜会原生地逐帧计算x和y。音频流直接复制。
scripts/crop.py 是实现代码。输出副作用:
<input>.crop.json— 包含裁剪方案的伴随文件<input>_cropped.mp4— 最终裁剪并缩放后的视频
伴随文件架构(<input>.crop.json)
{
"_about": "wjs-reframing-video crop plan for cam_a.MOV. Active-speaker detected via MAR variance.",
"_help": {
"source_size": "[width, height] in pixels.",
"target_size": "[width, height] of the final rendered output.",
"crop_window": "[width, height] of the moving crop in source coords.",
"chunks": "Speaker-aligned segments: {t0, t1, cx, cy, speaker_id}.",
"face_pick_mode": "speaker = MAR-variance active-speaker; largest = old behavior.",
"speaker_id": "Stable face track id. null means no face / silence fallback."
},
"schema_version": 2,
"source": "cam_a.MOV",
"source_size": [1920, 1080],
"target": "portrait",
"target_size": [1080, 1920],
"crop_window": [608, 1080],
"face_pick_mode": "speaker",
"sample_fps": 5.0,
"mar_var_window_sec": 1.0,
"mar_var_threshold": 1.5e-4,
"min_segment_sec": 1.5,
"chunks": [
{"t0": 0.0, "t1": 4.2, "cx": 808, "cy": 540, "speaker_id": 0},
{"t0": 4.2, "t1": 11.6, "cx": 1182, "cy": 540, "speaker_id": 1},
{"t0": 11.6, "t1": 14.0, "cx": 808, "cy": 540, "speaker_id": 0}
],
"face_sample_count": 1234,
"track_count": 2
}
性能
- 检测是最耗时的步骤。在 Apple Silicon 上以 2 fps 抽帧时,速度约为实时的 10–20 倍(30 分钟的源视频检测约需 1–2 分钟)。提高
--sample-fps会使检测变慢,但跟踪响应更灵敏。 - 渲染速度很快——只需执行一次 ffmpeg 处理,并使用硬件编码(macOS 上为
hevc_videotoolbox)。对于 1080p 源视频,通常快于实时。 - 对于非常长的源视频(>200 个片段),ffmpeg 表达式会变得繁琐;脚本会自动降低片段中点的采样密度,使表达式保持在约 200 个控制点以内。
常见问题
- 嘴部动作不等于说话——打哈欠、大笑、吃东西或吸气都会导致 MAR 方差升高。检测器可能会短暂地将这些动作误认为说话。对于高风险内容,请查看旁车文件中的说话者时间线(脚本会打印
face#N: Xs on screen (Y%)摘要),并在需要时使用不同的--mar-var-threshold重新运行。 - 侧脸或低头的面部——当面部相对摄像机旋转超过 60° 时,MediaPipe 可能无法可靠地定位嘴部关键点,因此 MAR 方差会趋于平坦。此时会启用“最大面部”说话者回退策略。如果有较长的侧脸镜头,可以考虑使用
--face-pick largest。 - 两张面部同时说话(打断/抢话)——两张面部的 MAR 方差都会升高,但只会有一个胜出。未胜出的面部会被视为听众。若要在串音情况下准确追踪每位说话者,请使用 wjs-editing-multicam 并分别使用各摄像机画面。
- 长时间静音(B-roll、音乐)——会回退到最大面部。如果最大面部不是说话者(例如,听众一直静止,而说话者的麦克风输入的是音乐),画面就会逐渐偏移。请预先将纯音乐片段分段处理。
- 源视频带有内嵌的下三分之一字幕/字幕——对于 H→V,下方区域会被裁掉;对于 V→H,下方区域会保留,但会被拉伸。运行前请移除内嵌字幕。
- 广角/鱼眼镜头——边缘处的面部可能无法检测到关键点。请先使用
ffmpeg lenscorrection校正畸变。 - 放大伪影——
608×1080 → 1080×1920相当于放大 1.78 倍,清晰的文字会出现明显失真。如果想让叠加内容保持清晰,请以原始裁切尺寸渲染(--output-size 608x1080),再交由平台放大。 - 输出比特率超过平台限制——默认值为
--bitrate 12M。微信视频号(视频号)的上限为 10 Mbps;面向该平台时请传入--bitrate 8M。
未检测到面部时的回退方案:确定性固定裁切
“可能无法检测”的范围限制不只是警告——当 MediaPipe
检测到 0 张面部(宽镜头拍摄的远距离/静态双人访谈,距离约为
2 m 或更远)时,裁切日志会显示 0 face observations across N sampled frames,
脚本会对画面进行居中裁切——但在访谈场景中,裁切区域会落在
两人之间的背景上(壁炉、植物、
标志),而不是任何人的身上。输出效果会很糟糕,而且无论如何调整
--mar-var-threshold 都无济于事,因为根本没有检测到任何关键点。
务必先查看裁切日志,再判断输出是否可靠。 如果日志显示
0 face track(s) identified / (no face / fallback): … 100%,
请放弃 MediaPipe 裁切,改用确定性固定裁切。这类拍摄中的
摄像机是静止的,说话者在画面中的位置固定,因此手动设置 X 偏移量非常可靠:
# 1920×1080 → 9:16 ⇒ crop window 608×1080. X = speaker's screen position:
# left speaker → x=0 right speaker → x=1920-608=1312 centred → x=656
# Do crop + (HLG→SDR tone-map) + 30fps + dense keyframes in ONE pass so the
# body clip is final and HyperFrames can seek it (see /wjs-overlaying-video).
ZF=~/Library/Python/3.9/.../imageio_ffmpeg/binaries/ffmpeg-macos-aarch64-v7.1
"$ZF" -i clip.mp4 -vf \
"crop=608:1080:0:0,zscale=t=linear:npl=203,format=gbrpf32le,\
tonemap=tonemap=hable:desat=0,\
zscale=w=1080:h=1920:t=bt709:m=bt709:p=bt709:r=tv,format=yuv420p,fps=30" \
-c:v libx264 -crf 19 -preset medium -g 30 -keyint_min 30 \
-color_primaries bt709 -color_trc bt709 -colorspace bt709 \
-c:a aac -b:a 192k -movflags +faststart clip_vert.mp4
提交前,请提取一帧(-ss 20 -frames:v 1),确认说话者位于画面中央。对于确实需要跟随
两位说话者的片段(频繁来回交谈),请根据转录稿手动标注每位说话者的画面窗口,再拼接固定裁切片段;对于
一人独白的片段,使用单个固定 X 值即可。如果源视频已经是 SDR(bt709),请移除色调映射滤镜——将 HLG
方案用于 SDR 会导致颜色失真。