mcpField
W
wjs-reframing-video

wjs-reframing-video

github.com/jianshuo创意与媒体137187 次安装今天更新
代码仓库

关于 wjs-reframing-video

wjs-reframing-video 是用于将视频改为相反画面方向的处理技能。它通过嘴部运动追踪当前说话者,并按片段定位裁剪窗口;原始文件不会被修改。

主要功能

  1. 01根据嘴部运动检测说话者
  2. 02追踪采样帧中的人脸
  3. 03按说话者划分裁剪片段
  4. 04支持硬切或平滑移动
  5. 05音频流直接复制
  6. 06生成裁剪计划 JSON 旁文件

适用场景

  1. 01将横屏访谈转为竖屏短片
  2. 02将竖屏录像转为横屏视频
  3. 03重构单一说话者的视频画面
  4. 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 覆盖最终尺寸。

流程

  1. 探测:通过 ffprobe 获取输入尺寸、帧率和时长。
  2. 确定方向:根据宽高比自动判断(可用 --target portrait|landscape 覆盖)。
  3. 按 --sample-fps 抽取帧(默认值为 5;足以捕捉嘴部运动——语音的奈奎斯特频率约为 10 Hz,因此至少需要 4–5 fps)。
  4. 检测面部关键点:对每个抽取的帧使用 MediaPipe Tasks FaceLandmarker(478 个关键点)。为每张检测到的脸记录:中心点、尺寸估值、MAR(嘴部纵横比 = 内唇垂直距离 / 嘴角水平距离)。
  5. 跟踪人脸:按中心点距离匹配各帧中的人脸 → 为每张脸分配稳定的 face_id。
  6. 确定每个采样点的活跃说话者:对每条人脸轨迹,计算滑动窗口内 MAR 的方差(--mar-var-window-sec,默认值为 1 s)。方差最高的人脸即为“正在说话”。若方差低于 --mar-var-threshold,则无人说话 → 回退到画面中最大的人脸。
  7. 滞后切换:只有当新的说话者稳定达到 --min-segment-sec(默认值为 1.5 s)时,候选切换才会生效。更短暂的切换会被合并,避免裁剪因单帧误检而来回跳动。
  8. 按说话者对齐分段 → 对每个片段,取该说话者在片段内人脸中心点的平均值(cx、cy)作为裁剪中心,并在整个片段时长内保持固定。
  9. 构建 ffmpeg 阶梯函数表达式(--motion cut,默认值),使每个片段的裁剪位置保持不变,并在每个片段边界处瞬间跳转——呈现类似真实镜头切换的视觉效果。(--motion smooth 会切换为在各片段中点之间进行分段线性平移;对于口播内容,这种方式很少合适,因为画面看起来会在句中漂移。)
  10. 渲染:执行一次 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 会导致颜色失真。

评论

支持 Markdown